Skip to content

feat(goose): add the goose agent type - #1125

Merged
salman1993 merged 10 commits into
mainfrom
sm/goose-agent-plugin
Oct 15, 2026
Merged

salman1993 merged 10 commits into
mainfrom
sm/goose-agent-plugin

Conversation

@salman1993

Copy link
Copy Markdown
Contributor

Adds a bundled Goose agent type that runs goose locally over ACP and reaches the Buzz tools through a loopback HTTP MCP server.

This is the goose plugin from #1010 and #1096, aimed at main. #1096 was merged into sm/goose-agent-plugin, and #1010 was closed, so none of it reached main. #1110 stacks on this PR and bundles goose-acp with the app.

Why

Buzz agents run Claude Code or Codex today. goose adds an open-source agent that works with many providers.

What

  • Sessions:
    • Each agent runs one goose-acp process, with one ACP session per conversation: a thread, a DM, or a channel when the agent is set to one session per channel.
    • A quiet session closes after 15 minutes.
    • Watches and timers run as standalone turns (runOnce), each in a one-off session, as Claude and Codex do.
  • Buzz tools over HTTP MCP:
    • The app serves the Buzz tools to each goose session from a loopback HTTP MCP server, and passes its URL and token in session/new.
    • The agent's key stays in the app.
    • Any plugin can do this: declare "mcp": true in its manifest and call host.serveMcp(handler).
  • Prompt:
    • It reuses Claude's systemPrompt(), appended to goose's own system prompt.
    • Messages that arrive mid-turn steer the current turn.
    • A turn that writes text but never calls send posts that text as the reply.
  • Provider and model:
    • New agent and library cards show goose's default, or the agent's own choice, through main's CreateModel field.
    • The field opens a dialog to pick a provider, save its key into goose, pick a model, and test the connection.
    • Settings can change them per agent.
  • Activity tab: shows the agent's goose sessions and their transcripts.

How

  • Listener: the native listener (src-tauri/src/host_mcp.rs) uses hyper's HTTP/1 server, already in the tree through reqwest. It binds 127.0.0.1 on a random port.
  • Tokens: each server checks its bearer token. A refused request gets 404, because goose starts an OAuth sign-in on 401 or 403.
  • Lifetime: there is one server per ACP session, so each tool call maps to its conversation. It closes with its session, a page reload, or app exit.
  • Declaration: goose ignores mcpServers when _meta.enabledExtensions is sent, so the server is declared in both.
  • Ports to main: the last two commits move the model picker to main's CreateModel field (Streamline agent creation and library controls #966), and wakes to the shared runOnce and attention tools (feat(agents): share attention tools across Claude and Codex #1011).

Risk

Low. Goose agents are not released yet. The host MCP capability works only for plugins that declare "mcp": true, and only the goose plugin does. Until #1110 lands, a Goose agent needs goose-acp on PATH.

Testing

  • Before the ports: GOOSE_ACP_BIN=~/.local/bin/goose-acp pnpm exec vitest run src/bundled/goose/goose.live.test.ts passed all 3 tests against goose main (9686d6740): reply in Buzz, steer mid-turn, and the provider check.
  • In the app: I ran just desktop and @mentioned a Goose agent in a thread. It read the thread, fetched an image with media_get, and replied, and the transcript showed in the Activity tab.
  • After the ports: tsc --noEmit and biome are clean. vitest run src/bundled src/features/host src/features/agents src/buzz-mcp passed 2,807 tests. One claude-sh timeout passes alone.
  • Not yet run after the ports: the live goose tests and a watch or timer wake in the app.

🤖 Generated with Claude Code

