Skip to content

feat(restart): refuse to restart a stateful agent session unless --force - #73

Merged
myobie merged 1 commit into
mainfrom
feat/restart-agent-guardrail
Jul 12, 2026
Merged

feat(restart): refuse to restart a stateful agent session unless --force#73
myobie merged 1 commit into
mainfrom
feat/restart-agent-guardrail

Conversation

@myobie

@myobie myobie commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

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.cos on a live claude --resume agent 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 restart re-execs under the operator's shell env, so the respawn inherited a stray ST_AGENT and came back under the wrong bus identity → exit 129.)

Fix

pty restart is 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.

  • Detection (statefulAgentReason): a role=agent tag, OR claude --resume in the stored command.
  • On a match: exit nonzero with a clear message pointing at convoy up and --force to override.
  • Non-agent sessions: unaffected.
$ pty restart -y org.myobie.cos
Session "org.myobie.cos" looks like a stateful agent (role=agent tag).
`pty restart` kills its in-progress work and can wedge a `claude --resume`.
Cycle it through its supervisor (e.g. `convoy up`) instead — or pass --force to restart anyway.
$ echo $?
1

Tests

tests/restart-guardrail.test.ts: role=agent refuses; claude --resume argv refuses (tag-independent); a normal session restarts fine; --force overrides. 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 restart re-execing an agent under the operator's shell env (leaked ST_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).

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.
@myobie
myobie merged commit 7559784 into main Jul 12, 2026
1 check passed
@myobie
myobie deleted the feat/restart-agent-guardrail branch July 12, 2026 14:03
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).
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.

1 participant