What happened?
A same-run retry reuses a single run object across attempts. When attempt 1 finishes a clean but empty turn and then stalls (a retryable no-output timeout), and the retried attempt 2 then crashes (non-zero exit, no output), the run is finalized as succeeded instead of failed.
The cause is a stale flag. run.turnCompletedCleanly is set by a clean turn_end (applyClaudeStreamJsonRunBookkeeping) and is read by the terminal classifier (classifyChatRunCloseStatus) as a success override. The same-run retry teardown (tearDownAttemptForRetry in apps/daemon/src/server.ts) resets the other per-attempt fields (child, exitCode, signal, error, stdinOpen) before re-spawning, but leaves turnCompletedCleanly set — so attempt 1's clean-turn verdict vouches for attempt 2's crash.
User-visible impact: a run that actually failed is reported as succeeded (blank/failed result shown as success), and run_retry_finished reports retry_result: success, polluting retry telemetry.
Steps to reproduce
Reproduced end-to-end against a real daemon over the production HTTP API, with a fake claude CLI standing in for the external agent (its stdout is byte-for-byte the real claude-stream-json shape):
- Point the daemon at a fake
claude binary that, on its first spawn, emits a clean-but-empty turn ({"type":"assistant","message":{"content":[],"stop_reason":"end_turn"}}) then hangs; on its second spawn, exits 1 immediately with no output.
- Start a run (
POST /api/runs). Attempt 1 sets turnCompletedCleanly = true, then the inactivity watchdog fails it as a retryable no-output timeout → same-run retry.
- Attempt 2 crashes (exit 1, no output).
- Poll
GET /api/runs/:id.
Observed: status: "succeeded", exitCode: 1 — 6/6 runs on a live daemon.
Event chain: start → turn_end → error(watchdog) → run_retry_attempted → start → run_retry_finished → succeeded.
Expected behavior
The run finalizes as failed (attempt 2 crashed with a non-zero exit and produced no output). Attempt 1's clean-turn flag must not carry into the classification of a later, independent attempt.
Open Design version
0.14.1
Platform
macOS (Apple Silicon)
Additional context
What happened?
A same-run retry reuses a single
runobject across attempts. When attempt 1 finishes a clean but empty turn and then stalls (a retryable no-output timeout), and the retried attempt 2 then crashes (non-zero exit, no output), the run is finalized assucceededinstead offailed.The cause is a stale flag.
run.turnCompletedCleanlyis set by a cleanturn_end(applyClaudeStreamJsonRunBookkeeping) and is read by the terminal classifier (classifyChatRunCloseStatus) as a success override. The same-run retry teardown (tearDownAttemptForRetryinapps/daemon/src/server.ts) resets the other per-attempt fields (child,exitCode,signal,error,stdinOpen) before re-spawning, but leavesturnCompletedCleanlyset — so attempt 1's clean-turn verdict vouches for attempt 2's crash.User-visible impact: a run that actually failed is reported as succeeded (blank/failed result shown as success), and
run_retry_finishedreportsretry_result: success, polluting retry telemetry.Steps to reproduce
Reproduced end-to-end against a real daemon over the production HTTP API, with a fake
claudeCLI standing in for the external agent (its stdout is byte-for-byte the real claude-stream-json shape):claudebinary that, on its first spawn, emits a clean-but-empty turn ({"type":"assistant","message":{"content":[],"stop_reason":"end_turn"}}) then hangs; on its second spawn, exits 1 immediately with no output.POST /api/runs). Attempt 1 setsturnCompletedCleanly = true, then the inactivity watchdog fails it as a retryable no-output timeout → same-run retry.GET /api/runs/:id.Observed:
status: "succeeded",exitCode: 1— 6/6 runs on a live daemon.Event chain:
start → turn_end → error(watchdog) → run_retry_attempted → start → run_retry_finished → succeeded.Expected behavior
The run finalizes as
failed(attempt 2 crashed with a non-zero exit and produced no output). Attempt 1's clean-turn flag must not carry into the classification of a later, independent attempt.Open Design version
0.14.1
Platform
macOS (Apple Silicon)
Additional context
turnCompletedCleanlyreports: Claude stream-json usage with tool_use stop reason is treated as terminal #4197 was the flag being set too early (on atool_usestop reason); fix(daemon): surface Claude is_error result terminations instead of classifying the run as succeeded #5159 is the flag being set when it shouldn't (anis_errorresult frame). This one is the flag being carried forward across a retry — neither of those touchestearDownAttemptForRetry.Fixesthis issue.