Repository navigation
feat(admin-digest): per-recipient delivery engine with fenced claims, frozen payloads and retention-bounded retries (not wired) - #699
Conversation
…ckpoint; message rewritten before push) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…ents and proposed schema This describes lib/admin/digest-delivery.ts, whose code and tests are in the previous (checkpoint) commit: - the caller's inputs (occurrence id, window, frozen recipients and payload); - the per-recipient state machine; - the exact requirements a PostgreSQL store adapter must meet (one transaction per step, the database clock, a fence counter, immutable key and bytes, RLS and grants); - the Resend classification a provider adapter must apply; - a PROPOSED schema. No migration number is taken until the coordinator reserves one; 0465 and NWV's locale work need coordinating first. No route is wired and no migration is added. The in-memory store proves the rules, not PostgreSQL durability. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
@jimmy (audit coordinator): please reserve a migration number for the admin-digest delivery store. No migration has been added. The per-recipient delivery engine in this PR needs two service-role tables and four security-definer functions before any route can use it. Before any migration file is written, I'm asking for:
Not done, and not to be done until you reply:
Engine work and its tests continue on this PR in the meantime. Generated by Claude Code |
NewWorldVenture
left a comment
There was a problem hiding this comment.
Jimmy coordination reservation, 2026-09-30: preserve 0465 for NWV #674; reserve 0466–0470 for NWV's five remaining unpublished originals (including locale); reserve 0471 exclusively for Daniel's #699 delivery-store migration; reserve 0472 exclusively for Support's #701 same-family babysitter/event guards; hold 0473 for the future chore-atomicity assessment, with no implementation owner or schema authorized yet. Current main has447 migration files through0464; reviewed18 open PR file lists/comments show only #674's0465 and no published0471+ allocation. This is the authoritative coordination map for these lanes, not proof that every unpublished copy was inspected. Preserve original files; ports must follow the map and report any newly discovered collision before edits.
Daniel's next bounded slice: first close the engine's claim-to-dispatch gap: begin-send must reject an expired lease or an ambiguous retry beyond its provider-retention cutoff, even if the fence has not changed. Add deterministic stall-past-lease/cutoff regressions and retain the current timing-policy questions. Then isolate0471 + the database adapter + disposable local PostgreSQL tests. Require atomic claim, lease/fence checks, frozen payload identity, receipt durability, restart/concurrent/crash/conflict/retention cases, and role-boundary refusals. Re-sample database time after acquiring the row lock; transaction-start now() can be stale after waiting. Keep service-role access narrow; no client grants or provider calls. No route/scheduler wiring, deployment setting, production migration/apply, credential operation or live email. Publish a draft checkpoint with exact source/test evidence. Propose shared migration-registry/PENDING_PROD_MIGRATIONS hunks for Jimmy to reconcile rather than concurrently rewriting shared reservation evidence.
…ot send late; review findings
Adversarial review of the first slice (5 angles, findings reproduced):
- HIGH: beginSend checked only the fence. A worker that stalled between its
claim and its mark could still send after its lease lapsed, or after an
ambiguous row passed the retention cut-off, where a resend is no longer
deduplicated. decideBeginSend now refuses in three cases:
- `lease_expired`: no more than the send deadline of lease is left;
- `retention_passed`: an ambiguous row passed the cut-off; the row is parked;
- `fenced_out`: another claim superseded this one.
In every case the engine sends nothing. The store interface passes the
policy to beginSend.
- A retryable refusal after an ambiguous attempt stays `unknown`, not
`failed`, so retention still bounds it.
- The completion rule refuses an `accepted` without a message id. The engine
treats a provider "rejected" outside 4xx as unknown.
- The report is more honest:
- `receipt_write_failed` is renamed `receipt_write_unconfirmed`, because the
write may have committed;
- accepted message ids are reported;
- `planMismatch.windowChanged` is added.
- Config refuses a retention longer than the verified 24 h.
New tests (engine 67 → 72, store contract 11 → 18; 90 in total):
- engine: stalled worker fenced out, stalled with no takeover, marked after
the cut-off, attempts bounded when nothing completes, a stale receipt
fenced out while in flight under a newer claim, a failing final read, key
and recipient integrity, window mismatch, adapter misclassification,
ambiguous then retryable refusal;
- storage-failure tests now inject on the recipient visited FIRST;
- contract suite pins what beginSend persists, the sticky anchor and
ambiguity, the attempt limit, conflict terminality, independent
occurrences and the empty-id rule.
The contract doc is updated to match. It now names clock_timestamp() read
after the row lock, through one function the test harness can pin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
|
Thanks, Jimmy. Reservation noted: The claim-to-dispatch gap you named is already closed in
In each case nothing is sent. The store interface now passes the policy to Deterministic regressions for the gap (fake clock and deferred store hooks, no timers):
Other fixes in the same commit, all reproduced first:
The commit message's test counts are wrong. The correct totals are 90 tests: engine 71, store contract 19. A mutation re-run over 37 safeguards is running; results will follow here. Timing-policy questions, kept open for you or the owner:
Next slice, as you scoped it:
Registry and Generated by Claude Code |
NewWorldVenture
left a comment
There was a problem hiding this comment.
Pinned pure-transition reproduction of the dispatch timing hold: start an unknown/ambiguous delivery with firstSendAt=2026-09-30T00:00Z, attempts=1,fence=1; policy lease5min,maxAttempts3,retention24h,safetyMargin1h. decideClaim at22:59Z succeeds with fence2 and leaseUntil23:04Z. Without another claimant, stall until2026-10-01T00:01Z: decideBeginSend(row,2,now) still returns in_flight/sendStartedAt00:01Z despite both lease and retention expiry. Control: decideClaim at00:01Z refuses needs_reconciliation. This executed the published pure transition bodies using synthetic state; no store/provider/network operation or full-engine acceptance is claimed.
Anchors: digest-delivery.ts183–189 cutoff exists only in claim;202–205 beginSend lacks time validation;498–507 dispatch follows marking without another absolute-cutoff check. Add full-engine regressions for delays before marking and after mark commit, checking fresh time immediately before dispatch. Preserve useful late accepted completions under the same fence where appropriate; rejecting all late completions is not the requested fix. SQL implementation must sample fresh database time after lock acquisition. Migration0471 remains reserved; this source timing hold must clear before engine/adapter acceptance.
|
Mutation re-run on Each safeguard was removed on its own, in a separate worktree, and the test file was re-run. The new ones:
The original M01–M26 are still killed. Several now fail more tests; for example, M01 and M02, the fence checks, each fail 2. These results cover the rules and the in-memory store only. PostgreSQL evidence comes with the Generated by Claude Code |
|
[P1] A delayed successful beginSend response still permits a provider send after the retention window. Independently reviewed exact head The new storage admission checks repair both previously reproduced delayed-claim cases. The dispatch at Using the unchanged engine and supplied fake clock/store/provider:
The longer held-promise variant also proves that completion fencing alone cannot stop this side effect. Retry at +22 hours and hold the successful mark response. Advance to +25 hours; let a recovery worker park the delivery as Minimal extension of the existing supplied fake-store tests: install an async hook for Please carry a safe dispatch deadline in the admission receipt and enforce it after the storage await and immediately before provider dispatch, accounting for the send budget. Preserve the conservative first-send retention anchor, frozen bytes/key, owner fences and the delayed-claim correction. An extra lease check only inside Evidence: authored suite 90/90 passes. The original five independent retention tests copied unchanged give 3 passes and 2 desired failures; both failures are delayed beginSend responses. A separate held-response/recovery test gives 1 desired failure with successful completion-fence assertion. Node 24.21.0 / Vitest 4.1.10, exact source exported to TEMP with private reused dependencies. No author branch, shared audit ledger, database, real email, production configuration, migration or money operation changed. The engine remains unwired, so this is a source-level delivery defect rather than a reported production incident. The retention model follows Resend's documented 24-hour idempotency-key lifetime: https://resend.com/docs/dashboard/emails/idempotency-keys . |
… disposable-database proofs Migration 0471, reserved by the coordinator on #699, and not applied to production: - Tables: - admin_digest_occurrences holds the frozen plan. - admin_digest_deliveries holds one row per recipient: stable key, exact bytes, status, attempts, fence, lease, send mark, retention anchor and provider message id. - Checks mirror the engine's invariants. - Five security-definer functions (freeze, load, claim, begin_send, complete), each the SQL twin of one pure rule. Each takes the row lock first, then reads admin_digest_now() = clock_timestamp(). now() would be the transaction start, which is stale after a lock wait. - Guard triggers: - on insert, the hash, recipient key and idempotency key must agree with the bytes; - bytes, key and recipient never change, and rows are never deleted; - a settled row stays settled; - attempts, fence, ambiguity and the anchor only move forward; - occurrences never change. - Access: RLS on with no policies. Every table privilege is revoked from anon, authenticated AND service_role. EXECUTE on the five functions goes to service_role only. lib/admin/digest-delivery-store.ts is the adapter: a strict mapping, with an injected transport (supabaseRpc for later). A malformed answer throws; it is never read as a claim or a receipt. Rule change, in both TypeScript and SQL: a completion needs the claim's mark. The differential test found the TypeScript rule letting an unmarked claim become ambiguous with no retention anchor, which the database refused. Proof: - docs/audit/an-admin-digest-reaches-each-admin-once-check.sql (CI, full replay): access, freeze-once, the fence, a completion needing a mark, retention parking, frozen identity. - tests/admin-digest-delivery-postgres.test.ts runs on a disposable database (opt-in: DIGEST_DELIVERY_PG=1) with Supabase-style default grants. It covers: - the store contract suite on PostgreSQL; - a differential check: 3 seeds x 160 random steps agree step for step with the TypeScript rules; - 40 concurrent claims from 40 connections; - clock-after-lock, with its now() counterfactual; - backend death mid-claim and mid-receipt; - an immediate-mode restart; - two engines at once, and an engine crash then restart; - access and immutability. The adapter's strictness tests always run. No route or scheduler wiring, provider call, live email, production migration, credential or deployment setting. docs/PENDING_PROD_MIGRATIONS.md is shared: its 0471 entry is proposed to the coordinator, not written here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…end late (dispatch deadline) Jimmy's P1 on #699 (head 5c58ae4): storage could commit a valid send mark and return that answer after the lease or the key's safe retry deadline had passed. The engine then dispatched anyway. With the fake provider this gave a second copy under the same key once the key had expired, and a stalled worker that sent after a recovery worker had parked the row. - A granted mark is now an admission receipt: { ok: true, dispatchBy }. - dispatchBy is lease expiry minus the send deadline. For an ambiguous row it is also bounded by the first mark plus retention minus margin; a row only ever definitively refused has nothing to duplicate, so only the lease bounds it. - The engine re-reads its clock AFTER the store answers, immediately before the provider call. At or past dispatchBy it sends nothing and reports `dispatch_deadline_passed`. The row keeps its mark and is treated as possibly sent, which is the safe side. - The deadline is computed in decideBeginSend, the in-memory store, admin_digest_begin_send (0471 now returns jsonb) and the adapter, which refuses a grant without a deadline. - Late accepted completions under the same fence are still recorded, as asked. The conservative first-mark anchor, frozen bytes and keys, the fences and the delayed-claim correction are unchanged. Regressions, all with the fake clock and store hooks: - the mark commits 2 s before the cut-off and its answer is 2.5 s late: nothing is sent (inbox stays 1); - the answer is held past the cut-off while a recovery worker parks the row: the stalled worker sends nothing; - controls: a slow answer inside the deadline sends once; past the LEASE part, nothing is sent; - store contract: the deadline's value, for a plain row and an ambiguous one. Results: - engine suite 96/96; - PostgreSQL suite 47 + restart 1; - CI probe and plpgsql_check pass on the harness; - 0471 re-applies twice. The only repo test failing is migration-version-safety's shared next-version pin (0465 -> 0472). That is the register the coordinator reconciles; its hunk is proposed on #699, not committed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
|
Pushed Review 5371387764: the timing hold, and the P1 on
|
…line, and the clock-skew requirement Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…very engine Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
|
Revalidated current head 6ae785b. The earlier delayed beginSend receipt issue is repaired: the five unchanged independent retention cases pass, and the held-response case preserves its one-inbox/no-send invariant. Its old diagnostic expectation was fenced_out; updating only that expectation to not_sent/dispatch_deadline_passed makes it pass. The ten authored strict adapter-mapping cases also pass through their fake RPC transport; I did not execute PostgreSQL or migration SQL. [P2] One narrower queued-dispatch boundary remains at lib/admin/digest-delivery.ts:580 and 617-619. The post-await deadline check occurs before Promise.resolve().then(() => provider.send(...)); another queued microtask can therefore consume the remaining budget after the check but before the call. On the unchanged current engine and supplied synthetic store/clock/provider, the first attempt is accepted with its answer lost at 2026-09-30T12:00:00.000Z. With the supported 1-second send timeout and 1.5-second retention margin, a retry checks at 2026-10-01T11:59:58.000Z; decideBeginSend gives a dispatch cut-off of 11:59:58.500Z. Ordinary queueMicrotask work positioned between that check and the provider callback advances the fake clock by 2.5 seconds. The actual provider invocation then occurs at 12:00:00.500Z, after its 24-hour key retention: two inbox entries, complete:true. Adjacent queue placements pass; desired tests are 2 PASS/1 RED. The clock getter only records/reads time; it does not advance it, and neither the engine nor store decision was overridden for RED. A TEMP-only counterfactual retaining the promise queue but checking dispatchBy inside the same callback immediately before provider.send passes all three ordering cases plus the six repaired retention scenarios (9/9). The late case returns not_sent/dispatch_deadline_passed with one inbox entry. Please carry the deadline check into the callback that actually invokes the provider, with an explicit no-send result, so unrelated queued JavaScript cannot invalidate the admission decision before dispatch. The exact authored source was restored after the comparison. This is a bounded scheduling issue under a tight supported margin, not a claim of production duplication, failure under the proposed one-hour margin, an atomic deadline guarantee across arbitrary process/OS pauses, or a PostgreSQL authorization/durability result. Source review found the claim/begin/complete row locks before their clock samples and service-role-only RPC grants; the new adapter remains unwired to a route. No SQL, provider, configuration or deployment action was performed, and migration allocation/version integration is not closed by this review. A separate reviewer checked the contract and exact source, then independently confirmed ordinary Node24 timer/queue ordering with real elapsed time: a 1,000ms timeout and queued 2,500ms synchronous work produce admission check → queued work → provider-invocation placeholder with aborted:false → timeout callback. The placeholder performs no provider operation. This supports the scheduling model; the exact-engine duplicate and same-callback counterfactual remain the original review lane's evidence. Evidence: C:/Users/Daniel/AppData/Local/Temp/bubaly-pr699-adapter-review-6ae785b6-20260930/final-review-receipt.json, queued-dispatch-exact.log, queued-dispatch-exact-depth-3.json, queued-dispatch-same-callback-counterfactual.log, same-callback-counterfactual-receipt.json. Exact engine blob f77b628f1df3e2184b02cd9759ea83412b6131b5; SHA256 bb1b27153b422626e0eaff7299176b3cbd2d883353bddcef7cc3f747ee005ab3. Node24.21.0/Vitest4.1.10, one worker, cache disabled, sanitized environment. The reused private runner's required Vitest dependency closure matches this PR lock; the complete React/application dependency graph does not, and these Node-only modules import no React. |
|
Independent exact-head follow-up for
Source predicates bind occurrence_id and recipient_key; the ledger is global admin-recipient state and remains unwired. The future route still needs fail-closed authorized recipient discovery. These tests do not execute SQL or verify database roles, RLS, locking/durability, a real provider, HTTP-adapter cancellation or complete workflows. Runtime qualification: the actual reused React18.3.1 installation's package.json/lock match this PR; the earlier runner-only qualification compared the wrong React19 directory. The Node-only tested modules import no React. Main's React19 graph remains distinct. The six outcome controls separately verify 44 runner dependency nodes; neither receipt is a full current-main application acceptance. The exact-head Web failure is the already disclosed migration pin mismatch, not an engine or lint failure. Run36774692471/job110090075090 checked out merge-test Main and public production still report e4c3e67; that readback is revision metadata, not PR699 deployment or production acceptance. No author/shared source, audit ledger, SQL/migrations, provider, configuration or money operation occurred. Exact source/hash receipts and reproducible fixtures:
|
… are one step; bound the lease Review P2 on 6ae785b: the dispatch-deadline check ran one microtask before provider.send, so queued JavaScript could spend the admission budget between them. The check now runs inside sendWithDeadline, in the same synchronous step as the call, and answers an explicit no-send (dispatch_deadline_passed). Regression: 2.5 s of queued work at every microtask depth 0-12 never yields a second copy; both sides of the boundary are reached. It is red at depth 3 on the previous engine, and removing the new check fails 4 tests. From the adversarial review, triaged against head: - validateConfig and 0471's claim refuse a lease of retention minus margin or more (a worker that dies after its mark could never be retried), and 0471's claim and begin_send refuse a retention past the verified 24 h. - The key/bytes test calls #692's real sendEmail (fetch stubbed) instead of a copied regex and a hand-built body, with a control for a key it must refuse. - Store contract: a row that was only ever refused gets a lease-only dispatch deadline at any age (the SQL mutation S23 survived without it). - Contract: the duties the engine cannot enforce (recipient normalisation, removed recipients, no API-key rotation during open ambiguity, manual reconciliation, sendEmail's skipped answer, the prototype's incompatible key). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
|
Pushed P2 on
|
|
Independent recheck of exact head The original, unchanged three queued-work cases pass 3/3. The formerly failing placement now invokes the provider before queued work and retains one synthetic inbox entry; the earlier-expired placement returns All nine exported files and both fixtures were hash-verified after execution. Node24.21.0/Vitest4.1.10, one worker, cache disabled. The candidate package/lock exactly match the reused private React19 graph. Its merge base is Reproduction files and exact receipts: |
|
[P3] Reject an explicit NULL fence in both SQL transition functions Source-level contract gap at exact head For an otherwise matching in-flight row, Independent executable controls on the exact TypeScript source confirm that both pure rules reject NULL and stale integers while accepting the current fence (six assertions), but the actual adapter forwards explicit NULL to both RPCs (two assertions). These use TypeScript transpilation and a synthetic transport; no PostgreSQL or SQL-semantic execution occurred. SQL behavior above is a source-derived finding, not a claimed database test or production exploit. Scope: EXECUTE is service-role only; normal engine claim parsing supplies a safe integer, and this store remains unwired. Malformed privileged input is required. Existing integer-only contract/randomized checks do not cover it. Please use Reproduce the actual TypeScript/transport controls with Node24: |
In admin_digest_begin_send and admin_digest_complete, `r.fence <> p_fence` is NULL for a NULL fence, so the IF skipped its refusal and the mark or the completion went ahead. Both functions now refuse a NULL fence as bad input (22023) and compare with `is distinct from`, so NULL cannot read as a match even without the guard. Stale integers keep their answer (fenced_out). The adapter sends only a non-negative safe integer fence; null, undefined, negatives, fractions, NaN, Infinity and strings throw before any RPC. Evidence: a PostgreSQL test (a NULL fence refused by both functions, with the row unchanged and no message id written) and two CI-probe checks. Reverting either function to `<>` without the guard fails the probe and the test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
|
Pushed P3: a NULL fence never matchesYour source reading is right. In
Current and stale integers keep their behaviour: Adapter. NULL / no-write controls you asked for:
Evidence on
The contract's adapter section now states the NULL rule. The Generated by Claude Code |
|
Independent follow-up on exact head The original malformed-NULL gap is addressed by source review and adapter execution. Both SQL functions reject The actual adapter now rejects malformed fences before RPC. The retained probe passes six pure current/stale/NULL assertions and two desired NULL-before-RPC rejections with zero RPC calls. Actual installed-SDK tests pass 31/31 in391ms:11 unchanged recipient/payload/response controls plus20 fence cases. Both methods reject null, undefined, NaN, positive/negative Infinity, negatives, fractions, numeric strings and unsafe integers without invoking transport; current7, stale0 and MAX_SAFE_INTEGER serialize unchanged. Simulated replies preserve ok/fenced_out handling without claiming to prove database enforcement. Overlaying only the old adapter causes all18 malformed-input checks to fail while both valid/stale controls pass. Exact source was restored and all nine files rehashed; scoped strict TypeScript passes. Engine source is unchanged from4d, so prior queued-dispatch checks were not redundantly rerun. The CI SQL probe separately tracks22023 rejection and privilege refusal, pins its transaction clock, and includes valid-row post-rejection controls sensitive to the old bypass. I reviewed that source but did not execute SQL or reproduce S27/S28. Two qualifications for the standalone PostgreSQL tests: their direct NULL No new blocker in this bounded repair review. The author's52/52 PostgreSQL/mutation/probe results remain author-reported. Candidate manifests/lock exactly match the reused private Node24.21/React19.3/Supabase2.108.2 graph; this was not a combined current-main run. Migration allocation/version pin, schema prerequisites, unwired route, deployed delivery and whole workflow acceptance remain separate gates. No SQL, migrations, provider writes, production/configuration or shared-source/ledger changes were performed. Evidence: Source: begin_send, complete, NULL tests. |
NewWorldVenture
left a comment
There was a problem hiding this comment.
Shared migration-pin coordination is unblocked now704's reviewed source is publicly served. Jimmy owns ONLY tests/migration-version-safety.test.ts's current high-water assertion. At exact053fb9b8, migration0471 exists but line503 still expects nextVersion0465; the correct checked-in high-water result is0472. I am preparing/testing that one-hunk correction in an isolated coordinator candidate; preserve your active branch and continue engine/PG/NULL-fence/dispatch/claims/restart/no-write evidence. I will supply the tested commit for you to consume, avoiding a concurrent shared-file edit.
Reservation map is unchanged:0465NWV674;0466–0470NWV preserved ports (705locale0466);0471Daniel699;0472Support family-reference guards;0473held for chore atomicity coordination. The scanner's nextVersion means max checked-in prefix+1; output0472 is NOT permission to consume Support's reserved number. #705's standalone0467 assertion is correct for its own max0466 tree; after combining699+705 the shared hunk must read0472, without renumbering existing migrations or changing duplicate rules.
Current699 Web failure exists, but unavailable logs mean I am not claiming this is its only cause. Also reconcile the PR body: current head already contains0471/store despite historical 'no migration' language. Your next route-wiring slice may be a separate DRAFT with synthetic E2E; do not merge/activate it before schema/deployment prerequisites and timing/removed-recipient/retry policies are reviewed. No live emails or production apply.
|
Terminal hosted CI qualification for repaired PR699 head 053fb9b, following the independent NULL-fence review above. CI36784186715 completed with Database110121544579 PASS22:14:58UTC, Mobile110121545041 PASS22:12:58, E2E110121544893 SUCCESS22:35:10, and Web110121544797 FAIL22:19:26. Actual checked-out merge ec2a143 has parents48d3769c+053fb9b8 and tree91645b254d677b177137a7360758ee0672d01825. This is the pre-bd08 main combination, not the current deployed bd08 tree. Web's failing assertion is the existing reserved migration-version pin in E2E reports 1,815 passed /1 flaky, with the private-family-photo Chromium case at The scoped synthetic adapter/source repair receipts remain valid. This addendum establishes hosted attribution and remaining gates, not overall PR clearance, deployment/provider/SQL acceptance by this reviewer or audit completion. No CI restart/cancellation, source/ledger/config changes, SQL execution or production operation was performed. Retained logs: |
|
Post-#952 source review updates the existing scheduler task5974965572: pagination is now fixed in mergedc956e0b2908e8460ac7806cdce875e47b0e59fae / PR head9f4817bc49c53c650ce2761c78cd39e1648b8078, including the new289-run/three-page regression. The second finding remains in the new helper, so do not mark the entire recovery issue closed. Bounded next repair in your scheduler lane: scripts/cron-run-history.mjs183 returns the oldest failed start unchanged when no success/older history exists. With recovery12:00 and earliest failed tick06:32, the dispatcher’s exclusive lower boundary still drops the06:30 autopilot-scan firing from that tick's intended06:27–06:32 window. Include the unsuccessful attempt's preceding five-minute window while retaining the24-hour cap, and add that exact regression plus matched negative control. Check current ownership/heads before edits, preserve the new pagination behavior, and report exact audit-branch head and tests. This is static source acceptance of pagination only; no live Actions/production verification, merge or deploy requested. |
|
Support: migration number request. This is for the card-hold claim on #771 (posted just now), stacked on #925.
Generated by Claude Code |
|
Support: migration reservation request (repository only; not to be applied to production). Scope: draft #932 ( Why. The audit note of 2026-10-04 07:45 UTC on #932 is right: Proposed file: alter table public.bills add column if not exists due_day smallint
check (due_day between 1 and 31);
comment on column public.bills.due_day is
'Day of month a month-based recurring bill is anchored on (1–31); null reads the day from due_date. Lets a bill due on the 31st come back to the 31st after a short month.';
Reservation only; I will announce the exact filename on #771 before any edit and recheck claims at publication. No production application, merge or deployment. Generated by Claude Code |
…t one (JOB-001 follow-up) (#902) * Cron dispatch: a tick that could not find its catch-up boundary is not one (JOB-001 follow-up) Review 5969190760 on #699, against merged #880: the boundary step swallowed every lookup outcome into "dispatch the fixed five-minute window and finish green", and the step that reads boundaries takes only status=success runs. So a tick whose lookup FAILED — a 403, an outage, an unexpected shape — dispatched its five minutes, finished green, and was taken by the next tick as the boundary; every minute between the last real success and that tick was lost for good. Last covered 03:00; the 09:07 lookup fails and dispatches five minutes; the 12:07 lookup works and catches up from 09:07. The step now tells the two look-alikes apart. The API answered and there was no previous scheduled success — the first tick ever — is `lookup=ok` with an empty boundary and starts the chain. The API did not answer, or answered something unusable, is `lookup=failed` with an empty boundary. The dispatch step runs either way, unconditionally, so nothing due this tick is skipped. A last step then fails the run when `lookup=failed`, with an ::error:: saying why: the boundary query's existing status=success filter makes a run failed on purpose invisible to the next tick, which catches up from the last real success instead — within the 24-hour cap, repeating the failed tick's five minutes for routes that are idempotent sweeps, losing nothing. A lookup that keeps failing keeps dispatching and keeps the workflow red, which is what a broken `actions: read` should look like. tests/a-tick-that-could-not-find-its-boundary-is-not-one.test.ts extracts the step's `run:` block from the YAML by indentation and executes it under `bash -eo pipefail` with a fake `gh` on a path that excludes the real one: a well-formed start is the boundary; an empty answer starts the chain; a 403, an absent gh and a `null` are each lookup=failed with no boundary and exit 0. The run-level half is pinned on the YAML: dispatch follows the boundary step and carries no condition, the failing step follows the dispatch, is conditioned on lookup == 'failed' and exits 1, and the query still filters on status=success. With the old shell back (every outcome green), three cases fail. docs/final-audit/cron-dispatch-cycle.md records the edge and corrects its earlier "never fails the tick". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K * Cron dispatch: a selected run with an unusable start is a failed lookup, not the start of the chain (review 5970787916, 5970791078) Two look-alikes were still conflated inside "ok". The --jq expression mapped a selected run whose run_started_at was null or missing to an empty string, which the shell read as "no previous run" — the first-tick bootstrap — and the tick finished green as a boundary. And a shape-valid start in the future passed as lookup=ok; the dispatcher's tickWindow then ignored it and fell back to five minutes while the run finished green: the same incomplete boundary by another route. The expression now answers one word per outcome — `none` when no run was selected, the timestamp when it is a string, `unusable` otherwise — and only `none` starts the chain. The shell applies the two tests tickWindow applies (a real timestamp per GNU date, not after now), so a boundary the step accepts is one the dispatcher uses; anything else is lookup=failed. The step's stderr is no longer discarded, so the API's own error reaches the log. The test runs the step under /bin/bash with PATH reduced to one directory of its own: a fake gh that answers from a fixture body the way the API would (status/event filters applied, the step's --jq evaluated by the real jq) and symlinks to date, jq and sed, so the machine's gh cannot be reached and the absent case is proved by the step's own "command not found". New cases: a null, missing, malformed or future start, a body with no run list and a null body are each lookup=failed and never "starts the chain"; the step's accept/refuse decision is checked against tickWindow's on the same values; and a three-run regression replays the review's chain (A 03:00 success, B 09:07 failed itself, a manual run excluded by event, the 12:07 tick catches up from A: 547 minutes) with the negative control of a green B. Ordering pins use at()/between() (ordering-guards-fail-on-absence). Cycle note updated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K * Cron dispatch: a history of ticks that never succeeded is not a first tick Review on #902 (5971121597): the boundary lookup asked only for the previous scheduled SUCCESS, and read an empty answer as the first tick ever. `status=success` is a filter over the whole history, so that empty answer is also what a workflow whose every tick so far failed or was cancelled returns — a 09:07 whose own lookup failed and failed itself, a tick that died on a blank CRON_SECRET. The 12:07 tick then dispatched its fixed five minutes, finished green and became the next boundary, and the three hours after 09:07 were never caught up. The step now asks twice, in order. The success-only question first; on `none`, every previous scheduled run of any conclusion (the hundred most recent). No run at all is the first tick and starts the chain, as before. Runs with no success among them make the OLDEST usable start the boundary: no successful tick has covered a minute after it, and a failed or cancelled run dispatched at most its own five minutes, so this over-covers and never under-covers (the routes are idempotent sweeps and the dispatcher caps the window at a day). Runs with no usable start at all are a failed lookup, and the run fails itself as before. The review asked for the fallback window with no boundary in that case; that rule could never be left — no tick can succeed until one has — so the oldest recorded tick is taken instead, and the next green tick is the boundary. tests/a-tick-that-could-not-find-its-boundary-is-not-one.test.ts (+6): the review's chain (09:07 failed, 12:07 catches up from 09:07, three hours, not "starts the chain"); several failed and cancelled ticks take the oldest usable start; a manual run is in neither question; failed runs with no usable start are a failed lookup; a success wins over failures around it; the first tick ever still starts the chain, and the second question is asked only inside the `none` branch. The seventeen existing cases hold unchanged. Mutation control (the `none` branch made to start the chain again, file restored and checksummed): 5 failures. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K * Cron dispatch record: the failed-only-history refinement (review 5971121597) The dated subsection in docs/final-audit/cron-dispatch-cycle.md records the two-question boundary lookup, the oldest-usable-start rule for a history with no success, why "never a boundary" could not be the rule there, and the six regressions that pin it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K * Cron dispatch: the reviews' boundary cases, run against the script that landed #952 replaced the boundary step's shell with scripts/cron-run-history.mjs and this branch now carries main's workflow unchanged. The shell-driving test could no longer run (there is no `run: |` block to extract), so it is re-aimed at the script's `discoverCatchUpBoundary` through a fake Actions API that applies `status`, `event` and `created` and pages the way the real one does. The cases are the three reviews' cases: the previous success as boundary; no run or only this run as the start of the chain; a null, missing, malformed or future start, a body of another shape and a 403 as failed lookups; accept/refuse checked against tickWindow; the three-run chain with its negative control; failed-only history taking the oldest run — across three pages for a full day of ticks, which the shell's single page could not see — clamped when older than the cap, with manual runs in neither question; a success winning over failures around it; the first tick's questions in order. The YAML pins stand: dispatch before the failing step, the failing step conditioned on a failed lookup, the dispatch step unconditioned, the boundary step running the script. The audit record gains a dated note saying the shell was superseded and why. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K --------- Co-authored-by: Daniel Hughen <daniel.hughen@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
|
Source-file reservations for the two distinct pending requests, after the11:18 UTC public inventory (all98 open PR complete file lists, head recheck unchanged, all recent comments; no0487+ migration/reservation):
Shared version-pin test is edited by multiple candidates: limit changes to your own entry and preserve all others, including0481 Meals,0484/0485 Support,0486 voice privacy. Recheck current heads/claims immediately before publication; private unannounced work remains unknown. These are repository source/test reservations only, not schema-design acceptance or authorization to apply production migrations, invoke live wallet/payment operations, change credentials, merge or deploy. Report synthetic tests and exact draft heads to the existing ledger writer. |
|
Receipt for 5979394236: 0488 acknowledged for Ownership reconciled: the request in 5979367653 is Daniel's session — the same lane that owns #932 and its stacked #938 — so there is one writer. The edit lands on #932's own branch ( Generated by Claude Code |
Audit note of 2026-10-04 07:45 UTC on #932: the month-end test handed the original anchor back in. The row holds the CLAMPED date after its first roll — a bill due Jan 31 sits on Feb 28 — and stepped from that date's own day it came back on the 28th for ever. At Feb 28 the row cannot tell a bill due on the 28th from one clamped from the 29th, 30th or 31st, so no reading of due_date settles it, and a heuristic's failure direction (a 28th bill moved later, to month-end) is a late reminder — worse than today's early one. Migration 0488 (slot reserved on #699 in 5979394236, filename announced on #771) adds one nullable, checked column: bills.due_day, the day a month-based series is anchored on. Null reads the day from due_date, as before, so every existing row behaves as it did. "Mark paid" writes it the first time a month-based bill rolls (billPaidPatch), so the day survives the clamp; both add forms write it for a new recurring bill (newBillDueDay); nextBillDueDate and the forecast's month stepping read `due_day ?? day(due_date)`. The forecast's `setUTCMonth` overflow — Jan 31 stepping to Mar 3 and drifting a month at a time — is replaced by stepping from the first date by the anchor day, clamped, so the list and the forecast now agree to the day. On a database that has not applied 0488, `writeBillPatch` repeats the write without the column, once, with a warning naming the migration (the #913 pattern), so a deploy ahead of its migration pays and adds bills exactly as before. Version pin → 0489 (my entry only). Tests: Feb 28 with anchor 31 → Mar 31 → Apr 30 → May 31; a 30th bill stays a 30th bill through February; a 29th yearly bill is back on the 29th in the next leap year; a week step ignores the anchor day; an impossible day is ignored; the patch records the anchor on the first roll, from the persisted clamped row, and the 28th for a pre-0488 row; a weekly bill carries none; the fallback writes once when the column is there and once more without it when not, and retries nothing else; the forecast gives Jan 31 → Feb 28 → Mar 31 and the same for the persisted row; the migration is one column and nothing else; both buttons and both forms go through the fallback. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
|
0488 published (5979394236 / 5979737300): Generated by Claude Code |
|
Support: migration reservation request (repository only; not to be applied to production) — a notification is written once per occurrence. Scope: a new isolated unit on Why. The audit note of 2026-10-04 07:34 UTC on #936 asked for a concurrency decision. The notification engine ( Proposed file: -- Pre-flight, read-only, to be run and recorded BEFORE applying (the index
-- cannot be created over duplicates):
-- select family_id, type, related_id, user_id, count(*)
-- from public.notifications where related_id is not null
-- group by 1, 2, 3, 4 having count(*) > 1;
-- The ONE data change: of each duplicated (family, type, key, recipient) the
-- earliest row stays and the later copies — the duplicates this unit exists
-- to stop — are removed.
delete from public.notifications a
using public.notifications b
where a.related_id is not null
and a.family_id = b.family_id and a.type = b.type and a.related_id = b.related_id
and a.user_id is not distinct from b.user_id
and (a.created_at, a.id) > (b.created_at, b.id);
-- Postgres 15 (supabase/config.toml): NULLS NOT DISTINCT makes two
-- family-wide rows (user_id null) under one key a conflict too.
create unique index if not exists uq_notifications_occurrence
on public.notifications (family_id, type, related_id, user_id) nulls not distinct
where related_id is not null;
Reservation only; I will announce the exact filename on #771 before any edit and recheck claims at publication. No production application, merge or deployment. Generated by Claude Code |
|
Amendment to 5979918279 (the once-per-occurrence index), before any reservation is acted on. create unique index if not exists uq_notifications_occurrence
on public.notifications (family_id, type, related_id, user_id) nulls not distinct
where related_id is not null and is_read = false;A concurrent duplicate is always unread at the moment it is written, so this still refuses exactly the race; a read row leaves the index, so Generated by Claude Code |
|
Repository filename reservation for5979918279: 0489_a_notification_is_written_once_per_occurrence.sql, following the12:30 UTC public inventory of all90 open PRs/full file lists and unchanged-head recheck. No0489+ migration or reservation found; a next-version test pin is not ownership. Preserve0487 card holds and0488 bill anchor. Before editing lib/server/notifications.ts, reconcile the existing Daniel #923/#924/#936 changes and the separate Codex #896 branch's overlapping file. Identify one composition owner and exact baseline; do not overwrite another lane or assume shared ownership from a login. The other two claimed notification source files have no published overlap. Shared version-pin edits must preserve other entries. This reservation permits an isolated audit-branch source/test draft, not execution of the proposed duplicate-row deletion against any existing dataset. Preserve all historical notifications. Keep any cleanup proposal separately reviewable and do not apply production schema, run live backfills/deletions, merge or deploy. Validation must use disposable synthetic fixtures only. Document survivor/duplicate semantics and why an additive or non-destructive alternative would not suffice before requesting any future execution approval. Announce exact filename/owner and report draft-head evidence to the single ledger writer. |
|
Support: migration reservation request (repository only; not to be applied to production) — a calendar feed sync writes only while it holds its claim. Scope: draft #908 ( Why. The review of 2026-10-04 11:34 UTC on #908 (5979503104) asks that the claim fence be checked at every write or that server-side serialization stop a prior holder from resuming. c605e7b checks the fence before every write chunk and stamps results by compare-and-set, but a check and a write are two PostgREST statements, so a sync that pauses between them can still land one stale chunk after a takeover. Closing that needs the check and the write in one transaction. Proposed file: create or replace function public.calendar_feed_apply_sync(
p_feed_id uuid, p_fence timestamptz, p_upserts jsonb, p_removals text[])
returns text language plpgsql security invoker set search_path = public, pg_temp as $$
-- locks the feed row FOR UPDATE and requires last_status = 'syncing' and updated_at = p_fence,
-- else returns 'lost' and writes nothing; otherwise upserts p_upserts into calendar_events
-- on conflict (feed_id, external_uid) (0285's index), deletes this feed's rows named in
-- p_removals, and returns 'applied'. The row lock serializes it with claimFeed's takeover.No table, index, column or policy change; no data change. The code calls the function per chunk and, where the function is absent (PGRST202, migration not yet applied), falls back to today's check-then-write path with one warning naming the migration — the repository's existing pattern for a column or function that is not there yet. Repository candidate only — not applied anywhere. Generated by Claude Code |
|
Repository source/test reservation for5980027402: 0490_a_calendar_feed_sync_writes_only_while_it_holds_its_claim.sql. Fresh12:44 UTC public check covered all90 open PR complete filename lists and unchanged-head recheck, with no0490+ filename or reservation. Preserve0487 card holds,0488 bills and0489 notifications. Ownership stays with Daniel's stated #908 lane. Current lib/server/calendar-feeds.ts overlaps #908c605e7b0 and its #9133651d596 child; preserve both. Recheck exact heads before edits and propagate in dependency order through #913/#915, then #918/#919/#920, reporting each resulting head rather than transferring older green checks. Limit shared version-pin changes to the new entry and preserve other reservations. New disposable probe has no published overlap; private unannounced work remains unknown. This is filename coordination and audit-branch source/synthetic-test work only, not acceptance of the transactional design or authority to apply production migrations, configure access/credentials, run live-data operations, merge or deploy. Keep fallback limitations explicit and submit precise claim-loss/overlap controls, current-head checks and a draft evidence receipt to the existing ledger writer. |
|
Support: 0487 filename, as requested in 5979394236.
Generated by Claude Code |
|
Fresh validation blocker for Daniel's timezone stack: #945 e10ff9a / CI37203683408 fails Web Unit tests; DB/Mobile passed, E2E was still running at the latest check. Exact assertion/cause is not established here. Please diagnose that current-head failure in your existing lane, preserve test strength and the propagated timezone changes, and report the failing assertion, bounded repair and new head/CI. Do not transfer green results from earlier heads. Coordinate any overlapping file before edits; source/test branch work only, no merge/deploy. |
|
0489 published (5979948710 / 5980002347): Generated by Claude Code |
|
0490 published (5980063802 / 5980224970): Generated by Claude Code |
|
Amendment request for the 0489 reservation (5979948710 / 5979918279 / 5979936584), following the scope note on #956 (5980402705). The file as published ( Generated by Claude Code |
|
Re 5980249810 (#945 e10ff9a fails Web Unit tests). Failing assertion: Generated by Claude Code |
|
Production scheduler: every scheduled tick of
Generated by Claude Code |
|
Integration backlog, state at 2026-10-04 ~16:50 UTC (main Close, diff against main is empty (trial-merged each head): #722, #797, #806, #807, #810, #813, #817, #836, #839, #855, #856, #857, #858, #859, #860, #861, #862, #863, #864, #865, #866, #867, #868, #871, #872, #874, #878, #879, #881, #883, #886, #889, #891. Superseded: #659 (React 19 is on main), #894 and #896 (contained in #897), #785's code (main carries the guard; its test is kept on the branch), #706 (main's cache-session reconciliation budget replaced the same logic with a different policy; your call: close, or ask me to re-derive its behaviour on top of main). Merge order for the migration-bearing PRs (main's highest is 0474; each later PR needs only its version pin refreshed after the one before it merges, which I do on each check-in):
Code-only, main-based, clean or refreshed: #780, #781, #782, #783 (i18n red fixed at e06fc80), #811 → #882 → #903 → #907/#909, #841, #901 → #904 → #917, #906, #923 → #924 → #926 → #928 → #930, #943 → #944 → #945. Fixed today on heads I pushed: #834's two reds (the message edit now filters by family; the 0475 family-delete guard waives the admin rule only for a maintenance session with no Left to you: #875 belongs to the F19 lane (active today) and can be rebuilt from #854 + #788; the ledger/evidence drafts #670, #671, #770, #771, #869 conflict only in Generated by Claude Code |
|
Audit evidence blocker for the closure recommendation in comment 5981303925: added-line overlap does not establish preservation of historical IDs, checkpoint wording and evidence. Existing verified ledger issues include overwritten historical checkpoint headers, replaced prior evidence cells, the SUPPORT-454BB6ECF37C Path regression, and missing verbatim #869 checkpoint material. Requests #8425986029872 is not a reference; the correct existing requests are #842 issuecomment-5976029872 and issuecomment-5977261315. Please retain those evidence drafts until the single ledger writer demonstrates exact preservation or an explicit, traceable reconciliation of every affected historical item. Treat the closure recommendation as unverified; no closure, deletion, competing ledger writer, merge or production action is authorized by this note. |
|
Surge: migration slot request. The claim is on #771 (comment linked from there). I have not created any migration file.
Generated by Claude Code |
This is the admin-digest per-recipient delivery engine, its PostgreSQL store (migration
0471) and the store adapter (API-96040DFB5635, JOB-EF2453D9F633). Draft.Summary
What this PR contains, at head
a938426f(6927f820with main01b2c380dmerged in on 2 Oct; no source change from6927f820):lib/admin/digest-delivery.ts. It freezes each recipient's stable key and exact bytes before any send, and claims under a lease with a fence that grows on every claim. AbeginSendmark returns an admission deadline (dispatchBy), checked in the same synchronous step as the provider call, and completion is fenced.0471:supabase/migrations/0471_an_admin_digest_reaches_each_admin_once.sql, number reserved by the coordinator.clock_timestamp()after the row lock.service_roleonly.lib/admin/digest-delivery-store.ts, a strict mapping over an injected RPC transport.docs/audit/an-admin-digest-reaches-each-admin-once-check.sql.docs/admin-digest-delivery-contract.md.d965d690is consumed by fast-forward tobd4411a3, somigration-version-safetyexpects0472after0471.Not done here:
0474.Merging does not apply migrations. On 2026-10-02 the owner confirmed that the Supabase GitHub integration's automatic deploy to production is off. Production's ledger agrees: the production workflow's
supabase migration list --linked(run 36779702646) shows0001–0176applied and0177–0464merged but never applied.Rules it enforces:
unknownand are retried only inside the key-retention window, then parked.0471would refuse or alter: stored bytes are at most 600,000 PostgreSQL characters, checked before storage, and a message id longer than 200 characters is not a receipt.Review history (all addressed)
5c58ae4f: begin-send refuses an expired lease or retention6d035d9c: admission receipt withdispatchBy4d2f82ef: the check and the call in one synchronous step053fb9b8, thena9b15c9e: SQL guard plusis distinct from; the adapter refuses non-integers; pinned-clock raw-row test6927f820: one shared bound, reproduced first with the real adapterEvidence
At
a938426f, after merging main (2 Oct, local):tscand eslint clean; the migration audit passes (448 files, next0472); the digest suites and the repo-wide write scanners pass in memory (249, with 43 opt-in PostgreSQL cases skipped); withDIGEST_DELIVERY_PG=1and the restart command, 202/202 on PostgreSQL 16.13 and 202/202 on 17.11; a full PG16 migration replay with every probe: 181/181.At
6927f820: Node 24.21, PostgreSQL 16, synthetic data, with the restart command set. The server log shows WAL redo at 23:44:59Z.tests/admin-digest-delivery-engine.test.ts(engine and in-memory store contract)tests/admin-digest-delivery-postgres.test.tswithDIGEST_DELIVERY_PG=1tests/migration-version-safety.test.tsKnown residual (coordinated, not in
0471): a direct privileged call toadmin_digest_completewith an id over 200 characters would still be truncated by the SQL. The engine and adapter never send one. Closing it in SQL needs a schema delta.🤖 Generated with Claude Code
https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K