Skip to content

feat(cli): add /new to start a fresh session without restarting - #10767

Merged
alexhancock merged 5 commits into
aaif-goose:mainfrom
johanndrews:feat/cli-new-session-command
Aug 10, 2026
Merged

feat(cli): add /new to start a fresh session without restarting#10767
alexhancock merged 5 commits into
aaif-goose:mainfrom
johanndrews:feat/cli-new-session-command

Conversation

@johanndrews

@johanndrews johanndrews commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a /new slash command to the interactive CLI: it starts a fresh session inside the
running process instead of requiring a restart.

Today /clear empties the conversation but keeps the same session id, and it only
resets the current usage — the lifetime accumulated_*_tokens carry on. So several
unrelated tasks in one sitting all end up under one session id, unless you quit and start
goose session again, which drops the loaded extensions and the provider connection.
--resume --fork creates a new session, but it is a startup flag and it copies the
conversation, so it is not "start clean" either.

/new creates a new session row and switches the live session over to it, carrying over
everything 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
SessionEnd hook fires before the switch. SessionStart for the new session needs no
special handling — the agent emits it on the first turn of a session it sees as empty.

Carrying provider_name and model_config over matters because they are the per-session
source of truth for the displayed model and the context limit
(Agent::model_config_for_session); without them a /model switch made before /new
would 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:

  • MCP clients pin themselves to the first session id they see.
    McpClient::set_session_id asserts the id never changes, and every request routes
    through it (send_request_with_context). Since the CLI lists extension prompts at
    startup, every loaded extension is already pinned before the user types anything — so
    swapping the id would panic on the next tool call. /new therefore tears the extensions
    down and re-adds them under the new id, which gives them fresh clients. Teardown happens
    after the swap so the previous session's extension_data stays intact, and it issues no
    MCP request of its own.
  • Provider mode. update_goose_mode is the only path that tells the provider about a
    session'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.
  • Goal and grind are process-level fields on the agent, so the reply loop would keep
    nudging the fresh session towards the previous objective. A real restart drops them.
  • Pending steers are keyed by session id and would never be delivered after the swap.

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. /new declines for those, matching how /model already refuses to switch when
manages_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 /new is
deliberately not registered as an agent-level command in execute_commands.rs and does not
affect Desktop/ACP.

Kept out on purpose: a /new <name> argument (naming stays automatic, as at startup), and
any change to /clear.

Testing

  • cargo test -p goose-cli — 270 passed, including tests that create_successor_session
    carries 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 /new from /clear), and leaves both the
    old session's messages and its extension_data intact; plus that /new parses and is
    offered by completion.
  • cargo clippy -p goose-cli --all-targets -- -D warnings — clean.
  • The extension restart itself is not covered by an automated test: it needs a live agent
    with MCP clients, and goose-cli has no such fixture today. Rather than add a test that
    would not exercise it, the ordering is argued from the call sites above and the path was
    exercised manually on Linux: with /mode approve set, /new restarted the extensions
    and 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.

/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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +1102 to +1103
self.messages.clear();
self.run_mode = RunMode::Normal;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +1129 to +1130
if let Err(e) = self.agent.add_extension(config, &self.session_id).await {
output::render_extension_error(&name, &e.to_string());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/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 alexhancock self-assigned this Jul 30, 2026

@alexhancock alexhancock left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be emitted from somewhere inside the agent already when a session ends?

@johanndrews

johanndrews commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

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

@alexhancock

Copy link
Copy Markdown
Collaborator

Looking at it again I am OK with it as-is, and good to get this in

@alexhancock
alexhancock merged commit aba0943 into aaif-goose:main Aug 10, 2026
25 checks passed
michaelneale added a commit that referenced this pull request Aug 10, 2026
* 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)
lifeizhou-ap added a commit that referenced this pull request Aug 11, 2026
* 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)
johanndrews added a commit to johanndrews/goose that referenced this pull request Aug 15, 2026
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.
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.

2 participants