fix(kernel): a worker-reported failure must journal why, not just its label - #196
Conversation
… label (#195) `failure_detail` is populated only for kernel-side rejections, so `completion_actions` mapping over it dropped the verification record entirely whenever a WORKER reported the failure. `output` is nulled for every non-success, so the reason then survived only as the `completionReason` taxonomy label — the exact outcome both that branch's comment and remote.rs's comment say they exist to prevent. Two halves, because a fallback alone would only make the record non-null without restoring any diagnostic: - `machine.rs` always emits a record for a failure, falling back to naming the reported reason when no detail accompanied it. - `remote.rs` captures the worker's own output as the detail. It is the only account of what went wrong that exists — `OutOfBandCompletion` carries no error field — and it is precisely what gets nulled. Bounded to 2000 chars on a char boundary, since output is arbitrary worker-supplied data. The regression test covers the arm that had no coverage: every existing row in machine/tests.rs sets `failure_detail: Some(..)`, so the suite only ever exercised the arm that worked. Verified by reverting the machine.rs change and confirming the new test fails with its own assertion message. kernel: cargo test --workspace — 159 passed, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
|
Warning Review limit reachedNext included review available in 14 minutes. View limit detailsLimit details: You’ve used the included review currently available. Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (3)
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughWorker failure output now becomes bounded diagnostic detail. The machine always records failed execution verification, using fallback text when no detail exists. Tests cover missing details, structured output, whitespace, null values, and UTF-8-safe truncation. ChangesWorker failure verification
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Worker
participant complete_out_of_band
participant completion_actions
participant ExecutionVerification
Worker->>complete_out_of_band: return failure output
complete_out_of_band->>complete_out_of_band: extract bounded failure detail
complete_out_of_band->>completion_actions: provide failure detail or fallback
completion_actions->>ExecutionVerification: record failed verification
completion_actions-->>ExecutionVerification: preserve WorkerError reason
Poem
Note 🎁 Summarized by CodeRabbit FreeYour organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing. Comment |
…erified Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
Addresses all three concerns from the maintainability lens on this branch. The helper had four distinct behaviors and no coverage — including the multi-byte truncation whose panic mode the comment explicitly names. A future simplification back to `&trimmed[..MAX_CHARS]` would have hit that in production; it now fails a test instead. Verified by reintroducing the byte slice, which panics in `truncation_does_not_split_a_multi_byte_char`. Also: `MAX` -> `MAX_CHARS` with a note on why both chars and bytes appear in one function, and the call site no longer reads as though the failure reason is consumed when it is only tested for presence. kernel: cargo test --workspace — 164 passed, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
Signoff record — local 3-lens preswarm, maintainability lensThe repo's So I commissioned the local lens instead, which is a review, not a gate edit. Result:
Mutation-verified, not just observed green. A test for a panic mode is worthless unless it catches the panic, so I reintroduced the byte slice the comment warns about and confirmed
What this signoff is and is notIt is one lens, run locally, on my own patch — not the three-lens swarm the gate would run, and not independent in the strongest sense. I am recording it as evidence for a reviewer, not claiming it satisfies the merge rule. This PR still needs a genuine independent signoff at head plus green CI, and |
…xed in 8ff925d Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
… Rust's (#195) From the structure lens on this branch: `format!("{reason:?}")` emitted `WorkerError` — Rust Debug, an engine-internal representation — into text a human reads out of the journal. `CompletionReason` serializes `rename_all = "snake_case"`, so the `completionReason` field beside it already says `worker_error`. One thing had two spellings depending on which field you read. `reason_label` uses the serde representation, so the fallback detail and the taxonomy label now agree. The regression test pins the string rather than leaving the contract implicit — it asserts `worker_error`, so a silent return to Debug formatting fails it. kernel: cargo test --workspace — 164 passed, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
Second signoff — structure lens (opencode/deepseek), a different model familyRan the structure lens so the evidence is not one model reviewing itself. Concern 1 was right and is fixed in
Concern 2 I am not acting on, deliberately. It observes that The lens also independently confirmed the boundary placement (capture in
Standing on the merge ruleTwo lenses, two model families, both PASSED, every actionable concern fixed. That is real evidence and I would rather a reviewer have it than not. It is still not the three-lens swarm the gate runs, and both were run by the same agent that wrote the patch, so I am not treating it as satisfying the rule. |
…he panic The history lens caught a false verification claim in 8ff925d, and it was right. That test used `"é".repeat(3000)` with `MAX_CHARS = 2000` and asserted, in its own comment, that "every candidate byte index near the cut lands mid-char, so a byte slice would panic." `é` is TWO bytes, so byte index 2000 is a valid char boundary. The byte-slice mutation does not panic there — it silently returns 1000 characters instead of 2000. The test did fail, but on a length assertion, which is a far weaker signal than the panic it advertised. 8ff925d's message said the mutation "panics in truncation_does_not_split_a_multi_byte_char". A failed `assert!` is technically a panic, so the sentence was defensible and still misleading: it implied the UTF-8 boundary panic the test claims to pin, and that is not what was observed. Switched to `€` (THREE bytes), so byte index 2000 falls at 666 chars + 2 bytes, mid-character. The mutation now panics at the slice itself: thread 'engine::remote::worker_failure_detail_tests::truncation_does_not_split_a_multi_byte_char' panicked at relayflowd/src/engine/remote.rs:374:53: end byte index 2000 is not a char boundary; it is inside '€' (bytes 1998..2001 of string) With the fix restored: running 5 tests test engine::remote::worker_failure_detail_tests::truncation_does_not_split_a_multi_byte_char ... ok test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 30 filtered out cargo test --workspace: 164 passed, 0 failed A test whose stated rationale is false is worse than no test, because the next reader trusts it. The comment now records what was wrong with the old one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
Third lens — history (codex):
|
…195) The history lens blocked 92a25e1, correctly. `reason_label` serialized and fell back to `format!("{reason:?}")`, so the fallback path could journal `WorkerError` beside `completionReason: worker_error` — the same engine-internal spelling leak DRIVE-LOG records being removed from `RunSnapshot`. That also made 92a25e1's message false as written: it said "the fallback detail and the taxonomy label now agree" and the doc comment said a reader is "never shown two names", when the fallback did exactly that. Replaced with an exhaustive match returning `&'static str`. There is no wildcard, so a new `CompletionReason` variant is a compile error until it is given a journal label: the boundary now fails closed at build time rather than at runtime. Hand-written spellings can drift from serde, so `every_reason_label_matches_its_serialized_form` pins all nine variants against `serde_json::to_value` rather than spot-checking one. test machine::tests::every_reason_label_matches_its_serialized_form ... ok test result: ok. 53 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out cargo test --workspace: 165 passed, 0 failed Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
History lens, second pass:
|
…ine; both fixed Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
…ted failure From the maintainability lens at 39c779c. `reject` overwrote `failure_detail` unconditionally, so a worker that reported its own failure AND then tripped `validate_agent_completion` lost its account entirely — the rejection replaced it. `validate_agent_completion` runs for every agent completion, not only successful ones, so that path is reachable. That is the loss this branch exists to stop, reintroduced one layer up: the completions that lose the most information are exactly the ones where the most has gone wrong. Both are kept now — "rejected: {error}; worker reported: {detail}" — because they answer different questions. The rejection says why the kernel refused the completion; the worker's output says what went wrong upstream of that. Also records, on the drift test, that its variant list is hand-maintained: the wildcard-free match in `reason_label` makes a NEW variant a compile error, but a variant merely missing from the test array is caught by nothing. Noted rather than solved, since removing the second list means an iterable-enum dependency, which is not a decision a diagnostic fix should smuggle in. cargo test --workspace: 165 passed, 0 failed Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
All three lenses PASSED at
|
Clean 3/3 at
|
Additional evidence: the full SDK suite passes against this kernelMy earlier evidence on this PR was kernel-only — That matters here for two specific reasons:
The three skips are all in Standing position unchanged: not self-merging. All lens runs and all of this evidence were produced by the agent that wrote the patch. |
Closes #195. Found while investigating #189's
NEEDS_HUMAN.md.The defect
engine/remote.rsderivesfailure_reasonfrom the worker's own reported completion, butfailure_detailis assigned in exactly one place — therejectclosure, which handles kernel-side rejections only. A worker-reported failure therefore reachescompletion_actionsasfailure_reason: Some(_),failure_detail: None, and this:maps over
Noneand recordsverification: null.outputis nulled for every non-success, so nothing else carries the reason either — it survives only as thecompletionReasonlabel.Both comments around that code assert the opposite invariant.
machine.rssays, in as many words, "without this the reason exists only in the taxonomy label and the diagnostic is gone." The guard covers kernel-side rejections and misses worker-reported ones — the common case: an agent CLI erroring, timing out, or exiting non-zero. A journal-first system was discarding why work failed.The fix, in two halves
A fallback alone would make the record non-null while restoring no actual diagnostic — the taxonomy label is information the journal already had. So:
machine.rsalways emits a record for a failure, falling back to naming the reported reason when no detail accompanied it. The fallback says "without detail" rather than implying one was given.remote.rscaptures the worker's ownoutputas the detail.OutOfBandCompletionhas no error field, so that output is the only account of what went wrong that exists — and it is exactly what gets nulled downstream. Bounded to 2000 chars, truncated on a char boundary because output is arbitrary worker-supplied data and byte slicing would panic on multi-byte input.Why no test caught it
Every row in
machine/tests.rssetsfailure_detail: Some(..). The entire suite exercised the arm that worked; the broken arm had no coverage at all. The new test covers it.Verification
cargo test --workspacefromkernel/: 159 passed, 0 failed.machine.rsarm to its original form makes it fail with its own assertion message (a worker-reported failure must journal WHY, not just its taxonomy label), and restoring the fix makes it pass.Scope note
This does not by itself prove #189's run took this path — that needs the
completionReasonon theirstep.completed, which I have asked for on the PR. The defect is established from source independently of that run.Not self-merging: needs an independent signoff at head plus green CI.
🤖 Generated with Claude Code
https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR