feat(cli): add /new to start a fresh session without restarting - #10767
Conversation
/clear wipes the conversation but keeps the same session id and the lifetime accumulated_* token counters, so unrelated tasks in one sitting all land under a single session unless the process is restarted. /new creates a new session in the running process and switches the live session over to it, carrying over what describes the process itself: provider, model config, goose mode, loaded extensions, working directory and recipe. The previous session stays on disk untouched, and its SessionEnd hook fires before the switch. The session id is only swapped once every fallible step has succeeded, so a failure leaves the running session usable.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c42adfcbdd
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| .emit_hook(goose::hooks::HookEvent::SessionEnd, &self.session_id) | ||
| .await; | ||
|
|
||
| self.session_id = new_session_id; |
There was a problem hiding this comment.
Reset managed-context providers on /new
When the current provider manages its own context, /new only swaps Goose's session id and clears self.messages, leaving the existing provider instance and its upstream conversation id intact. For example, AcpProvider::stream still prompts self.acp_session_id(), and GeminiCliProvider resumes its cached cli_session_id with -r (crates/goose/src/acp/provider.rs:482, crates/goose/src/providers/gemini_cli.rs:68-111), so after /new users of ACP or gemini-cli continue the old upstream conversation even though the CLI reports a fresh session.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch — confirmed for ACP, gemini-cli and claude-code (manages_own_context() == true). Swapping goose's session id would have left AcpProvider's session.id and GeminiCliProvider's OnceLock<cli_session_id> pointing at the old upstream conversation.
Rather than recreating the provider mid-session, /new now declines for those providers, matching how /model already refuses to switch when the provider manages its own context (session/mod.rs:898). Fixed in 649dc12.
ACP, claude-code and gemini-cli keep their upstream conversation inside the provider instance (AcpProvider::stream prompts self.acp_session_id(), GeminiCliProvider resumes its cached cli_session_id via -r), so swapping goose's session id alone would report a fresh session while the provider carried on with the old conversation. Refuse the command for those providers instead, matching how /model already declines to switch when manages_own_context() is true.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 649dc1293a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| .emit_hook(goose::hooks::HookEvent::SessionEnd, &self.session_id) | ||
| .await; | ||
|
|
||
| self.session_id = new_session_id; |
There was a problem hiding this comment.
Propagate the mode after switching session ids
Switching self.session_id here without also calling agent.update_goose_mode(..., &new_session_id) leaves per-session provider state behind. This is observable with the Codex provider: it stores modes in mode_by_session and defaults missing session ids to GooseMode::Auto, which maps to --yolo, so a user who ran /mode approve or /mode chat before /new gets the new session executed under Auto even though the session row was created with the stricter mode.
Useful? React with 👍 / 👎.
| let new_session_id = | ||
| create_successor_session(session_manager, &old_session, self.agent.goose_mode().await) | ||
| .await?; | ||
| self.agent.persist_extension_state(&new_session_id).await?; |
There was a problem hiding this comment.
Rebind extensions before persisting the successor
Persisting the extension configs into the new row does not rebind or restart the already-running MCP clients. Those clients record the first session id they see and assert it never changes in GooseClient::set_session_id, while stdio extensions are also launched with AGENT_SESSION_ID from the old session; with any loaded MCP extension that has been listed or called before /new, the next tool/prompt/resource request under the new id can panic or continue operating against the old session instead of the fresh one.
Useful? React with 👍 / 👎.
MCP clients pin themselves to the first session id they see a request for: McpClient::set_session_id asserts the id never changes, and every request routes through it, so swapping the session id panicked on the next tool call or prompt listing. The CLI lists extension prompts at startup, so every loaded extension is already pinned before the user types anything. /new now tears the extensions down and re-adds them under the new session id, which gives them fresh clients. Teardown happens after the swap so the previous session's extension_data is left intact, and it sends no MCP request of its own. It also propagates the current mode to the new session, so providers that track modes per session id (Codex keeps mode_by_session) do not fall back to their default for the fresh session. Every step after the swap reports failures instead of propagating them, so a failing extension leaves a usable session rather than tearing down the process.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e48aaf7890
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| self.messages.clear(); | ||
| self.run_mode = RunMode::Normal; |
There was a problem hiding this comment.
Reset goal/grind state for the fresh session
When /new is run while a /goal or /grind is still set, this only clears the visible conversation; the Agent's goal/grind fields are process state and survive the session id swap. The next turn in the new session can still receive the old objective nudge from the reply loop, so the supposedly fresh session continues the previous task until the user manually runs /goal off or /grind off; reset those fields here to match a real restart.
Useful? React with 👍 / 👎.
The agent keeps goal and grind in process-level fields, so they survive a session id swap and the reply loop would keep nudging the fresh session towards the previous objective. A real restart drops them, and /clear does not touch them either — only /goal off and /grind off do.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bc612f2b0a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if let Err(e) = self.agent.add_extension(config, &self.session_id).await { | ||
| output::render_extension_error(&name, &e.to_string()); |
There was a problem hiding this comment.
Abort /new when extension restart fails
With any configured extension whose process cannot be relaunched at /new time (for example a deleted binary, bad env, or a port already in use), add_extension returns Err here, but the code has already switched self.session_id, cleared the conversation, emitted SessionEnd, and still prints that the new session started. The new session therefore runs without an extension that was loaded in the old session, contrary to the command’s promise to keep extensions; fresh evidence is that the updated rebind loop renders the add error and continues instead of rolling back or aborting before announcing success.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Partly addressed. /new deliberately does not abort: this matches session start, where builder.rs warns "Failed to start extension '' (), continuing without it" and starts the session anyway. Aborting is also more than it sounds at this point — the swap and teardown have already happened, so it would mean relaunching the extensions under the old id rather than just returning.
The misleading part was real, though. The confirmation now names what did not come back ("Continuing without these extensions: ..."), so it no longer reads as if everything was carried over. af42a49
Report which extensions did not come back so the confirmation no longer reads like everything was carried over, matching how the builder warns and continues when an extension fails at startup. Also drop the old session's pending steers, which are keyed by session id and would otherwise never be delivered.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: af42a49b6b
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| } | ||
| s if s == CMD_ENDPLAN => Some(InputResult::EndPlan), | ||
| s if s == CMD_CLEAR => Some(InputResult::Clear), | ||
| s if s == CMD_NEW => Some(InputResult::New), |
There was a problem hiding this comment.
Add /new coverage to the self-test recipe
Because this adds a user-visible CLI feature, leaving goose-self-test.yaml unchanged means the repo’s required feature-validation recipe will not exercise the new-session transition, such as creating a new id while clearing history/tokens and preserving provider/extensions. Please add a self-test scenario for /new before landing.
AGENTS.md reference: AGENTS.md:L71-L71
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
/new is only reachable from the interactive loop: handle_slash_command is called from input::get_input, which has exactly one caller — run_interactive (session/mod.rs:573). The self-test runs via goose run --recipe, which takes the headless path (session/mod.rs:1378) and never reaches that code, so a scenario there could not exercise the transition.
That is also why the recipe covers no slash command today — /clear, /model and /compact are all absent. It exercises agent and tool capabilities, not the interactive CLI. Happy to add coverage if you would rather have it, but it would need a PTY-driven session rather than a recipe step.
alexhancock
left a comment
There was a problem hiding this comment.
I like the idea and thanks for submitting the change!
I am a bit concerned about how much manual state manipulation is required in handle_new
It feels like we should try to have the session manager handle more of what is needed in the normal way (just like when starting a fresh session in the CLI or desktop)
Thoughts?
|
|
||
| let extension_configs = self.agent.get_extension_configs().await; | ||
|
|
||
| self.agent |
There was a problem hiding this comment.
Shouldn't this be emitted from somewhere inside the agent already when a session ends?
|
@alexhancock thank you for getting back to the PR. I am on vacation until the end of next week, afterwards I will get back to you. |
|
Looking at it again I am OK with it as-is, and good to get this in |
* origin/main: fix(mcp): prune dead notification subscribers (#11032) chore: remove the extension and tool count suggestion (#10869) feat: compaction in the GDK (#11042) fix(provider): retry transient errors on first stream item before ending turn (#10968) feat(cli): add /new to start a fresh session without restarting (#10767) feat(acp): title new sessions from _meta.sessionTitle (#10712) fix: adjust rmcp::model::Meta ref (#11107) Skip hook loading and lifecycle events for subagents (#10596) Sanitize Unicode tags in Responses output (#10745) fix(conversation): sanitize nested tool responses (#10609) fix(hints): bound recursive file expansion (#10546) fix(providers): drop stale signed thinking blocks after a mid-conversation model switch (#10007) fix(desktop): clarify compact cost display (#11093) Index messages by (session_id, created_timestamp, id) to stop on-disk sort storms (#10874) docs: add tool shim guide covering when to enable, backends, and troubleshooting (#10858) fix(deep-link): route extension/session deep links to regular windows not standalone app windows (#10908) fix(ui): raise chat input z-index so slash menu appears above loading indicator (#11015) fix(ui): support remote working directory for external backend (#10827)
* main: fix(mcp): prune dead notification subscribers (#11032) chore: remove the extension and tool count suggestion (#10869) feat: compaction in the GDK (#11042) fix(provider): retry transient errors on first stream item before ending turn (#10968) feat(cli): add /new to start a fresh session without restarting (#10767) feat(acp): title new sessions from _meta.sessionTitle (#10712) fix: adjust rmcp::model::Meta ref (#11107) Skip hook loading and lifecycle events for subagents (#10596) Sanitize Unicode tags in Responses output (#10745) fix(conversation): sanitize nested tool responses (#10609) fix(hints): bound recursive file expansion (#10546) fix(providers): drop stale signed thinking blocks after a mid-conversation model switch (#10007) fix(desktop): clarify compact cost display (#11093) Index messages by (session_id, created_timestamp, id) to stop on-disk sort storms (#10874) docs: add tool shim guide covering when to enable, backends, and troubleshooting (#10858) fix(deep-link): route extension/session deep links to regular windows not standalone app windows (#10908) fix(ui): raise chat input z-index so slash menu appears above loading indicator (#11015) fix(ui): support remote working directory for external backend (#10827)
Conflicts all came from /new landing upstream (aaif-goose#10767) while coe/cli had grown /resume on top of it: - session/mod.rs, input.rs, completion.rs: kept our side, which generalises upstream's handle_new into switch_to_session(SessionTarget::New|Existing). - session/mod.rs imports: took upstream's (merge_consecutive_messages_for_request) without the task_execution_display import, which our cleanup had removed. - session/output.rs: kept both — upstream's new display_banner and our session name in the context bar. - completion.rs: dropped the duplicate test_complete_slash_commands_new. The kimi_code mapping resolved itself: upstream now carries the same line in name_builder.rs, so only our regression test remains on our side. cargo test -p goose-cli: 331 passed. clippy --all-targets -D warnings: clean.
Summary
Adds a
/newslash command to the interactive CLI: it starts a fresh session inside therunning process instead of requiring a restart.
Today
/clearempties the conversation but keeps the same session id, and it onlyresets the current
usage— the lifetimeaccumulated_*_tokenscarry on. So severalunrelated tasks in one sitting all end up under one session id, unless you quit and start
goose sessionagain, which drops the loaded extensions and the provider connection.--resume --forkcreates a new session, but it is a startup flag and it copies theconversation, so it is not "start clean" either.
/newcreates a new session row and switches the live session over to it, carrying overeverything that describes the process rather than the conversation: provider name and
model config, the current goose mode, the loaded extensions, working directory, session
type, recipe and project id. The previous session stays on disk untouched, and its
SessionEndhook fires before the switch.SessionStartfor the new session needs nospecial handling — the agent emits it on the first turn of a session it sees as empty.
Carrying
provider_nameandmodel_configover matters because they are the per-sessionsource of truth for the displayed model and the context limit
(
Agent::model_config_for_session); without them a/modelswitch made before/newwould silently be lost.
State that lives on the process, not the session
Most of the work here is about state that would otherwise survive the session swap and
make the "fresh session" a lie:
McpClient::set_session_idasserts the id never changes, and every request routesthrough it (
send_request_with_context). Since the CLI lists extension prompts atstartup, every loaded extension is already pinned before the user types anything — so
swapping the id would panic on the next tool call.
/newtherefore tears the extensionsdown and re-adds them under the new id, which gives them fresh clients. Teardown happens
after the swap so the previous session's
extension_datastays intact, and it issues noMCP request of its own.
update_goose_modeis the only path that tells the provider about asession's mode; providers that track modes per session id (Codex keeps
mode_by_session) would otherwise fall back to their default for the new session.nudging the fresh session towards the previous objective. A real restart drops them.
Providers that manage their own conversation context (ACP, claude-code, gemini-cli) keep
their upstream conversation inside the provider instance, which a session id swap cannot
reach.
/newdeclines for those, matching how/modelalready refuses to switch whenmanages_own_context()is true.The session id is only swapped once every fallible step has succeeded, so a failure leaves
the running session usable. After the swap nothing propagates errors — a failing extension
is reported and the confirmation names what did not come back, matching how the builder
warns and continues when an extension fails at startup.
This is CLI-only. Swapping the client's session id is a client concern, so
/newisdeliberately not registered as an agent-level command in
execute_commands.rsand does notaffect Desktop/ACP.
Kept out on purpose: a
/new <name>argument (naming stays automatic, as at startup), andany change to
/clear.Testing
cargo test -p goose-cli— 270 passed, including tests thatcreate_successor_sessioncarries provider, model config, working dir and session type to the new session, applies
the live goose mode, starts with empty history and empty token counters (including
accumulated_*, which is what distinguishes/newfrom/clear), and leaves both theold session's messages and its
extension_dataintact; plus that/newparses and isoffered by completion.
cargo clippy -p goose-cli --all-targets -- -D warnings— clean.with MCP clients, and
goose-clihas no such fixture today. Rather than add a test thatwould not exercise it, the ordering is argued from the call sites above and the path was
exercised manually on Linux: with
/mode approveset,/newrestarted the extensionsand a following extension tool call ran without the panic described above, while the
approval prompt still appeared — so the mode reached the new session. In the session
store the new row started with empty history and its own token counters from zero, and
the previous row kept both its messages and its
extension_data.