feat(restart): refuse to restart a stateful agent session unless --force - #73
Merged
Conversation
Incident: `pty restart -y org.myobie.cos` on a live `claude --resume` agent wedged it — the abrupt kill + re-exec of the stored argv killed in-progress work and left claude frozen on its exit screen with an orphaned daemon (and, since restart re-execs under the OPERATOR's shell env, the respawn inherited the wrong ST_AGENT and came back under the wrong bus identity, exit 129). `pty restart` is a dumb "SIGTERM the daemon + re-run the stored argv" — great for a stateless daemon, a footgun for a stateful interactive agent. Refuse it for agent-shaped sessions unless --force, converting "claude agents cycle via their supervisor, not pty restart" from convention into a CLI-enforced guardrail. Detection (statefulAgentReason): a `role=agent` tag, or `claude --resume` in the stored command. On a match, `pty restart` exits nonzero pointing at `convoy up` and `--force` to override; non-agent sessions are unaffected. tests/restart-guardrail.test.ts: role=agent refuses; `claude --resume` argv refuses; a normal session restarts fine; --force overrides. Existing restart tests (displayName preservation, gc-flap clear) still pass.
This was referenced Jul 12, 2026
myobie
added a commit
that referenced
this pull request
Jul 12, 2026
…#75) Incident follow-up #3 — the deeper root cause. `pty restart` (and the dead- session "Restart? [Y/n]" path) re-run a session's stored command under the RESTARTER's shell environment. When cos was restarted from smalltalk's shell, that shell's ST_AGENT=smalltalk-claude (and ST_ROOT) leaked into the re-exec, so cos came back under the wrong bus identity and died (exit 129). Strip the bus-identity vars (ST_AGENT/ST_ROOT) from an operator-initiated restart's environment so a session re-exec'd from a different shell can never inherit that shell's identity. Mechanism: a new `scrubEnv?: string[]` on spawnDaemon deletes the named keys from the daemon's env before it spawns — and therefore before the session child inherits them (spawnViaNode path). The two operator-restart call sites (cmdRestart, handleDeadSession) pass RESTART_SCRUBBED_ENV. Scoped deliberately to restart: a fresh `pty run` is unaffected, because a convoy-launched create legitimately inherits its own identity. And this keeps `pty restart` appropriately un-blessed for agents (consistent with the #73 guardrail): it's now safe — it won't resurrect a session under the wrong identity — but the correct way to restart an agent with a valid identity is still its supervisor (convoy). pty itself reads neither var, so scrubbing them changes only what the child inherits. tests/restart-env-scrub.test.ts: a restart run from a shell carrying ST_AGENT=smalltalk-claude / ST_ROOT=/leaked yields a child that records UNSET|UNSET (identity scrubbed, not leaked); a fresh `pty run` still inherits its creator's ST_AGENT/ST_ROOT (create path unaffected).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up #1 (highest leverage) from the cos-restart incident. Converts "claude agents cycle via convoy, not
pty restart" from convention into a CLI-enforced guardrail.What happened
pty restart -y org.myobie.coson a liveclaude --resumeagent wedged it: the abrupt SIGTERM + re-exec of the stored argv killed in-progress work, claude froze on its exit screen, the daemon orphaned. (Root cause of the wrong-identity wedge, found with convoy-claude:pty restartre-execs under the operator's shell env, so the respawn inherited a strayST_AGENTand came back under the wrong bus identity → exit 129.)Fix
pty restartis a dumb "SIGTERM daemon + re-run stored argv" — fine for a stateless daemon, a footgun for a stateful interactive agent. Refuse it for agent-shaped sessions unless--force.statefulAgentReason): arole=agenttag, ORclaude --resumein the stored command.convoy upand--forceto override.Tests
tests/restart-guardrail.test.ts: role=agent refuses;claude --resumeargv refuses (tag-independent); a normal session restarts fine;--forceoverrides. Existing restart tests (displayName preservation #61, gc-flap clear) still pass; typecheck clean.Related follow-up (not in this PR)
The incident's wrong-identity wedge came from
pty restartre-execing an agent under the operator's shell env (leakedST_AGENT). The guardrail blocks the accident; separately, whether restart should scrub/preserve bus-identity env on re-exec is worth a look — flagging for after the shutdown-hang backstop (#2).