Repository navigation
feat(goose): add the goose agent type - #1125
Conversation
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]>
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]>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 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".
wesbillman
left a comment
There was a problem hiding this comment.
🤖 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)
- 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. - Server pool exhaustion: Goose sessions can exhaust the app-wide pool of 64 MCP servers. After that, every new conversation fails.
- 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.1only. It requires an exactHost, refuses anyOrigin(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.mcpgrant, 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.yamlwhen 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.tsxassertion is fixed at this head.
Not inlined (optional)
- Runtime tests:
runtime.test.tscovers onlysessionTitle. The lifecycle tests advanceflush(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.providercontract 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.
| 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)); |
There was a problem hiding this comment.
🤖 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.
There was a problem hiding this comment.
🤖 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.
There was a problem hiding this comment.
🤖 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.
There was a problem hiding this comment.
| this.lastError = undefined; | ||
| this.acted = false; | ||
| this.written = ""; | ||
| for (const resolve of this.waiters.splice(0)) resolve(settled); |
There was a problem hiding this comment.
🤖 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.
There was a problem hiding this comment.
🤖 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.
There was a problem hiding this comment.
🤖 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.
There was a problem hiding this comment.
🤖 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
There was a problem hiding this comment.
🤖 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.
There was a problem hiding this comment.
🤖 @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 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
There was a problem hiding this comment.
🤖 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>) { |
There was a problem hiding this comment.
🤖 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.
There was a problem hiding this comment.
🤖 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.
There was a problem hiding this comment.
🤖 [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.
There was a problem hiding this comment.
🤖 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).
There was a problem hiding this comment.
🤖 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.
There was a problem hiding this comment.
🤖 @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".
There was a problem hiding this comment.
🤖 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.
| return this.with(signal, async (goose, spawn) => { | ||
| const cwd = await absolute(spawn, workspace); | ||
| return (await list(goose, cwd)).filter((row) => | ||
| row.title.startsWith(`${name} · `), |
There was a problem hiding this comment.
🤖 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.
There was a problem hiding this comment.
🤖 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.
There was a problem hiding this comment.
🤖 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) }), |
There was a problem hiding this comment.
🤖 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.
There was a problem hiding this comment.
🤖 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.
There was a problem hiding this comment.
🤖 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(); |
There was a problem hiding this comment.
🤖 [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.
There was a problem hiding this comment.
🤖 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.
There was a problem hiding this comment.
🤖 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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
🤖 [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.
There was a problem hiding this comment.
🤖 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.
There was a problem hiding this comment.
🤖 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.
|
🤖 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
left a comment
There was a problem hiding this comment.
🤖 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
- 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. - 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.
- 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
errorblocks 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-145is onlyid: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.
| private retire(process: Process) { | ||
| if ( | ||
| !this.disposed && | ||
| process.fingerprint === this.options.processFingerprint() |
There was a problem hiding this comment.
🤖 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:
- Keep a session busy in process A1.
- Change the model, so process B starts.
- Change it back, so
process()starts a new A2:currentwas B, so the fingerprints differ. - 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.
There was a problem hiding this comment.
🤖 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.
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]>
|
🤖 Re: Carl's re-review at 2570b16. ba39d40 addresses it as follows. Fixed here
Startup race: narrowed, not closed. The window is now the few milliseconds before goose's own In #1142, which is stacked on this PR and changes the same |
| 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 { |
There was a problem hiding this comment.
🤖 [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.
There was a problem hiding this comment.
🤖 @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]>
There was a problem hiding this comment.
🤖 Approved as directed. Unresolved findings: bootstrap impersonation, opening-session retirement, concurrent reopening, cancellation, burst replies, and server capacity.
#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]>
* 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
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 intosm/goose-agent-plugin, and #1010 was closed, so none of it reachedmain. #1110 stacks on this PR and bundlesgoose-acpwith the app.Why
Buzz agents run Claude Code or Codex today. goose adds an open-source agent that works with many providers.
What
goose-acpprocess, with one ACP session per conversation: a thread, a DM, or a channel when the agent is set to one session per channel.runOnce), each in a one-off session, as Claude and Codex do.session/new."mcp": truein its manifest and callhost.serveMcp(handler).systemPrompt(), appended to goose's own system prompt.sendposts that text as the reply.main'sCreateModelfield.How
src-tauri/src/host_mcp.rs) uses hyper's HTTP/1 server, already in the tree throughreqwest. It binds127.0.0.1on a random port.mcpServerswhen_meta.enabledExtensionsis sent, so the server is declared in both.main: the last two commits move the model picker tomain'sCreateModelfield (Streamline agent creation and library controls #966), and wakes to the sharedrunOnceand 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 needsgoose-acponPATH.Testing
GOOSE_ACP_BIN=~/.local/bin/goose-acp pnpm exec vitest run src/bundled/goose/goose.live.test.tspassed all 3 tests against goose main (9686d6740): reply in Buzz, steer mid-turn, and the provider check.just desktopand @mentioned a Goose agent in a thread. It read the thread, fetched an image withmedia_get, and replied, and the transcript showed in the Activity tab.tsc --noEmitand biome are clean.vitest run src/bundled src/features/host src/features/agents src/buzz-mcppassed 2,807 tests. Oneclaude-shtimeout passes alone.🤖 Generated with Claude Code