Skip to content

[Bugfix #1241] Auto-restart only on unnatural exits — a deliberate quit ends cleanly - #1244

Merged
waleedkadous merged 8 commits into
mainfrom
builder/bugfix-1241
Jul 25, 2026
Merged

[Bugfix #1241] Auto-restart only on unnatural exits — a deliberate quit ends cleanly#1244
waleedkadous merged 8 commits into
mainfrom
builder/bugfix-1241

Conversation

@waleedkadous

Copy link
Copy Markdown
Contributor

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:

  1. Builder launch scriptsspawn-worktree.ts generates while true; do <agent>; sleep 2; done in five places (resume / role / no-role in startBuilderSession, role / no-role in buildWorktreeLaunchScript). The loop never looked at $?.
  2. Shellper auto-restartsession-manager.ts setupAutoRestart incremented restartCount and re-SPAWNed on any exit. Builder sessions don't set restartOnExit, 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.ts prints [Process exited — restarting...] and arms a 10s wait-for-the-respawn timer whenever restartOnExit is 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 === 0 is not sufficient

node-pty reports a signal death as exit code 0 with the signal attached — probed directly here:

normal exit  -> {"exitCode":0,"signal":0}
SIGKILL      -> {"exitCode":0,"signal":9}
exit 3       -> {"exitCode":3,"signal":0}

ShellperProcess then stringifies that field. A naive code === 0 check 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

File Change
shellper-protocol.ts isDeliberateExit() — the single predicate both layers use
session-manager.ts Deliberate exit → log, emit session-clean-exit, drop the session. Not counted, not respawned. The shellper husk is left alive (no SIGTERM).
pty-session.ts Deliberate exit → print [Agent exited at your request — not restarting.] and end cleanly; no false "restarting" notice, no 10s timer
spawn-worktree.ts All five loops share one LAUNCH_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, so afx workspace start brings 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 kimi is 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 prints Restarting in 2 seconds.
  • shellper-protocol.test.tsisDeliberateExit across code/signal combinations, including the code-0-with-signal case.
  • session-manager.test.ts — clean exit does not spawn, does not increment restartCount, emits session-clean-exit, drops the session; signal death (code 0 + signal 9) and crash both still respawn. The real-shellper crashLoopFallback test now asserts the session ends rather than exhausting maxRestarts.
  • tower-shellper-integration.test.ts — clean exit emits exit immediately 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.

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.
@waleedkadous

Copy link
Copy Markdown
Contributor Author

CMAP 3-way review — unanimous APPROVE

Model Verdict Confidence Key issues
Gemini APPROVE HIGH None
Codex APPROVE HIGH None
Claude APPROVE HIGH None

All three independently confirmed the isDeliberateExit predicate handles the node-pty signal-death trap (SIGKILL{exitCode: 0, signal: 9}), that all five launch-loop generation sites are covered, and that the architect deviation from "leave the PTY open" is sound.

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 _restartOnExit, so non-restart sessions are unaffected, and that the pre-#1241 tests which used exit 0 to mean "crash" were correctly migrated to exit 1.

No changes requested by any reviewer; nothing to address.

@waleedkadous
waleedkadous merged commit c3c3089 into main Jul 25, 2026
6 checks passed
mohidmakhdoomi added a commit to mohidmakhdoomi/codev that referenced this pull request Jul 25, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

shellper/launch loops: auto-restart should only trigger on unnatural exits — a deliberate quit should end cleanly

1 participant