fix(observability): preserve claude's stream on a nonzero exit (closes #326) #327
No reviewers
Labels
No labels
Agent
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No assignees
2 participants
Notifications
Total time spent: 37 seconds
Due date
igor
37 seconds
No due date set.
Dependencies
No dependencies set
Reference
joshtronic/igor!327
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/preserve-claude-stream-on-nonzero"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #326.
When claude exits nonzero on its own (
rc=${PIPESTATUS[0]}inclaude_run_with_cost), the function returns cleanly — so the tick-death crashlog path (crashlog_preserve, which scans worktrees for a lingering in-flight marker) never fires. The worktree, with the raw stream, is torn down, and there's no post-mortem for why claude exited nonzero.That's the exact blind spot behind vps-showdown#41 and #46: both bombed with
claude exited 1, and we could only infer the cause — OOM for #41 (confirmed by the 4.7 GB peak); unknown for #46, which peaked at a normal 236 MB after the re-scope but still exited 1. The re-scope fixed the memory blowup; it exposed a second, invisible exit-1.Fix
lib/crashlog.sh— newcrashlog_preserve_scratch <rc> <state_dir> <call_site> <scratch>: copies the known.agentscratch'sclaude-stream.jsonl+claude-output.logtocrash-logs/<stamp>-rc<rc>-<call_site>/, pruned toCRASHLOG_KEEP(20). Unlikecrashlog_preserve, it needs no in-flight marker or worktree scan — it's handed the scratch directly.lib/claude.sh—claude_run_with_costcalls it onrc != 0, before the in-flight marker is cleared and before the caller tears down the worktree. Guarded (declare -F+|| true) so a missing helper or any error can never break the tick.bin/test-crashlog.sh— 5 new assertions on the helper.Every
claude exited Nnow leaves a readable stream under~/.local/state/agent/crash-logs/. Next time #46 (or anything) bombs, we read the cause instead of guessing.Testing
bin/check-sync.shgreen;test-crashlog.shextended.🤖 Generated with Claude Code
🤖 Review —
COMMENT(automated)CI for
b5348e4b: successAdds
crashlog_preserve_scratchto capture claude's stream/output log on a nonzero exit (before worktree teardown), wired intoclaude_run_with_cost, plus 5 unit assertions. Small, in-scope, well-commented, CI green. My only reservation is that the actual integration point isn't verifiable from this diff.Findings
Unverifiable integration variable (
lib/claude.sh): The new call passes"$scratch", but the surroundingclaude_run_with_costbody isn't in the diff, so I can't confirm$scratchis defined at that point and points at the.agentscratch containingclaude-stream.jsonl/claude-output.log. If it's the wrong/undefined var, the whole feature silently no-ops (everything is|| true-guarded, so nothing breaks — but it also wouldn't preserve anything, defeating the fix). This is exactly the behavior the PR exists to deliver, and the new tests only exercise the helper in isolation, never the call site. Please confirm$scratchis in scope and correct there (or point me at the surrounding lines).Same-second collision (
lib/crashlog.sh):stampis second-precision (%Y%m%dT%H%M%S). Two nonzero exits in the same second with the samecall_site/rcproduce the samedestand the secondcpoverwrites the first. Low-likelihood in practice, but worth a nanosecond/PID suffix if double-preserve is plausible.Positives
crashlog.sh,claude.sh,test-crashlog.sh) match the diff; the "5 new assertions" claim is accurate (stream, display log, call-site, rc-in-dirname, missing-dir no-op).call_siteis sanitized (tr -c 'A-Za-z0-9._-' '_') before use in the path — no path-injection concern.declare -F,2>/dev/null,|| true); can't break the tick.Not blocking, but I'm withholding APPROVE because the one thing that actually matters — that
$scratchresolves to the real stream dir inclaude_run_with_cost— can't be confirmed from the diff and has no test around it. A quick confirmation (or an integration-level assertion) would move this to approve.Independent review by the harness on
claude-opus-4-8(effort: high). The human reviewer is requested once Igor has reviewed; a human still merges.