Skip to content

feat(admin-digest): per-recipient delivery engine with fenced claims, frozen payloads and retention-bounded retries (not wired) - #699

Merged
NewWorldVenture merged 16 commits into
mainfrom
claude/admin-digest-delivery-engine
Oct 2, 2026
Merged

NewWorldVenture merged 16 commits into
mainfrom
claude/admin-digest-delivery-engine

Conversation

@NewWorldVenture

@NewWorldVenture NewWorldVenture commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

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 (6927f820 with main 01b2c380d merged in on 2 Oct; no source change from 6927f820):

  • Engine: 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. A beginSend mark returns an admission deadline (dispatchBy), checked in the same synchronous step as the provider call, and completion is fenced.
  • Migration 0471: supabase/migrations/0471_an_admin_digest_reaches_each_admin_once.sql, number reserved by the coordinator.
    • Two service-role tables, with guard triggers that make bytes, keys and recipients immutable.
    • Five security-definer functions that read clock_timestamp() after the row lock.
    • RLS on with no policies; EXECUTE granted to service_role only.
    • Not applied to any production database (PROD-DB-0177).
  • Adapter: lib/admin/digest-delivery-store.ts, a strict mapping over an injected RPC transport.
  • CI probe: docs/audit/an-admin-digest-reaches-each-admin-once-check.sql.
  • Integration contract: docs/admin-digest-delivery-contract.md.
  • Shared register: Jimmy's pin-only commit d965d690 is consumed by fast-forward to bd4411a3, so migration-version-safety expects 0472 after 0471.

Not done here:

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) shows 0001–0176 applied and 0177–0464 merged but never applied.

Rules it enforces:

  • A confirmed receipt (a 2xx with a storable message id) is never re-sent.
  • Every retry uses the same key and bytes.
  • Ambiguous outcomes stay unknown and are retried only inside the key-retention window, then parked.
  • A payload conflict is parked and never re-keyed.
  • Stale, expired or NULL fences write nothing.
  • The engine admits nothing 0471 would 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)

Review Fix
Jimmy's claim-to-dispatch gap 5c58ae4f: begin-send refuses an expired lease or retention
P1: a delayed begin-send answer 6d035d9c: admission receipt with dispatchBy
P2: a queued microtask before the call 4d2f82ef: the check and the call in one synchronous step
P3: NULL fence 053fb9b8, then a9b15c9e: SQL guard plus is distinct from; the adapter refuses non-integers; pinned-clock raw-row test
Review 5373043643: payload and message-id parity 6927f820: one shared bound, reproduced first with the real adapter

Evidence

At a938426f, after merging main (2 Oct, local): tsc and eslint clean; the migration audit passes (448 files, next 0472); the digest suites and the repo-wide write scanners pass in memory (249, with 43 opt-in PostgreSQL cases skipped); with DIGEST_DELIVERY_PG=1 and 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.

Suite Result
tests/admin-digest-delivery-engine.test.ts (engine and in-memory store contract) 104/104
tests/admin-digest-delivery-postgres.test.ts with DIGEST_DELIVERY_PG=1 56/56: store contract on PostgreSQL; TypeScript-vs-SQL differential; 40 concurrent claims; clock after lock; backend death mid-claim and mid-receipt; an immediate (crash) restart; engine crash and resume; access and immutability; NULL fence; storage bounds at and over the limit
tests/migration-version-safety.test.ts 4/4
CI probe on the harness passes
Mutations 28/28 SQL safeguards and 5/5 parity safeguards caught when removed; 37/37 earlier engine safeguards

Known residual (coordinated, not in 0471): a direct privileged call to admin_digest_complete with 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

…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
@vercel

vercel Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
bubaly Ignored Ignored Oct 2, 2026 10:50am UTC

Request Review

Copy link
Copy Markdown
Owner Author

@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:

  1. A reserved migration number. Main ends at 0464. 0465 and NWV's locale work must be coordinated first, so I have not assumed any number; the earlier scheduler prototype noted 0465–0470 as possibly held by another session. One number is enough (tables, trigger, RLS and functions in one file).
  2. Agreement on the proposed schema. It is in docs/admin-digest-delivery-contract.md §5, on this branch:
    • Tables: admin_digest_occurrences (the frozen plan per occurrence) and admin_digest_deliveries (one row per recipient: stable key, exact payload bytes, status, attempts, fence, lease, send mark, retention anchor, provider message id).
    • Protection: checks and an immutability trigger; RLS on with no policies; grants revoked from anon and authenticated.
    • Functions: admin_digest_freeze, admin_digest_claim, admin_digest_begin_send and admin_digest_complete. Each mirrors one pure rule in lib/admin/digest-delivery.ts and uses the database clock.
    • Requirements: the exact adapter requirements are in §3 of the same document.
  3. Sequencing. None of this can reach production before the PROD-DB-0177 migration-ledger remedy. No app_settings shortcut will be used.

Not done, and not to be done until you reply:

  • no file under supabase/migrations/;
  • no route wiring;
  • no production configuration;
  • no finalaudit.md edit.

Engine work and its tests continue on this PR in the meantime.


Generated by Claude Code

@NewWorldVenture NewWorldVenture left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Owner Author

Thanks, Jimmy. Reservation noted: 0471 is exclusively for this delivery-store migration. 0465 is NWV #674, 0466–0470 are NWV's originals, 0472 is Support #701 and 0473 is held. No other number will be used, and I'll report any collision I find before editing anything.

The claim-to-dispatch gap you named is already closed in 5c58ae4f, pushed before your review. The adversarial review of the first slice found it independently.

decideBeginSend now refuses at the last moment before the provider call, even when the fence has not changed:

  • lease_expired: the lease has no more than the send deadline left.
  • retention_passed: an ambiguous row reached its cut-off after it was claimed. The row is parked as needs_reconciliation.
  • fenced_out: another claim superseded this one.

In each case nothing is sent. The store interface now passes the policy to beginSend.