salman1993 and others added 7 commits October 13, 2026 17:59
A bundled `buzz.goose` plugin whose agents run as one `goose-acp` process
each, with one ACP session per conversation. The Buzz tools reach goose over
MCP-over-ACP (`mcp/message` on the same pipe), so the process holds no Buzz
key. Uses the Claude plugin's Buzz prompt, appended with
`_goose/unstable/session/system-prompt/set`; names sessions with
`_meta.sessionTitle`; steers mid-turn messages with
`_goose/unstable/session/steer`. Provider and model come from the owner's
goose config unless the agent names its own.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Salman Mohammed <[email protected]>
- New agent: the Goose type menu offers goose's own default provider and
  model, or a dialog to choose a provider, save its key into goose, pick a
  model, and test the connection. The card body keeps only instructions.
- Settings: provider and model per agent, and Runtime diagnostics listing
  the agent's goose sessions with their transcripts, read through
  session/list and _goose/unstable/session/export.
- A turn that writes text without calling send is posted as the agent's
  reply rather than reported as a failure.
- A missing goose-acp says so; test connection deletes its session.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Salman Mohammed <[email protected]>
…MCP (#1096)

Signed-off-by: Salman Mohammed <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
main replaced the type menu's CreateModelMenu with a CreateModel field, so
goose shows its default or chosen provider and model as a button that opens
the provider dialog.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
main's #1011 moved watch and timer wakes, their prompts and the attention
tools into the shared agent harness. goose now runs a wake as a standalone
turn (runOnce) in a one-off session, as Claude and Codex do, and drops its
own copy of the lanes and wake prompt. Every session lists the attention
tools, and the end-of-turn note comes from turnNote.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@salman1993
salman1993 requested a review from a team as a code owner October 14, 2026 21:18
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-14T21:23:29.591849Z 128b6ea PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@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: 128b6ea1a5

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/bundled/goose/goose.ts Resolved
Comment thread src/bundled/goose/goose.ts Resolved
Comment thread src/bundled/goose/tabs.tsx Resolved
Comment thread src/bundled/goose/tabs.tsx Resolved

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

🤖 Carl, an automated reviewer, commenting via Wes’s GitHub account.

Verdict: three recommended blockers. Fix them before merge. Reviewed head 128b6ea1 against main ea96e211. Princess Donut covered UI/setup, Mongo the runtime, and Mordecai the security boundary; I integrated and re-checked each finding at this head. The plugin code was first reviewed in #1110 before it moved here. #1010 closed without reviews, and #1096's approval targeted the feature branch. So this is the plugin's first review against main.

Recommended blockers (details inline)

  1. Bearer tokens persisted: each session's MCP bearer token is written to goose's shared sessions.db. A prompt-injected agent can use it to act as another agent in Buzz.
  2. Server pool exhaustion: Goose sessions can exhaust the app-wide pool of 64 MCP servers. After that, every new conversation fails.
  3. Enter in an API key field: it saves the agent instead of the key, which leaves the agent pointed at a provider with no key.

Should fix

  • Fallback reply: any tool call suppresses it.
  • Steered bursts: they post duplicate fallback replies or warnings.
  • Hung goose: it wedges an agent indefinitely.
  • Dialog focus: the provider dialog drops focus on close.
  • Activity: it is matched by display name.

What checks out

  • MCP listener: binds numeric 127.0.0.1 only. It requires an exact Host, refuses any Origin (DNS rebinding), and compares a 256-bit token per server in constant time.
  • Access grants: the capability is main-webview only, gated on the installed plugin's host.mcp grant, and the import prompt discloses it.
  • Agent identity: BUZZ_*/NOSTR_* are stripped from goose's environment. Mentions require owner or delegated authority (fromOwner).
  • Provider keys: they reach only goose (providers/config/save), never agent config or logs. goose falls back to plaintext ~/.config/goose/secrets.yaml when the OS keyring is unavailable, so the dialog's "saved in goose" copy may want that caveat.
  • Design system: the UI uses the shared components and modal composition, apart from the focus issue inline.
  • CI: the stale tabs.test.tsx assertion is fixed at this head.

Not inlined (optional)

  • Runtime tests: runtime.test.ts covers only sessionTitle. The lifecycle tests advance flush(30) microtask rounds instead of awaiting a fake-process barrier, which goes against AGENTS.md "Deterministic tests" and misses the paths above.
  • Test connection: it checks goose's saved config, so a typed but unsaved key fails with an auth error.
  • Host contract: Goose skips the host AgentType.provider contract that claude-code and codex implement. Whether to adopt it is a product call.

Product decision for owners: the plugin is enabledByDefault (src/bundled/index.ts, plus enabled_by_default("buzz.goose")). Merging ships the Goose agent type to every Mac and Linux desktop build. #1110 describes Goose agents as "not released yet". Confirm that is intended, or gate it.

Comment thread src/bundled/goose/goose.ts Resolved
let session: GooseSession | undefined;
if (!launch.bare) {
if (!this.serve) throw new Error("This goose serves no Buzz tools");
server = await this.serve((message) => this.mcp(session, message));

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.

🤖 Recommended blocker: Goose sessions can exhaust the app-wide pool of 64 MCP servers.

Every non-bare session starts its own server here. host_mcp.rs caps servers at MAX_SERVERS = 64 across all plugins, and rejects the 65th before binding. AgentSessions keeps each conversation's session, and therefore its server, for IDLE_MS (15 minutes) after its last turn. There is no count cap or LRU, and watch/timer one-offs take a slot as well.

Failure: sessions default to one per thread. Four Goose agents each mentioned in about 16 threads within 15 minutes fill the pool. Every new conversation then gets goose could not start: Too many MCP servers are running, posted as ⚠️ I couldn't finish that (sessions.ts deliver → runtime.ts report). Retries keep failing until an idle timer frees a slot. The plugin is on by default.

Fix: add a runtime-wide idle-session LRU below the native ceiling, shared across all Goose agents. Evict the oldest non-busy conversation before opening a server, and retry once on the explicit capacity error. A per-agent cap can't protect the global pool, and raising 64 only moves the failure.

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.

🤖 Agreed this is real. I am deferring it to a follow-up PR instead of fixing it here. To hit it, the 64-server pool must fill with live sessions inside the 15-minute idle window, which is unlikely during early use. The right fix is the runtime-wide idle LRU you describe, with one retry on the capacity error. That changes how sessions.ts opens and evicts sessions. The reopen race (Kalvin's thread) and the RPC timeouts need the same code, so I would rather redo session lifecycle once, with tests for all three, than bolt pieces onto this PR.

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.

🤖 Fixed in #1142. All goose agents now share one pool of at most 48 MCP servers. Opening a session closes the least recently used idle one when the pool is full. A "Too many MCP servers" error from the host closes one and retries once. Tests cover eviction across agents, busy sessions that are never closed, and the retry.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Rechecked at ba39d40: the app-wide 64-server ceiling remains (host_mcp.rs:48,149), with one server per Goose session and no shared pooling/eviction in this candidate. The referenced #1142 capacity fix has not entered #1125 and was not validated in this review.

Comment thread src/bundled/goose/tabs.tsx Resolved
Comment thread src/bundled/goose/goose.ts Resolved
this.lastError = undefined;
this.acted = false;
this.written = "";
for (const resolve of this.waiters.splice(0)) resolve(settled);

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.

🤖 Should fix: a steered burst posts the same fallback or error once per mention.

Each send() adds a waiter, and settle() resolves all of them with the same Settled. Every delivery installs its own post/report handler (runtime.ts:258-264). So two mentions folded into one running turn post two identical fallback replies, and a crash posts one ⚠️ warning per mention.

Fix: give each burst's result one posting owner, normally the latest acknowledged delivery. Resolve the other waiters without text or error ownership.

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.

🤖 Deferring to the follow-up. This only shows when a burst with two or more steered mentions ends with no send, or when goose crashes. The user then sees the same fallback or warning twice. Nothing is lost or wrong. Giving each burst one posting owner changes how settle() and runtime.ts share a result. I want to make that change together with the other session-lifecycle changes.

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.

🤖 Fixed in #1142. settle() now gives the error or text to one owner, the latest acknowledged message (or else the latest sent). The other waiters get only ok, and the runtime posts only when a result has an error or text. Tests cover the fallback text and a crash.

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.

🤖 One correction to "nothing is lost or wrong". In channel or DM scope, one session serves several threads (the key is the channel). Every delivery in a settlement gets the same Settled, and each run posts it through its own captured event (runtime.ts:257-264). So the fallback or ⚠️ text from one burst is copied into each triggering thread, including threads it was not written for. That is wrong-thread content, not just a duplicate. It still fits the deferred fix: give each settlement one posting owner and destination.

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.

🤖 Agreed, thanks for the correction. #1142 fixes this too. settle() now gives the error or fallback text to one waiter, the latest acknowledged message. The other waiters get only ok, and runtime.run posts only a result that has error or text. Each delivery posts through its own captured event, so in channel or DM scope the burst posts once, to the owner's thread, and other threads get nothing.

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.

🤖 @wesbillman Confirmed: #1142 fixes the wrong-thread case (#1142). GooseSession.settle() gives the burst's error or text to one waiter. That is the latest acknowledged message, or the latest sent when none was acknowledged. The other waiters get only { ok }. runtime.run posts only a result that carries error or text, through that delivery's own event. So in channel or DM scope, a burst's fallback or ⚠️ goes to the owner's thread only. The new test "posts a channel session's burst once, in the latest message's thread" in runtime.test.ts covers it. It sends two mentions in different threads into one channel-scope session and checks for one post in the second thread. It fails with a shared Settled. The Me-chat test "continues an owner's Me chat as one channel session posted at the top level" now expects one ⚠️, not two.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Rechecked at ba39d40: goose.ts:559–565 still resolves every waiter with the same result, and runtime.ts:276–289 publishes per delivery. Duplicate/wrong-thread fallback or error publication therefore remains in #1125. The proposed single-owner fix is in separate #1142, which this review did not validate.

}

/** Sends a request and resolves with its result. */
request(method: string, params: Record<string, unknown>) {

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.

🤖 Should fix: a goose process that is alive but unresponsive wedges the agent and keeps its MCP server allocated.

request() has no timeout or abort. open() starts the server before awaiting session/new. If goose stays alive but stops answering, AgentSessions.opening and current.ready never settle (sessions.ts), so later messages for that agent wait indefinitely. The server stays allocated, which adds to the 64-server limit. runOnce's 5-minute deadline starts only after open, and then awaits an unbounded close() in finally, so it doesn't cover this.

Fix: bound the initialize, session/new and session/close RPCs. On expiry, kill the process: its exit handler already rejects calls and closes sessions and servers. Thread the delivery's abort signal through startup.

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.

🤖 Deferring to the session-lifecycle follow-up. This needs a goose that is alive but not answering, which we have not seen. The fix touches the same open and close paths as the pool LRU and the reopen race. Timeouts on initialize, session/new and session/close, with a kill on expiry, belong in that PR, so the three interact under one set of tests. session/prompt stays unbounded, since a turn can run for a long time.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 [P2] This also affects finite startup delays at 2570b16; an unresponsive process is not required.

runtime.ts:271–283 discards StandaloneTurn.signal. The runner aborts that signal after five minutes including readiness (runner.ts:865–876), while Goose starts its own five-minute timer only after readiness and prompt construction (sessions.ts:167–182). A 30-second startup delay therefore permits execution until 5:30, and readiness completing after the host deadline can still start a turn. The runner continues awaiting the harness after aborting.

Forward the shared signal through startup, check it before prompting, and cancel/close on abort. This is established by source tracing; no live timing reproduction is claimed.

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.

🤖 Fixed in #1142. initialize, session/new, the system-prompt set and session/close time out after 60s, and goose is then killed. session/prompt stays unbounded. The delivery's signal now bounds startup. runOnce uses the runner's standalone signal in place of its own deadline, checks it before prompting, and closes a session that opens after the turn was given up (Kalvin's follow-up).

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.

🤖 Fixed in #1142. runOnce now takes the runner's StandaloneTurn.signal in place of goose's own deadline, so the deadline counts readiness too. It checks the signal before prompting, and on abort it cancels and closes the session. A session that opens after the turn was given up is closed once it opens. Mentions pass the delivery's signal through startup the same way. Tests cover an abort mid-turn and an abort while the session is still opening.

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.

🤖 @kalvinnchau #1142 handles this (#1142). GooseRuntime.runOnce now passes StandaloneTurn.signal to AgentSessions.runOnce, which drops goose's own 5-minute timer. The runner's deadline therefore counts readiness too. The signal bounds the wait for the session to open. It is checked again after prompt() and before the turn is sent, and it bounds the turn. On abort, the turn returns the signal's reason, and the session is cancelled and closed. If the session opens after the turn was given up, it is closed as soon as it opens and prompt() is never called. Mentions pass the delivery's signal through startup the same way. Tests in sessions.test.ts: "stops a one-off turn when its signal aborts" and "gives up a one-off turn whose session is still opening, and closes it once open".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Rechecked at ba39d40: runtime.ts:299–309 still discards StandaloneTurn.signal, so the shared deadline does not bound startup and work can begin after it expires. Forward the signal through startup and cancel/close on abort. The #1142 implementation described above is absent from this candidate; this review did not validate #1142.

Comment thread src/bundled/goose/tabs.tsx Resolved
return this.with(signal, async (goose, spawn) => {
const cwd = await absolute(spawn, workspace);
return (await list(goose, cwd)).filter((row) =>
row.title.startsWith(`${name} · `),

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.

🤖 Should fix: Activity matches an agent's sessions by display name.

recent() keeps rows whose title starts with ${name} · . Names aren't unique and can change. Two Goose agents named "Helper" in the default workspace see each other's sessions, and renaming an agent hides its own recent sessions. Opening a row then reads that session id without checking that the session belongs to this agent's pubkey.

Fix: filter by the agent pubkey recorded in the session's first prompt (as startedFor already does for find), not by title.

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.

🤖 Deferring to a follow-up. Activity is a read-only view of the owner's own goose sessions on their own machine, so no data crosses users. The mix-up only happens between the same owner's agents that share a name and a workspace, or after a rename. Filtering by the pubkey in the first prompt, as startedFor does, is the right fix. I will add it in the follow-up.

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.

🤖 Fixed in #1142. recent() now takes the agent's pubkey. It lists the sessions that mention the pubkey, and keeps those whose first prompt names it in Agent:, the same check startedFor makes. The test covers a renamed agent and another agent with the same name.

read().then(
(value) => current && setState({ status: "ready", value }),
(error: unknown) =>
current && setState({ status: "error", error: message(error) }),

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.

🤖 Optional: useRead drops the shown value on error.

The loading branch keeps prior.value, but the error branch replaces it. If the provider re-read after "Save to goose" fails, the Provider select disables and the key fields and models disappear. Keep value: prior.value in the error state too, as the type already allows.

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.

🤖 Leaving this one for the follow-up, since it is optional. A failed re-read clears the fields until the user picks Try again, and that brings everything back. Keeping prior.value in the error state is a one-line change, and I will fold it into the follow-up.

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.

🤖 Fixed in #1142. The error state now keeps prior.value. A new test makes the re-read after Save fail. The Provider select stays enabled and shows the chosen provider, next to the error and Try again.

return this.answer(key, live, live.session.busy ? steer : text, activity);
}
// Perhaps built for a session closed since, which had been shown the rest.
if (this.had.has(key) && fresh) text = await fresh();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 [P2] Reserve the reopening slot before rebuilding context

After idle expiry, a settings change, or a process exit, two close mentions can both await fresh() before either registers opening[key]. Both then open sessions, and adopt() overwrites the first entry without closing it. The orphan's idle timer cannot close it because it only acts on the currently mapped session, leaving its Goose session and MCP server allocated until process exit. Dispatcher serialization does not prevent this: runtime.run() returns while delivery is unresolved (runtime.ts:246-266).

A preserved scripted ACP probe from #1110 opened two replacement sessions for one conversation and tracked only the second; the session implementation and adapter are byte-identical at this head. This was not rerun at the current head. This is distinct from normal 15-minute idle retention exhausting the server pool.

Reserve a single opening promise before any fresh() await, with context rebuilding and opening inside it. Add a concurrent-reopen test with gated context rebuilding that asserts one replacement session and complete cleanup.

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.

🤖 Thanks. Deferring to the session-lifecycle follow-up. The orphan is one goose session and its MCP server, and it lasts until the goose process exits. The token change in 2570b16 means the orphan's token admits nothing outside goose. Reserving the opening promise before fresh() is the right fix. It lands best next to the pool LRU, which changes the same opening path. That PR will add the gated concurrent-reopen test you describe.

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.

🤖 Fixed in #1142. deliver now reserves the opening promise before any await, and fresh() runs inside it. The new gated test holds fresh(), sends two messages, and checks for one replacement session. It also checks that both sessions and both servers are closed after the idle timeout. The test fails on the old order.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Reproduced again at ba39d40: concurrent reopening creates three sessions overall, leaves session-2 and its MCP server unclosed, and tracks only session-3. The reservation still follows the fresh() await (sessions.ts:126–128). #1142 is a separate proposed fix, not a fix present in this candidate.

goose saves each session's extensions, with the Buzz server's bearer
token, in its sessions.db. The token now admits one MCP session: the
agent's initialize gets a random Mcp-Session-Id, and every later request
must send it, so a token read from disk admits nothing. A message before
initialize (rmcp first asks server/discover) needs no session.

- Enter in a provider's key field saves the key, not the agent settings.
- Only a send, edit or react that succeeded suppresses goose's text; a
  read or a failed send still posts it.
- A turn that ends while a steer into it is answered now settles.
- The provider dialog returns focus to its trigger.
- Settings refuses a workspace that is not absolute or ~/…

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Salman Mohammed <[email protected]>
}

/** Stops `process` when it has old settings and nothing runs in it. */
private retire(process: Process) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 [P2] Keep processes alive while sessions are opening

Saving a model, provider or workspace change while goose.open() is pending calls retire(), but this only counts live and once users. The opening session has not been adopted into either, so its process is killed. Delivery fails with goose could not start: goose is not running and is not retried; the fingerprint retry at lines 244–248 only runs after a successful open.

Reproduced at 2570b16 using the PR's fakes: warm the process, hold serve while opening a second conversation, change the environment, call reconfigure(), then release it. The process is killed and delivery fails. Count in-flight opens as process users, decrement in a finally block, and retry retirement after decrement. Add this gated-open regression test.

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.

🤖 Fixed in #1142 (e9bde62). open() now counts its session as a user of the process until the caller records it, decrements in a finally, and calls retire() again after. The new test holds serve while a second conversation opens, saves new settings, then releases it. The delivery now succeeds in the old process, and the process stops once nothing runs in it. The test fails at 2570b16.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Rechecked at ba39d40: this still reproduces in #1125. Holding serve during an open, changing settings, calling reconfigure(), then releasing returns goose could not start: goose is not running with the process killed. The opening session is still omitted from retirement's users (sessions.ts:287–297). The referenced fix is in separate #1142; it is not part of this candidate and was not validated in this review.

@salman1993

Copy link
Copy Markdown
Contributor Author

🤖 On the product decision in the review: Goose staying enabled by default is intended. Merging this PR ships the Goose agent type to every Mac and Linux desktop build.

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

🤖 Carl, an automated reviewer, commenting via Wes’s GitHub account.

Verdict: recommended blockers remain. This re-review covers head 2570b168 against main ea96e211 and follows my review at 128b6ea1. Princess Donut took the MCP server, Mongo the runtime and sessions, and Mordecai setup and transcripts, plus a challenge pass. I re-checked each finding at this head.

Prior blockers

  1. Bearer token: fixed for the steady state. After initialize, the token alone admits nothing (constant-time session ID, 404 otherwise). The startup window is still open (Kalvin's P1 in the token thread). The new claim handshake also has an availability regression, covered in a reply there.
  2. Pool of 64 servers: still open. The author deferred it, so merging accepts that. It is the merge owner's call, now that Goose ships enabled by default.
  3. Enter in a key field: fixed and tested.

New recommended blocker (inline)

  • Me chat: goose skips #1000's Me-chat policy, so a goose agent threads its replies in the owner's Me chat.

New should-fix items (inline)

  • Process leak: settings changed A→B→A leave an old goose process running until the app quits.
  • Activity: goose's persisted error blocks are dropped, so failed turns show up empty.

Prior should-fix items: the fallback suppression, steer settlement, dialog focus and workspace items are fixed. Double posting, a hung goose and Activity name matching are deferred. A reply on the double-post thread shows it also posts into the wrong thread. We independently confirmed Kalvin's runOnce signal finding.

Optional, not inlined

  • Stale models: after re-saving a configured provider (new host or key), the model list keeps the old endpoint's models. The read key at tabs.tsx:142-145 is only id:configured.
  • Button names: every Recent sessions row has the same accessible name, "View transcript" (tabs.tsx:563). Add the session label.

Validation: source review only at this head. CI is green; there were no local or live runs.

Comment thread src/bundled/goose/runtime.ts Resolved
private retire(process: Process) {
if (
!this.disposed &&
process.fingerprint === this.options.processFingerprint()

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.

🤖 Should fix: settings changed A→B→A leak the old goose process.

retire() returns early for any process whose fingerprint matches the current settings, not only the actual current process. To reproduce:

  1. Keep a session busy in process A1.
  2. Change the model, so process B starts.
  3. Change it back, so process() starts a new A2: current was B, so the fingerprints differ.
  4. Let A1's session finish and idle out.

drop() then calls retire(A1), which returns because A1's fingerprint equals processFingerprint(). A1 has no users and is not current, but it stays alive until app exit or the next settings change. This differs from Kalvin's retire-while-opening thread: A1 here was a real adopted user.

Fix: keep only the process that this.current resolves to, by identity, and kill any non-current process with no users.

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.

🤖 Fixed in #1142 (e9bde62). retire() now keeps only the process that this.current resolves to, by identity, and only while its settings are current. Any other process with no running or opening sessions is killed. The new test runs A→B→A with a busy session in A1, lets it idle out, and checks that A1 stops and A2 keeps running. The test fails at 2570b16.

Comment thread src/bundled/goose/transcript.ts Resolved
From the re-review of #1125 at 2570b16:

- An initialize holds the session only while it waits. A success claims
  it; a failure, or a client that hangs up, releases it, so goose can
  connect again. Before the claim, only initialize, server/discover and
  ping are answered, so the token alone calls no tools.
- A second initialize means two clients hold the token, so the server
  stops, and goose closes the session with an error. A process that read
  the token from goose's sessions.db before goose connected is cut off.
- goose follows #1000's Me-chat policy, as Claude Code and Codex do: one
  channel conversation, top-level posts, and a failed turn when the
  placement cannot be read.
- Activity shows goose's error blocks and lasting notices, so a failed
  turn is not empty.
- A provider's models are read again after its fields are saved.
- Each Recent sessions button is named for its session.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Salman Mohammed <[email protected]>
@salman1993

Copy link
Copy Markdown
Contributor Author

🤖 Re: Carl's re-review at 2570b16. ba39d40 addresses it as follows.

Fixed here

  • Claim handshake: three states, plus a pre-claim allow-list. A second initialize stops the server. See the token thread.
  • Me-chat policy: matches Claude Code and Codex, with tests.
  • Activity: goose's error blocks and lasting notices now show.
  • Stale models (optional): saving a provider's fields re-reads its models. The read key now counts saves.
  • Button names (optional): each Recent sessions button is named View transcript: <session>.

Startup race: narrowed, not closed. The window is now the few milliseconds before goose's own initialize, and it fails loudly. Closing it fully needs goose to stop persisting ACP-provided MCP headers. Details are in the token thread.

In #1142, which is stacked on this PR and changes the same sessions.ts code: retire-while-opening, the A→B→A process leak, the runOnce startup signal, and one posting owner per burst (which also fixes the wrong-thread copy). The pool LRU stays there as well, as discussed.

Comment thread src-tauri/src/host_mcp.rs Outdated
Ok(Ok(body)) => match String::from_utf8(body.to_bytes().into()) {
Ok(body) => match join(shared, named, &body) {
Ok(join) => {
let claim = (join == Join::Claiming).then_some(Claim {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 [P1] Construct the claim guard lazily

then_some(Claim { … }) eagerly constructs its argument even for Join::Inside. The unused guard immediately drops and Claim::drop (lines 455–459) resets the session to UNCLAIMED before forwarding. Thus initialize → notifications/initialized succeeds, but the next request with the valid session ID gets 404. A holder of the persisted bearer can initialize again without triggering duplicate-client shutdown; discovery/ping during CLAIMING can also reset ownership.

An isolated Rust reproduction confirmed the reset and successful reclaim; this was not native HTTP or Goose E2E. Use .then(|| Claim { … }). Add native tests for several consecutive authenticated requests and discovery/ping during held initialization. The existing tests stop after one admitted exchange, and the Node substitute does not exercise this destructor.

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.

🤖 @kalvinnchau Good catch, confirmed. The unused guard reset every request's claim. 5ff7d67 builds the guard only for the claiming initialize, and its Drop now releases only CLAIMING, so a stray guard can never undo a finished claim. Two native tests cover what you described: keeps_the_session_across_requests and holds_the_claim_while_initialize_waits. Both failed before the fix.

`then_some` built the `Claim` guard for every request, and the unused
guard reset the claim to UNCLAIMED when it dropped. So the agent's
second request in its session got 404, and a holder of the saved token
could initialize again without stopping the server.

Build the guard only for the `initialize` that claims, and let a guard
release only the CLAIMING state it holds.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Salman Mohammed <[email protected]>

@kalvinnchau kalvinnchau left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@salman1993
salman1993 merged commit 31cdec9 into main Oct 15, 2026
32 checks passed
@salman1993
salman1993 deleted the sm/goose-agent-plugin branch October 15, 2026 02:38
salman1993 added a commit that referenced this pull request Oct 15, 2026
#1125's goose files on main match 5ff7d67, which this branch already has, so each add/add conflict keeps this branch's version.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Salman Mohammed <[email protected]>
johnmatthewtennant added a commit that referenced this pull request Oct 15, 2026
* origin/main:
  fix(navigation): share one abort reason so Settings → Back stops leaking DOM on V8 (#1161)
  Unify sidebar resizing and polish Solo navigation (#1132)
  Organize Builderlab agent identity and runtime settings (#1153)
  fix(channels): keep consecutive tab closes in the closing pane (#1158)
  feat(goose): add the goose agent type (#1125)
  feat: edit the last message with Cmd+Up (#1042)
  fix(channels): make Enter press the prominent lifecycle button (#1152)
  feat(me): drag chats between projects and Chats (#1112)
  fix(search): find open channels past the first 500 in Command-K (#1128)
  Stop in-flight request mocks from failing finished browser tests (#987)
  fix(channels): resolve template agents from ready inventory (#922)
  [buzz-app] Fit uploaded videos to the relay's 4K envelope and name video rejections (#1129)
  feat(github): reveal the PR comment a link points at (#947)
  fix(messages): stop WebKit freeze from muted composer video previews (#1143)

Signed-off-by: John Tennant <[email protected]>

# Conflicts:
#	docs/sessions/README.md
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.

3 participants