[Bugfix #1241] Auto-restart only on unnatural exits — a deliberate quit ends cleanly - #1244
Conversation
A deliberate quit (double Ctrl+C, /quit) exits 0, and both restart surfaces were exit-code blind, so the user's choice was overridden by a respawn 2 seconds later — which they then had to race another Ctrl+C to undo, and which can collide with the dying predecessor's session lock (the #1224 family). - shellper-protocol: isDeliberateExit(), the shared predicate. Code alone is not enough: node-pty reports a signal death as exit code 0 with the signal attached, so a deliberate exit is code 0 AND no signal. - session-manager: a deliberate exit is neither counted nor respawned; the session ends and the shellper husk is left alive. - pty-session: a deliberate exit no longer shows the 'restarting' notice or arms the 10s wait-for-respawn timer — it ends cleanly, which clears the architect row so 'afx workspace start' can relaunch. - spawn-worktree: the five builder launch loops share one tail; exit 0 clears the screen and gates the relaunch on Enter (EOF exits rather than spins). Nonzero and signal deaths keep the 2s auto-restart.
- launch-loop-exit-code: executes the generated launch script under bash (pattern-matching cannot prove shell control flow) — exit 0 runs the agent once and waits, Enter relaunches once, exit 7 still loops. - spawn-worktree: every generated script variant carries the exit-0 gate. - shellper-protocol: isDeliberateExit, including the signal-death case that node-pty reports as code 0. - session-manager / tower-shellper-integration: clean exit does not respawn or count a restart; crashes and signal deaths still do. The two #418 tests that simulated exit 0 now simulate a crash, which is the case they were always about.
CMAP 3-way review — unanimous APPROVE
All three independently confirmed the Gemini — "Excellent, highly targeted bugfix with precise exit classification and execution-based test coverage across all restart layers." Codex — "Correctly stops auto-restart on deliberate exits, preserves restart behavior for crashes/signals, and is protected by strong regression coverage." Verified the shared loop tail is applied in every script-generation path, not just the runtime start path. Claude — "The bash-execution test is a standout — testing shell control flow by pattern-matching is fragile; executing it proves the behavior." Also confirmed the deliberate-exit branch is guarded by No changes requested by any reviewer; nothing to address. |
…n Kimi provider-owned loops Deliberate exit 0 now gates relaunch on a keypress instead of blind auto-respawn, matching the post-cluesmith#1244 builder launch-loop contract. LAUNCH_LOOP_TAIL moves to utils/harness.ts (exported) so provider-owned scripts share it without a circular import; tests pin the tail across all Kimi launch shapes (fresh/resume/bare, seeded/bare interactive). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
Auto-restart now only fires on unnatural exits. A deliberate quit — double Ctrl+C,
/quit, any clean shutdown — ends the agent instead of respawning it two seconds later.Fixes #1241
Root Cause
Two surfaces respawned the agent, and both were exit-code blind:
spawn-worktree.tsgenerateswhile true; do <agent>; sleep 2; donein five places (resume / role / no-role instartBuilderSession, role / no-role inbuildWorktreeLaunchScript). The loop never looked at$?.session-manager.tssetupAutoRestartincrementedrestartCountand re-SPAWNed on any exit. Builder sessions don't setrestartOnExit, so this is the layer that governs architect terminals, which Tower launches directly with no bash loop.So quitting forced the user to race a second Ctrl+C into the 2s window — and a mistimed one lands in the fresh agent. It also feeds the #1224 class: a respawn within ~2s can collide with the dying predecessor's session lock.
A third surface, not named in the issue, had to move with them:
pty-session.tsprints[Process exited — restarting...]and arms a 10s wait-for-the-respawn timer wheneverrestartOnExitis set. Once layer 2 stops restarting clean exits, that notice is a lie and the timer tears the session down 10s later anyway.The trap:
code === 0is not sufficientnode-pty reports a signal death as exit code 0 with the signal attached — probed directly here:
ShellperProcessthen stringifies that field. A naivecode === 0check would have stopped restarting killed agents — the exact opposite of what the issue asks for. Hence one shared predicate,isDeliberateExit(): code 0 and no signal (null,''or'0').Fix
shellper-protocol.tsisDeliberateExit()— the single predicate both layers usesession-manager.tssession-clean-exit, drop the session. Not counted, not respawned. The shellper husk is left alive (no SIGTERM).pty-session.ts[Agent exited at your request — not restarting.]and end cleanly; no false "restarting" notice, no 10s timerspawn-worktree.tsLAUNCH_LOOP_TAIL: exit 0 clears the screen, prints the hint, and gates the relaunch on Enter; EOF on stdin exits rather than spins. Nonzero/signal keeps the 2s auto-restart.96 lines of source; the rest is tests.
One deviation from the prescribed design
The issue says the shellper layer should "leave the PTY open". For the architect that would wedge it: Tower gates architect creation on
!entry.architects.has('main'), so a registered-but-dead terminal is unrelaunchable without a full workspace stop/start (which takes every builder with it). Ending cleanly clears the architect row, soafx workspace startbrings the architect back. The shellper process itself is not killed. Builders are unaffected — their bash loop never exits on a clean quit, so they get the literal keypress-gated behavior the issue describes.Not found: the Kimi variants
The issue mentions "the Kimi provider-owned variants" of the launch script. There is no Kimi provider in this repo (
grep -ri kimiis empty) and all launch-loop generation lives in the five sites above — all covered.Test Plan
launch-loop-exit-code.test.ts(new) — executes the generated script under bash rather than pattern-matching it; a shell control-flow regression is only provable by running it. Exit 0 → agent launched exactly once, script exits 0, no restart message. One Enter → exactly two launches. Exit 7 → still loops, still printsRestarting in 2 seconds.shellper-protocol.test.ts—isDeliberateExitacross code/signal combinations, including the code-0-with-signal case.session-manager.test.ts— clean exit does not spawn, does not incrementrestartCount, emitssession-clean-exit, drops the session; signal death (code 0 + signal 9) and crash both still respawn. The real-shellpercrashLoopFallbacktest now asserts the session ends rather than exhaustingmaxRestarts.tower-shellper-integration.test.ts— clean exit emitsexitimmediately with the at-your-request notice and nothing left armed; signal death still takes the restart path. Two pre-existing Dashboard: Auto-restart architect terminal when it exits #418 tests simulated exit 0 to mean "crash"; they now simulate a crash.spawn-worktree.test.ts— every generated variant (resume / role / no-role) carries the exit-0 gate, so a new launch-script code path can't be added without it.Full suite: 3655 passed, 0 failures; build green.
Not exercised: a live architect quit against a running Tower — that needs a Tower restart, which would kill the active builder sessions. The shellper layer is covered by the real-shellper integration test (
/bin/sh -c 'exit 0'under an actual shellper process), and the builder layer end-to-end by the bash-execution tests.