Deterministic regressions for the gap (fake clock and deferred store hooks, no timers):

  • a worker stalled between claim and mark while another took over: fenced out, sends nothing;
  • stalled with no takeover and its lease gone: lease_expired, sends nothing, and the next run sends once;
  • an ambiguous row claimed one second inside the window and marked after the cut-off: retention_passed, parked, nothing sent;
  • store contract: beginSend is refused with too little lease left, and parks an ambiguous row at the cut-off.

Other fixes in the same commit, all reproduced first:

  • A retryable refusal after an ambiguous attempt stays unknown, so retention still bounds it.
  • The completion rule refuses an accepted without a message id.
  • An adapter's "rejected" must be a 4xx.
  • receipt_write_unconfirmed replaces the old name, and accepted message ids are reported.
  • planMismatch.windowChanged is added.
  • A retention longer than the verified 24 h is refused.

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:

  1. leaseMs: the engine only requires leaseMs > sendTimeoutMs. The proposed default is 5 minutes against a 15-second send deadline.
  2. retentionSafetyMarginMs: proposed at 1 hour. It must exceed the send deadline plus clock skew.
  3. maxAttempts: proposed at 5, counting claims, so a crash before the mark uses an attempt.
  4. The retention anchor is the first mark of any attempt. That is conservative: it stops early rather than late while Resend's behaviour on refused requests is undocumented.

Next slice, as you scoped it:

  • the 0471 migration;
  • a PostgreSQL adapter that mirrors the three rules, re-sampling clock_timestamp() after the row lock through one pinnable clock function;
  • disposable local PostgreSQL tests that run the same store contract suite plus restart, concurrency, crash, conflict, retention and role-boundary refusals.

Registry and PENDING_PROD_MIGRATIONS changes will be proposed as hunks in the checkpoint for you to reconcile, not written into the shared files. No route or scheduler wiring, deployment setting, production migration or apply, credential operation, provider call or live email.


Generated by Claude Code

@NewWorldVenture NewWorldVenture left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Owner Author

Mutation re-run on 5c58ae4f: 37/37 safeguards killed. The baseline has 0 of 90 tests failing.

Each safeguard was removed on its own, in a separate worktree, and the test file was re-run. The new ones:

ID Safeguard removed Tests that fail (of 90)
M27 beginSend refuses when too little lease is left 2
M28 beginSend re-checks the retention cut-off 2
M29 a retryable refusal after ambiguity stays unknown 2
M30 an adapter "rejected" must be a 4xx 1
M31 accepted message ids are reported 2
M32 a changed window is reported 1
M33 the claim-side attempt bound, alone 2
M34 the completion-side attempt bound, alone 1
M35 retention is capped at the verified 24 h 1
M36 the completion rule refuses an accepted without an id (store layer alone) 1
M37 test store: beginSend persists its mark 14

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 0471 slice.


Generated by Claude Code

@NewWorldVenture

Copy link
Copy Markdown
Owner Author

[P1] A delayed successful beginSend response still permits a provider send after the retention window. Independently reviewed exact head 5c58ae4fc8c75d51b7fe514821b19708bf158376, helper blob fd3b30ab49b6caa5125a01cc594af92bb1c80b2a.

The new storage admission checks repair both previously reproduced delayed-claim cases. The dispatch at lib/admin/digest-delivery.ts:559 still follows the awaited beginSend at lines 546–550 without checking whether its successful receipt remains timely. Storage can commit a valid mark, then return that receipt after the key's safe retry deadline or lease has expired. The send timeout starts after this storage wait.

Using the unchanged engine and supplied fake clock/store/provider:

  1. At T0 the provider accepts the first email but loses its answer.
  2. Use a valid configuration: 24-hour key retention, 1,500 ms safety margin, 1,000 ms send timeout and five-minute lease.
  3. At T0 + 24 hours - 2,000 ms, retry. Let beginSend commit its ordinary successful mark.
  4. Delay only its response by 2,500 ms with a store after hook, then release it.
  5. The engine dispatches after key expiry: two delivered messages use identical keys and payload bytes, and the occurrence reports complete:true.

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 needs_reconciliation. Release the old response: the stalled worker still dispatches msg-2. Its completion is correctly fenced_out, but the duplicate email already exists.

Minimal extension of the existing supplied fake-store tests: install an async hook for method === 'beginSend' && phase === 'after', signal a deferred entered promise, and await a deferred release promise. After entered, advance the fake clock beyond the original provider retention, run the recovery worker and assert its parked status, then resolve release. Assert provider inbox length stays one; it is two on this head. The short case can instead call clock.advance(2500) in that same after hook after claiming 2,000 ms before retention expiry.

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 beginSend leaves its delayed-response window open.

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

Copy link
Copy Markdown
Owner Author

Pushed c9daac74 (the 0471 slice) and 6d035d9c (the P1 fix). Head: 6d035d9c.

Review 5371387764: the timing hold, and the P1 on 5c58ae4f

Both reproductions are right. 5c58ae4f already refused a stale claim at mark time. But a mark that commits validly and whose answer arrives late still led to a send, as your short 2.5 s case and your held-answer/recovery case show. Fixed in 6d035d9c, as you proposed:

  • 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 that was only ever definitively refused gets the lease bound alone, because nothing can be duplicated. The first version bounded it too, and the existing "definitively refused, retried at 53 h" regression caught that.
  • 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, so it is treated as possibly sent (the safe side).
  • The same rule is in all four places: decideBeginSend, the in-memory store, admin_digest_begin_send (which now returns jsonb, computed from the locked row) and the adapter. The adapter throws on a grant that has no deadline.
  • Kept as you asked: late accepted completions under the same fence are still recorded. The first-mark anchor, frozen bytes and keys, fences and the delayed-claim correction are unchanged.

Regressions (engine level, fake clock and store hooks):

  • your short case, where the mark commits 2 s before the cut-off and its answer is 2.5 s late: nothing is sent, and the inbox stays at 1;
  • your held case, where the answer is held until +25 h while a recovery worker parks the row: the stalled worker sends nothing;
  • two controls: a slow answer inside the deadline sends once, and past the lease part of the deadline nothing is sent.

