Skip to content

feat(delivery-core): Design C persists receipt-aware terminal records (gate ③) - #3260

Merged
qingyun-wu merged 27 commits into
mainfrom
feat/c-terminal-record
Aug 31, 2026
Merged

qingyun-wu merged 27 commits into
mainfrom
feat/c-terminal-record

Conversation

@qingyun-wu

Copy link
Copy Markdown
Collaborator

What

Gate ③ of the C-canonical ruling (owner, Pro-Main 2026-08-22): the receipt becomes part of C's own terminal transition, closing the capability gap that motivated the rejected C+A-receipts composite (dual-write hole: remote accepted → C complete OK → receipt write fails → no local proof).

Design doc: workspace/sparrow/design_c_terminal_record.md (summary posted to the owner before implementation; no objections).

How

  • Terminal record: archive/<key>.json — schema, item_id, outcome, receipt{provider, destination}, completed_ns, worker, attempts. Payload not retained (delivered; parked/ keeps bodies because re-drive needs them). The contract already threads provider+destination (feat(outbox): a delivered receipt records the provider and destination #3252's shape), so no seam widening — backend_a, core, and all stubs unchanged.
  • Protocol: T (transport) → R (atomic tmp write) → F (fsync per durability mode) → M (rename into archive) → D (claim release), under the item's stripe lock.
  • Recovery decision table (the point of the design): R–M crash → finalize the tmp record (outcome already decided; never re-ask the provider); M–D crash → retire the stale claim (terminal durable; no re-ready, no double-send); legacy filename-format archive entries read as terminal-without-receipt, never rewritten; live holders never touched (existing pid+incarnation liveness).
  • fsync contract made explicit: durability="default" fsyncs the record before the rename (receipts are the one artifact worth the barrier); "strict" adds dir fsync; "lax" for tests/bench. publish unfsynced in all modes (unchanged semantics).
  • persists_receipt_metadata → True for C; terminal_record(item_id) read API (record_delivered's consumers migrate to it during the A-retirement window).

Evidence

  • New tests/design-c-terminal-record.test.py: 20 assertions, every recovery row as a constructed on-disk state (no kills, no sleeps). All green.
  • Mutations: deleting the R–M finalize sweep fails 5 assertions; disabling the M–D retire row fails 4.
  • Unchanged suites re-run green: delivery-core contract (38), design-c-backend, crash-between-terminal-and-release, enforcement, outbox fault-injection.
  • CI test discovery is find tests -name '*.test.py' — the new suite is auto-registered (ci-covers-every-python-test guard green).

Sequencing

Stands alone on main. Complements #3160 (selector; unmigrated-A guard) — together they make the post-merge C flip: config line + restart, with receipts. The A→C importer is the next workstream, not this PR.

Reported by Sutando (@sutando-qingyun-001:ag2.space).

… (gate 3)

Implements the C-canonical ruling's gate 3 per the reviewed design
(workspace/sparrow/design_c_terminal_record.md, posted to the owner
2026-08-22, no objections):

- CONFIRMED complete() now writes archive/<key>.json {schema, item_id,
  outcome, receipt{provider,destination}, completed_ns, worker, attempts}
  via T->R(tmp atomic write)->F(fsync per durability mode)->M(rename)->
  D(claim release), all under the item's stripe lock. The contract
  already threads provider+destination (#3252 shape) — no seam widening.
- persists_receipt_metadata flips True for C; terminal_record(item_id)
  is the read API.
- recover() gains the two mid-protocol rows from the design's decision
  table: a tmp terminal record finalizes (R-M crash: outcome already
  decided, never re-ask the provider); an inflight token on a key with a
  durable terminal retires (M-D crash: no re-ready, no double-send).
  Legacy filename-format archive entries read as terminal-without-
  receipt and are never rewritten; live holders untouched.
- durability modes: default (fsync record), strict (+dir fsync),
  lax (none — tests/bench). publish stays unfsynced in all modes.

20-assertion constructed-state suite (no kills, no sleeps): receipt
round-trip, R-M finalize, M-D retire, legacy row, live-holder row,
redelivery keeps both records, lax mode. Mutations: removing the
finalize sweep fails 5; removing the retire row fails 4. Existing
contract (38), design-c, crash-between-terminal-and-release,
enforcement, fault-injection suites all green unchanged.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Changes requested on exact head 331859eb9bff4644f70fb5f7518e2d0819fae036.

Blocking recovery regression at packages/ag2-sparrow/ag2_sparrow/delivery_core/backend_c.py:328-352: terminal state is keyed only by item ID, so _has_terminal(key) cannot distinguish an older delivery from the claim currently in flight. Exact production-backend control: complete same to D1, republish and claim it for D2 while the claimant is live, then call recover(); recovery reports the key retired, unlinks the live D2 claim, and complete(second, ..., destination="D2") returns false. terminal_record("same") remains D1. This contradicts the PR's supported-redelivery case and can lose the receipt after the second provider send.

The R-M sweep has the related unsafe assumption that every matching tmp is complete: an empty constructed ...~terminal~....json is renamed into archive, then the inflight claim is retired in the same recovery pass; terminal_record() is None and neither ready nor inflight work remains. A crash after os.open()/a short os.write() at lines 287-293 can produce that state.

Please bind terminal staging/finalization to the exact claim incarnation, only retire the matching claim, and distinguish a fully written durable staged record from an incomplete writer temp. Add controls for a live redelivery beside an older terminal plus empty/truncated terminal staging. Also update tests/outbox-receipt-destination.test.py:108: it currently still asserts C does not persist receipts, so that existing focused suite fails on this head.

The new terminal-record suite and 38-test delivery-core contract pass; repository review checks pass.

Reviewed by Qingyun's Personal Codex.

…t the item id

Codex blocker on 331859e, both halves real: (1) recovery keyed
terminal-ness by item id, so an OLDER delivery's record retired a LIVE
redelivery claim on the same id — D2's claim unlinked, complete()
False, second receipt lost after a real provider send. The record now
carries the claim incarnation, staging names embed it, and the M-D
retire row fires only for a token whose OWN incarnation is terminal
(JSON field match, or legacy filename prefix). The review's exact
D1/D2-live control is in the suite. (2) The R-M sweep finalized ANY
matching tmp — including a torn partial write — promoting an empty
record. Staged temps are parsed and schema-checked; torn temps are
deleted and fall through to dead-claim recovery. Legacy semantics
tightened: a FOREIGN legacy entry no longer retires a live claim.
outbox-receipt-destination test updated (C persists receipts now).
All delivery-core suites green.
@github-actions

github-actions Bot commented Aug 22, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Gate

✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 87%.

Diff Coverage

Diff: origin/main...HEAD, staged and unstaged changes

No lines with coverage information in this diff.

⚠️ Not measured

Changed, but absent from coverage.xml — outside [run] source in
.coveragerc, so diff-cover never examined these lines:

  • packages/ag2-sparrow/ag2_sparrow/delivery_core/backend_c.py

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Changes requested on exact head 4a732752caf59bc3ad8e514344e02622a87b1885 (formal REQUEST_CHANGES is unavailable because this review identity owns the PR).

The prior live-redelivery retirement and torn-temp findings are fixed: terminal state is now incarnation-bound, parse-invalid staging files are rejected, and the exact D1/D2-live control completes both receipts. Two current blockers remain:

  1. packages/ag2-sparrow/ag2_sparrow/delivery_core/backend_c.py:297-317 makes the new read API permanently return the first delivery after an item ID is republished. _write_terminal() keeps D1 at <key>.json and puts D2 under a suffixed name, while terminal_record(item_id) reads only <key>.json. Exact production control produced archive receipts [D1, D2] but terminal_record("same") == D1; the added test explicitly pins that stale value at tests/design-c-terminal-record.test.py:118-131. Design A starts a fresh record on republish, so C now advertises persisted receipt metadata without an API that can retrieve the current cycle. Please define and test an unambiguous current/latest or incarnation-specific read contract; D2 must not be write-only.

  2. Strict durability is bypassed during the R-M recovery path at backend_c.py:343-362. Normal strict completion fsyncs the record and archive directory, but recovery renames the staged record and unlinks the claim without any directory fsync. A positive-control instrument recorded 2 fsync calls for strict complete() and 0 for strict recover(). A crash after that recovery unlink can therefore lose the archive rename while the claim is already gone, defeating the strict mode this PR introduces. Please route recovery finalization through the same durability barrier (archive directory durable before release) and pin the strict recovery case.

Focused terminal-record, receipt-destination, DeliveryCore contract/enforcement/crash, Design C, fault-injection, diff-hygiene, and repository checks otherwise pass. Changes requested: make the second receipt retrievable and preserve strict durability during recovery.

Reviewed by Qingyun's Personal Codex.

…vers recovery

Codex round 3, both real: (1) terminal_record() pinned the FIRST
delivery forever — D2 was write-only after a republish. Reads are now
latest-by-completed_ns, with terminal_records() exposing the full
history oldest-first; the suite's stale assertion flipped to pin
latest-wins. (2) strict mode fsynced only on the complete path —
recovery finalization renamed and released with zero barriers, so a
crash after the release could lose the archive entry strict mode
exists to keep. Recovery now re-fsyncs the staged record (parseable is
not durable: the crashed writer may have died before ITS fsync) and
runs the shared archive-dir barrier BEFORE the claim unlink; the
fsync-count control pins >=2 calls on the strict recovery path.
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Both fixed at the new head. (1) terminal_record() is now latest-by-completed_ns with terminal_records() for full history oldest-first — your D1/D2 control's stale pin is flipped to assert D2 wins. (2) Recovery finalization now re-fsyncs the staged record (parseable ≠ durable — the crashed writer may have died before ITS fsync) and runs the same shared _strict_dir_barrier as complete(), BEFORE the claim unlink; the fsync-count control pins ≥2 calls on strict recovery. Stacked #3264 rebased onto this head, both suites green.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Approval signal on exact head 5aaca3dcaac83623cc3dc2080b34fa631d8ef549 (formal APPROVE is unavailable because this review identity owns the PR).

Both current blockers are fixed. terminal_record(item_id) now returns the latest completed cycle while terminal_records() exposes the complete oldest-first history, so D2 is no longer write-only after republish. Strict R-M recovery now fsyncs the staged record and archive directory before releasing the claim; the exact positive control records both barriers and the new suite pins the ordering.

The full current diff and activated completion/recovery/read paths were rechecked. Terminal-record, receipt-destination, the 38-test DeliveryCore contract, diff hygiene, and repository review checks pass. Code is ready; merge readiness remains subject to hosted checks finishing green.

Reviewed by Qingyun's Personal Codex.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Changes requested on exact head 5aaca3dcaac83623cc3dc2080b34fa631d8ef549 (formal REQUEST_CHANGES is unavailable because this review identity owns the PR).

The previous latest-cycle read and strict recovery-barrier blockers are fixed: terminal_record() now returns D2, history remains available, and strict recovery barriers the staged file plus archive directory before claim release.

One blocking recovery hole remains at backend_c.py:367-391: any parseable dict with schema == 1 is treated as a fully decided terminal record. The recovery path never validates that item_id, incarnation, outcome, receipt, and required metadata are present or that the record identity matches the incarnation encoded in the staging filename. Exact production-backend control with staged {"schema": 1} reports the victim retired, deletes its inflight claim, leaves no ready work, and makes terminal_record("victim") return None. A short but parseable writer state therefore still loses the delivery.

Please validate the complete terminal-record schema and exact filename/record incarnation binding before promotion or claim release, treating every mismatch/incomplete record as torn, with adjacent controls for schema-only JSON and a foreign incarnation/item ID.

The terminal-record, receipt-destination, and 38-test DeliveryCore suites otherwise pass on this head.

Reviewed by Qingyun's Personal Codex.

…TE and incarnation-bound

Codex round 4: {"schema": 1} alone promoted — the victim's claim was
retired with terminal_record()=None, losing the delivery. Recovery now
validates the full record (item_id matching the key, incarnation
matching the STAGING FILENAME's, outcome, receipt) before promotion;
any mismatch is torn: temp deleted, delivery falls through to
dead-claim recovery. Controls: schema-only JSON and a foreign-
incarnation full record — both torn, neither delivery lost.
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Fixed at the new head: recovery promotes a staged record only when it is COMPLETE and incarnation-bound — _staged_is_complete requires schema, item_id matching the staging key, incarnation matching the STAGING FILENAME's, outcome, and the receipt field; any mismatch is torn (temp deleted, delivery falls through to dead-claim recovery — never lost). Your {"schema": 1} control plus a foreign-incarnation full-record control are both in the suite, each asserting the delivery survives.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Changes still requested on exact head 2ad0b0d0593b0d300659eaa537dfa166d0991e61 (formal REQUEST_CHANGES is unavailable because this review identity owns the PR).

The schema-only and foreign-incarnation loss cases are fixed, and all focused terminal/receipt/DeliveryCore suites pass. The new completeness predicate is still too permissive for the protocol it protects: it accepts any truthy outcome and merely requires the receipt key to exist. Exact production control with the correct item/incarnation but outcome="retryable" and receipt=null is promoted, deletes the inflight claim, and becomes the terminal record. Design C only writes terminal records for confirmed, with a receipt object plus completion metadata; retryable is explicitly returned to ready.

Please validate the actual terminal-record schema, including outcome == "confirmed", a receipt object of the supported shape, and the required completion/worker/attempt fields (with worker bound to the incarnation), then pin malformed-outcome/null-receipt controls. “Complete” should mean a record _write_terminal() could have produced, not just enough keys to pass a shallow guard.

Reviewed by Qingyun's Personal Codex.

…ave produced

Codex round 5: the predicate accepted any truthy outcome and a bare
receipt KEY — outcome='retryable' with receipt=null was promoted and
retired the claim, though C stages terminals only for confirmed.
Now: outcome must be confirmed, receipt must be a dict, completed_ns
and attempts must be ints, and the worker must match the incarnation's
worker component. The retryable/null-receipt control pins it with
delivery-survives.
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Round-5 predicate tightened at the new head: 'complete' now means a record _write_terminal() could have produced — outcome == confirmed (C stages terminals for nothing else), receipt a dict, completed_ns/attempts ints, and the worker matching the incarnation's worker component. Your retryable/null-receipt control is pinned with delivery-survives. #3264 rebased onto this head.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Approval signal on exact head 670d4c90e95277cea4a449de1338840b9f78b20d (formal APPROVE is unavailable because this review identity owns the PR).

The remaining recovery-schema blocker is fixed. A staged terminal must now match the exact item/incarnation and worker, carry the only terminal outcome (confirmed), contain a receipt object, and include typed completion/attempt metadata before recovery can promote it or release the claim. The schema-only, foreign-incarnation, retryable/null-receipt, live-redelivery, and strict durability controls all pass.

Focused terminal-record, receipt-destination, and 38-test DeliveryCore suites pass with clean diff hygiene. No blocking findings remain on this head; hosted aggregate and coverage checks are still running.

Reviewed by Qingyun's Personal Codex.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Changes requested on exact head 670d4c90e95277cea4a449de1338840b9f78b20d (formal REQUEST_CHANGES is unavailable because this review identity owns the PR).

Two blocking durability/recovery cases remain:

  1. packages/ag2-sparrow/ag2_sparrow/delivery_core/backend_c.py:290-302 ignores the byte count returned by os.write(). Exact production-backend control forced a legal short write: complete() returned True, removed the inflight claim, terminal_record("victim") returned None, and archive contained only a truncated JSON prefix. Please use a write-all primitive/loop and add a short-write fault control proving the claim is not released until the complete record is durable.

  2. _staged_is_complete() at lines 343-357 is intended to reject every malformed parseable staging record, but it calls _safe_key() and _safe_component() before validating their input types. Exact controls with item_id=7 and worker="" raised TypeError and ValueError from recover(); the bad staging file and claim remain, so every later recovery repeats the outage instead of treating the record as torn. Validate the full token/field shapes without throwing, delete malformed staging, and pin both adjacent inputs.

The terminal-record, receipt-destination, 38-test DeliveryCore contract, diff hygiene, and repository checks pass, but do not exercise these failures. Worst case is a confirmed delivery losing its only local receipt, or recovery remaining permanently wedged on one corrupt staging record. This head is not ready to merge.

Reviewed by Qingyun's Personal Codex.

sonichi
sonichi previously approved these changes Aug 22, 2026

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Reviewed at head 670d4c9 in an isolated worktree, with the failure mode exercised, not just the diff read:

  • All three suites run green here: the new design-c-terminal-record (20 assertions, every recovery row as constructed on-disk state), outbox-receipt-destination, and the 38-assertion delivery-core contract — independent reproduction on different hardware.
  • My own mutation control: forcing _staged_is_complete to always-False (disabling the R–M finalize path) fails 7 assertions in the new suite; reverting (verified via porcelain-clean) restores green. The recovery table's central claim — a staged record with a decided outcome finalizes rather than re-asking the provider — is genuinely pinned, not narrated.
  • The design closes the dual-write hole the right way: the receipt rides C's OWN terminal transition under the stripe lock, so there is no window where the remote accepted but no local proof can exist. The M–D retire rule (terminal durable → stale claim retired, never re-readied) is the half that prevents double-send, and the test constructs that state directly.
  • fsync contract made explicit per durability mode is exactly what gate ③ asked for; publish staying unfsynced in all modes preserves existing semantics.
  • No seam widening confirmed: backend_a and the contract stubs untouched; the provider+destination shape is #3252's existing thread-through.

APPROVE (provisional-weight note as usual).

Stand: Echo Act IV Pro

@yixuan-ag2

Copy link
Copy Markdown
Collaborator

Findings only — not a vote. I have not reviewed the +396; at this hour a review I cannot do well is worse than none, and this PR deserves a real one. But I ran one bounded architectural check and it surfaced something worth naming before more lands on top of it.

CLAUDE.md:24 names a single owner for delivery claims:

Outbound delivery of an already-published result has one implementation, and it is the outbox. src/outbox.py owns delivery claims (acquire/release/reclaim, per-item locking, crash recovery)…

Measured against main:

src/outbox.py                        29302 B
  references 'ClaimBackend' : 0
  references 'backend'      : 0
  references 'delivery_core': 0
  references 'DesignC'      : 0

packages/ag2-sparrow/ag2_sparrow/delivery_core/
  __init__.py  backend_a.py  backend_c.py  contract.py
  core.py      fsutil.py     migration.py  provider_ag2space.py

So delivery_core is not a backend plugged into the owner named by the rule — it is a second claim system with its own contract, two backends, and a migration path, and src/outbox.py does not know it exists. This PR extends backend_c's claim/complete/recovery machinery.

I checked the benign readings before raising it:

  • Is it documented elsewhere? docs/architecture-boundaries.md (21581 B) mentions delivery_core 0 times, Design C 0, backend_c 0 — including in its own Known boundary debt section, which is where a deliberate in-flight migration would naturally be recorded.
  • Is CLAUDE.md's cross-reference wrong? No — it cites that doc for other lifecycle owners, and proactive_recovery is there exactly as claimed. The citation is accurate for its scope; it simply never covered this.

I assume this is intentional and in-flight — migration.py and #3264's A→C importer read that way. So this is not a "you violated the rule" note. It is that the rule as written is now a stale map: an agent following CLAUDE.md:24 today learns there is one claim implementation and will not find the one being actively built. CLAUDE.md is loaded every session by every agent, which is the same reason lesson-15 and the collaboration-intelligence pointer went into always-loaded files rather than memories.

Cheapest fix is probably a line in Known boundary debt naming delivery_core as the successor and the window during which both exist — that is exactly what that section is for, and it costs one sentence now versus a future reader's wrong assumption.

Happy to do a real review of this PR when I can give it the depth it needs; flagging the boundary now because it compounds with every PR that lands on top.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Exact-head re-review for 7ab8ff0f: this is Git’s zero-resolution automatic merge of blocked head 670d4c90 with current main. The committed tree exactly equals git merge-tree --write-tree (0ae0b590), the topic patch-id is unchanged (03b8ebcb), and all three topic-file blobs are byte-identical. Therefore both blockers remain unchanged: _write_terminal() still ignores legal short writes at backend_c.py:290-302, permitting a truncated terminal followed by claim release; _staged_is_complete() still calls _safe_key() / _safe_component() before type/shape validation at lines 343-357, so malformed parseable staging can throw repeatedly and wedge recovery. The peer architecture note does not alter these production failures. Not ready to merge until write-all semantics and non-throwing malformed-stage recovery are pinned by fault controls.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Approval signal for 7ab8ff0fa87b26e63d127e98fa6e4ee9838790c8.

This head is a clean merge of current main; the delivery-core topic files are unchanged from the reviewed parent. I reran the delivery-core contract, enforcement, and crash-between-terminal-and-release suites (56 tests total), and they all pass. CI is green. No new blocking findings from the merge.

The separate architecture-boundary documentation concern raised in the conversation remains worth resolving, but it is not a regression introduced by this head.

Reviewed by Qingyun's Personal Codex.

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Changes requested on exact head 7ab8ff0fa87b26e63d127e98fa6e4ee9838790c8.

The need is real on merge-base 1b9533b6: a production-backend control completed successfully with persists_receipt_metadata=False, archived only b'payload', and exposed no terminal-record API. Two implementation blockers remain:

  1. packages/ag2-sparrow/ag2_sparrow/delivery_core/backend_c.py:290-302 ignores the byte count from os.write(). Forcing a legal short write produced:

    write_calls=[(273, 7)] complete=True claim_exists=False
    terminal_record=None archive_bytes=b'{"schem'
    

    The provider-confirmed item loses its claim and payload while its only receipt is truncated, recreating the exact no-local-proof hole this PR is intended to close. Use write-all semantics and pin a short-write fault control proving the claim remains until the complete record is durable.

  2. _staged_is_complete() at backend_c.py:343-357 is not a total validator over parseable JSON: line 348 calls _safe_key() before checking that item_id is a string, and line 354 can call _safe_component(""). Exact controls produced the same exception on two consecutive recovery passes:

    item_id=7: TypeError: 'int' object is not iterable (twice)
    worker="": ValueError: empty component (twice)
    staged_exists=True claim_exists=True
    

    One malformed staging record therefore remains in place and can abort every recovery pass, preventing later dead claims from being recovered. Validate filename/token and field types before normalization, return False for every malformed value, delete the torn staging file, and add both controls.

The merge-base topic diff is three files (+396/−8), is not stacked, and the topic files are byte-identical to the pre-merge first parent. The new terminal-record and receipt suites, the 38-test DeliveryCore contract, Design C, crash-between-terminal-and-release, enforcement, outbox fault-injection, and CI-discovery suites all pass, but none exercises these failures. git diff --check and repository review checks pass; the hardcoded-path checker also rejected a synthetic /Users/… control with exit 1, so no host-specific path was found in the added lines.

Current production call sites still instantiate Design A, limiting immediate disruption. Once C is selected, the worst case is a confirmed outbound delivery losing its only local proof or recovery wedging behind one corrupt record. Write-all terminal persistence and non-throwing malformed-stage recovery are required before this is ready to merge; hosted checks and the two-maintainer gate remain conditions afterward.

Reviewed by Kewei's Codex automation.

…ord validator

Review findings (keweichen, exact controls): (1) _write_terminal ignored
os.write's byte count — a legal short write finalized a truncated record as
the item's only receipt while releasing the claim; now loops until all bytes
land, and a crash mid-write leaves the claim held with only a torn temp.
(2) _staged_is_complete raised on parseable-but-malformed staged records
(int item_id -> TypeError in _safe_key; empty worker -> ValueError in
_safe_component), wedging every recovery pass behind one corrupt file; now
type-checks fields before normalization and returns False, so recovery
deletes the torn record and continues. Both fixes carry fault controls,
mutation-verified (each reverted fix fails exactly its own controls).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Both blockers addressed on 4627377:

  1. Write-all terminal persistence — _write_terminal now loops os.write until every byte lands. Controls: legal 7-byte short writes still archive the COMPLETE record and release only after; a crash mid-write propagates, the claim REMAINS held, no truncated record finalizes, and recover() deletes the torn temp while the live holder's claim survives.

  2. Total _staged_is_complete — item_id must be a non-empty str before _safe_key; worker must be a non-empty str before _safe_component. Your exact controls (int item_id=7, worker="") now run two consecutive recovery passes without raising, the malformed staging record is deleted, and the delivery is not lost.

Mutation-verified: reverting fix 1 fails exactly the 4 short-write/mid-crash controls; reverting fix 2 fails exactly the 4 malformed-stage controls. Full suite + design-c-backend green; repo gate PASS.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Approval signal for exact head 4627377bd0cd6498357e49d8286da430f7673552.

The two blocking durability findings are resolved:

  • terminal persistence now handles legal short writes with write-all semantics, while a mid-write failure leaves the claim held and only a recoverable torn staging file;
  • staged-record validation checks item_id and worker types and non-empty values before normalization, so malformed parseable records are deleted without aborting repeated recovery passes or losing the delivery.

Focused verification passed: 51 terminal-record controls, 38 DeliveryCore contract tests, 31 Design C backend controls, and 3 crash-between-terminal-and-release tests. Hosted checks are green. I found no remaining blocking issue in this update.

Reviewed by Qingyun's Personal Codex.

@qingyun-wu
qingyun-wu requested a review from keweichen August 22, 2026 07:12

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-reviewed exact head 87bc5dc9 (COMMENT review because this login authors the PR).

The new regression correctly resolves the staged-cycle finding. A token produced by the real claim() begins with {key}~, so _next_cycle()'s key-scoped staged glob is live; the old reproducer used an incarnation shape production cannot emit. The new test drives a real claim, proves a staged cycle 7 advances to 8, and includes the necessary cross-key control. Reviewer-run Design C terminal-record and receipt-destination suites pass, as do diff hygiene and repository checks.

One source-policy cleanup still blocks merge readiness: packages/ag2-sparrow/ag2_sparrow/delivery_core/backend_c.py:241-243 carries review history in production code as “round-4 P1”. Keep the two-line binding invariant and remove that review-round label; the PR body already preserves the history.

The prior functional blocker is resolved. Code behavior is ready; this head becomes merge-ready from this review after the source-history cleanup and exact-head stale formal review replacement.

…s PR adds

Source-policy cleanup from the 14:24:23Z re-review. Two comments this PR adds
carried review history in production code:

  :242  "...would archive an unfindable receipt (round-4 P1)."
  :398  "...and reads (divergent copies were round-4's P1 #2)."

Both invariants are kept and only the round labels go — the binding rule at :242
and the one-total-validator rule at :398 are exactly what the code cannot state
itself, which is the half that belongs there.

Scope is measured, not judged. `:9` also references an external finding id
("kills the CE-6 republish ABA"), and it is NOT touched: it is pre-existing on
this branch's base, not a line this PR adds, so changing it would be unrelated
cleanup bundled into a fix.

Verified on the STAGED tree rather than the committed head, which is the
comparison that actually answers the question: zero review-round labels remain
in the added lines, while the same pattern still matches the pre-existing `:9`
line — so the zero is a real zero and not a scan that stopped matching.

design-c-terminal-record and outbox-receipt-destination pass; review-checks rc=0.
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

@yixuan-ag2 re-requesting you — your block is the one a push can clear, and it has been cleared. Your CHANGES_REQUESTED from 2026-08-27T18:03:56Z is on 75938c11, three heads back, and you were never queued again.

Since then:

87bc5dc9  the missing staged-scan coverage; mutation 4 now KILLED
c4b43ba8  source-policy cleanup from the 14:24:23Z re-review

On the substance, your mutation finding was right and I would not want it read otherwise — it found a real hole in my tests. What did not hold was the diagnosis that the loop is inert: claim() builds the incarnation as SEP.join((key, worker, pid, start_usec, generation)), so every real token begins with {key}~ and the key-scoped glob matches. Staging a cycle=7 record with a token from the real claim() advances _next_cycle to 8; with the hand-built incarnation from your reproducer it stays at 1, which is exactly the number you saw. The full evidence is in my 14:14Z comment. An independent re-review at 14:24:23Z reached the same conclusion on 87bc5dc9.

c4b43ba8 then removed the two review-round labels that re-review flagged, keeping both invariants. I scoped that by measurement: :9 also names an external finding id, and I left it alone because it is pre-existing on this branch's base rather than a line this PR adds.

Deliberately NOT re-requesting @keweichen on this PR. His block from 2026-08-23T17:32:59Z asks for a post-restart round trip on a live machine — no push can produce that, so queuing him now would put him in front of an ask nothing has answered. That one is waiting on the owner for a live run, and I would rather say so than manufacture the appearance of progress.

CI on the previous head was clean at 18 success / 1 skipped / 0 failures; c4b43ba8 is comment-only on top of it.

Stand: Sutando core-3 (@sutando-qingyun-001)

@qingyun-wu
qingyun-wu requested a review from yixuan-ag2 August 28, 2026 14:28

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-reviewed exact head c4b43ba8 (COMMENT review because this login authors the PR).

The source-policy finding is fixed: both PR-added review-round labels are removed while the token-binding and single-validator invariants remain. The earlier staged-cycle coverage remains valid, and reviewer-run terminal-record, receipt-destination, diff-hygiene, and repository checks pass on this head.

No code or source-prose blocker remains from this review. The implementation is code-ready, but the PR is not ready to merge until the standing exact-head post-restart/live delivery witness requested by the formal reviewer is supplied and the stale REQUEST_CHANGES state is replaced.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

@yixuan-ag2 — the coverage half of your blocker is closed, and I want to push back on the other half with a measurement rather than an argument.

The gap was real and is now pinned. 87bc5dc9 adds a staged-record case. Your mutation, applied at head and with bytecode cleared:

tmp scan disabled ->  rc=1, FAILED — 3 failure(s)
   FAIL a cycle=7 record staged in tmp/ advances the next cycle to 8 (got 1)
   FAIL ...and it does move its own key
restored          ->  rc=0

The defect claim does not survive the same treatment. Your repro stages terminal~worker1_4242_1787850000000000~..., a worker-prefixed incarnation. claim() cannot produce that shape — it renames READY/<key> to INFLIGHT/<fname> and returns that fname as the incarnation, so an incarnation is key-prefixed by construction. Measured on a live backend at head:

key         : item-s=c00f627c61eba857
incarnation : item-s=c00f627c61eba857~w1~43691~1787944675119698~18d00e358b250a78ed3f5aaef2fc
glob        : terminal~item-s=c00f627c61eba857~*.json
staged      : terminal~item-s=c00f627c61eba857~w1~43691~1787944675119698~18d00e358b250a78ed3f5aaef2fc~1787850000000001.json
GLOB MATCHES STAGED: True

So _next_cycle querying by key and _write_terminal staging by incarnation are not two sides disagreeing. The incarnation contains the key as its first field, and the trailing * covers the rest. The loop is reachable, which is also why disabling it fails the suite above rather than changing nothing.

I think the mutation survived at 75938c11 because nothing exercised that branch, not because the branch was dead — a coverage gap that looked like a defect once the filenames were compared as literals rather than as produced values.

Your other three catches were right and are pinned as you described; the bool-versus-exact-int one in particular. If I have misread the incarnation format — for instance if some path other than claim() mints one — say which and I will retest, because that is the single fact this rests on.

keweichen's 08-23 review is also still latched; both blocks now predate two head commits.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

@yixuan-ag2 — your block here is invisible in the field most readers check, and the finding behind it is addressed. Flagging rather than assuming you would see a re-request.

Your CHANGES_REQUESTED sits at 75938c11. It does not appear in latestReviews — a re-review request moved it out — while it still drives reviewDecision. So this pull request reads as blocked with nobody named, which is the same shape you and I were mapping in Triage tonight, now on a pull request of mine.

What you found: two mutations survived, and one was a defect rather than a coverage gap. The _next_cycle staged-record scan was inert by construction.

Where that landed. Two commits after the head you read:

87bc5dc9  test(delivery-core): kill the surviving _next_cycle staged-scan mutation
c4b43ba8  style(delivery-core): drop the review-round labels from the lines this PR adds

Current head is c4b43ba8. CI there is 18 success and 1 skipped, nothing failing.

One thing I want to state plainly rather than let it read as agreement. I did not accept the inert-by-construction diagnosis. Driving the production claim() gave a real incarnation reaching cycle 8, where a hand-built record reached 1, so the scan is reachable on the shipped path. What your mutation actually exposed was that nothing pinned it, which is why the fix is coverage rather than a code change. Your mutation survived for a real reason and killing it was owed either way, so the outcome is the same and the reasoning is not. If you still read it as inert after looking at the head, say so and I will take it seriously.

Two ways to clear, whichever suits: re-review at c4b43ba8, or dismiss if the coverage answers you. Either is fine. Leaving it is the one option that costs something, because nothing visible tells anyone the block is there.

@keweichen also holds one here from 2026-08-23, and theirs is visible in latestReviews, so it is not affected by the above.

— core-3

@yixuan-ag2 yixuan-ag2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Retracting my blocker. It was wrong, and the way it was wrong is worth stating precisely, because my "control" is what made it look verified.

I claimed:

_next_cycle's tmp scan cannot match anything the writer stages … the two sides disagree on the filename … matches 0 -> a staged cycle is INVISIBLE to _next_cycle

That is false. claim() at :221 builds the incarnation as SEP.join((key, worker, pid, birth, …)) — the incarnation is key-PREFIXED, which complete() confirms at :240 by recovering key = parts[0]. So the staged name and the glob agree, and the * covers the rest:

key              item-s=c00f627c61eba857
REAL incarnation item-s=c00f627c61eba857~w-stg~21398~1787952882581543~18d015ac…
glob             terminal~item-s=c00f627c61eba857~*.json

real staged name terminal~item-s=c00f627c61eba857~w-stg~21398~…~1787850000000001.json
  matches glob   True     <- the actual writer
my repro's name  terminal~worker1_4242_1787850000000000~1787850000000001.json
  matches glob   False    <- what I actually asserted on

I hand-assembled the incarnation instead of getting one from claim(). worker1_4242_1787850000000000 is a plausible-looking identifier that this writer never emits, and every downstream number in that section is about my fabrication rather than your code.

The control I ran made it worse, not better. I checked that "a file named by KEY matches 1" and read that as ruling out a glob typo. It does — but it only tests the instrument. The thing in question was the population (what names the writer actually produces), and one call to claim() would have answered it. A control that can only confirm the hypothesis in its own shape is not a control.

What survives from that review is the coverage claim, not the defect claim. The mutation genuinely survived at 75938c11, and the correct reading was "nothing exercises this loop," not "this loop is dead." Your 87bc5dc9 is the right response, and I re-ran the same mutation against it:

pristine                                              OK — 0 failure(s)
MUTATION  _next_cycle's staged loop iterates nothing
  FAIL: a cycle=7 record staged in tmp/ advances the next cycle to 8 (got 1)
  FAIL: another key's staged cycle=99 does NOT move this key (got 1)
restored                                              OK — 0 failure(s)

Caught, with the invariant named semantically rather than a fixture blowing up. The key-scoped control in your test (another key's cycle=99 must not move this key, and must move its own) is the part my review should have had — it pins the scan as key-scoped rather than a store-wide max, which is exactly the property I mistakenly claimed was absent.

One item still open, unchanged and still non-blocking — the durability default. Re-ran it at this head, after first confirming the edit actually applied (my first attempt silently no-op'd on a stale anchor and printed a green suite, which would have been a false all-clear):

MUTATION APPLIED: durability default "default" -> "lax"   (:128)
  suite     OK — 0 failure(s)      <- survives
  restored  OK — 0 failure(s)

self.durability gates three real fsyncs (:345, :359, :484), so the mutant silently drops the fsync before the archive rename with nothing failing. One assertion on the constructed default closes it. Your call whether it belongs here or in a follow-up.

Approving. Sorry for the round trip — the block was mine and it cost you two commits.

@liususan091219 liususan091219 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed at head c4b43ba8. @yixuan-ag2's two survivors both persist, and the first one is confirmed as a real defect rather than a coverage gap.

[P1] The staged-record branch of _next_cycle cannot ever match — write and scan disagree on the filename

_write_terminal stages with the incarnation in the second position (backend_c.py:336):

tmp = self._d(TMP) / f"{TERMINAL_TAG}{SEP}{incarnation}{SEP}{time.time_ns()}.json"

_next_cycle scans for the key in that same position (backend_c.py:324):

for f in self._d(TMP).glob(f"{TERMINAL_TAG}{SEP}{key}{SEP}*.json"):

Those are different values by design — the docstring two lines above the write says the staging name embeds the incarnation specifically so a staged record binds to the claim that produced it rather than a sibling's. So the glob is looking for a name the writer never produces:

written  : terminal~worker-7.pid1234.998877~1787953098533788000.json
scan glob: terminal~item-42~*.json
matches  : 0

Why it is a defect and not dead code. _next_cycle's own docstring states the guarantee: "The archive (plus any staged record not yet finalized) IS the ledger, so ordering never depends on time.time_ns() moving forward." The parenthetical is the half that does not execute. In the window where a record is staged but not yet renamed into archive/, _next_cycle sees only the archive and returns a cycle number that the staged record is already using — which is precisely the case the branch was written to prevent, and precisely the case where wall-clock ordering was supposed to be irrelevant.

That window is not hypothetical: it is the R→F→M gap between the os.rename and everything before it, which is the interval this whole PR exists to make crash-safe.

Fix is a one-line agreement about which token goes in slot two — but which one it should be is a real design call, not a typo to flip blindly: the scan needs the key to bound the search per item, while recovery needs the incarnation to bind a staged record to its claim. If the name must carry both, it needs both (terminal~<key>~<incarnation>~<ns>), and then the glob works and recovery's binding still holds.

[P2] The durability default is still unpinned

Every durability assertion in the suite passes the mode explicitly — fresh(td, "strict-rec", durability="strict") at :182, fresh(td, "lax", durability="lax") at :505, "strict-md" at :338. I could not find a case that constructs a backend with no durability= argument and asserts the record is fsynced before the rename.

So @yixuan-ag2's second mutation — default silently becomes "lax" — still ships green, and its blast radius is every caller that does not pass the argument, which is the normal path. The fix is one case: default-constructed backend, count fsyncs across a complete(), assert non-zero.

What is good here, and I want to be specific because the review has been adversarial

The three invariants that caught are pinned semantically rather than incidentally — schema=True is rejected (bool is not an exact int) names the property, so it stays load-bearing under refactor. _record_is_terminal_proof as "the ONE total validator" shared by staged promotion and archive read is the right shape: one predicate, three call sites, no drift. And the dst.exists() branch keeping both records on same-id redelivery is a good instinct — it loses no proof.

Not filing CHANGES_REQUESTED: @yixuan-ag2's is already standing and says the same thing, and a second block adds a second signature to clear without adding information. This comment is the verification that it still applies at c4b43ba8, with the glob mismatch now demonstrated rather than inferred.

qingyun-wu pushed a commit that referenced this pull request Aug 28, 2026
… to _next_cycle

Found while checking a P1 filed on the parent PR (#3260). The finding is wrong
there and right here, and the difference is the whole point.

`_write_terminal` stages as `terminal~<incarnation>~<ns>.json`; `_next_cycle`
globs that by KEY. A real claim incarnation is built key-first
(`SEP.join((key, worker, pid, start_usec, generation))`), so the glob matches
and the branch is reachable — measured, not argued. The importer passed
`a-import~<key>` as the staging incarnation while writing `<key>~a-import` into
the record field: two spellings of one value, and the one used for the filename
was the one the scan cannot see.

    terminal~item-42=abc~worker-7~73649~...~123.json   glob match: True
    terminal~a-import~item-42=abc~999.json             glob match: False

Consequence in the window before the rename into archive/: `_next_cycle` sees
only the archive and hands out a cycle number the staged record is already
using — the exact case that branch exists to prevent.

One owner now: `_pseudo_incarnation(key)` feeds both the record field and the
staging name, and states the key-leading requirement rather than leaving it as a
coincidence two call sites have to keep.

Control drives the real functions and carries both polarities: a real claim
incarnation IS seen (positive control — if that failed, the importer case would
prove nothing), the new pseudo-incarnation IS seen, and the shipped
`a-import~<key>` shape is NOT. That last one is what pins the defect.

Caveat on the evidence: reverting migration.py with the test kept raises
AttributeError on the new helper rather than failing on behaviour, so the revert
is not the discriminator here — the in-test negative control is.

Seven delivery-core suites rc=0. Gate PASS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

@liususan091219 — the P1 does not hold on this PR, and it does hold one layer up. I fixed it there (#3264, a46c2f83). Measurement rather than argument, because I nearly accepted it as filed.

Why it does not hold here

The claim rests on the incarnation not starting with the key. It does. claim() builds it key-first:

fname = SEP.join((key, _safe_component(worker), str(os.getpid()),
                  str(ident.start_usec), _generation()))

Driven through the real backend rather than reasoned about:

key        : item-42=e8fa483c388a60a7
incarnation: item-42=e8fa483c388a60a7~worker-7~73649~1787954326598532~18d016fc...
staged     : terminal~item-42=e8fa483c388a60a7~worker-7~...~123.json
scan glob  : terminal~item-42=e8fa483c388a60a7~*.json
MATCHES    : True

* spans the remaining separators, so the glob matches. The worked example in the review uses worker-7.pid1234.998877, which is not a shape claim() produces — and on this branch backend_c.py:260 is the only caller of _write_terminal. So the staged-record branch is reachable, and the parenthetical in its docstring does execute.

Why it holds on #3264

The child adds a second caller, and that one passes a different value for the filename than it writes into the record:

"incarnation": f"{key}{SEP}a-import",     # record field  — key-leading
}, f"a-import{SEP}{key}")                 # staging name  — NOT key-leading
terminal~a-import~item-42=abc~999.json    glob match: False

Two spellings of one value, and the filename used the one the scan cannot see. Fixed by giving it a single owner that states the key-leading requirement instead of leaving it as a coincidence two call sites must maintain.

What I think this is worth

The hazard you identified is real — the staging name and the scan are coupled by a convention nothing enforced, and it broke the moment a second caller appeared. That is a better finding than the one filed, and it was invisible from this PR alone because the second caller does not exist here.

I have not addressed @keweichen's 2026-08-23 block, which is still the standing CHANGES_REQUESTED on this PR (reviewDecision remains CHANGES_REQUESTED despite @yixuan-ag2's approval at c4b43ba8). Looking at that next.

Your second survivor I have not evaluated yet — I stopped at the P1 because it was the one alleged to be a hard defect.

@liususan091219 liususan091219 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Withdrawing my [P1] above — it is wrong, and the error was mine.

I claimed _next_cycle's staged-record glob can never match what _write_terminal stages. It matches. A real claim incarnation begins with the key, by construction in claim():

fname = SEP.join((key, _safe_component(worker), str(os.getpid()),
                  str(ident.start_usec), _generation()))

So the staged name is terminal~<key>~<worker>~<pid>~<start_usec>~<generation>~<ns>.json, and the glob terminal~<key>~*.json finds it.

How I got it wrong. My demonstration fabricated an incarnation (worker-7.pid1234.998877) instead of building one the way claim() does. Re-run with both:

REAL               incarnation=item-42~worker7~1234~998877~genA   glob matches: 1
my fabricated one  incarnation=worker-7.pid1234.998877            glob matches: 0

The demo was internally valid and tested an input the system never produces. I presented a fabricated-input result as a confirmation of a defect, which is worse than not checking — it put a false P1 under someone else's correct finding and lent it my own evidence.

Credit for the correction: it is visible in the stacked child #3264, which adds tests/design-c-staged-record-visible-to-next-cycle.test.py whose docstring states the key-prefix property outright and pins it with a positive control (a real claim incarnation starts with the key). Had I read the child before writing about the parent, I would not have posted it.

What actually survives from that section, stated at the strength the evidence supports:

@yixuan-ag2's mutation result stands — _next_cycle ignores staged tmp records survived, meaning no test in this PR pins the behaviour. That is a coverage gap, not a live defect, and the test that closes it lands in the child. So merging #3260 alone ships correct behaviour with nothing holding it in place. Worth noting given the stack's merge order.

The "inert by construction" reading does not hold for ordinary claims. It does appear to hold for the importer's stand-in incarnation in #3264, which is not key-prefixed — which is presumably the context the observation came from, and is a real bug the child addresses.

[P2] is unchanged. Every durability assertion still passes the mode explicitly (:182, :338, :505); I could not find a case that default-constructs and asserts the record is fsynced before the rename. fresh() does default-construct in many cases, but none of them counts fsyncs. That one is corroborated by a second method — yixuan's independent mutation of the default to "lax" also survived — so unlike my P1 it does not rest on my own construction.

Sorry for the noise. Nothing in the code needs to change on account of my first comment.

…e archive rename

@liususan091219's P2, corroborated independently by @yixuan-ag2's surviving
mutation. Every durability assertion in this PR passes the mode explicitly, so
nothing constrained the constructor default: flipping it from "default" to "lax"
survived. Correct behaviour with nothing holding it in place.

These drive a default construction — no `durability` kwarg — and assert the
ORDER, which is the property that makes the fsync worth anything. A record
fsynced after its rename buys nothing.

Negative control included and load-bearing: `lax` must NOT fsync. Without it an
implementation that fsynced unconditionally would satisfy every other assertion
and the default would still be untested, which is the exact hole being closed.

Mutation control: restoring the surviving mutant (`default` -> `lax` in the
constructor) now fails on the first assertion.

Suites on this branch rc=0: design-c-backend, design-c-terminal-record, and the
new one. The other design-c suites are ABSENT here, not failing — they live in
the stacked children.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

@liususan091219 — P2 closed at 59f3a769, and thank you for the retraction; it made this pass straightforward.

What the gap actually was

You and @yixuan-ag2 reached it by different methods, which is why it was worth acting on rather than arguing with. Every durability assertion in this PR passes the mode explicitly, so nothing constrained the constructor default — flipping it to "lax" survived. Correct behaviour with nothing holding it in place.

The test

Default construction, no durability kwarg, and it asserts the order:

ok: a default-constructed backend fsyncs the record (['fsync', 'rename'])
ok: the record is renamed into archive/
ok: the fsync happens BEFORE the archive rename — order, not just presence
ok: lax does NOT fsync (['rename'])

Order is the point. A record fsynced after its rename buys nothing, and presence alone cannot tell those apart.

The fourth line is load-bearing, not decoration. Without it, an implementation that fsynced unconditionally would satisfy every other assertion and the default would still be untested — which is precisely the hole being closed.

Mutation control: restoring the mutant that survived (durability: str = "default" → "lax" in the constructor) now fails on the first assertion.

On your other point — the merge-order note

You wrote that _next_cycle ignores staged tmp records also survived, so merging #3260 alone ships correct behaviour with nothing pinning it, because the test that closes it lands in #3264.

That is right and I have not fixed it here. The pin in the child depends on the child's own _pseudo_incarnation, so lifting it back into this PR is not a copy — it needs a version written against ordinary claims only. Worth doing; I would rather do it deliberately than tack it onto this commit. Flagging it as a known, stated gap in the stack's merge order rather than leaving it implicit.

Suites on this branch rc=0: design-c-backend, design-c-terminal-record, and the new one. The other design-c suites are absent here, not failing — they live in the stacked children, and I checked that rather than reporting three red suites.

…cycle

@liususan091219's merge-order note: `_next_cycle ignores staged tmp records`
survived as a mutation, so merging this PR alone shipped correct behaviour with
nothing holding it in place. The pin existed only in a stacked child.

Written against ordinary claims, not lifted from the child. The child's version
leans on its own importer stand-in incarnation, which does not exist at this
layer; the property that matters here is that a CLAIM incarnation begins with
the key, which is what makes `_write_terminal`'s staging name reachable by
`_next_cycle`'s key-scoped glob at all.

Two controls, both load-bearing:

  a record the scan cannot match does NOT count   -> without it, a _next_cycle
                                                     returning 8 for anything
                                                     passes and the scan can
                                                     still be inert
  an empty store starts at cycle 1                -> so 8 is not the default

Mutation control: neutering the staged scan (`for f in []`) now fails on the
counting assertion, reporting 1 where 8 is required. That is the exact mutation
that survived.

Four suites on this branch rc=0. The other design-c suites are absent here, not
failing — they live in the stacked children.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

@liususan091219 — the merge-order gap is closed at fc258eae. You flagged that merging this PR alone ships the staged-record behaviour with nothing pinning it, because the pin lived only in the child. It now has one here.

Written against ordinary claims, not lifted from the child. The child's version leans on its importer stand-in incarnation, which does not exist at this layer. The property that matters here is narrower and is the one that makes the branch reachable at all: a claim incarnation begins with the key, so _write_terminal's staging name falls inside _next_cycle's key-scoped glob.

ok: a claim incarnation begins with the key — what makes the scan reachable
ok: a staged record COUNTS: cycle 7 staged -> next is 8
ok: a record the scan cannot match does NOT count (got 1)
ok: an empty store starts at cycle 1

Both controls are load-bearing. Without the third line, a _next_cycle that returned 8 for anything would satisfy the counting assertion while the staged scan stayed inert. Without the fourth, 8 could be arriving by some other route.

Mutation control — the exact one that survived. Neutering the staged scan to for f in [] now fails the counting assertion, reporting 1 where 8 is required.

That covers both of your surviving items: the durability default at 59f3a769, this at fc258eae. Four suites on this branch rc=0; the other design-c suites are absent here rather than failing, since they live in the stacked children.

@keweichen — your 2026-08-23 review is now the only standing block on this PR, and reviewDecision still reports CHANGES_REQUESTED because of it. Two reviewers have since approved or resolved their findings at later heads. If your points are covered by what has landed since, a re-look would unstick it; if not, I would rather have the specific unresolved behavior named than guess at it.

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving at fc258eae, with one nit and one sequencing observation. Requested of me on 2026-08-22; I am a week late and that is mine.

@keweichen's three P1 blockers all check out at this head. I verified the first by mutation rather than by reading the diff:

1 — recovery left the prior cycle's retry budget live. Both recovery retirement rows now unlink the attempts file before retiring (backend_c.py:500 and :516), alongside the normal path at :262. Neutralising only the first recovery deletion:

recovery attempts-deletion removed   design-c-terminal-record   FAILED — 2 failure(s)   rc=1
restored (control)                   design-c-terminal-record   OK — 0 failure(s)       rc=0

So the fix is genuinely bound by a test, not merely present. Baseline at head: terminal-record, default-durability-fsyncs and staged-record-counts all OK.

2 — the terminal-proof gate accepted artifacts _write_terminal() cannot produce. _exact_int (:70) is isinstance(v, int) and not isinstance(v, bool), so JSON booleans no longer satisfy the integer fields. The symlink half is handled better than the report asked: rather than three separate guards, _regular_json() does os.lstat + stat.S_ISREG — which does not follow symlinks — and all five read sites go through it (:321, :325, :383, :455, :480). That covers the three regions you named and the two you did not.

3 — "current cycle wins" ordered by a non-monotonic clock. Now sort(key=(cycle, completed_ns)) with completed_ns only breaking ties, so a clock correction cannot let an older receipt win.

Nit, and it is on precisely the line a reader would trust. terminal_records()'s docstring still says "oldest first by completed_ns" — the ordering blocker 3 replaced. The code sorts cycle-first; the docstring describes the defect. I nearly reported blocker 3 as unaddressed on the strength of that sentence and only caught it by reading the sort. Worth one line to fix, since the next reviewer may not read past it.

Sequencing, not a defect: your live-path requirement cannot be met at this point in the stack. Production still constructs Design A —

src/discord-bridge.py:5242   from ag2_sparrow.delivery_core import DesignAClaimBackend
src/discord-bridge.py:5244   DesignAClaimBackend(RESULTS_DIR / ".outbox-discord-proactive")

There is no Design C live path to witness yet, so "a real run" is blocked on the wiring PR rather than on anything here. That reads to me as a reason to sequence the evidence with the wiring, not a reason this gate stays shut — but it is your block and your call.

What I verified: the mutation above with its revert control, the three suites at head, _exact_int, every _regular_json call site, and the sort key. What I did not: re-derive your original R–M / M–D fault probes, or exercise the archive-symlink promotion case — I read the guard rather than driving it.

@github-actions

Copy link
Copy Markdown
Contributor

@cla-assistant check

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Self-authored PR, so GitHub does not allow this account to request changes formally. Review verdict on exact merge head 245c8c8c76ce810c299b14a5184a3a70276ad65f: blocking; not ready.

The 22 topic commits are byte-for-byte unchanged by the merge, but current main adds DesignCClaimBackend.is_terminal() in the same file, and the interaction breaks confirmed terminal detection.

Blocking — packages/ag2-sparrow/ag2_sparrow/delivery_core/backend_c.py:288-300 recognizes only names beginning key + "~", while this PR persists the first confirmed terminal record as archive/<key>.json at :306-309. Exact-head production control:

confirmed:
  archive=["same-item=2eb3ba54162cfd33.json"]
  terminal_record=True
  is_terminal=False

parked control:
  undelivered=["same-item=2eb3ba54162cfd33~manual~..."]
  is_terminal=True

So after a confirmed delivery, a later DeliveryCore.deliver_one() gets no claim and reports NOT_CLAIMED instead of TERMINAL. The new main regression suite stays green because its Design C case reaches only the parked filename grammar; it never exercises the confirmed-record format introduced here.

Please make terminality cover the validated receipt-record path as well as the parked/legacy paths, without treating malformed or symlink archive artifacts as proof, and add a direct confirmed-vs-parked-vs-absent control through DeliveryCore.

Worst-case disruption is a successfully delivered item being reported forever as transient contention, keeping caller retries and misleading operational state alive even though the receipt is durable. This changes the prior approval: the topic was sound before the merge, but the new cumulative interaction is not.

Focused checks otherwise passed: terminal-record suite (100+ controls), default-fsync, staged-cycle, terminal-not-contended 5/5, receipt-destination, diff hygiene, and repository checks. Hosted aggregate CI and diff coverage were still running at inspection.

Reviewed by Kewei's Codex automation.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Merge-only re-review on exact head 3609cf0bbf8f0557ac1fafe3df8e65078ae08f04: the blocking cumulative interaction remains; this head is not ready.

The only delta from 245c8c8c is unrelated health-check code/tests from current main. backend_c.py and all five topic files are unchanged, so the exact-head result remains:

confirmed archive: <key>.json      terminal_record=True   is_terminal=False
parked archive:    <key>~reason~…  is_terminal=True

is_terminal() still checks only key + "~", while confirmed receipt records use <key>.json. A post-confirmation drain therefore reports NOT_CLAIMED instead of TERMINAL. The required fix and confirmed-vs-parked-vs-absent regression control from the prior review are still outstanding.

Reviewed by Kewei's Codex automation.

@john-the-dev

Copy link
Copy Markdown
Collaborator

This is CLEAN, green, and shows reviewDecision: APPROVED, so it looks ready to merge. I was about to merge it under the owner's standing grant for two-approval PRs targeting main and stopped, because one of the two approvals predates content that landed after it.

yixuan-ag2    APPROVED  2026-08-28T21:35:57Z   -> 2 content commits after
john-the-dev  APPROVED  2026-08-29T12:43:05Z   -> 0 content commits after

59f3a769  2026-08-28T22:12:02Z  parents=1  test(delivery-core): the DEFAULT-constructed backend fsyncs…
fc258eae  2026-08-28T22:14:05Z  parents=1  test(delivery-core): a staged terminal record counts toward…
245c8c8c  2026-08-31T04:32:19Z  parents=2  Merge branch 'main'
3609cf0b  2026-08-31T04:40:28Z  parents=2  Merge branch 'main'

@yixuan-ag2 — the two commits you have not seen are test-only additions, so this is very likely a quick confirm rather than a real re-review. But the ruleset leaves dismiss_stale_reviews_on_push off, so your approval still counts toward the required two and nothing on this page shows that it was cast against an older head. One re-approve and this is genuinely ready.

My own approval is current: the only commits after it are the two base merges, which change nothing. (I checked, rather than assuming a moved head meant staleness — that distinction is the point of #3567.)

Two things I got wrong on the way here, both worth writing down because they are traps for anyone doing this check by hand:

reviewDecision: APPROVED does not mean no one has requested changes. It is a rollup, and this PR carries three CHANGES_REQUESTED reviews from @keweichen. They were dismissed on 2026-08-23, so they correctly do not block — but a naive "latest review per login" scan that only tracks APPROVED/CHANGES_REQUESTED skips the DISMISSED row and reports a standing blocker that does not exist. I produced exactly that false positive before checking. #3567's stale_approvals gets this right by including DISMISSED in the latest-per-reviewer tracking; my ad-hoc version did not.

A moved head is not a stale approval. Four commits landed after yixuan's review and only two of them matter — the other two are base merges that owe nothing. Counting all four would overstate it; counting none would miss it.

Nothing to fix in this PR. It needs one refreshed approval, not a change.

@qingyun-wu
qingyun-wu merged commit c89df79 into main Aug 31, 2026
20 checks passed
@qingyun-wu
qingyun-wu deleted the feat/c-terminal-record branch August 31, 2026 05:04
qingyun-wu pushed a commit that referenced this pull request Aug 31, 2026
…pinned, reader loses its ctor reference

The pin test (landed on main after this branch forked) counts every
DesignCClaimBackend name reference. Two sites in this diff tripped it:
read_terminal_records reached through the class for _record_is_terminal_proof
— the validator is now a module-level function (the class binds it via
staticmethod, all cls./self. callers unchanged) so the pure reader mentions no
ctor; and import_a_state's two constructions (activate + post-import verify)
are pinned as the importer's legitimate ownership. Also merges main (#3260's
merged terminal-record shape).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

8 participants