Repository navigation
fix(watcher): every post-probe dispatch decision is logged; a stranded task is re-dispatched - #6183
qingyun-wu wants to merge 4 commits into
Conversation
…d task is re-dispatched A live core watcher received a task, ran the task-handler probe and then neither announced, ran nor held it, with nothing on stderr. Each decision after the probe now logs one line (the probe's rc and the outcome: announced, handled, or held with the live claim owner), and the silent hold paths name their reason. Each catch-up pass also re-dispatches a task still in the inbox a pass later that this watcher never admitted and that is neither held nor answered, so a drop with no known cause heals within two passes. Answered tasks are remembered per lifetime, so a long-lived inbox is not re-checked every pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sonichi
left a comment
There was a problem hiding this comment.
Requesting changes at 76ea8dc.
dispatch_note looks good: every decision now logs one line, and the held line names the live owner.
Blocking: redispatch_stranded (src/watch-tasks-stream.sh:1201) checks the identity of the inbox entry, but dispatch_task (:946) records the identity of the resolved payload. In a worker's deliveries/<w>/ inbox these never match, so every pending delivery reads as "never admitted" on every pass. When the payload changes while the delivery is pending, the worker gets a second TASK_FILE. That breaks the never-twice invariant through a path main doesn't have, because the worker's watcher doesn't watch tasks/. Repro, with fswatch silent and catch-up at 2s:
- Over 8s: 3 "re-dispatching" lines and 4 resolver calls here; 0 and 1 on main.
- After appending a line to
tasks/task-a.txt: TASK_FILE count 2 here, 1 on main.
Without an edit nothing is announced twice, but the pass still logs a false line and runs the resolver (up to 5s, inline) per pending delivery on every pass. An unreadable inbox file does the same, because the pass computes a different identity than the one recorded when cksum fails.
Suggested fix: at admission, record the inbox entry's basename (or its own identity) as well, and have redispatch_stranded skip anything this watcher ever admitted under that key. Re-announcing changed content stays the event path's job; this pass only needs to heal a task that was never admitted. Please add a worker-inbox case to the dispatch-decisions test: a pending delivery is never re-dispatched, and an edited payload is not announced twice.
Checked and fine: .accepted renames (no .txt, not found); a restart re-announces the same as main, since the startup sweep marks every pending task as sent; core-inbox edits already re-announce on main; pool-routed payloads stay marked as sent.
Not blocking: ANSWERED_NAMES grows with every answered task and is never pruned.
Tests (single files, clean env): dispatch-decisions 11/11, catch-up 10, degraded-identity 4, session-default 4, sentinel-identity 8/8 all pass. CI is green apart from one pending job.
(agent-assisted, Blue for Chi's seat)
Coverage Gate✅ No Python changes — nothing to gate (bar: 95% on changed lines). |
liususan091219
left a comment
There was a problem hiding this comment.
Requesting changes at 76ea8dc.
I checked the open change request separately against this head, and it holds:
- incomplete_fix: the stranded check and admission use different identity keys.
dispatch_tasksetstask_path="$resolved"(src/watch-tasks-stream.sh:927) and recordsidentity="$filename|$(task_file_identity "$task_path")"(:946), so the key is the resolved payload.redispatch_strandedcomputesidentity="$fn|$(task_file_identity "$f")"on the raw inbox entry (:1201). In an inbox whose entries are sentinels or deliveries, the two keys never match. So every pending entry reads as "never admitted" on every pass: it logs a falsere-dispatchingline, runs the resolver inline, and announces again if the payload changed. That last case breaks the never-twice invariant through a path main does not have. The same mismatch happens whencksumfails, because :946 falls back toinode:m<mtime>and :1201 has no fallback.- Ask: record the inbox entry's own key (its basename, or its identity) at admission, and have
redispatch_strandedskip anything this watcher has ever admitted under that key. - Ask: add a worker or sentinel inbox case to
tests/watch-tasks-stream-dispatch-decisions.test.shthat fails on this head, e.g. TASK_FILE count after the payload is edited while its delivery is pending.
- Ask: record the inbox entry's own key (its basename, or its identity) at admission, and have
- Merge conflict. GitHub reports
CONFLICTING/DIRTYagainst main. Please rebase.
dispatch_note is fine as it is: one line per decision, and the held line names the live owner. CI and CLA are green at this head.
— Sutando (susans-MacBook-Pro)
Preserve both sides of the sole CHANGELOG-PENDING.md conflict: the watcher dispatch entry and main's collaborator, remedy-timer, and Hatch entries. No executable conflict or manual code change. Sutando-Task: task-ca9dd8ad83eb5baa6d
sonichi
left a comment
There was a problem hiding this comment.
Re-checked at 576f532. The only change since 76ea8dc is the merge of main (the sparrow 0.6.3 release, hatch hook, worker-pool remedy timer and claude-local collaborators). src/watch-tasks-stream.sh and its tests are unchanged, so my blocking finding still stands: redispatch_stranded compares the inbox entry's identity with the resolved payload's identity recorded by dispatch_task, so in a worker's deliveries/ inbox a pending delivery is re-dispatched, and an edited payload is announced twice. The change request stays until that's fixed, with the worker-inbox test case.
(agent-assisted, Blue for Chi's seat)
liususan091219
left a comment
There was a problem hiding this comment.
Requesting changes at 576f532d. My change request of 2026-11-03 at 76ea8dc8 still holds, re-checked at this head.
The only commit since then is a merge of main. src/watch-tasks-stream.sh and tests/watch-tasks-stream-dispatch-decisions.test.sh are unchanged:
dispatch_taskstill records the resolved payload's identity (task_path="$resolved"at :927,identity=at :946, and the cksum fallback at :952).redispatch_strandedstill computes the raw inbox entry's identity at :1201, with no fallback.
incomplete_fix: in a worker's deliveries/ inbox those keys never match. Every pending delivery reads as "never admitted" on every pass, and an edited payload is announced twice.
- Ask: key admission by the inbox entry too, and have
redispatch_strandedskip anything this watcher ever admitted under that key. - Ask: add a worker-inbox case to the dispatch-decisions test that fails on this head.
The branch conflicts with main again. GitHub reports CONFLICTING after #6151 and #6131 landed. Please merge or rebase again.
CI is green at this head, license/cla is success (1 context), and there are 0 unresolved threads. dispatch_note is still fine as it is.
— Sutando (susans-MacBook-Pro)
|
@cla-assistant check |
|
@liususan091219 @sonichi Addressed the blocking findings at
Local verification ran each suite with
|
|
@liususan091219 — rechecked each point from your 576f532 change request at current head 7da5101. Agent-assisted by Sutando for this account.
All requested code/test/conflict fixes were already on the starting head; no new commit, empty push, force-push or merge was needed. Please re-review this head and replace the stale change request if these findings are resolved. These are isolated fixture tests, not a production restart/delivery witness; no live core operation performed. |
A live core watcher received a task, ran the task-handler probe on it, and then neither announced it, ran the handler nor held it. Its stderr had nothing on any of those paths, so the task sat in the inbox until it was found by hand. This makes every such decision visible, and makes an unexplained drop heal by itself. It builds on #4594's catch-up.
Why
Hardening of the task watcher's dispatch path, from a live investigation on 2026-11-02:
Correction to this PR's first description. It described live tasks as "dropped after the probe". Further evidence showed the watcher announced each of them 1–3 s after landing: its "picked up" activity row is written right after it prints
TASK_FILE. The delay and loss were downstream, in the hosting session receiving the watcher's stdout. The changes below don't fix that. They make the dispatch path explain itself, and they re-send a task that really is left unadmitted.Changes (
src/watch-tasks-stream.sh)One stderr line per post-probe decision.
dispatch_noteprintsdispatch <task>: <outcome>:probe rc=0: announced to the live core (an earlier run left a fallback receipt)probe rc=0: running the task handler, thenhandled by the task handler (exit 0)probe rc=4: running the required task handlerprobe rc=3: the handler declined it; announced to the live coreheld: claimed by live watcher pid N; retried in 30s, orheld: claim not acquired (no live owner: …). This is the formerly silentacquire_task_claimhold inrun_handler_now.handler exit N, and the claim is no longer this watcher's; its owner settles it, also formerly silent.held for an immediate retry: a dead watcher's claim on it was retired, the formerly silent hold inreconcile_dead_claims.Probe rc ≥ 5 already logged its fallback, and the other holds already logged their reason.
Stranded tasks are re-dispatched. fix(watcher): add bounded catch-up scan #4594's catch-up lists each entry once, by ctime, so a task whose dispatch dropped silently got one more try and then none.
redispatch_strandedruns in every catch-up pass. It re-dispatches a task that is still in the inbox from before the previous pass, if all of these hold:It logs
re-dispatching: still in the inbox a catch-up pass later and never admitted by this watcher. An unexplained drop therefore heals within two passes, about 60 s at the default.Answered tasks are remembered for the watcher's lifetime, both those the startup sweep refused and those the stranded check finds. Each answered task costs one
find-readyrun at most once. Without that, elCaptnCode's "a stale inbox does not slow it" case (40 answered pointers) failed: the renamed-pointer catch-up was never reached in time.Tests
New
tests/watch-tasks-stream-dispatch-decisions.test.sh(11 checks), using a silent stub fswatch, a 2 s catch-up, and a fake handler whose probe and run exit codes come from files:TASK_FILE;shutdown.sentinel, is not announced while the gate stands. Once the gate lifts, a later pass re-dispatches it with the line and announces it exactly once;Revert check: with main's watcher, 7 of the 11 checks fail. The decision lines are absent, and the stranded task is never announced.
Also pass on this host under
/bin/bash3.2.57:watch-tasks-stream-catch-up(silent and--noisy),watch-tasks-stream-catch-up-degraded-identityandwatch-tasks-stream-catch-up-session-default;watch-tasks-stream-handler-terminal-rc;watch-tasks-stream-foreign-inbox-keeps-sentinel,watch-tasks-stream-standby-keeps-session-sentinelandwatch-tasks-stream-role-session-kills-standby;watcher-sentinel-ownershipandwatch-tasks-stream-trap-exit;task-notifier-supervisor-standby-gate;hermetic-env-structuralandwatcher-fixtures-are-bound.shellcheck -S erroris clean.🤖 Generated with Claude Code