feat(cli): let a shell outlive the turn that opened it - #275
Merged
Conversation
The registry landed in #273 owned by a single runAgent call, so a shell opened in one turn was closed before the next began — a slower Bash with extra steps. The whole point of a shell that keeps its working directory is that the next message still has it. The REPL now owns one registry for the session and threads it through every turn, the same way it already owns the background-task manager. `cd`, `export`, and an activated virtualenv now survive from one message to the next. `/shells` lists what is open and where each started; `/shells close <id>` closes one and whatever is still running in it. That command is part of this change rather than a follow-up: making these processes outlive a turn is exactly what creates the need to see and stop them. Cleanup is a wrapper, not a habit. `startRepl` closes every shell in a finally, so it also runs when the session ends by throwing — these are real processes in their own process group and do not die with the CLI. Background tasks and sub-agents deliberately do not share the session's shells. Two agents interleaving commands in one shell would each be wrong about its state, so they get their own, closed when their run ends. A test pins that, including an assertion that the delegation actually happened — otherwise it would pass for the wrong reason if Task were ever missing from the registry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
The follow-up flagged in #273. Closes the gap between "persistent shell" and what the CLI actually did with one.
The gap
#273 built the registry but left it owned by a single
runAgentcall, so a shell opened in one turn was closed before the next began.cdsurvived between twoShellRuncalls inside a turn and nothing further — a slowerBashwith extra steps. The point of a shell that keeps its working directory is that your next message still has it.The REPL now owns one registry for the session and threads it through every turn, the same way it already owns the background-task manager.
shellsis not aHostBoundOption, so it passes throughRuntimeHost.runwith no plumbing./shellsis part of this, not a follow-upMaking these processes outlive a turn is exactly what creates the need to see and stop them. Without it the user has no way to know the agent left a dev server running, or to kill it.
Cleanup is a wrapper, not a habit
startReplis now a thin wrapper that closes every shell in afinally, mirroring whatrunAgentdoes — so it runs when the session ends by throwing, not only on the way out of the loop. These are real processes in their own process group; they do not die with the CLI, and "the exit path will remember" is not a guarantee. It cost no re-indentation of the 190-line loop.Sub-agents deliberately do not share them
Two agents interleaving commands in one shell would each be wrong about its state, so background tasks and delegated sub-agents get their own registry, closed when their run ends. This was already true by construction —
runSubAgentbuilds its options from an explicit allowlist thatshellsis not on — but nothing pinned it, and my change is what makes it matter. There is now a test.That test asserts the delegation actually happened before checking the registries differ. My first version of it passed
new ToolRegistry([peek]), which replaces the built-ins, soTaskdid not exist, the parent ran the probe itself, and it failed for a reason that had nothing to do with shells. Without the delegation assertion the fixed version could have passed for the same wrong reason.Verification
pnpm typecheck,lint,format:check,docs:checkclean. Full suite green — core 1160 passed / 28 skipped (+1), cli 249 (+7), desktop 104, server 49, protocol 34, lsp 13, vscode 12, scripts 42.Smoke-tested against the built CLI with a throwaway
HOME:/shellsreports no shells open (provingctx.shellsis wired rather than reporting itself unavailable),/shells close shell-1reports the unknown id instead of claiming it closed something, and a bad argument is rejected rather than silently listing.What is covered by construction rather than by test: "a shell survives across turns" follows from one registry being passed to every
runtime.runplus the existing #273 test proving a host-owned registry is not closed by a run. Driving that end-to-end would need a real model turn, so I have not claimed a test for it.CHANGELOG and the parity table are updated in this PR — the previous round shipped six features without either, and release notes are taken from the CHANGELOG.