Repository navigation
fix(watcher): a must-handle task whose payload was moved away publishes no terminal failure - #5736
Conversation
…es no terminal failure When task-orphan-check archives tasks/ while the standby watcher is still running the router on them, the handler fails on the vanished file and the watcher posted "could not safely process ... restricted runtime failed" to each task's room. The archive owns those tasks now, so the watcher releases its claim and publishes nothing. A genuine failure with the payload in place still publishes the terminal failure. Refs #5673 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 92%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changesNo lines with coverage information in this diff. |
sonichi
left a comment
There was a problem hiding this comment.
Verdict: approvable (reviewed at 7056fae5; comment-only review)
What I checked
-
The bug exists on main, and the test fails for the right reason. I reverted only
src/watch-tasks-stream.shto the parent and kept the new test. One check fails,vanished: no terminal-failure result is published. Its stderr shows the old branch firing:required Team handler failed for task-demo.txt (exit 4); publishing safe terminal failure. At HEAD all 7 checks pass, andtests/watch-tasks-stream-pool-writer.test.shpasses too. -
How "already archived" is decided (
src/watch-tasks-stream.sh:759-762).- The new branch is reached only when the handler failed, the claim is still ours, the verdict is must-handle and the run did not time out.
- It does not decide "archived" as such. It decides "the payload is no longer at
$task_path". - Moved by a legitimate claim: the worker-pool router delivers by sentinel and never moves the payload (
pool_router.py,pool_delivery.py).resolve_inbox_entry.pyreturns the payload's own path. The oldclaim_task.pyrename to.claimed-core-Nno longer exists in the tree, and nothing else renames the payload as a claim. - Other movers I found: orphan-check (archives DONE tasks, aggregates deferred ones into the owner DM), the bridges' archive-after-delivery, the codex NO-REPLY
mvindiscord-bridge.py, and the voice-cancel unlink intask-bridge.ts. Each one deliberately takes the task over. "The mover owns it" holds for all of them. - Timing: the
-echeck runs after the handler exits and is not atomic with publishing. A move that lands after the check falls through to the old publish, so a race only ever errs toward the old behaviour.
-
No double delivery.
- The branch writes no result, withdraws the pending hold (
abandon) and releases the claim through the sharedclaim_settled=1path. - The payload is gone from the inbox, so the watcher cannot dispatch it again.
- If orphan-check later re-queues it (
mv tasks/archive/<id>.txt tasks/), it gets one fresh run under a fresh claim and nothing was published the first time. At most one answer goes out.
- The branch writes no result, withdraws the pending hold (
-
CI: every check is green at
7056fae5(the Windows jobs were skipped).
Non-blocking
src/watch-tasks-stream.sh:760: the silence leaves only a stderr line. If a future mover (for example a manual cleanup or a workspace migration) moves a payload without answering, the task disappears with no durable trace. Consider also writing a receipt or an activity row so the owner can audit the "another owner took it" decisions.tests/watch-tasks-stream-vanished-payload.test.pydoes not assert that theabandonworker record was written. The pool-writer test covers it only by counting call sites. A one-line functional assert on the worker record after the vanished run would pin it.- The PR body says orphan-check archives Team-tier tasks with
[no-send]. Since v0.1.4,skills/task-orphan-check/SKILL.mdfolds Team/other-tier orphans into the aggregated owner DM. The conclusion still holds; only the description is out of date.
Worst-case disruption: a failed must-handle task whose payload was moved by something that does not answer it ends silently instead of with a refusal. Today every mover I found answers the task or deliberately silences it, so this is acceptable. The receipt above would mitigate it.
liususan091219
left a comment
There was a problem hiding this comment.
Ready to approve once the CLA check is present and green, and a write-access maintainer files an APPROVED review. The code is clean at 7056fae5.
What I verified
- Revert-to-fail: with only
src/watch-tasks-stream.shreverted to the parent,tests/watch-tasks-stream-vanished-payload.test.pyfailsvanished: no terminal-failure result is published. At HEAD all 7 checks pass, andtests/watch-tasks-stream-pool-writer.test.shpasses. - The new test is behavioral: it runs the real watcher with a handler that moves its own payload mid-run, and it carries a payload-in-place control that still publishes. It does not re-implement the unit under test.
- Scope of the new branch (
src/watch-tasks-stream.sh:759-762): it fires only on a failed must-handle run that did not time out, with the payload gone. The genuine-failure path (:767) and the timed-out path (:758) are unchanged. I agree with the existing comment review that a race only errs toward the old publish. - CI is green (Windows skipped). There are no unresolved threads and no hardcoded paths.
Not yet met
license/clais ABSENT at this head. The commit-status endpoint returnsstate: pendingwith zero contexts. That is not a pass. The commit email is the GitHub noreply form, so a CLA re-check should clear it.- There is no APPROVED review. The only review is a COMMENTED "approvable" from the owner, and a comment does not count toward the approval gate.
Non-blocking
tests/watch-tasks-stream-pool-writer.test.sh:59-60bumps a source-grep count (grep -c 'record_worker_done ... abandon') from 1 to 2. This is the REVIEW.md #14 shape. A functional assert that the vanished run writes theabandonworker record would pin the same thing by behavior.- The vanished branch leaves only a stderr line. A durable receipt would let the owner audit "another owner took it".
— Sutando (susans-MacBook-Pro)
qingyun-wu
left a comment
There was a problem hiding this comment.
Agent-assisted review (Sutando qingyun-001) of the whole PR at 7056fae.
What I ran:
tests/watch-tasks-stream-vanished-payload.test.py: 7/7 pass.tests/watch-tasks-stream-pool-writer.test.sh: passes.scripts/review-checks.sh: passes.- I inverted the
-e "$task_path"guard atsrc/watch-tasks-stream.sh:759. Both the vanished case and the control fail, so the test exercises the real guard.
Idempotency: the branch runs once per claim, writes no result, and the payload is gone, so it cannot be redispatched. If the payload reappears after the -e check, the code falls back to the existing publish path, never a second send.
Non-blocking, already noted by other reviewers: a failing record_worker_done abandon at :759-764 is swallowed (|| true), and the claim is released anyway. Only a stderr line records it. There is no double-publish risk.
Approving. The merge is the owner's call, after the post-restart round trip on an affected service.
|
REVIEW.md §15 post-restart round trip (live witness), run on a second host (Rui's Sutando II, Codex core) on 2026-10-20 UTC. Candidate Live round trips through the restarted gateway:
Edge probes on the candidate, as controlled fixtures:
Boundaries:
|
|
@cla-assistant check |
Priority
Level: high
Reason: P1 (master-DB row
rmvcrcbjtbh8et0): after a usage-limit stop, ~92 false "restricted runtime failed" replies were posted into team rooms in one burst.Closes
Refs #5673 (fixes its suggested fixes 2 and 3; fix 1, orphan-check honoring watcher claims, is not needed for this symptom — see below).
Summary
/startup's task-orphan-check archived the inbox (mv tasks/<id>.txt tasks/archive/) while the standby watcher was still running the worker-pool router on those same tasks.pool_route_handler.read_taskthen hitFileNotFoundError,guarded_mainreturned must-handle (4), andrun_handler_now(src/watch-tasks-stream.sh, theverdict 0/failedbranch) calledpublish_terminal_failure, which wrote "I could not safely process this Team-tier task because the restricted runtime failed…" as each task's result, delivered to its room.Fix: on a failed must-handle run, if the payload no longer exists, the watcher logs it, withdraws its pending worker-stage hold (
abandon) and releases the claim, publishing nothing. Whoever moved the payload owns the task now (orphan-check archives Team-tier tasks with[no-send]and aggregates owner-tier ones into one DM). A genuine failure with the payload still in place publishes the terminal failure exactly as before; the timed-out branch is unchanged.Why not the classify.py option: a "held when a standby claim exists" verdict only protects tasks claimed at the instant the classifier runs. The standby's sweep can still reach a task right after it is archived, which fails the same way, so the guard has to be where the failure is published.
Before / after evidence
New test
tests/watch-tasks-stream-vanished-payload.test.pyruns the real watcher with a handler that probes 4, then moves its own task file intotasks/archive/mid-run and exits 4.Parent
b028ebbb(fix reverted, test kept):HEAD:
(
__pycache__cleared before each run.)Test plan
Ran every
tests/watch-tasks-stream-*.test.{py,sh}plustests/task-dispatch-contract.test.pylocally on macOS (bash 3.2):handler-terminal-rc,dead-worker-reap,shutdown-leaves-inflight-claim,task-dispatch-contract, and the new test.watch-tasks-stream-pool-writer.test.sh: its structural count ofrecord_worker_done "$filename" abandonsites goes from 1 to 2 (the new branch withdraws the pending hold too). Expectation and its comment updated; it passes.origin/mainb028ebbbon this host, so they are pre-existing and not from this change:config-hot-reload(2/3 on both),fallback-receipt-scope(4/6 on both),interrupted-task-retry(8 failures on both),malformed-roster-row(same 3 FAILs on both). CI is the check for those.Not done: a live post-restart round trip (block a core on a usage limit, let team tasks queue under the standby, run
/startup). The change is one branch on the failure path of a must-handle run.🤖 Generated with Claude Code