Removing the engine's deadline check fails 3 of these.

Clock skew requirement, now stated in the contract: dispatchBy is database time and the engine compares it with its own clock. App-to-database skew must stay well inside leaseMs − sendTimeoutMs and margin − sendTimeoutMs, which NTP-level skew does.

The 0471 slice, as you scoped it

Migration (0471_an_admin_digest_reaches_each_admin_once.sql):

  • Tables: admin_digest_occurrences and admin_digest_deliveries, with checks that mirror the engine's invariants.
  • Functions: five security-definer functions. Each takes the row lock, then reads admin_digest_now(), which is clock_timestamp().
  • Guard triggers:
    • on insert, the hash, recipient key and idempotency key must agree with the bytes;
    • bytes, key and recipient are immutable, and rows are never deleted;
    • settled rows stay settled;
    • attempts, fence, ambiguity and anchor only move forward.
  • Access: RLS on with no policies. Table privileges are revoked from anon, authenticated and service_role, and EXECUTE on the five functions goes to service_role only.

Adapter: lib/admin/digest-delivery-store.ts, a strict mapping with an injected transport (supabaseRpc). A malformed answer throws rather than being read as a claim or a receipt.

Rule change found by the differential test: a completion now needs the claim's mark, in both TypeScript and SQL. The TypeScript rule had let an unmarked claim become ambiguous with no retention anchor; the database's check refused it.

Evidence (local; Node 24.21, PostgreSQL 16.13; synthetic data only):

  • Engine (in-memory): 96/96.
  • CI probe docs/audit/an-admin-digest-reaches-each-admin-once-check.sql: passes on the full replay. It covers access refusals, freeze-once, the fence, a completion needing a mark, retention parking and frozen identity.
  • All boundary probes: 181/181 on the harness. I installed plpgsql_check locally, so the one normally skipped probe (plpgsql-bodies-resolve) runs too, and passes.
  • Disposable PostgreSQL (tests/admin-digest-delivery-postgres.test.ts, DIGEST_DELIVERY_PG=1): 47/47, plus the restart test 1/1. It covers:
    • Rules:
      • the store contract suite on PostgreSQL;
      • a differential check where 3 seeds × 160 random steps agree exactly between TypeScript and SQL, answers and full state, including dispatchBy.
    • Concurrency: 40 concurrent claims from 40 connections, with exactly one winner.
    • Clock after lock: a claim that waited 1.6 s on the row lock sees an expired lease. Swapping the clock to now() answers leased.
    • Crashes and restarts:
      • a backend killed mid-claim leaves nothing behind;
      • one killed mid-receipt leaves no durable receipt, and the next claim treats the send as possibly made;
      • a committed receipt survives pg_ctl -m immediate restart, with WAL redo in the server log;
      • two engines at once each deliver once;
      • an engine crash after acceptance, then a new process resuming, gives one delivery.
    • Access and immutability: covered for every role.
  • Mutations: 37/37 in-memory safeguards killed on 5c58ae4f, plus the dispatch-deadline check on 6d035d9c. A 24-mutation SQL safeguard run on the final migration is in progress; results will follow here.
  • Static checks: migration filename audit, ON CONFLICT targets, tsc and eslint are clean.

Shared register and documentation hunks, for you to reconcile (not committed)

1. tests/migration-version-safety.test.ts. 0471 lands ahead of the reserved 0465–0470, so the audit's next version becomes 0472. This is the one test that fails on this branch; everything else in the 310-file repo-guard set passes.

-    expect(audit.nextVersion).toBe('0465');
+    // 0471 (reserved on #699, ahead of NWV's 0465–0470) stores the admin digest's
+    // per-recipient deliveries. The audit reports max + 1, so the pin reads 0472
+    // while 0465–0470 stay reserved and unused.
+    expect(audit.nextVersion).toBe('0472');

Once 0471 is on main, the NWV ports of 0465–0470 will not move this number. Their own pin bumps would then be no-ops, which is worth knowing when you sequence them.

2. docs/PENDING_PROD_MIGRATIONS.md. Append after the 0464 entry:

Proposed 0471 entry

0471 — an admin digest reaches each admin once (API-96040DFB5635, JOB-EF2453D9F633)

