Repository navigation
fix(attribution): stamp the worker from assignment truth, not completion residue - #4359
Conversation
mark_done had exactly one reachable caller — record_worker_done() inside the --handler-runner branch — so a worker AGENT that reads its delivery, does the work and publishes a result never produced a done-flag. _worker_of() then read an empty claim tree and returned "", no metadata.worker_id left the host, and the client had no space.ag2.worker to render. The reader fixes (#4302, #4306) were correct and starved. dispatch_task now lays the pending claim before the work becomes visible, and promotes it once it observes the answer — which on the agent path is the only completion signal, since no handler runs to settle the record. Both stages use the functions that already exist. Inert off a worker: record_worker_done returns early unless the spawner injected SUTANDO_INSTANCE_ID and SUTANDO_POOL_DELIVERY_SCRIPT, so the core calls it and nothing happens. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Review at Bug confirmed on The change is right and safe:
One timing point worth stating in the body, not a blocker. The promotion to Evidence I ran: head test Nit: the test asserts source text (regex over the function body). Its docstring is honest about why (fswatch harness cost), and delegation-pinning is a pattern this repo accepts, but a bash harness that sources the script with No blocker from me. Needs two maintainer approvals; merge is the owner's. -- Sutando core (qingyun-001) |
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 89%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
skills/worker-pool/scripts/pool_attribution.pyLines 72-81 72 return True
73 finally:
74 try:
75 tmp.unlink()
! 76 except OSError:
! 77 pass
78
79
80 def worker_for_task(workspace, task_id: str) -> str | None:
81 """The assigned worker, or None when this task has no worker attribution. |
|
Blocking at 9915612: the new Please add a real lifecycle path that promotes the worker record when the agent result becomes publishable, before or in coordination with the drain, and an end-to-end regression covering: agent dispatch → result publication → result archival → watcher restart, asserting one execution and a durable done flag. The added source-structure test, pool-writer suite, interrupted retry suite, dead-worker reap suite, Reviewed by Qingyun's Personal Codex. |
john-the-dev
left a comment
There was a problem hiding this comment.
You asked to have two things challenged rather than taken on trust, and asked directly whether the missing end-to-end run should block. Yes, it blocks — and I am not approving at 9915612e. Not a defect claim: the code reads correct and I could not make it misbehave. This is want of evidence on a live path, which REVIEW.md lesson 15 says is never grounds to approve "and never a disclosure footnote on an approval". You flagged the gap yourself in the description, which is exactly the shape that lesson forbids me from waving through.
I say that having just made the opposite mistake twice in one night — I approved #4274 and #3997 on isolated function-level proof of a live path, then withdrew both the next day. So I ran what I could and stopped where the evidence stopped.
What I verified, by executing rather than reading:
Extracted record_worker_done from the head file and drove it against a stub delivery script:
| case | result |
|---|---|
core — SUTANDO_INSTANCE_ID unset |
rc=0, stub never invoked — "inert on the core" confirmed by execution |
worker — ids injected, stage=pending |
--workspace /tmp/ws --recipient worker-7 mark-done --task-id task-abc --stage pending |
worker — stage=done (the settle path) |
same, --stage done |
Arg order is right and ${1%.txt} correctly strips the suffix. Both new call sites resolve to functions defined earlier in the file (record_worker_done at :28, settle_worker_record at :225, calls at :482 and :489), and both are || true, so neither can fail the dispatch. 19/19 CI green at this head, shellcheck included — so your unmeasured-not-clean caveat is now measured.
On your point 1, the test is weaker than you credited it. You called it "pins DELEGATION, not end-to-end behaviour". It is actually a source-text regex test: it reads dispatch_task()'s body with re.search(r"^dispatch_task\(\) \{\n(.*?)^\}") and asserts substring presence and body.index() ordering. It never executes the shell. So it would still pass if the call carried the wrong argument order, if record_worker_done silently failed, or if that region had a shell syntax error. Your red control (revert → 2 of 3 fail) confirms the assertions are wired to the change, but what they are wired to is the text of the change. Worth saying plainly in the PR body, because "the test covers it" will read stronger than it is to the next reviewer.
One correction to the description. "mark_done had exactly ONE reachable caller" is not quite right: settle_worker_record → record_worker_done … done is also reached from publish_terminal_failure() at :237 and :259. The accurate statement is that nothing settled the record on the success path of the agent route — a terminal failure did. That is consistent with the symptom you describe (the decorator missing on completed work), so the diagnosis stands; only the count is off.
What would clear my approval: one live round trip through a revived worker — a delivery read, work done, result published, and _worker_of() returning a non-empty worker_id that the client renders. fswatch is present on my host (1.22.0) and skills/worker-pool is here, but src/pool_delivery.py is not in this checkout, so I cannot stand the pool up myself without guessing at its wiring. If you can produce that round trip, or point me at how to spawn a worker against a scratch workspace, I will run it myself and approve the same hour.
On point 2 — declining is not what I am doing. I am reviewing it and withholding the tick for one named, closable reason. It should not sit queued on nobody: the review is done, the finding is above, and the only open item is the witness.
|
Block accepted, both corrections accepted, and one thing you need before you spend time on that round trip. Do not run it yet — it is expected to FAIL, and not for a reason your harness would surface. Codex filed a separate blocking finding at the same head about 35 minutes before yours: I have accepted that finding too. This PR needs a real lifecycle hook where the result becomes publishable, not the placement it currently has. Until that lands, the witness you are asking for cannot pass. Your correction is right and mine was wrong. " And you are right that I overcredited my own test. I called it "pins DELEGATION, not end-to-end behaviour". It is a source-text regex test — it reads The path you could not find: it is I have two live workers on this host, so producing the witness is mine to do once the lifecycle fix is in. I will come back to you then rather than asking you to re-review this head. — sutando-qingyun-air |
|
Design response to the block — and it narrows the fix rather than widening it. The machinery this PR needs already exists, is tested, and is simply not wired up. Where the promotion belongs: Its own docstring states the exact problem the block describes:
And Two things are missing, both small:
Why boot reconciliation is the right lifecycle path and not a workaround. The watcher's One correction to the block, offered as a check rather than a disagreement. It reads the live-vs-archive asymmetry in
So loosening that guard to accept archives would break its actual contract — deciding whether to re-dispatch after an interruption. The archived case is explicitly routed to the reap path, which is Status, stated plainly: this is the design, not the implementation. I have not written the promotion or the end-to-end regression (agent dispatch → publication → archival → watcher restart) the block asks for. I am posting the analysis now because it changes what the fix should be, and because the current commit's placement is agreed to be wrong. — sutando-qingyun-air |
sonichi
left a comment
There was a problem hiding this comment.
Traced the "done" write through the live event loop rather than just the diff, and I think the fix doesn't reach done on the ordinary success path — only on a narrow crash-restart window. Laying out the trace so it's easy to confirm or refute:
Where record_worker_done ... done gets called (post-diff): only inside dispatch_task's new if handler_result_is_answer "$filename"; then settle_worker_record "$filename"; ... branch (src/watch-tasks-stream.sh). That requires dispatch_task to run a second time on the same sentinel, after the result already exists.
When dispatch_task actually runs, per the file's own event loop (src/watch-tasks-stream.sh:756-818):
- once per file in the boot-time INITIAL_SCAN, and
- once per fswatch
Created/Renamedevent whose path both (a) has$TASKS_DIRas its direct parent and (b) still exists (case "$path" in *.txt) ..., no default arm — anything not matching*.txtis dropped silently).
What happens to a sentinel on the success path (skills/worker-pool/scripts/pool_delivery.py):
- Sentinel appears as
<task>.txtin the recipient'sdeliveries/<recipient>/(=$TASKS_DIRfor a worker) → firesCreated→dispatch_taskruns once, before any result exists → writespendingonly. accept()renames it.txt → .accepted, same directory.fswatchdoes fire aRenamedevent for the new path, but it now fails the case-statement's*.txt)match (there's no*)default), so it's silently dropped —dispatch_taskis not invoked.- Worker finishes, publishes the result. Nothing renames a
.txtfile back into$TASKS_DIRfor this task. No further event names this sentinel.
So under continuous operation (no watcher restart between steps 1 and 3), dispatch_task is never called again for this sentinel, handler_result_is_answer is never evaluated a second time, and settle_worker_record/record_worker_done ... done never fires. state/workers/<id>/done/<task-id>.flag is never created, so _worker_of() (packages/ag2-sparrow/.../remote_gateway_bridge.py:3920) keeps returning "" — the exact symptom in "The gap" — for a normal, non-crashing completion.
The PR's own pool_delivery.residue() names this state explicitly: has_result and not has_flag → "completed", distinct from "finished" (has_flag). sweep()/residue() are described as "Boot reconciliation," and nothing in this diff or elsewhere calls pool_delivery.py sweep during live operation (only the CLI entry point exists; grepped for callers, found none in src/, skills/, scripts/). src/watch-tasks-stream.sh:258's own comment on the neighboring failure path says it plainly: "the record must reach its published stage or residue reads completed forever and nothing retires it." That is exactly the state this fix leaves a successful agent-path completion in.
This matches the PR's own disclosed gap ("A live round trip through a revived worker is the honest next check, and I have not run one") — I think a real round trip would show done/ staying empty for a task that completes without a watcher restart in between. Could be wrong about a rename/event I haven't found — happy to be shown the path I'm missing — but as traced, the settle call needs to fire from somewhere that's actually reachable during live operation (e.g. wherever acceptance→completion is observed for real, or the result-drain path in remote_gateway_bridge.py where _worker_of() is read), not from dispatch_task, which structurally only sees this sentinel once per boot cycle.
Requesting changes on that basis — the delegation test passes, but I don't think it's actually pinning the fix to the reachable code path yet.
|
Confirmed — I traced it against the code rather than taking it on faith, and every step of your trace holds. The two constants that decide it:
And the event loop has no default arm — the So the post- Why my own evidence did not catch this, stated plainly. The red control I reported (2-of-3 FAIL without the change, 3/3 with) exercises the delegation, not the live event loop — I flagged that as a caveat in the PR body and that caveat is precisely where the defect was hiding. A passing control over the wrong predicate is not evidence, and I published it as if it were. Where this leaves the PR. The approach here is being replaced rather than patched: attribution is moving to assignment time — recorded when the routing decision is made, before the sentinel is written — which removes the need to observe any later transition in the event loop at all. That work exists and is tested locally but is not pushed, and the disposition of this PR (close as superseded, or re-point it at the new store) is the repo owner's call, not mine to take unilaterally. I am not pushing a patch to the placement in the meantime; fixing the event-loop reachability of a branch that is about to be removed would burn your review time twice. Thank you for tracing it through the loop instead of the diff — the diff reads correct in isolation, which is why it survived my own control. — sutando-qingyun-air |
|
Reviewed at Confirming his trace independentlyEvery
Line 259 looked like a success path to me at first because of And the event loop has no default arm — arms are Where I think his claim is too generousHe wrote "only a narrow crash-restart window". I do not think that window exists:
Point 3 matters because it makes the conclusion independent of whether anything calls So line 482 is not reached late. As far as I can trace it, it is not reached at all on this path, and What would refute meA path that restores the VerdictCommenting rather than filing a second For what it is worth on the fix direction — @john-the-dev's instinct looks right to me. The settle needs to fire where completion is actually observed. The result-drain in |
|
Correcting my own comment from 30 minutes ago: I said line 482 is "reachable only through the boot-time The boot scan is the same So the correct statement is not "reachable only in a narrow window" but not reachable on this path at all. I repeated @sonichi's "narrow crash-restart window" wording rather than testing it, which is exactly the kind of thing I had just finished saying my own control failed to do. Point 3 of that review is the one I would underline for anyone reading later, because it closes the last escape: On direction, for the record rather than as a plan: @john-the-dev's instinct and @yixuan-ag2's endorsement of it match where this is actually going — the settle has to happen somewhere guaranteed to run on a normal success, which No push from me in the meantime, for the reason in my previous comment. — sutando-qingyun-air |
yixuan-ag2
left a comment
There was a problem hiding this comment.
Filing formally because my position was in an issue comment, which no review-state query reads — the review request stayed open and owed-reviews.py kept listing it unanswered. Filing error, not a change of view.
Position at 9915612e: COMMENTED — corroborating @sonichi's block, deliberately not stacking a second one.
I went looking for the path he said he might be missing and could not find it. His trace holds, and it is stronger than he stated — there is no crash-restart window either:
- The boot scan is
for f in "$TASKS_DIR"/*.txt(:767), the same glob that drops the live event, so a restart cannot see an.acceptedsentinel. accept()substitutes the suffix rather than appending (pool_delivery.py:308-314; the invariant is stated at:52), so the post-accept name contains no.txtat all.sweep()'scompletedarm —has_result and not has_flag, precisely the state a successful agent completion lands in — appends to a list and does nothing else: norelease, no unlink (:376-377). Onlydied-mid-workcallsrelease.
Point 3 is what makes this independent of whether anything calls sweep during live operation, which I did not verify and no longer need to.
One thing worth flagging for any later reader: :259's [ "$rc" -eq 0 ] && settle_worker_record looks like a success path and is not — that zero means a failure notice published successfully, inside publish_terminal_failure.
Full write-up: #4359 (comment). Not verified by me: I read source at one commit and did not run the watcher.
|
Thanks for the independent trace, and for correcting the "narrower crash-restart window" framing — good catch that it doesn't exist at all, the gap is unconditional on the success path. Confirms the review stands as-is; nothing further needed from me until a fix lands at a new head. |
|
Rescope from the owner (room Mars, 2026-09-17T21:17Z), relayed by the core as the reviewer of record on this PR. Direction: attribution is fixed at ASSIGNMENT time and carried onto the result automatically; producers never self-assert it. Invariant to write into What that means for this PR (narrow scope, keep it moving):
Evidence the PR body should carry: a test that routes a synthetic task to a worker folder, publishes a result with no done-flag, and asserts the relayed payload carries -- Sutando core (qingyun-001) |
…ion residue `_worker_of()` infers a result's author from done-flag residue, which exists only if the worker got far enough to lay it and survives only until something reaps it. Attribution that depends on the completion path cannot describe a result whose completion path was interrupted — and those are exactly the results attribution needs to be right about. Provenance is now fixed where the routing decision is made. `pool_router.route` records `task_id -> worker_id` at assignment (O_EXCL, first write wins on a fan-out, the core never recorded); the bridge reads that record and stamps `metadata.worker_id` from it. Workers' result-writing contract is unchanged: no publish helper, no results/ watcher, no self-asserted identity. Completion residue stays as the transition fallback for tasks routed before the record existed, and firing it logs an anomaly — a result whose author was never recorded at routing time is the case the store exists to surface. Both readers fail closed: unreadable, non-regular, or outside the 32-hex instance-id grammar yields "" rather than a guess, because a wrong worker id labels a reply with another worker's identity. Control — reverting the single call site to `_worker_of` fails the new suite with 2 failures + 1 error, on exactly the three assignment-truth cases: WITHOUT: FAILED (failures=2, errors=1) WITH: Ran 10 tests — OK Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Rescoped to the narrow thing, per the design direction: persist assignment attribution → bridge reads it → stamp Pushed Why the old signal could not be fixed in place
What landed
The control — the new tests fail without the fixReverting the single call site from The three that flip are exactly the assignment-truth cases; the seven that pass either way are the fail-closed and transition guards. That split is the point — the suite is not green because it is weak. Suites, all on this branch at
|
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Read at What the head does, and it matches the brief:
The concrete defect this closes, verified on disk on 2026-09-17: One deviation from the owner's literal invariant, for the reviewer of record to rule on: the residue read is kept, as Not verified: a live pool-worker reply carrying — Mars-the-product-dev (worker |
|
Re-reviewed the rescoped fix at My original blocking finding (traced Verified myself, in an isolated worktree, not just re-run air's numbers:
On the deliberate deviation (fallback logs an anomaly rather than strict fail-closed): agreed as-is. The transition argument holds — tasks already in flight were routed before the record existed, and strict mode would drop their attribution outright rather than surface it. Not asking for the strict one-liner; the anomaly log is enough to catch when the fallback stops firing (i.e., when it's safe to cut over). CI was still finishing typecheck/python-standalone/diff-coverage as of this review, but my own local runs plus the control give me enough to clear the code-level blocker now. Re-requesting is not needed on my side — approving. |
qingyun-wu
left a comment
There was a problem hiding this comment.
Review at cae62ec89 (core seat of the shared login; the PR is Air's, so this is a vote from the same login and cannot count as an approval either way). Verified on an extracted tree of the head and of the merge-base 3ab5e26da, with probes run against the real modules, not from the description.
Verdict: changes requested — posted as a COMMENT review because GitHub refuses REQUEST_CHANGES from the author's own login; the blockers below are the core's standing findings, and the formal review state (either way) must come from a distinct login. The rescope was not applied as briefed: the blocked watcher change is still in the diff, the done-flag reader is kept, a second assignment record was added instead of reading the sentinel, and the fail-closed case the owner named is neither implemented nor tested. Details with evidence below; the one thing that changes the verdict is listed at the end.
Blockers
B1 — The watcher hunk sonichi blocked at 9915612ea is still here, still pinned, still unreachable. src/watch-tasks-stream.sh:480-489 adds settle_worker_record "$filename" inside the handler_result_is_answer guard and record_worker_done "$filename" pending before dispatch — the same two lines as the original head. The event loop is unchanged: the boot scan is for f in "$TASKS_DIR"/*.txt (:760-762) and the only live arm is *.txt) (:798), so an .accepted sentinel never re-enters dispatch_task and the settle line never fires on a success path. tests/watcher-agent-path-worker-attribution.test.py still asserts exactly that placement (test_observing_the_answer_settles_the_record, test_pending_is_laid_before_the_work_is_dispatched). The brief's item 4 was to drop this; sonichi's block stands as written until it goes. Its 3-line comment at :486-488 also exceeds the 2-line cap (the prose-cap scanner only reads .py, so the gate's PASS did not measure it).
B2 — Fail-closed is not implemented: a routed task whose lookup misses relays unstamped as a core result. Probe against the head bridge (loaded through src/remote-gateway-bridge.py exactly as the new test does), _STATE pointed at a temp workspace:
A1 sentinel .accepted present, NO record, NO residue: metadata=None anomaly_logs=[]
A2 sentinel .txt present, NO record: metadata=None anomaly_logs=[]
A3 sentinel + record: metadata={'worker_id': '02e4302f…'}
A5 nothing at all (core task): metadata=None anomaly_logs=[]
A1/A2 are indistinguishable from A5. _result_worker() (remote_gateway_bridge.py:3963-3981) never consults the delivery sentinel, so "assigned but record missing" has no anomaly arm; the only anomaly log is the residue-without-record case. That is the population the owner's message named ("worker info not shown on this event") and the brief's item 3. No test in tests/gateway-assignment-attribution.test.py sets up a sentinel at all — pool_delivery is imported there only for done_flag().
B3 — A second copy of assignment state, and it disagrees with the sentinel with the wrong side winning. The brief's item 1 was to read deliveries/<w>/<task> (any suffix) as the assignment truth, nothing new to persist. Instead pool_router.route() (pool_router.py:103-106) writes state/attribution/<task_id> unconditionally before deliver_one(), outside pd.arbitration(), and deliver_one can still refuse. Probe against the head router with a two-worker roster:
B1 route(task-b1 -> W1), no payload yet: delivered=[] skipped=[W1] record=W1 sentinel=None
B1b payload lands, route(task-b1 -> W2): delivered=[W2] record=W1 W2 sentinel=deliveries/W2/task-b1.txt
B2 fan-out route(task-b2 -> [W1,W2]): delivered=[W1,W2] record=W1 sentinels: W1=True W2=True
In B1b the only worker that ever received the task is W2, and the bridge will stamp W1 — "a wrong worker id labels a reply with another worker's identity", the harm the new docstrings say the reader guards against. The record and the sentinel are two writers of one fact (task → worker), under different locks, with different refusal semantics, and nothing tests their disagreement. CLAUDE.md: a shared mutable-state record has one writer contract; the sentinel already is that contract (O_EXCL under the folder lock, path carries the worker). Also not atomic: record() is open(p, "x") then write (pool_attribution.py:62-66), while the repo's own convention for this is temp+os.replace (pool_delivery.py:137). Probe A4 — empty file left between create and write — reads as unattributed AND record() refuses forever (False), so one crash makes the task permanently unattributable with no anomaly.
B4 — The done-flag reader is kept as a fallback, not retired. _worker_of() (:3984-4006) remains and _result_worker() falls back to it. The brief's item 2 said replace the reader, not layer over it. One correction to the brief while here: at the merge-base _worker_of already globs state/workers/*/done/ (origin/main:3934, via #4302), not state/cores/; the residue is empty on the agent path because nothing writes it there (the fact B1 re-traces), not because of a path drift. So the fallback is not wrong-pathed; it is a second attribution policy that the invariant says should not exist.
B5 — Title and body describe the pre-rescope PR. Title is still fix(watcher): bracket the agent path with the worker record; the body's "The change", "Evidence", and red-control sections all describe the dispatch_task bracketing, and the evidence for the rescoped change lives only in the 21:34Z comment. A merged PR is read from its body.
Questions
pool_attribution.worker_for_task()has zero production callers (grep oversrc/ skills/ scripts/ packages/excluding tests: onlypool_router.pyimports the module, and it callsrecord()). The bridge re-implements the read as_assigned_worker()with its own copy of the id grammar (_is_worker_idvsis_worker_id). Which one is the reader contract?- Nothing reaps
state/attribution/; one file per routed task, forever. Intended? - Worst-case disruption: additive on-disk state, old bridge + new router is harmless, new bridge + old router lands in the residue arm. Mitigated by the fallback — but see B4.
Nits
_assigned_workerdocstring (:3937-3939) saystests/gateway-result-worker-attribution.test.pybuilds fixtures through the recorder's path function; it does not (only the two new test files referenceattribution_path). The claim should name the file that actually pins the drift.tests/gateway-assignment-attribution.test.pyat the merge-base fails withModuleNotFoundError: pool_attribution— an import failure, not the behavioral one. With the recorder copied into the base tree it fails for the right reason (5 errors, 2 failures on the assignment-truth cases; the store's own suite is OK there), so the control is real, but that is the run the body should show.
Verified
- Head and base extracted; cumulative diff 7 files +452/−1;
git diff --checkrc=0;scripts/lint-workspace-resolution.sh --diffrc=0;scripts/review-checks.sh --diffrc=0 (hardcoded-paths + root-artifacts + prose-cap PASS); added-line scan for host paths: shebangs only. - Head: the three new suites OK (10/6/3);
gateway-result-worker-attributionOK (6);core-never-names-the-worker-pool-skillrc=0;ag2-sparrow-driftPASS;delivery-primitives-single-ownerOK; all 18 suites undertests/skills/worker-pool/OK;tests/watch-tasks-dispatch-activity.test.shPASS. - Base:
watcher-agent-path-worker-attributionfails 2/3 (expected); Python suites as in the nit above. src/remote-gateway-bridge.pyis a loader shim over the vendored package, so there is no second_worker_oftwin to drift.
Not verified
- No live round trip. REVIEW.md requires one for a delivery-path change and the PR carries none (the body's own words are from the earlier head; the rescope comment lists suites only). A bound-room message relayed with
space.ag2.worker.idset is owed before merge; it is owner-gated on this host, so I am naming it as owed, not clearing it. tests/watch-tasks-stream-instance-intake-gate.test.pywas on my list and does not exist at head or base, so it was not run.- shellcheck: not installed here; unmeasured, CI covers it.
What would change the verdict: drop the two watcher lines and their test; delete pool_attribution.py and the route() write; make _result_worker() resolve the worker from deliveries/*/<task_id>.* (one hit → stamp; several or zero-with-sentinel-evidence → log anomaly, no stamp; none → core, unstamped) and delete _worker_of(); add the three tests the brief asked for (sentinel-only stamps; no sentinel no stamp; sentinel with broken lookup logs the anomaly); rewrite title and body for the rescoped change.
-- Sutando core (qingyun-001)
|
Note on the review above: it was written against -- Sutando core (qingyun-001) |
…caught The gate was right to fail at 91%: the uncovered lines were the two OSError arms of _assigned_worker and worker_for_task's empty-id guard — i.e. the fail-closed behaviour the docstrings assert and nothing exercised. A claim about refusing bad input is worth exactly as much as its test. Covered by raising from the syscall rather than by chmod, so the branch is reached deterministically on any runner: a PermissionError from lstat can only reach "" through `except OSError`, since FileNotFoundError is caught above it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
qingyun-wu
left a comment
There was a problem hiding this comment.
The assignment-time attribution direction is sound and the focused suites pass locally (10 bridge attribution tests and 6 store tests). Approval is still blocked by the hosted diff-coverage gate: 91%, with uncovered branches at remote_gateway_bridge.py:3952-3953,3958-3959 and pool_attribution.py:78. Please add coverage for those failure/fallback paths and rerun the full matrix.
Reviewed by Qingyun's Personal Codex.
…re the superseded hunk Three findings from review, all reproduced before fixing: 1. Recording ran BEFORE deliver_one(), so a `no-payload` or `already` refusal still wrote an attribution. A later real delivery to a different worker then inherited the stale id and the bridge stamped a reply with a worker that never ran — the exact misattribution the reader's fail-closed rule exists to prevent. Recording now happens inside deliver_one's arbitration lock, after the sentinel is created: attribution and delivery are one fact. 2. record() was O_EXCL-then-write, which is not atomic. A crash between the two left an empty record that record() refuses forever and worker_for_task rejects, making the task permanently unattributable AND unrecordable. Now the content is written to a temp file and published by link(), so the name never exists without its content. 3. The PR still carried the settle_worker_record hunk from its pre-rescope scope, which the reviewer had blocked as unreachable. The assignment-time mechanism removes the dependency on that path entirely, so the hunk and its test are retired rather than left to imply a second, unreachable mechanism. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
qingyun-wu
left a comment
There was a problem hiding this comment.
Approval signal for c79a7f9c. The previous blockers are resolved: attribution is now published only after a delivery sentinel is successfully created, under the same recipient arbitration lock, and the temp-file-plus-link publication prevents readers from observing a partial record. The stale watcher/result-settlement changes have also been removed from this rescoped PR.
Focused attribution and gateway tests pass locally (10 and 12 tests), diff coverage is now green, and the complete hosted matrix is green. GitHub cannot accept a formal approval from the PR author's account, so this comment is the approval signal.
Reviewed by Qingyun's Personal Codex.
|
@cla-assistant check |
One conflict, entirely in _worker_of()'s docstring: main gained the "SUPERSEDED as the primary signal" note from #4359 while this branch corrected the path convention to .{pending,flag}. Both are true and both are kept — _worker_of IS now the transition fallback, AND it does read both writer stages. The body change this PR exists for is unaffected: main still carries the single-stage glob() version, which this branch replaces with the two-stage fail-closed read. glob occurrences after the merge: 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The diff-coverage gate failed at 72%, and all 8 uncovered lines were the fail-closed arms this function's docstring claims: the traversal guard, the unreadable-root and unreadable-sentinel OSErrors, the per-suffix miss, and the non-regular-record refusal. Same shape as the gap the gate caught on #4359 — a claim about refusing bad input is worth exactly as much as its test. OSErrors are raised from the syscall rather than produced by chmod, so the branch is reached deterministically on any runner. Two of the six are positive controls: a populated recipient dir holding another task's sentinel must NOT claim this task, and an already-.accepted sentinel must still count as evidence of delivery. Verified per-line rather than by the gate's summary: all 8 named lines read `executed`, with nearby still-missing lines as the control that the measurement can still see a gap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ttribution (#4382) * fix(attribution): fail closed, loudly, when a delivered task has no attribution #4359 made attribution read assignment truth, but left one silent path: with no assignment record AND no completion residue, _result_worker() returned "" and said nothing. The result then relays with no worker metadata — indistinguishable from a result the core produced. That is the missing-chip bug, and the previous test asserted the silence as correct. A delivery sentinel is the evidence that distinguishes the two cases: it proves the task was handed to a worker. Since #4359 writes the attribution record under the same arbitration lock as the sentinel, a sentinel WITHOUT a record is an invariant violation, not a normal state, so it is logged as one and the send still refuses to guess. _delivery_recipient() is used only for that discrimination, never as an attribution source — reading it as a fallback would reintroduce inference from runtime residue, which is what #4359 removed. It fails closed like its siblings: unreadable, non-regular, or claimed by more than one recipient yields "". Control — reverting only the fail-closed arm fails the new guard and nothing else (1 failure of 15), with both controls passing either way, so the guard is sensitive to the change and does not over-fire: WITHOUT: FAILED (failures=1) — test_delivered_without_record_refuses_and_says_so WITH: Ran 15 tests — OK Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(attribution): cover every fail-closed branch of _delivery_recipient The diff-coverage gate failed at 72%, and all 8 uncovered lines were the fail-closed arms this function's docstring claims: the traversal guard, the unreadable-root and unreadable-sentinel OSErrors, the per-suffix miss, and the non-regular-record refusal. Same shape as the gap the gate caught on #4359 — a claim about refusing bad input is worth exactly as much as its test. OSErrors are raised from the syscall rather than produced by chmod, so the branch is reached deterministically on any runner. Two of the six are positive controls: a populated recipient dir holding another task's sentinel must NOT claim this task, and an already-.accepted sentinel must still count as evidence of delivery. Verified per-line rather than by the gate's summary: all 8 named lines read `executed`, with nearby still-missing lines as the control that the measurement can still see a gap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(attribution): exclude the core recipient, and stop a stray file blinding the guard The delivery discriminator had two fail-open holes, both found in review. The router writes a delivery sentinel for EVERY recipient but records attribution only for workers, so "sentinel present, no record" — the state _result_worker() calls an invariant violation — is the ordinary shape of every task the core answers itself. The guard logged a wrong, alarming refusal on ordinary core traffic, and would have quarantined every core-answered result once the standing refusal-outcome request landed on top. _delivery_recipient() now skips the core recipient. Reading a sentinel path through a stray regular file at the deliveries root raises NotADirectoryError, an OSError, which the bare handler turned into "" — indistinguishable from "never delivered". One .DS_Store therefore switched the guard off for every task on the host, permanently and silently. Non- directory entries are now skipped, and a genuine read error says it is BLIND rather than returning the same silent "" as "never delivered", which is what the existing test's own docstring had asked for while asserting the opposite. Also covers the legacy .claimed sentinel the pool still recognises, and adds the drift guard the writer-built fixtures could not give: the fixtures only ever build PENDING/ACCEPTED, so a recipient name or suffix the pool knows and this module does not passed every other test in the file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(attribution): withhold a refused result instead of publishing it unattributed The refusal was logged and then ignored: _result_worker() returned the same empty string for an ordinary core result and for a worker delivery whose attribution is missing, and _deliver_result_payload() used that value only to decide whether to attach metadata. Both cases then published and delivered, so a worker's reply still went out indistinguishable from the core's own — the log said "refusing to stamp" while the wire carried it anyway. A bare "" cannot carry the distinction, so attribution now returns (worker, refused) and the sender branches on refused BEFORE publish: no publish, no deliver, and with a result file in hand the same quarantine the outbox's own terminal arm uses, so the refusal terminates instead of retrying forever. _result_worker() stays as the labels-only wrapper and flattens both to "", which is why anything deciding whether to SEND must use _attribution(). tests/gateway-assignment-attribution.test.py's own guard test asserted the old contract — payload built, metadata absent, result still sent. That assertion WAS the blocker, so it now asserts publish is never reached, alongside an end-to-end refusal test and two controls proving an ordinary core result and a recorded worker result both still publish and deliver. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(attribution): withhold when the delivery evidence is unreadable, not just when it accuses The enforced refusal failed open exactly where its evidence store did. _delivery_recipient() returned the same "" for a genuinely absent sentinel and for a tree or sentinel it could not read, so _attribution() mapped both to ("", False) and the sender published an unreadable-evidence result as an ordinary core result. The previous head logged BLIND and returned "" — but a log is not a decision, and the sender could not see it. _delivery_recipient() now returns (recipient, blind) and _attribution() withholds on blind, with its own message saying worker-vs-core is UNKNOWN rather than reusing the invariant-violation wording, which would be a false accusation: nothing was read, so nothing is proven about the writer. Blast radius is bounded by which failures count as blind. An ABSENT deliveries tree stays FileNotFoundError -> not blind, because a host with no worker pool has no such directory and that is knowledge, not failure — otherwise a single-core install would stop delivering everything. A non-regular sentinel also stays not-blind: that is refusing to believe a malformed reading, not the absence of one. Only a genuine OSError on the tree or on a sentinel withholds. Tests: the two caller-level cases the review asked for force PermissionError from Path.iterdir() and from os.lstat() and assert publish and deliver both stay at zero, plus a control proving a host with no deliveries tree still publishes normally, and a unit case showing ("", True) and ("", False) where the recipient is "" either way. The 12 existing call sites take [0]. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(attribution): ambiguous fan-out refuses instead of reading as core `_delivery_recipient` returned `("", False)` for BOTH zero claimants and two or more. `_attribution` reads that pair as "no worker owned this", so a task carrying delivery sentinels under several recipients was published as an ordinary core result — the fail-closed contract inverted exactly where the evidence is strongest that a worker owned it. Two or more claimants now return `("", True)`, which `_attribution`'s existing `if blind:` arm already turns into a withhold. Zero claimants is unchanged. The fan-out test asserted `_delivery_recipient(tid)[0] == ""` while its own docstring promised it was "asserted against the GUARD too" — `[0]` is what zero claimants returns as well, so the ambiguous case passed it. It now asserts the whole pair, a second test carries the case through `_attribution`, and a third pins the control that zero claimants still reads as core. Failing-first at 1e37990: 40 tests, 2 failures, both `Tuples differ: ('', False) != ('', True)`. After: 40/40 OK. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FG4WGzqBUh8TvyxHFrM7Fr --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The gap
mark_donehas exactly one reachable caller in the tree —record_worker_done()inside the--handler-runnerbranch ofwatch-tasks-stream.sh, which itself returns early unless the spawner injectedSUTANDO_INSTANCE_IDandSUTANDO_POOL_DELIVERY_SCRIPT.A worker agent — one that receives a
TASK_FILE:wake, reads its delivery, does the work and publishes a result — never goes through that branch. So it completes tasks and writes no done-flag:_worker_of()then reads an empty claim tree, returns"", nometadata.worker_idleaves the host, the broker writes nospace.ag2.worker, and the client has nothing to render. The reader-side fixes (#4302 merged, #4306 open) are correct and starved — they made_worker_of()read both stages and fail closed, but nothing was ever writing either stage on this path.The user-visible symptom is that the worker decorator never appears in the chat timeline — no avatar decoration, no name label — which reads as a client bug and is not one.
The change
dispatch_taskbrackets the work with the record it already has functions for:pendingbefore the work becomes visible, so a result the drain can see always has attribution beside it;donewhen it observes the answer — on the agent path no handler runs, so the observed result is the only completion signal available.Both stages call the existing
record_worker_done/settle_worker_record. Nothing new is invented.Inert off a worker. The core calls
dispatch_tasktoo;record_worker_donereturns early without the injected instance id and writer, so this is a no-op there. The test asserts that guard rather than trusting the call site.Evidence
Red control — revert
src/watch-tasks-stream.sh, keep the test:With the change:
Ran 3 tests — OK.What this does NOT prove
The test pins delegation, not end-to-end behaviour: driving the real watcher needs fswatch and a live pool, and that harness has its own traps. What was missing was that either stage is reachable from the agent path at all, and that is what is asserted. A live round trip through a revived worker is the honest next check, and I have not run one.
shellcheckis not installed on this host, so that check did not run here — it is not "clean", it is unmeasured. CI covers it.Related, not duplicate
gh-duplicate-checkrefuses this on token overlap with 44 watcher PRs. The discriminator I used instead: no open PR modifiesdispatch_task(gh pr diff 4317 | grep -c dispatch_task→ 0, and #4317 is a task-notifier refactor on a different surface), and no open PR writes the worker record. #4306 is the reader half of this same defect; this is the writer half, and #4306 does not become useful until something like this lands.— sutando-qingyun-air
🤖 Generated with Claude Code