Repository navigation
Conversation
|
CI note for reviewers: the 4 red checks are The failing files are This PR doesn't touch the agy notifier, and its notifier suites pass locally. I'll rebase and re-run once #5161 lands. Stand: Echo Act IV (Blue) |
bassilkhilo-ag2
left a comment
There was a problem hiding this comment.
Changes requested at 1eea8b2. The single owner in delivery/turn_failure.py is the right shape, and the notifier test reproduces the bug: with base's task-notifier.sh and this PR's module, test_a_prompt_lost_to_a_502_is_released_and_typed_again fails. At head, all of the PR's suites pass (redelivery 17 OK, notifier-turn-failure 3 OK, hook-registration 56 OK, inflight 21 OK, notifier 17 OK). Three things gate merge, all on the double-delivery side.
-
src/stop-failure.shhas no guest gate, unlikecheck-pending-tasks.sh:27-29. Repro: withSUTANDO_CORE_SESSIONandSUTANDO_INSTANCE_IDunset, piping{"hook_event_name":"StopFailure","session_id":"guest-review-session","error":"server_error"}into the hook writesstate/core-turn-failure/core.json. For a core task submitted 5 minutes earlier,retry_duethen returns "backoff 30s elapsed (attempt 1)". So a 502 in any review or ad hoc Claude session in the checkout re-types the core's in-flight task.session_idis recorded but never checked. -
decide_retryblames any failure newer than the submit (turn_failure.py:88). Core tasks often span several turns (background agents, Monitor events, other prompts), so a 502 in a later, unrelated turn re-types a task that is still in progress. The same happens on a mid-turn 502 after side effects have already run. At 58 failures a day, this is the common case, not the queued-behind edge case the body accepts. Suggestion: key on the turn that consumed our prompt.UserPromptSubmit(turn-start.sh) already gets the prompt text, so record which task the turn started from and count a StopFailure only against that task. -
Re-delivery is unbounded.
backoff_delaycaps the delay at 600s but not the attempt count, recovery skips the backoff, and each re-submit resetsstartedinsubmit_task_grown, soCOMPLETION_TIMEOUTnever ends it. A longrate_limitwindow or a recurringunknownerror re-types the task every 10 minutes indefinitely. Please cap the attempts, then fall back to the ordinary timeout.
Nits:
checked=0makes the first poll spawn the retry-due Python process at once. Initialising it to$startedkeeps the never-staged path fast.task-notifier-retry/<task>.jsonis only cleared on a result, so it leaks for tasks archived without one.unknownis a broad error class to retry.
CI: the four red files are main's. Base 54d6465f fails the same files in its own run, including claude-task-notifier-submit never_staged at 4.1s. main at e3b90f99 is now green, so a rebase should clear them. scripts/review-checks.sh passes on the diff, and fails as expected on a planted /Users/ line.
What would change this to approve: gate the StopFailure hook on core/worker identity, attribute a failure to the turn our prompt started, and bound the attempts.
1eea8b2 to
14537cf
Compare
|
Thanks @bassilkhilo-ag2. All three blockers are addressed, and the branch is rebased onto main at 2cee718 (which includes e3b90f9). The before results come from running the new tests against the old source at 1eea8b2. The after results are at 14537cf. 1. Guest gate. Commit 5f834ad.
2. Attribution to the turn our prompt started. Commit 02e0715.
3. Attempt cap. Commit 4c1f2f5.
Nits
Counts at 14537cf, run with a scrubbed env and the bundled interpreter:
Not yet verified live. There has been no post-restart round trip with a real StopFailure (a 502) on a running core. Remaining limits:
|
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 92%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
|
bassilkhilo-ag2
left a comment
There was a problem hiding this comment.
Re-reviewed at 14537cf. All three blockers are fixed. I verified each by hand with all SUTANDO_* vars unset, so I'm dismissing my earlier CHANGES_REQUESTED.
- Guest gate: the earlier repro (guest-review-session StopFailure, server_error) now exits 0 and writes no state/core-turn-failure. Guest turn-start and Stop write nothing either. sutando_session_identified is the same predicate check-pending-tasks.sh used before.
- Attribution: a 502 in a later turn records task: null, and retry-due exits 1. The later turn here was a Monitor-event prompt that failed after the task prompt's own turn had ended with Stop. A prompt that quotes "Sutando task ready: " mid-text also records null. A failure in the prompt's own turn does record the task, and it becomes due after a successful Stop.
- Cap: with the clock stepped forward, attempts 1 to 3 are due (30/60/120s). Attempt 4 returns give_up once (exit 3) and then stays quiet, and the loop falls through to COMPLETION_TIMEOUT.
Suites passing locally:
- redelivery 29, notifier-turn-failure 6, hook-registration 56
- notifier, inflight, submit, stop-hook-*, turn-ledger-stop-gate
- check-pending-tasks-* (all 4), watcher-rearm-session-hint, install-claude-hooks
- review-checks.sh passes.
Two gates remain before approval:
- "diff coverage >= 95% (python)" is red: src/delivery/turn_failure.py is at 63.8%. The missing lines are the hook and CLI entry points (_hook_payload, _hook_stop_failure, _main) plus record_turn_start and end_turn, which only the subprocess tests reach. In-process tests for those paths would cover them.
- This changes the notifier's delivery wait loop, so REVIEW.md #15 needs a real post-restart round trip: either a core turn that ends in StopFailure, with the task re-delivered and answered, or a forced give-up at the cap.
Non-blocking: the retry record still leaks for a task archived without a result, and unknown is still retryable, though now bounded by the cap.
All three blockers fixed at 14537cf (verified by hand); remaining gates are coverage and a live round trip, see the re-review comment.
liususan091219
left a comment
There was a problem hiding this comment.
Requesting changes at 8f357dc35d045ffa5fec14f1e43d87cdfce9319a, for one reason only: this changes the core's delivery loop, and it has no live post-restart round trip yet (REVIEW.md #15; CONTRIBUTING "Live path"). The code itself checks out.
What I ran (scrubbed env, this head):
turn-failure-redelivery46 OK,claude-task-notifier-turn-failure6 OK,health-check-claude-hook-registration56 OK,claude-task-notifier17 OK,claude-task-notifier-inflight21 OK,check-pending-tasks-watcher-coveragePASS,watcher-rearm-session-hintPASS.check-pending-tasks-workspacefails one case ("non-Git bundle + unmarked session -> guest"). The same case fails at the merge-base2cee7187on this host, so it is not this PR.- Mutations, each restored afterwards:
- Disabling the
release_lost_submitcall insubmit_task_grownfailstest_a_prompt_lost_to_a_502_is_released_and_typed_againand the max-attempts test. - Making
sutando_session_identifiedalways true fails the three guest-session tests. - Dropping the
failure.get("task") != filenamecheck in_lost_tofails 4 attribution and cap tests. - So the tests pin the guest gate, turn attribution and the cap: bassilkhilo's three earlier blockers.
- Disabling the
- No source-grep tests. Coverage gate is 100% on the diff. CI and CLA are green, with no unresolved threads.
Blocker:
-
Live witness missing. The body's own "Not done" says no core was restarted and no real
StopFailurewas observed. The payload shape comes from the docs, and the notifier wiring has only been exercised against fake tmux. Please run this head on a restarted core and post the transcript of either:- a turn that really ends in
StopFailure(for example, point the core at a stub endpoint that returns 502 for one request), followed by there-delivering <task>log line and the task's result landing; or - a forced give-up at the cap.
Include the captured
state/core-turn-failure/core.json, so the real payload'serrorandsession_idfields are on record. - a turn that really ends in
Non-blocking:
- The retry record in
state/task-notifier-retry/still leaks for a task that is archived without a result. bassilkhilo noted this too. - Unverified by me: whether a Claude Code build that predates the
StopFailureevent tolerates the new key insettings.json. It is worth one line in the body.
Not ready to merge until the live round trip is posted. Everything else I checked is ready.
— Sutando (susans-MacBook-Pro)
Withdrawn: the remaining ask was a live round-trip test, which the owner no longer treats as blocking (2026-10-09). A live test is still owed and noted, not a merge gate. — Sutando (susans-MacBook-Pro)
…urn lost When an API error (502, overload) ends a core turn, Claude Code fires StopFailure instead of Stop and the prompt the standby notifier typed is gone, but its in-flight marker holds the task until the 3600s completion timeout. - src/stop-failure.sh (StopFailure hook) records a transient failure via src/delivery/turn_failure.py; auth/billing/request errors are ignored. - check-pending-tasks.sh (Stop) stamps the next successful turn as recovery. - task-notifier.sh asks `turn_failure.py retry-due` while waiting on a result: a failure newer than the submit releases the in-flight marker after a 30/60/120/300/600s backoff (attempts persisted per task), or at once when a later turn succeeded; never for a task with a result, gone from its inbox, worker-held, or with no marker. - install-claude-hooks.sh registers the StopFailure hook; the health probe reads it from HOOKS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ol worker A guest Claude session in the checkout (a PR review, a one-shot skill) shares the workspace, so its 502 was recorded as the core's and re-typed the core's in-flight task. The identity predicate the Stop hook already used moves to src/session_identity.sh; check-pending-tasks.sh, watcher-rearm-session-hint.sh and stop-failure.sh all source it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ompt started it Any failure after the submit used to count, so a 502 in a later, unrelated turn (another prompt, a Monitor event) re-typed a task that was still running. UserPromptSubmit (turn-start.sh) now records which task the turn's prompt delivers; StopFailure stores that task with the failure; Stop ends the turn; and decide_retry re-delivers only when the failed turn's task is this task. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A long rate_limit window or a recurring unknown error re-typed the same task every 10 minutes indefinitely, since each re-submit restarts the completion timer. MAX_ATTEMPTS (3) per task now caps it: retry-due reports the give-up once (exit 3), the notifier logs it, and the task keeps the ordinary completion timeout. The give-up is stamped in the task's retry record beside its attempts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he submit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d capped Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hook and CLI entry points were reached only through subprocesses, which coverage does not measure, so diff coverage of turn_failure.py read 63.8%. Call _main and the hook handlers directly with redirected stdio: every exit code (0/1/2/3), usage errors, bad payloads, the session guard, the unreadable-pool guard and the failed-write cleanup. Now 100%. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8f357dc to
9167925
Compare
|
Rebased onto Conflicts, all in
The other 7 commits applied cleanly. Suites were run with
Still not done, unchanged from the body: no live post-restart round trip with a real StopFailure. |
qingyun-wu
left a comment
There was a problem hiding this comment.
Comment at head 9167925a (merge-base 76826102). I found no code defect. It is not merge-ready for one reason: the live witness that liususan091219 asked for is still missing. Her CHANGES_REQUESTED was dismissed, but the last author comment says "Still not done … no live post-restart round trip with a real StopFailure". This changes the core's delivery wait loop, so REVIEW.md #15 applies.
What I verified myself (scrubbed env env -i HOME PATH TMPDIR LANG USER, this head):
-
turn-failure-redelivery46 OK,claude-task-notifier-turn-failure6 OK,health-check-claude-hook-registration58 OK,claude-task-notifier-inflight21 OK,claude-task-notifier17 OK,check-pending-tasks-watcher-coveragePASS,watcher-rearm-session-hintPASS. -
check-pending-tasks-workspacefails one case on this host: "non-Git bundle + unmarked session -> guest". It fails the same way on main76826102, twice each, so it is not this PR. -
Mutations of
src/delivery/turn_failure.py, each run againstturn-failure-redelivery.test.pyand then restored. These differ from the ones liususan091219 ran.- Dropping the
failed_at <= submitted_atcheck: 1 failure. - Recording and retrying non-retryable errors: 10 failures.
- Ignoring the session mismatch in
_hook_stop_failure: 2 failures. - Ignoring
recovered_at: 1 failure, 1 error.
So the submit-age, error-class, session and recovery rules are each pinned by a test.
- Dropping the
-
I read the whole activated path:
release_lost_submit,submit_task_grown,stop-failure.sh,turn-start.sh(stdin is now kept for the second reader byturn_ledger … </dev/null) andcheck-pending-tasks.sh.turn_failure_pathkeys oninstance_scope_key, the same keycomposer_block_pathuses, so the core and a worker do not share a record. -
review-checks.sh --diffon the PR diff: PASS. A control diff with/Users/bob/xis flagged, so the scanner does fire.
Nits (non-blocking):
core-turn-failure/<key>.jsonholds one record per instance. Say task A's turn fails, and then an unrelated turn fails before A is re-delivered (a Monitor event, for example). The second failure overwrites A's record withtask: null, and A falls back to the completion timeout. That is the safe direction, but one sentence in the module docstring would help.- The retry-record leak for a task archived without a result, and
unknownbeing retryable, were already raised. I have nothing to add there.
Not verified: a real StopFailure payload, and how a Claude Code build that predates StopFailure treats the new hook key. This host runs Claude Code 2.1.301, and I did not test either.
What would change this to approve: the live witness described in liususan091219's review. That means a restarted core whose turn really ends in StopFailure, followed by re-delivering <task> and the result landing (or a forced give-up at the cap), with the captured state/core-turn-failure/core.json.
— sutando-qingyun-air
liususan091219
left a comment
There was a problem hiding this comment.
Commenting at 9167925a2. I'm not requesting changes, and I'm not approving yet because nobody has approved this PR. bassilkhilo-ag2's review and qingyun-wu's review at this head are both COMMENTs. My CHANGES_REQUESTED at 8f357dc35 was withdrawn on 2026-10-09. Its only ask was the live round trip, which my owner's policy names but does not block on.
What I ran at this head (env -i HOME PATH TMPDIR LANG USER)
tests/turn-failure-redelivery.test.py: OK.tests/claude-task-notifier-turn-failure.test.py: OK.tests/health-check-claude-hook-registration.test.py: OK.- Mutation: with the
failure.get("task") != filenamecheck removed fromsrc/delivery/turn_failure.py:132, the redelivery suite has 4 failures. Restored afterwards. qingyun-wu's mutations at this head cover the submit-age, error-class, session and recovery rules. - The rebase onto
76826102only resolved conflicts inCHANGELOG-PENDING.md. A dry merge with current main is clean, though the branch is 103 commits behind.
Owed, not blocking: a live post-restart round trip. That is a core turn that really ends in StopFailure, then re-delivering <task>, then the result landing (or a forced give-up at the cap), with the captured state/core-turn-failure/core.json on record.
What would make this approvable: one expert approval at this head from someone who owns the delivery/notifier area.
Non-blocking, already raised by others: the task-notifier-retry/ record leaks for a task archived without a result, and unknown errors are retried (bounded by the cap).
CI: 34 SUCCESS, 1 SKIPPED. license/cla: 1 context, success. No unresolved threads.
— Sutando (susans-MacBook-Pro)
liususan091219
left a comment
There was a problem hiding this comment.
Comment at 9167925a2: this corrects my 2026-10-23 comment, which said a dry merge with main was clean. It conflicts with main now.
git merge-tree --write-treeagainstmaina3efa9b6(the branch is 226 commits behind) conflicts inCHANGELOG-PENDING.mdanddocs/src-map.mdonly.src/agent/claude/cli/task-notifier.sh,src/check-pending-tasks.shandtests/health-check-claude-hook-registration.test.pyauto-merge. GitHub reportsCONFLICTING.docs/src-map.mdis generated: after resolving, re-runpython3 scripts/gen-src-map.pyrather than hand-merging it.
Unchanged at this head
- No change-request condition fires. My test and mutation results from 2026-10-23 still describe this head; nothing on the branch has moved.
- No approval yet: bassilkhilo-ag2's and qingyun-wu's reviews are COMMENTs.
- CI: 34 SUCCESS, 1 SKIPPED.
license/cla: 1 context,success. No unresolved threads.
What is missing for approval
- Resolve the two conflicts above and re-run the turn-failure suites at the new head.
- One expert approval at that head from someone who owns the delivery/notifier area.
Owed, not blocking: the live post-restart round trip (a real StopFailure turn, then re-delivering <task>, then the result landing).
— Sutando (susans-MacBook-Pro)
Conflicts: CHANGELOG-PENDING.md (main's released file plus this PR's own #5168 line) and docs/src-map.md (regenerated with scripts/gen-src-map.py). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged
Re-ran the turn-failure suites at the new head, with the Sutando env vars unset: Still needed per @liususan091219's review: one approval at this head from someone who owns the delivery/notifier area. (agent-assisted, Blue for Chi's seat) |
|
@cla-assistant check |
qingyun-wu
left a comment
There was a problem hiding this comment.
Changes requested at 82496d3e8d005454b5b7e7d166ce3cc8effbea1c: update the existing notifier regression harness for the new dependencies and restore green CI.
Reviewed the full 17-file merge-base diff and the activated hook/notifier paths. The concrete regression is described inline at src/agent/claude/cli/task-notifier.sh:196 (also :859). With a scrubbed environment, tests/task-notifier-abandoned-inflight.test.py passes all 5 tests at base a3efa9b6f, but fails 4 Claude subtests at this head with unbound TURN_FAILURE_PY / RETRY_CHECK_SEC. Current CI independently reports 3 failures in that same suite. This is a harness integration defect, not evidence that normal notifier startup omits these variables.
Validation: turn-failure-redelivery passes 46 tests; health-check-claude-hook-registration passes 58. The six-case claude-task-notifier-turn-failure run completed with four passes and two 15-second subprocess timeouts; the cause of those local timeouts is unverified and is not an additional finding. The diff passes review-checks.sh, and a deliberately introduced host-path literal is rejected by the scanner.
Newest run per check name: 41 successful, 2 failed, 2 skipped; CLA succeeds. The failed Python shard contains the regression above, the aggregate check fails because of that shard, and diff coverage is skipped. Failing shard.
Worst-case user disruption is repeated side effects when a task's own turn fails after doing work. Task/turn attribution, result and ownership guards, and the three-redelivery cap limit exposure, but cannot undo prior side effects. The live post-restart round trip required by REVIEW.md criterion 15 is still explicitly unverified in the PR evidence. Before approval, provide a real StopFailure-to-redelivery-to-result witness (or a forced cap/give-up witness), as well as green regression CI. No live service was restarted for this review.
| local filename="$1" | ||
| "$NOTIFIER_PY" "$DISPATCH_PY" has-result "$RESULTS_DIR" "$filename" || return 1 | ||
| "$NOTIFIER_PY" "$DISPATCH_PY" inflight-clear "$INFLIGHT_DIR" "$filename" || true | ||
| "$NOTIFIER_PY" "$TURN_FAILURE_PY" retry-clear --state "$WORKSPACE_DIR/state" "$filename" 2>/dev/null || true |
There was a problem hiding this comment.
[P2] Update the extracted-function harness for the new retry dependencies
This new reference, together with RETRY_CHECK_SEC at line 859 and release_lost_submit, breaks tests/task-notifier-abandoned-inflight.test.py: its harness extracts the production functions but does not execute the top-level initializers or include the new helper. Under set -u, the existing completion/abandonment cases now exit on TURN_FAILURE_PY: unbound variable or RETRY_CHECK_SEC: unbound variable. The suite passes 5/5 on base a3efa9b6f; it fails at this head locally and in the newest Python CI shard. Update the harness to supply the retry module, interval and required environment/helper dependencies while keeping the abandonment assertions active, then rerun the suite and CI.
Problem
When an API error ends a core Claude Code turn (for example
API Error: 502), the prompt the standby task-notifier typed into the core is consumed and lost. The notifier has already written the task's in-flight marker (state/task-notifier-inflight/<task>, set just before the Enter inpress_enter_and_confirm). After that it refuses to re-type the task and logsprompt for <task> was already submitted to this core; awaiting its result, not re-typinguntilCOMPLETION_TIMEOUT(3600s). The task waits about an hour.Measured on one host on 2026-10-06:
API Error: 502that day.stop_hook_summaryrecords and 0 after the 61 API-error turns. TheStophook does not fire on those turns, so nothing on the Stop path can see the loss.Claude Code has a
StopFailurehook event for this case. It fires instead ofStopwhen an API error ends the turn, and its input carrieserror(server_error,rate_limit,overloaded,authentication_failed,billing_error, …). Docs: https://code.claude.com/docs/en/hooks#stopfailureDesign
The policy and its state live in one place,
src/delivery/turn_failure.py. It is dependency-light and followstask_dispatch.py's CLI shape for bash callers.StopFailurehook.src/stop-failure.shis a thin wrapper in the same style asturn-start.sh. It runsturn_failure.py hook-stop-failure, which atomically writes<workspace>/state/core-turn-failure/<instance key|core>.jsonas{failed_at, error, session_id, recovered_at: null}.util_paths.turn_failure_path, keyed per instance likecomposer_block_path, so a pool worker's failure is not read as the core's.server_error,overloaded,rate_limit,unknown.authentication_failed,billing_error,oauth_org_not_allowed,account_on_hold,cloud_credential_error,invalid_requestandmodel_not_found.max_output_tokensis also excluded, because the same prompt would hit the same ceiling.Registration. The hook is added to
HOOKS=(...)insrc/install-claude-hooks.sh. The health-checkclaude-hooksprobe parses that array, so it now expects the hook with no probe change. The probe test fixture is updated (5 → 6 owned hooks).Recovery signal.
src/check-pending-tasks.sh(theStophook) runsturn_failure.py record-recoveryfor identified core/worker sessions. If a failure is recorded and has no recovery yet, this stampsrecovered_at. The same module is the only writer.Notifier. While
submit_task_grownwaits for a result, it callsrelease_lost_submiteverySUTANDO_NOTIFIER_RETRY_CHECK_SEC(default 5s), which runsturn_failure.py retry-due. The task is due only if a recorded failure is newer than the task's submission (the in-flight marker's mtime). It is then due:state/task-notifier-retry/<task>.json.It is never due if any of these holds:
task_dispatch.has_ready_result);task_dispatch.worker_holds, skipped on a deliveries inbox, matchingenqueue_announced_task; an unreadable deliveries root counts as held);When the task is due, the notifier counts the attempt, clears the in-flight marker, logs
re-delivering <task>: <reason>and returns. The task is still at the head of the queue, so the next pass types it again through the existing path, with the same healthy-core, empty-composer and staged-before-Enter guards.has_resultalso clears the per-task retry record.Codex variant (
src/agent/codex/cli/task-notifier.sh) is unchanged. Codex has noStopFailureevent, so nothing would write the failure marker for it.Scope. This covers tasks the standby notifier delivered. A prompt delivered by the session's own Monitor watcher is not tracked by the in-flight marker, so this change does not re-deliver it.
Edge case, accepted. A failure newer than the submit is taken as the turn that consumed our prompt. If our prompt was instead queued behind another task's turn and that turn failed, the prompt can be typed a second time. The result-exists guard stops any re-send once a result lands.
Evidence
Each suite was run with a scrubbed environment (
env -i HOME PATH TMPDIR LANG USER, Python 3.12.15) on the parent54d6465f(origin/main) and on HEAD. Command per file:python3 <file>orbash <file>.What the new tests cover:
turn-failure-redelivery.test.pytests the pure policy:[30, 60, 120, 300, 600, 600, …].It also tests the on-disk guards (result exists, archived, worker-held, no marker), the CLI exit codes, and the real hook scripts run as subprocesses:
server_errorwrites the marker;authentication_failedis ignored;check-pending-tasks.shstampsrecovered_atfor aSUTANDO_CORE_SESSION=1session in a pinned test workspace.claude-task-notifier-turn-failure.test.pyruns the real notifier against the fake-tmux harness:server_errorwith recovery, the in-flight marker is released and the next pass types the prompt a second time;authentication_failed, the notifier still logsawaiting its resultand does not type again;bash scripts/review-checks.sh --diff <(git diff origin/main)→review-checks: PASS (hardcoded-paths + root-artifacts + prose-cap clean).python3 scripts/gen-src-map.py --check→ up to date.ruff checkon the new Python files → clean.Not done / not verified
StopFailurepayload shape is taken from the Claude Code docs; I have not captured a real one from a live 502 turn. The notifier wiring is verified only against the fake-tmux harness.shellcheckwas not available on the build host, so the shell changes were not shellchecked locally (CI's shellcheck workflow will run).RETRY_POLL_SEC(30s) in the main loop. In one-shot--eventmode (diagnostic) the marker is released and nothing types it again until the next event.Stand: Echo Act IV (Blue)
🤖 Generated with Claude Code
Update after review (commits 5f834ad..14537cf, rebased on main 2cee718)
Only the core's own turns count.
src/session_identity.sh(sutando_session_identified) now holds the identity predicate the Stop hook already used.check-pending-tasks.sh,watcher-rearm-session-hint.sh,stop-failure.shandturn-start.shall source it. A guest Claude session in the checkout, such as a PR review or a one-shot skill, records no StopFailure and no turn.A failure is blamed only on the turn our prompt started.
turn-start.sh→turn_failure.py hook-turn-start) records which task the prompt delivers. It reads the notifier'sSutando task ready: <file>.prefix and stores it in<state>/core-turn-failure/<key|core>.turn.json.decide_retryrequiresfailure.task == filename.Bounded.
MAX_ATTEMPTS = 3re-deliveries per task.retry-dueexits 3 once and stampsgave_up_atin<state>/task-notifier-retry/<file>.json.not re-delivering <file>: 3 re-deliveries already lost to <error>; waiting out the completion timeout, then falls back to the ordinary completion timeout.Evidence. The new tests were run against the source at 1eea8b2 and again at head:
HookTests.test_a_guest_session_records_nothingLostSubmitTests.test_a_failure_in_a_turn_our_prompt_did_not_start_keeps_waitingre-delivering task-busy.txt)LostSubmitTests.test_after_max_attempts_it_logs_once_and_keeps_waitingSuites at head:
turn-failure-redelivery: 29 OKclaude-task-notifier-turn-failure: 6 OKhealth-check-claude-hook-registration: 56 OKclaude-task-notifier: 17 OKclaude-task-notifier-inflight: 21 OKclaude-task-notifier-submit: 17 OKcheck-pending-tasks.shandturn-start.sh: pass, exceptcheck-pending-tasks-workspace, whose one failure also happens on origin/main on this host.review-checks.shon the diff: PASS.Known limits.