mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-25 14:00:40 +00:00
be54575954
* feat(apps): per-endpoint webhook delivery state, a per-install rate cap, and a 24 h drop (TASK-3408, SPEC-6 U10c) webhook_deliveries records each app hook's decision per outbox event. A terminal row (delivered, permanent, refused, skipped, dropped) is never retried, so a retry goes only to app endpoints still owed, and an event an app was refused once (held, disabled, not visible) stays refused, which closes U10b's deliver_from precision and clock-skew residuals for any event refused once. A pending row (transient, deferred, rate_limited) keeps the event owed. Owner hooks record only "delivered", used only to skip re-sending an event an owner endpoint already received while it stays owed to an app (lead ruling, day 87). Owner failure, retry and ack are unchanged; full owner per-endpoint state is TASK-3409. A per-install cap of 600 a minute (burst 60, per instance) defers the excess without charging an attempt. A delivery still owed 24 h after its event is dropped, counted on the hook and shown to the owner as undelivered_dropped. Migrations 124 / pg 098. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * fix(apps): rate cap counts every attempt and only subscribed targets; limiter pruned; bulk skips recorded (codex r1 on U10c, TASK-3408) Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * fix(apps): only owed deliveries are dropped; the rate cap is asked after admission; the ledger counts requests sent (codex r2 on U10c, TASK-3408) Admission's checks become one function run under the install lock for admission and as a plain read for AppDeliveryRefusal, so a 24 h-old delivery admission would refuse is recorded refused, not dropped and counted. The rate gate moves after admission, so a refused attempt spends nothing. DeliverAppEvent returns the requests it sent, which the ledger adds. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * fix(apps): a request that failed before sending is not counted (codex r3 on U10c, TASK-3408) appfetch.Poster wraps dial and TLS-handshake failures in ErrNotSent; the dispatcher counts a request as sent only when the failure is neither that nor a policy refusal. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * fix(apps): the drop decision runs under the install lock; a write failing before its first byte is unsent (codex r4 on U10c, TASK-3408) DropOwedDelivery takes the install row and runs admission's checks in the same transaction that records the outcome, so a disable or upgrade cannot land between the check and the drop. The Poster counts bytes written through its connection; a write that fails at zero bytes is ErrNotSent. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * fix(apps): a late delivery replaces a drop and takes it off the count (codex r5 on U10c, TASK-3408) A request admitted before another drainer dropped its event (the first drainer's outbox claim expired mid-request) did reach the app. RecordDelivery now reads the row under its own transaction: delivered replaces dropped and decrements dropped_count; every other terminal decision still stands. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * fix(apps): RecordDelivery inserts before it locks, so a concurrent drop is seen (codex r6 on U10c, TASK-3408) SELECT ... FOR UPDATE locks nothing when the row does not exist, so a drop committing between the read and the upsert was overwritten with the count left raised. RecordDelivery now inserts with ON CONFLICT DO NOTHING (which waits for a concurrent inserter), and only when the row already existed locks and reads it before deciding. A deterministic test drives a drop between the two statements; run against the previous code on Postgres it ends delivered with dropped_count 1. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * test(apps): the drop/record race test waits for the drop to reach its write (codex r7 on U10c, TASK-3408) Against the previous RecordDelivery on Postgres it fails 3 of 3 (delivered, dropped_count 1); with the fix it passes on both dialects. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS