Skip to content

fix(compat): preserve continuation replay fences - #1062

Merged
bokelley merged 2 commits into
mainfrom
codex/continuation-ledger-retention
Aug 21, 2026
Merged

fix(compat): preserve continuation replay fences#1062
bokelley merged 2 commits into
mainfrom
codex/continuation-ledger-retention

Conversation

@bokelley

@bokelley bokelley commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • never purge a continuation before token expiry and replace eligible payloads with permanent issuance tombstones
  • enforce replay retirement in SQLite with an atomic migration and fail-closed guards for mixed-version workers
  • retain pre-fingerprint authorization rows that cannot be compacted safely
  • make per-principal logical-byte quotas account for the same persisted fields as global quotas, including target_binding

Why

The continuation ledger could delete a terminal row while its token was still valid, then deterministically reissue the same token and execute another purchase under a new idempotency key. Per-principal quotas also omitted routing bindings, allowing one tenant to consume global storage beyond its configured share. This follow-up preserves replay fences across payload cleanup and rolling upgrades while making tenant accounting symmetric.

Validation

  • uv run pytest -q tests/test_purchase_continuation.py — 120 passed
  • uv run pytest -q — 6,875 passed, 40 skipped, 9 deselected, 1 xfailed
  • scoped pre-commit suite — all hooks passed
  • protocol review — release-ready after mixed-version and prior-schema migration audits
  • security review — release-ready after reproducing and closing the migration publication race

Comment thread tests/test_purchase_continuation.py Fixed
if legacy is not None:
retired_legacy = conn.execute(
"SELECT 1 FROM adcp_compat_issuance_tombstones "
"WHERE issuance_fingerprint IS NULL AND principal_id = ? "

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium: This guard never fires. Every tombstone is inserted in _purge_candidate_batch only for rows where issuance_fingerprint is not None, so issuance_fingerprint is always non-NULL on tombstones. This query filters issuance_fingerprint IS NULL, which therefore matches zero rows — the or retired_legacy is not None arm is dead, and the legacy_equivalence_hash written onto every tombstone is write-only. If the intent is to fence a mixed-version pre-fingerprint reissue against a purged fingerprinted authorization (the exact rolling-upgrade case this PR claims to harden), it does not engage — drop the issuance_fingerprint IS NULL predicate. If the intent is only to fence purged legacy rows — which never occur, since legacy rows are deliberately never purged — then the query and the stored hash are dead weight. Either way the code and intent diverge.

END
""",
"""
CREATE TRIGGER IF NOT EXISTS adcp_compat_issuance_tombstones_delete_guard

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium: Tombstones are permanent (this guard) and count against both global and per-principal record/byte quotas (_enforce_ledger_quota), and purge replaces each removed continuation with one — never fewer. A long-lived, high-throughput principal accumulates tombstones monotonically with no in-code pruning path, so max_records_per_principal / max_bytes_per_principal is eventually exhausted, after which every new continuation for that principal fails closed with STORE_QUOTA_EXCEEDED. Fail-closed is the right posture for a replay fence, but the availability cliff is real and the docs' "archive them in an application-owned durable idempotency store" is the only escape — no hook is provided. Worth a follow-up: expose an operator-facing archival/prune path, or document the sizing math so deployments can bound the horizon.

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ladon verdict: Comment (human reviewer recommended)

Comment — 2 medium findings surfaced for human review.

This is a follow-up hardening of the SQLite legacy-continuation ledger. Core replay-fence logic (atomic-migration race, fail-closed triggers, claim races, expiry-safe purge, issuance tombstones, per-principal byte quotas) is sound and well-tested. No critical/high findings.

Medium findings

  • src/adcp/compat/sqlite_continuation_store.py:555 — retired_legacy tombstone guard never fires; equivalence hash is write-only (dead guard)
  • src/adcp/compat/sqlite_continuation_store.py:157 — Permanent tombstones grow unbounded and eventually exhaust ledger quota

Why not approve: The high_risk flag is set because src/adcp/compat/sqlite_continuation_store.py (modified) matches src/adcp/compat/**, and this run has medium findings. Decision-table row 5 (high_risk + (modified) + a medium finding) → escalate; however both mediums are quota-management/dead-code hardening concerns on durable state, not schema/data-loss/infra categories. Re-checking the table: row 1 no (no critical/high), row 2 no (gated_paths false), row 3 no (no deletion), row 4 no (neither medium is data-loss/schema/infra category), row 5 DOES fire (high_risk true + (modified) + medium finding present).

Correcting: row 5 fires → escalate.

Medium findings

  • src/adcp/compat/sqlite_continuation_store.py:555 — retired_legacy tombstone guard never fires; equivalence hash is write-only
  • src/adcp/compat/sqlite_continuation_store.py:157 — Permanent tombstones grow unbounded and eventually exhaust ledger quota

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ladon verdict: Approve

Approve — hardening follow-up resolves both prior Medium findings with no new blocking issues.

What I checked:

  • Both prior Medium findings are resolved: (1) the write-only legacy_equivalence_hash column is removed via an atomic in-migration rename/copy/drop (sqlite_continuation_store.py L179-L209) with the fence now keying on token_hash + (principal_id, issuance_fingerprint) directly; (2) permanent tombstones now count against global and per-principal record/byte quotas (L910-L911, L1071-L1081, L1136-L1147) with exhaustion failing closed as a documented, intentional availability boundary.
  • New purge logic (insert-before-delete, expires_at<=now gating, pre-fingerprint retention) is correct and well-tested; full suite reported green.
  • Change is confined to the compat continuation store, docs, and tests — no public-API/wire-shape surface change, no generated-code hand-edit, no type-layering violation.

Decision path: No critical/high findings (row 1 n/a). gated_paths is false (row 2 n/a). high_risk is true but only via a (modified) compat file, and the current run surfaced zero findings — so rows 3, 4, and 5 do not fire. Prior decision was comment, not escalate, and no new medium+ findings exist (row 6 n/a). No no-auto-approve team match (row 7 n/a). Zero medium findings (row 8 n/a). Falls through to row 9 → approve. Net improvement over the prior review.

@bokelley
bokelley merged commit be71316 into main Aug 21, 2026
28 checks passed
@bokelley
bokelley deleted the codex/continuation-ledger-retention branch August 21, 2026 14:59
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.

1 participant