supabase/migrations/0471_an_admin_digest_reaches_each_admin_once.sql (number reserved by the coordinator on #699)

Severity: none on its own; nothing calls it yet. Deploy order: any, after the PROD-DB-0177 ledger remedy.

This is the delivery store for lib/admin/digest-delivery.ts, the per-recipient engine that will let /api/cron/admin-digest send each super admin one digest per occurrence. The engine is not wired to any route or scheduler, so applying 0471 changes no behaviour.

What it adds:

  • Two service-role tables:
    • admin_digest_occurrences: the frozen plan.
    • admin_digest_deliveries: one row per recipient, holding the stable idempotency key, the exact bytes, and the status, attempts, fence, lease, send mark and retention anchor.
  • Five security-definer functions: admin_digest_freeze, _load, _claim, _begin_send and _complete. Each evaluates the engine's rule under a row lock, with clock_timestamp() read after the lock. _begin_send also returns the dispatch deadline the engine enforces before every provider call.
  • Guard triggers: bytes, keys and recipients never change, rows are never deleted, and settled rows stay settled.
  • Access: RLS on with no policies, and every table privilege revoked from anon, authenticated and service_role. EXECUTE on the five functions goes to service_role only.

Proof:

  • docs/audit/an-admin-digest-reaches-each-admin-once-check.sql runs in CI, on the full replay.
  • tests/admin-digest-delivery-postgres.test.ts runs on a disposable database with DIGEST_DELIVERY_PG=1. It covers the store contract suite; a differential check against the TypeScript rules; 40 concurrent claims; the clock read after the lock; backend death mid-claim and mid-receipt; an immediate-mode restart; and access and immutability.

After applying: select public.admin_digest_load('none') as the service role returns null. As authenticated, it fails with permission denied.

Proposed for CI, not changed here: the disposable-database suite is opt-in. CI's Database job could run it with DIGEST_DELIVERY_PG=1 against its service container, which already has plpgsql_check. That is a shared workflow file, so it's your call.

Still not done: no route or scheduler wiring, provider call, live email, production migration or apply, credential or deployment setting.


Generated by Claude Code

…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
@NewWorldVenture

Copy link
Copy Markdown
Owner Author

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.

@NewWorldVenture

Copy link
Copy Markdown
Owner Author

Independent exact-head follow-up for 6ae785b6aeeba8042fafea6b8f5e6be8a079dc34: 17 new bounded controls pass; no additional defect found in these lanes. The queued-dispatch deadline P2 remains open.

  • Six actual-engine controls cover abort rejection and recipient continuation, a late timeout loser, completion fencing before commit, a lost response after commit, parking a failed pre-commit receipt beyond retention, and recovery after acknowledgement/final-read failure. They use the authored in-memory store and a synthetic provider/clock with the real AbortController.
  • Eleven actual-engine → PostgreSQL adapter → installed Supabase/PostgREST SDK controls cover all five serialized RPC argument sets, frozen Unicode bytes and recipient lists across retries, the same recipient in different occurrences, reversed concurrent claim responses, duplicate normalization, and refusal of 403/malformed/null/substituted admission/claim receipts. Scoped strict TypeScript passes. HTTP is intercepted; malformed receipts are fault controls, not asserted SQL behavior.

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 ef3462c899cd21c658fa9177e261af50316a680c, whose parents are actual main e4c3e679 and candidate 6ae785b6; recorded base2775866a does not describe that tree. migration-version-safety.test.ts:503 expects next version0465 but receives0472 from the 448-file tree ending at0471. Hosted unit summary: 22,661 passed / 1 failed / 38 skipped, 1,781 files. Lint passed; later timezone runs, build and typecheck were skipped. Mobile/Database/E2E passed separately. No rerun, allocation change or author-owned pin fix was performed.

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:

  • C:/Users/Daniel/AppData/Local/Temp/bubaly-pr699-outcome-review-20260930/{receipt.json,outcome-tests.log,source/tests/review-outcome-boundaries.test.ts}
  • C:/Users/Daniel/AppData/Local/Temp/bubaly-pr699-rpc-recipient-review-20260930/{final-review-receipt.json,rpc-recipient-current.log,tests/review-rpc-recipient-boundary.test.ts}
  • C:/Users/Daniel/AppData/Local/Temp/bubaly-pr699-ci-failure-review-20260930/{receipt.json,web-log-scoped-clean.txt}

… 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

Copy link
Copy Markdown
Owner Author

Pushed 4d2f82ef on top of bdd23486, which merges main (e4c3e679, bringing in #685, #692 and React 19) into the branch. Head: 4d2f82ef.

P2 on 6ae785b6: queued work between the admission check and the call

Your reproduction is right. The deadline check ran one microtask before provider.send, so queued JavaScript could spend the budget in between. Fixed as you proposed:

  • The check now runs inside sendWithDeadline, in the same synchronous step as provider.send, with no await between them.
  • A late check returns an explicit no-send: not_sent / dispatch_deadline_passed.
  • The send deadline now starts with the call itself.

Regression. Your tight config (24 h retention, 1.5 s margin, 1 s send deadline) and the same accept-then-lose first attempt, with 2.5 s of queued work placed 0 to 12 microtasks behind the mark's answer:

  • At every depth, the inbox stays at 1.
  • Both outcomes occur across the sweep: held before the call, and sent inside the window. So the sweep does cross the boundary.
  • On the previous engine it is red at depth 3, with 2 inbox entries, which matches your artifact.
  • Removing the new check fails 4 tests: this one, both of your earlier P1 cases, and the lease-deadline control.

As you said, this does not make the deadline atomic across arbitrary process or OS pauses after the call starts. The margin must cover those; the contract states the requirement.

From an adversarial review of the engine (5 lenses, each finding reproduced), triaged against head

Most findings were already closed at head. The open ones, now fixed:

  • Lease upper bound. validateConfig and 0471's admin_digest_claim refuse a lease of retention − margin or more. A worker that dies after its mark holds the row until the lease lapses, so a longer lease would park every such crash instead of retrying it. The unbounded case (leaseMs = 1e16) used to throw a RangeError in the claim rule.
  • 0471 enforces the 24 h cap itself. _claim and _begin_send now refuse a retention longer than the verified 24 h, instead of trusting the caller.
  • feat(email): optional caller-supplied idempotency key for sendEmail #692 reuse is executable, not copied. The key/bytes test calls the real sendEmail with fetch stubbed. Each engine key passes unchanged into the Idempotency-Key header, and the posted body is byte-identical to payloadJson. A control shows sendEmail refuses a key it would have to alter.
  • Contract: new section, "Duties the engine cannot enforce". It covers:
    • readSuperAdminRecipients does not trim, and one bad address refuses the whole occurrence, so the route must normalise first;
    • a recipient removed after the freeze is still sent on a retry of that occurrence; the options are a later withdraw step, or never resuming;
    • no RESEND_API_KEY rotation while any row is unknown inside its window, because Resend doesn't document key scope;
    • reconciliation is manual, and there is no store step to record its answer yet;
    • sendEmail's { ok: true, skipped: true } must map to a retryable refusal;
    • the scheduler prototype's key format never deduplicates against the engine's, so the two must not ship together.

SQL mutation proof on 0471: 26/26 safeguards caught

Each safeguard was removed from the migration on its own and applied to the harness. The CI probe and the disposable-database suite then ran against it.

  • S01–S21 and S24 on 6d035d9c: all caught. 12 are caught by the probe itself; every one is caught by the suite.
  • S22 (ambiguity is never cleared): its first anchor was a harness bug. Re-run on the current code, it is caught by 2 tests.
  • S23 (a real gap, now closed). Letting retention bind the dispatch deadline of a never-ambiguous row survived at first. A new store contract test now catches it: a row that was only ever refused gets a lease-only deadline, at any age. It runs against both the memory store and PostgreSQL.
  • S25 and S26: the new parameter bounds, each caught.

Current evidence

Node 24.21, PostgreSQL 16, synthetic data only.

  • Engine and store contract (in memory): 100/100.
  • Adapter strictness: 10/10.
  • Disposable PostgreSQL (DIGEST_DELIVERY_PG=1, with the restart command): 50/50, including the new contract test and the parameter refusals.
  • CI probe: passes on the new 0471.
  • tsc and eslint: clean.
  • Related guard tests: 18 files. The only failure is migration-version-safety, which gets 0472 where the pin expects 0465. That is the same red as CI on 6ae785b6. The pin is the shared register; the one-line hunk is in my comment above for Jimmy to reconcile, and I have not edited it here.

Still not done: no route or scheduler wiring, provider call, live email, production migration or apply, and no credential or deployment setting.


Generated by Claude Code

@NewWorldVenture

Copy link
Copy Markdown
Owner Author

Independent recheck of exact head 4d2f82efc6ab5ce6d1874357e9cc13a9cb515616: the queued-dispatch deadline finding is repaired for the retained reproduction.

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 not_sent/dispatch_deadline_passed. The admission predicate and provider invocation now execute in the same synchronous step. The unchanged engine → adapter → installed Supabase/PostgREST SDK checks pass 11/11, and scoped strict TypeScript passes. These checks use synthetic HTTP and an in-memory store; they establish no SQL/RLS, live provider, or production acceptance.

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 e4c3e679; current main was 48d3769c, four commits ahead on the main side. Combined-current-main acceptance remains separate.

Reproduction files and exact receipts: C:/Users/Daniel/AppData/Local/Temp/bubaly-pr699-4d2f82ef-review-20260930/, especially final-review-receipt.json, queued-current.log, rpc-current.log, and tests/pr699-queued-dispatch-deadline.test.ts. Prior failing evidence is retained. This closes the reproduced queued-hop issue only; the disclosed migration pin, route wiring and database/production prerequisites retain their separate status. No author/shared source, ledger, configuration, SQL or provider writes occurred.

@NewWorldVenture

Copy link
Copy Markdown
Owner Author

[P3] Reject an explicit NULL fence in both SQL transition functions

Source-level contract gap at exact head 4d2f82efc6ab5ce6d1874357e9cc13a9cb515616, migration blob 940a3ba5: begin_send line333 and complete line380 reject with r.fence <> p_fence, without a NULL parameter guard or STRICT declaration.

For an otherwise matching in-flight row, p_fence=NULL makes the rejection expression NULL. PostgreSQL skips an IF body unless its condition is true (official semantics). Consequently, the mark/completion path proceeds if its other eligibility conditions hold. Neither later UPDATE repeats the fence predicate. The stored fence stays unchanged/non-null, so its column constraint and monotonic trigger do not reject this input. This differs from the documented WHERE fence=$fence/no-write contract.

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 IS DISTINCT FROM or an explicit NULL guard in both functions, preserving current/stale integer behavior, and include NULL/no-write controls when the disposable SQL harness is run by its owner.

Reproduce the actual TypeScript/transport controls with Node24: C:/Users/Daniel/AppData/Local/Temp/bubaly-pr699-null-fence-review-20260930/probe.mjs; receipt.json records unchanged source hashes and all eight assertions. Independent seven-file source/line receipt: C:/Users/Daniel/AppData/Local/Temp/bubaly-release-gates-current-20260930/null-fence-source-receipt.json. No source, ledger, migration, configuration or provider writes occurred. This is separate from the repaired queued-hop finding.

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

Copy link
Copy Markdown
Owner Author

Pushed 053fb9b8 for the P3 on 4d2f82ef (comment 5920518211). Thanks for confirming the queued-hop fix.

P3: a NULL fence never matches

Your source reading is right. In admin_digest_begin_send and admin_digest_complete, r.fence <> p_fence evaluates to NULL for a NULL fence, so the IF skipped its refusal and the mark or completion went ahead. The fix has two layers in each function:

  • a NULL guard that refuses a NULL fence as bad input (22023, "a fence is required");
  • r.fence is distinct from p_fence, so NULL can never read as a match even if the guard were removed.

Current and stale integers keep their behaviour: 0 or any older fence still gets fenced_out.

Adapter. beginSend and complete send only a non-negative safe integer fence. null, undefined, negatives, fractions, NaN, Infinity and strings throw a TypeError before any RPC. Stale 0 still goes through for the database to judge, because the differential test relies on it.

NULL / no-write controls you asked for:

  • PostgreSQL (DIGEST_DELIVERY_PG=1):
    • on an in-flight, unmarked row, begin_send(…, NULL, …) is refused, and the row is unchanged: no mark and no retention anchor;
    • after a real mark, complete(…, NULL, accepted msg-forged) is refused, the row is byte-identical, and no message id is written;
    • control: the claim's own fence then completes.
  • CI probe: two new checks, one per function. Each is followed by a check that shows the NULL call wrote nothing: the unmarked-completion refusal for begin_send, and the real completion succeeding for complete. They catch invalid_parameter_value into a separate rejected flag, so the probe guard's rule (refused only for insufficient_privilege) still holds.
  • Mutations: reverting either function to <> without the guard fails both the probe and the PostgreSQL test. That makes S27 and S28, so all 28 SQL safeguards are now caught.

Evidence on 053fb9b8 (Node 24.21, PostgreSQL 16, synthetic data):

  • PostgreSQL suite with the restart command: 52/52;
  • adapter strictness: 11/11;
  • engine and store contract: 100/100;
  • the probe passes;
  • boundary-probes-actually-assert and -are-rerunnable pass;
  • tsc and eslint are clean.

The contract's adapter section now states the NULL rule.

The migration-version-safety pin is unchanged. It is the one expected red until Jimmy reconciles the register; the hunk is in comment 5919399170.


Generated by Claude Code

@NewWorldVenture

Copy link
Copy Markdown
Owner Author

Independent follow-up on exact head 053fb9b83a41babd8cf8035ec2ef0fd2a6703895, responding to repair 5920644675 and the prior NULL-fence finding 5920518211.

The original malformed-NULL gap is addressed by source review and adapter execution. Both SQL functions reject p_fence IS NULL with SQLSTATE22023 before SELECT/locking/writing, then compare using IS DISTINCT FROM. Removing only those guards/comparisons/comment reconstructs the previous migration exactly. All32 permission/RLS/definer/search_path declarations remain unchanged. Non-null current/stale integer comparison semantics remain intact. This is source-qualified clearance, not independently executed PostgreSQL acceptance.

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 fx.sql calls do not carry setup/adapter FakeClock pins, so the old-path negative control can become ineligible at later dates; pinning them would improve determinism. Their state comparisons establish unchanged relevant/adapter-visible fields plus provider_message_id checks; adapter timestamps normalize milliseconds, so raw byte identity is stronger wording than the assertions support. Neither qualification undermines the early current guard or the separately pinned CI probe.

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: C:/Users/Daniel/AppData/Local/Temp/bubaly-pr699-053fb9b8-adapter-review-20260930/final-review-receipt.json (fixtures/logs/counterfactual hashes alongside); companion source receipt C:/Users/Daniel/AppData/Local/Temp/bubaly-pr699-053-null-fence-source-review-20260930/source-review-receipt.txt and static-source-checks.json,12 verified old/new blobs. Adapter blob5e8ac5747deb9bcaaedb90a1cad7f4d8fd74e0d7.

Source: begin_send, complete, NULL tests.

@NewWorldVenture NewWorldVenture left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@NewWorldVenture

Copy link
Copy Markdown
Owner Author

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 tests/migration-version-safety.test.ts:503: expected0465, received0472. Unit summary is 22,676 passed /1 failed /41 skipped in1,782 files; the41 skips belong to database-dependent cases in admin-digest-delivery-postgres.test.ts. That job does not independently execute all52 of the author's standalone PostgreSQL suite. Database-job success is a separate hosted result. No new NULL-fence regression is inferred from this known pin failure; preserve coordinator ownership of its hunk.

E2E reports 1,815 passed /1 flaky, with the private-family-photo Chromium case at tests/e2e/family-photo-private-bucket.spec.ts:28:7 passing on retry. Do not relabel this as1,816 retry-free passes. The retry's cause has not been established by this review, and an isolated private-bucket fixture does not close the separately documented public production-bucket decision/SEC-001. Supabase Preview is skipped, not deployed acceptance.

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: C:/Users/Daniel/AppData/Local/Temp/bubaly-main-bd08-release-review-20260930/pr699-053-web.log and pr699-053-e2e.log.

Copy link
Copy Markdown
Owner Author

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.

Copy link
Copy Markdown
Owner Author

Support: migration number request. This is for the card-hold claim on #771 (posted just now), stacked on #925.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Support: migration reservation request (repository only; not to be applied to production). Scope: draft #932 (claude/a-paid-recurring-bill-comes-due-again-20261003), files lib/finance/hub.ts, components/finance/bills-view.tsx, components/modules/billing-module.tsx, lib/finance/timeline.ts, tests/a-paid-recurring-bill-comes-due-again.test.ts, the new migration and the version-pin test.

Why. The audit note of 2026-10-04 07:45 UTC on #932 is right: nextBillDueDate steps from the persisted due_date, so a bill due Jan 31 is persisted as Feb 28 after its first roll and steps to Mar 28 from there — the month-end cadence is lost. The test passed because it handed the original anchor back in. bills has no column that remembers the anchor day (id, family_id, name, amount, due_date, is_recurring, recurrence, status, category, autopay, created_by, timestamps), and at Feb 28 the row cannot tell a bill due on the 28th from one clamped from the 29th, 30th or 31st, so no heuristic settles it — and the heuristic's failure direction (a 28th bill moved later, to month-end) is a late reminder, worse than today's early one.

Proposed file: NNNN_a_month_end_bill_keeps_its_day.sql, two statements:

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.';
  • Nullable, no default, no backfill, no index, no policy or function change; RLS untouched. Existing rows read exactly as today (due_day null → the day of due_date).
  • Writers: "Mark paid" sets due_day from the anchor the first time it rolls a month-based bill, so the day survives the clamp; both add forms set it from the chosen due date of a recurring bill. On a database without the column the write is retried without it with one warning naming the migration (the pattern Calendar: a moved or cancelled occurrence leaves its old slot empty — exception dates on a recurring series (migration 0482) #913 uses for exception_dates), so a deploy ahead of its migration behaves exactly as today.
  • Readers: nextBillDueDate and the forecast's month stepping (lib/finance/timeline.ts, which today overflows Jan 31 → Mar 3 with setUTCMonth) step from due_day ?? day(due_date).

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

NewWorldVenture added a commit that referenced this pull request Oct 4, 2026
…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>

Copy link
Copy Markdown
Owner Author

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.

Copy link
Copy Markdown
Owner Author

Receipt for 5979394236: 0488 acknowledged for supabase/migrations/0488_a_month_end_bill_keeps_its_day.sql.

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 (claude/a-paid-recurring-bill-comes-due-again-20261003) and is then carried into #938. Files: the migration, lib/finance/hub.ts, lib/finance/timeline.ts, components/finance/bills-view.tsx, components/modules/billing-module.tsx, lib/database.types.ts (the bills row), tests/a-paid-recurring-bill-comes-due-again.test.ts, and my own entry in tests/migration-version-safety.test.ts (all other entries preserved). Announcing the filename on #771 before any edit; recheck of claims at publication.


Generated by Claude Code

NewWorldVenture pushed a commit that referenced this pull request Oct 4, 2026
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

Copy link
Copy Markdown
Owner Author

0488 published (5979394236 / 5979737300): supabase/migrations/0488_a_month_end_bill_keeps_its_day.sql on #932 at ab9e46204, carried into #938 at d40e644c4. One nullable, checked column (bills.due_day), no default, backfill, index, policy or function. Version pin moved to 0489 in my own entry; all other entries preserved. Rechecked open PR migration files at publication: no other 0488. Repository candidate only — not applied anywhere.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

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 main (Daniel's lane; one writer), files lib/server/notifications.ts, lib/services/notifications/index.ts, lib/notifications/medication-reminders.ts, the new migration, its tests, and my own entry in tests/migration-version-safety.test.ts.

Why. The audit note of 2026-10-04 07:34 UTC on #936 asked for a concurrency decision. The notification engine (generateFamilyNotifications) and the notify() service both dedupe by read-then-insert with nothing behind them in the database, and two of the engine's three callers — the daily notifications cron and the two-hourly push-scan — are each mirrored by Vercel and the GitHub dispatcher at the same minute. Two runs for one family can both read "not yet announced" and both insert: one duplicate per occurrence per concurrent pair, for every candidate type the engine writes. #936 fixed its own key and scoped this out as a unit of its own; this is that unit.

Proposed file: NNNN_a_notification_is_written_once_per_occurrence.sql:

-- 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;
  • Partial on related_id is not null, because a notify() call with no key is a free-standing message and two of them must not collide.
  • medication_due rows carry the bare medication uuid and recur daily; under this index they would collide from day two. They become ${medication}:${family day} (the engine already dedupes them per day, and entityIdFrom already picks the uuid out of a composite key; nothing in mobile/ reads related_id), with the same legacy bridge A renewed renewal, and a renewed document, are reminded about again #936 added so deploy day does not re-announce a dose already announced under the bare id.
  • Writers: PostgREST cannot aim ON CONFLICT DO NOTHING at a partial index (it needs the predicate in the conflict target), so the engine keeps its one batch insert and, on 23505, inserts the batch one row at a time, skipping the rows the index refuses — the concurrent run wrote them. notify() the same. The guardian routes write one row per manager under their own alert id and the contact-center writer upserts by primary key; neither changes. Behaviour is unchanged wherever the index does not fire.
  • No policy or function change; RLS untouched. The delete is the single data change and is stated as such; its exact scope is what the pre-flight query lists.

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

Copy link
Copy Markdown
Owner Author

Amendment to 5979918279 (the once-per-occurrence index), before any reservation is acted on. notify() (lib/services/notifications/index.ts) dedupes against UNREAD rows only by default — a row for the same key may be written again once the earlier one was read, and only once: true widens that to ever. A unique index over all rows would refuse that second, legitimate notice. The index therefore takes is_read = false into its predicate:

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 notify()'s unread-only rule is kept as it is. The pre-flight and the one data change shrink accordingly to UNREAD duplicates only (and a.is_read = false and b.is_read = false). Everything else in the request stands — the dated medication_due key with its deploy-day bridge, the 23505 row-by-row fallback in both writers, no change to the direct writers. Branch base: the unit stacks on #936 (claude/a-renewed-renewal-is-reminded-again-20261003), whose legacy-key bridge it reuses.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

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.

Copy link
Copy Markdown
Owner Author

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 (claude/calendar-feed-fidelity-20261003, Daniel's lane; one writer), files lib/server/calendar-feeds.ts, tests/a-subscribed-calendar-keeps-its-zone-its-series-and-its-cancellations.test.ts, the new migration, a Database-job probe docs/audit/a-calendar-feed-sync-writes-only-while-it-holds-its-claim-check.sql, and my own entry in tests/migration-version-safety.test.ts; carried down the stack (#913, #915, #918, #919, #920).

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: NNNN_a_calendar_feed_sync_writes_only_while_it_holds_its_claim.sql, one function, security invoker (RLS on calendar_feeds/calendar_events governs the user-client path exactly as today; the cron's service client is unchanged):

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

Copy link
Copy Markdown
Owner Author

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.

Copy link
Copy Markdown
Owner Author

Support: 0487 filename, as requested in 5979394236.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

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.

Copy link
Copy Markdown
Owner Author

0489 published (5979948710 / 5980002347): supabase/migrations/0489_a_notification_is_written_once_per_occurrence.sql on draft #956 (claude/a-notification-is-written-once-per-occurrence-20261004, stacked on #936) at 03bc57fae. Contents: the partial unique index uq_notifications_unread_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 read-only pre-flight query in the header (recorded, not executed by the file); ONE data statement, non-destructive — of each group of unread rows under one key the oldest stays unread and the later copies are marked read, nothing is deleted (delete does not appear; the test pins that), so every historical notification is preserved and the index can be built; and, beyond what was announced, create or replace of 0191's marketplace_log_price_change() with on conflict do nothing on its notification insert, because that trigger is the one writer inside the database and a second price drop while the first notice is unread would otherwise fail the seller's price change. Survivor semantics: oldest by created_at, then id; read rows and keyless rows are untouched; re-applying the file changes nothing. Why additive alone would not do: the index cannot be created over existing unread duplicates, and a delete would reach the same index with less history. Version pin moved to 0490 in my own entry; all other entries preserved. Synthetic verification only: applied and re-applied on a replayed local database; docs/audit/a-notification-is-written-once-per-occurrence-check.sql (the Database job) passes with it, as do the four existing notification/approval probes. Repository candidate only — not applied anywhere.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

0490 published (5980063802 / 5980224970): supabase/migrations/0490_a_calendar_feed_sync_writes_only_while_it_holds_its_claim.sql on #908 (claude/calendar-feed-fidelity-20261003) at 13a53323e, carried in dependency order into #913 d1ec88367 → #915 4b2f52af1; #918 44ca3f45c → #919 b5ddae45f → #920 bd96f5bce — each head typechecked and its feed suites run before pushing; no green result transferred. Contents: one function, public.calendar_feed_apply_sync(uuid, timestamptz, jsonb, text[]) returns text, security invoker, set search_path = public, pg_temp; no table, index, column, policy or data change. Overlap: lib/server/calendar-feeds.ts on #908 and its #913 child — both preserved; #913's exception_dates fallback is composed onto the fenced write (one conflict, resolved on #913 at the merge; its version pin resolves to 0491 with 0482 and 0490 both present). Fallback limitation, explicit: a database without the function takes the check-then-write path (two statements) with one warning naming this file; the window stays open there until the function is applied. Claim-loss controls: the probe docs/audit/a-calendar-feed-sync-writes-only-while-it-holds-its-claim-check.sql (live fence lands; taken-over fence and released claim write nothing; removals stay in their feed; unknown column refused) and the unit tests placing the takeover at the instant the function is entered, for an upsert chunk and for a removal chunk. Version pin moved to 0491 in my own entry; all other entries preserved. Repository candidate only — not applied anywhere.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Amendment request for the 0489 reservation (5979948710 / 5979918279 / 5979936584), following the scope note on #956 (5980402705). The file as published (0489_a_notification_is_written_once_per_occurrence.sql, #956 at a61d4c5) carries, beyond the index and the non-destructive repair, a create or replace of 0191's trigger function marketplace_log_price_change() with one clause added — on conflict do nothing on its notification insert; the body is otherwise 0191's statement for statement (diff -w: one clause, one comment). It is in the same file because the two cannot be applied apart: the index without the clause fails a seller's price update inside the trigger (23505) on a second drop while the first notice is unread; the clause without the index is inert. Ownership: 0191 is the function's only definition; a 13:30 UTC scan of all 93 open PRs' file lists finds no PR touching 0191 or that function besides #956. Regression for 0191's preserved behaviour is in the probe a-notification-is-written-once-per-occurrence-check.sql (every change logged; a read watcher told of the next drop; a rise tells nobody) and runs in the Database job. Please record the 0489 reservation as covering this function re-create, or say if it should be a separate numbered file applied in the same release step — I will restructure either way. The execution hold on the repair stands; the header now states exactly which rows change unread state and why.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Re 5980249810 (#945 e10ff9a fails Web Unit tests). Failing assertion: tests/a-server-path-does-not-read-the-hosts-calendar.test.ts > has no stale declaration — "Declared as still reading the host's calendar on a server path, but no longer does. Delete these lines: app/api/ai/chef/route.ts". Cause: the re-root onto current main brought #937 (a3712a9), which reads the chef route's busy evenings on the family's clock; the ratchet's own rule is that a file fixed elsewhere must leave its known-unfixed list, so the declaration this branch still carried for that file failed the ratchet exactly as designed. Bounded repair: the one declaration deleted (a5fa100); the ratchet, the eleven fixes and every propagated zone change are unchanged, and the test still refuses a stale entry. Reproduced locally on e10ff9a (1 failed / 7 passed), passes on a5fa100 (8/8); eslint clean. New head: #945 a5fa100b6; CI is running on it — I will report the result on this thread, not transfer the parents' green (#943 49ca079 and #944 5a51928 are green on their own current heads).


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Production scheduler: every scheduled tick of cron-dispatch.yml fails at the configuration check, and the evidence points at a missing CRON_SECRET Actions secret. Read-only finding; no branch, run or setting was changed.

  • Eight consecutive scheduled runs have failed, from run 176 (2026-10-03 06:51 UTC) to run 183 (37205512794, 2026-10-04 13:24 UTC). Each fails at step 5, "Validate configuration and dispatch due routes", with exit code 1 and no ::error annotation; step 4 (the catch-up boundary) succeeds.
  • In run 183 that step started and completed within the same second (13:24:42 → 13:24:42). scripts/cron-dispatch.mjs has one exit-1 path that returns before any request: CRON_SECRET is required for dispatch. Add the matching application secret under Settings → Secrets → Actions. (line 285). A malformed secret or CRON_BASE_URL exits 2; a route failure exits 1 only after the HTTP calls, which take seconds.
  • Consequence: GitHub has dispatched no cron route since then. Vercel's own schedule still runs the daily routes once a day, so the daily surfaces are not dark, but the sub-daily routes the dispatcher mirrors (/api/cron/ai-runs, social-publish, contact-center-urgent, guardian-sms-recovery, marketing, close-auctions) run only when Vercel's schedule reaches them.
  • Remedy, operator only: set the repository Actions secret CRON_SECRET to the application's value (and, if the deployment is not https://www.bubaly.com, the variable CRON_BASE_URL). The next tick recovers on its own; the bounded catch-up from Cron dispatch: a tick that could not find its catch-up boundary is not one (JOB-001 follow-up) #902/An admin digest ticked twice is sent once: the flag-off path reads the decided occurrence and sends each admin under one idempotency key #946 replays the missed sub-daily occurrences within its window. I cannot read secrets, variables or job logs through this session, so this is inference from the step timing and the script's exit paths, not from the log line itself.

Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Integration backlog, state at 2026-10-04 ~16:50 UTC (main bdaec6577, 90 open PRs, 89 drafts). Everything short of merging is done; merging, marking drafts ready and closing PRs are yours (this session is not permitted to do any of the three, and will not route around that).

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):

PR migration head
1 #674 0465 05525fb (main merged, pin resolved)
2 #705 0466 d3eea53 (main merged, pin resolved)
3 #712 0472 clean
4 #834 → #853 → #854 0475/0476 (+0293 replay guard) 951b98a → 729d81f → a837f21
5 #788 → #892 / #927 / #941 0477, 0486 9feaf00 and children (F19 lane)
6 #897 0478 b1b5072
7 #908 → #913 → #915; #913 → #918 → #919 → #920 0482, 0483, 0490 clean, holds answered
8 #925 → #954 0484/0485, 0487 clean
9 #932 → #938 0488 clean
10 #936 → #956 0489 876c044 (four review points answered)

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 SET ROLE in effect, so verify-messaging-database.mjs holds simulated members to it: 183/183 probes and the verification pass locally), carried to #853/#854; #897's two audit rules; #783's two hardcoded strings; the series stack's two page conflicts. #946 merged (thank you). Scheduler: see the comment above this one (CRON_SECRET).

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 docs/finalaudit.md; the production migration ledger at 0177.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Addendum to the integration state above: the five evidence/ledger drafts are already on main in later form and can be closed.

No branch was changed for any of these; closing is yours.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

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.

Copy link
Copy Markdown
Owner Author

Surge: migration slot request. The claim is on #771 (comment linked from there). I have not created any migration file.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants