Skip to content

Forward goose agent turns through the agent gateway, behind a flag - #2921

Open
philazar wants to merge 21 commits into
coder1/dxa-1156-claude-neutralfrom
coder2/dxa-1155-goose-caller
Open

philazar wants to merge 21 commits into
coder1/dxa-1156-claude-neutralfrom
coder2/dxa-1155-goose-caller

Conversation

@philazar

@philazar philazar commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Goose agents can now run their turns through the agent gateway, the way Claude Code agents do in the base branch. Goose's gateway opt-in stays behind GATEWAY = false in src/bundled/goose/index.ts until a pinned goose (v1.57.0) has run forwarded turns end to end. The flag gates only that opt-in. The shared changes below ship either way:

  • the ACP load, hold and control seams;
  • the shared forwarding opener;
  • the native child registration command;
  • the goose transcript's fallback-notice handling.

This PR is stacked on the Claude Code forwarding branch and contains only the goose work and the shared seams it needs.

What changes

Native: child rendezvous (src-tauri/src/host_forward.rs)

  • goose's delegate subagents call the model under their own session id, which a generation doesn't know yet. Those requests now wait, bounded, until the goose caller registers the child for that generation.
  • Then they pass through with their own session header unchanged.
  • An unknown session is still refused after at most one bounded wait, and at once when the generation is sealed, closed or revoked.
  • The wait can only reject, never admit: 5 pending requests per generation, 5 s each, capped children and calls.
  • plugin_host_forward_register_child is the only new command.

Shared ACP seams (src/features/agent-harness/acp/)

  • Opt-in session/load in AcpProcess.open.
  • A conversation can be held for handoff work. Holds queue behind a session left running, with a start deadline that only bounds waiting and never cancels accepted work.
  • A session's value is saved with it, and New session is confirmed as saved before the live session is dropped, so a failed save leaves it in place.
  • A null session/load answer reads as no settings.
  • An optional observe callback on AcpSession, which changes nothing when unset. One that throws is logged, and the update is still handled.

Shared forwarding opener (src/features/agents/gateway/forwarding.ts)

  • The opener is now shared by the Claude Code and goose callers.

goose caller (src/bundled/goose/)

  • Process per turn. Each forwarded turn gets its own generation and its own goose-acp, started on the generation URL. Nothing is retried.
  • Session. The conversation's saved goose session is resumed with its saved binding, or the turn fails visibly. It never quietly falls back to a new session or to the user's own configuration.
    • goose v1.57.0 keeps the provider a session was saved with. So a resumed session's first turn sets the chosen provider again with set_config_option, and fails visibly if goose refuses.
    • A forwarded conversation keeps resuming its saved session after the agent's instructions, team or memory change, until New session; an ordinary session reopens instead. Each turn sets the current system prompt on the resumed session. This is deliberate for now and can be revisited after live acceptance.
  • Retirement. Every exit retires the turn in order: seal, interrupt, close, stop. The generation closes only after the process has exited. Agent retirement waits for its forwarded turns; a turn that won't stop is reported and keeps its owner. The conversation waits only for the process to exit. If native can't close a generation, that turn and the agent's retirement fail, but the conversation's later turns still run.
  • Delegates. delegate children are registered from the prompt's own tool calls (forward-children.ts), matched on _meta.goose.toolCall.toolName, because goose adds the source to the title. A registration failure is shown on the turn.
    • Forwarded spawns pin GOOSE_SUBAGENT_PROVIDER (and GOOSE_SUBAGENT_MODEL when the turn names a model), so neither the model's delegate arguments nor goose config can move a child off the gateway. A recipe's own goose_provider still wins (goose summon.rs resolve_provider_config); recipes are rare, and this was accepted.
  • Compact. Compact on a forwarded session runs through the gateway, or is refused with the reason.
  • Pool and records. Forwarded processes count in the shared process pool until they close. A new forwarded session's prompt is recorded like any other.
  • Fallback notice.
    • Once goose runs through the gateway, a plugin, watch or timer wake in a conversation that ran without it shows "Ran without the agent gateway" beside its prompt in Activity. Standalone runs (runOnce) show none.
    • goose v1.57.0 returns no id for the prompt Buzz sends. So the wake's prompt ends with a Buzz-owned <buzz-turn id="…"/> mark that names the turn, and the transcript matches the mark to goose's own message.
    • The mark is read only at the very end, or just before the steer note Buzz adds. It is hidden from the transcript even after the notice is gone. Any <buzz-turn in text Buzz didn't write is escaped, so it can't be forged.
    • Notices are kept per agent, in memory, and capped at 100. They clear when the agent retires successfully; a failed retirement keeps them with the agent. A reload or compaction loses them (docs/buzz-help.md, docs/configuration.md).
  • Shared helper. withNotices moved to src/features/agents/activity-transcript.ts, and both callers now use it.

Validation

Done, at eca0231c1 (review fixes on top of 7a66492c1):

  • Focused Vitest over src/features/agent-harness, src/bundled/goose, src/bundled/claude-code and src/features/agents: 110 files passed, 4 skipped; 2,370 tests passed, 13 skipped.
  • tsc is clean, and biome check --error-on-warnings . reports no errors or warnings.
  • cargo test --lib host_forward: 25 passed. Only doc comments changed in Rust.
  • Each review fix has a regression test that fails without the fix and passes with it:
    • the delegate env pin;
    • provider reapplied on load, and a visible failure when goose refuses;
    • a delegate titled delegate · <source>;
    • later turns run after a failed native close;
    • New session ordering;
    • a throwing observe;
    • a null load answer.
  • Earlier, at 7a66492c1: the native rendezvous unit tests and mutation checks on the retirement, deadline, pool, prompt-record and notice paths, each caught by a test. One deadline mutation made the reserve wait loop forever, so that check fails as a hang rather than an assertion.
  • Source review of every slice by a second agent, and a four-slice review at 7a66492c1.

Not done yet:

  • Live acceptance with pinned goose v1.57.0, which is required before GATEWAY can be turned on. It covers:
    • continuity, reload, reset and compaction;
    • loading a session created outside the gateway;
    • interrupt and retirement;
    • two delegates whose requests reach the gateway only after registration, with their session headers unchanged;
    • the fallback notice in Activity.
  • Decide how forwarded goose spawns source the provider token. Today they set only DATABRICKS_HOST. This blocks turning GATEWAY on.
  • Full app suite, full native test suite and native build in CI.
  • A person trying it in the app.

Unresolved before acceptance, from the pinned-goose acceptance plan:

  • Ordering evidence. Showing that a child's requests reach upstream only after it is registered needs a correlated timeline:

    • the native registration (insertion and wake under the generation lock);
    • native admission events;
    • upstream session headers.

    RPC-return timestamps or gateway logs alone can't prove that order, and the app doesn't log these events today.

  • Token policy. Covered by the provider-token item above. It is still open before activation.

  • Gateway listing. The gateway's descriptor and access check must list the goose harness.

  • Stuck retirement. The case where a forwarded goose won't stop is covered by unit tests only.

  • Ordinary → forwarded load. Resuming a session created outside the gateway needs a successful continuity run. A visible refusal shows only that it fails safely, not that it works.

  • Windows. No Windows acceptance yet.

🤖 Generated with Claude Code

philazar and others added 19 commits November 6, 2026 14:53
A goose delegate child sends inference under its own session, and nothing
orders its first request against the ACP update that names it. A goose
generation now holds the children its parent's live delegate calls
registered, frozen to it, with caps on children, calls and parked
requests. Inference naming an unknown opaque session waits once, holding
no upstream connection, until its child is registered (then admission
runs again in full), the generation is sealed, revoked or closed, or a
5 s deadline refuses it with 504 child_unregistered. A full queue gets
429 child_pending_full. Expiry never admits and nothing falls back to
the parent's identity. Membership checks, parking and registration share
the registry lock, so a wake cannot be lost, and each parked request
frees only its own slot however it ends.

`plugin_host_forward_register_child` and `registerForwardChild` add the
registration, with five refusal reasons; contract violations reject.
`admitted` events say whether a child sent the request. `AcpSession`
gains an optional `observe` hook, and `forwardChildren` registers only a
completed `delegate` call announced earlier in the same live prompt,
surfacing every refusal and rejection.

Co-authored-by: coder2 <4acc49cb93576448ba82e631688c51a40bf03d6a889d78fddf57917d198de10f@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Philip Azar <pazar@squareup.com>
A registration that throws synchronously, or whose result handling
throws, no longer escapes the session update or leaves a rejected
promise unhandled; each is reported through `failed`.

Co-authored-by: coder2 <4acc49cb93576448ba82e631688c51a40bf03d6a889d78fddf57917d198de10f@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Philip Azar <pazar@squareup.com>
The deadline case asserts only the refusal and elapsed bound, never the
brief parked state; tests that observe parking use a deadline they never
reach. A new case parks two requests for the same child, cancels one and
shows registration admits only the survivor.

Co-authored-by: coder2 <4acc49cb93576448ba82e631688c51a40bf03d6a889d78fddf57917d198de10f@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Philip Azar <pazar@squareup.com>
…est waits

The observer's failure reporter is now called at most once per failure and
never from its own error. A reporter that throws stops registration, so a
later child's inference is refused as child_unregistered instead of running
unseen. Covered on the synchronous, refusal and rejection paths.

Parked rendezvous tests keep their 3600s deadline but bound each response
wait at 5s, so a lost wake fails fast.

Co-authored-by: coder2 <4acc49cb93576448ba82e631688c51a40bf03d6a889d78fddf57917d198de10f@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Philip Azar <pazar@squareup.com>
`open({ load })` resumes a saved session or fails visibly; it never opens a
new session in its place. It needs `loadSession`, refuses a system prompt
that only `session/new` could carry, and lets one open own a saved id at a
time, including one whose close the harness has not acknowledged.

The session is registered only after the load answers, so replayed history
never reaches it. Naming, title and new-session meta are skipped; the Buzz
MCP server is declared afresh with a new token; a hook system prompt is set
again under its key, and a first-prompt one is not repeated.

Co-authored-by: coder2 <4acc49cb93576448ba82e631688c51a40bf03d6a889d78fddf57917d198de10f@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Philip Azar <pazar@squareup.com>
`forwardChildren` now returns `{ observe, settled }`. `settled()` resolves,
once every registration the generation started has been answered, to its
first failure, kept before the notifier runs and whatever the notifier
does. The caller awaits it after the prompt and before reporting success.

Co-authored-by: coder2 <4acc49cb93576448ba82e631688c51a40bf03d6a889d78fddf57917d198de10f@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Philip Azar <pazar@squareup.com>
`AgentSessions.exclusive(key, work)` gives `work` the conversation to
itself. Messages wait; its live session finishes and is closed, and work
starts only once that close and every earlier one of the conversation's
sessions is acknowledged or their process has exited. Never after one that
failed, nor once interrupted or stopped. Saved sessions are never deleted.

Callers waiting on an opening session or on another hold re-check and
reserve with nothing awaited between. `interrupt` and `dispose` abort the
work's signal without waiting for it; `shutdown` waits for it. A session
the work leaves running (`outlives`) blocks the next hold until it ends.
`keep(id)` reorders the saved sessions only while the work holds the key.

Co-authored-by: coder2 <4acc49cb93576448ba82e631688c51a40bf03d6a889d78fddf57917d198de10f@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Philip Azar <pazar@squareup.com>
The next ordinary message after `exclusive` rebuilds with `fresh` like any
later session. The doc comment states that an interrupt during the close
wait lands within REQUEST_TIMEOUT_MS, that refusals reject with
"<label> was stopped", and that a failed close refuses the conversation
while the process runs.

Co-authored-by: coder2 <4acc49cb93576448ba82e631688c51a40bf03d6a889d78fddf57917d198de10f@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Philip Azar <pazar@squareup.com>
`outlives(done)` now holds back the conversation's messages as well as
the next hold, until `done` (the process's `exited`) resolves; other
conversations proceed. A rejected `done` leaves the owner unknown: holds
stay refused and messages fail visibly instead of clearing. A waiting
message gives up on its signal or on `dispose`.

Co-authored-by: coder2 <4acc49cb93576448ba82e631688c51a40bf03d6a889d78fddf57917d198de10f@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Philip Azar <pazar@squareup.com>
A held conversation's work may name more than one session it leaves
running. Each `outlives` now adds to the ones before instead of
replacing them, so both delivery paths stay blocked until every one of
those processes has confirmed its exit, and a rejection of any of them
keeps the conversation refused as having an unknown owner. A call made
once the work has settled throws rather than touching a barrier that
may already belong to the conversation's next holder.

Co-authored-by: coder2 <4acc49cb93576448ba82e631688c51a40bf03d6a889d78fddf57917d198de10f@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Philip Azar <pazar@squareup.com>
Claude's gateway generation opener moves to the agents feature as
`GatewayForwarding`, taking the harness it declares to the gateway and
native's name for it, so goose can open its generations the same way.
`ClaudeForwarding` is that opener for Claude, and Claude's call sites are
unchanged.

A generation's session is optional at open: goose names it afterwards,
and its generation gains `associate` and `registerChild`, bound to its
owner and id, which Claude never calls. Native already decides which
harness may omit a session or associate one.

`close` now rejects when native could not close the generation, the
same rejection every time, instead of resolving as a clean close: a
caller that holds a conversation until its generation has retired must
not take a failure for that. Claude ends its turns the same either way,
so its two call sites contain the rejection as before.

Co-authored-by: coder2 <4acc49cb93576448ba82e631688c51a40bf03d6a889d78fddf57917d198de10f@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Philip Azar <pazar@squareup.com>
A generation opened after its turn was cancelled still fails with the
cancellation when native could not close it, and a Claude one-off turn
still ends as before when its generation's close is refused.

Co-authored-by: coder2 <4acc49cb93576448ba82e631688c51a40bf03d6a889d78fddf57917d198de10f@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Philip Azar <pazar@squareup.com>
… session

Held work now waits its turn behind a session another hold left
running, as messages already do, instead of being refused, so a second
forwarded turn queues behind the first one's retirement. Only a
retirement that could not be confirmed refuses either. One reservation
serves every hold, after the last check with nothing awaited, and each
wait in it ends on `interrupt`, which reaches a hold still waiting, on
the caller's signal, or on dispose.

Held work saves its session together with a value of its own (`keep`),
which reports whether the store reads the write back. That value is
given back only with the session it was saved with; one an ordinary
session has since replaced gives none, and anything stored that can't
be read is given as malformed, never as none. What is stored is kept
raw across every write.

New session marks the conversation under a hold, keeping every saved
session listed: held work then starts a new session, reading nothing
stored, until a session that replaces the newest is saved, by held work
or by an ordinary turn. A retirement that could not be confirmed
refuses it, writing nothing. Compaction with a forwarded path chooses,
under the same hold, between the open session, the forwarded one, and
nothing; damage takes the forwarded path, never nothing.

Co-authored-by: coder2 <4acc49cb93576448ba82e631688c51a40bf03d6a889d78fddf57917d198de10f@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Philip Azar <pazar@squareup.com>
New session and compaction under a hold check the hold's signal after
waiting for the turn, so an interrupted control writes no marker, keeps
the session, and sends no /compact. New session reads its marker back
and fails visibly when the store did not keep it. keep() refuses an
undefined value.

Co-authored-by: coder2 <4acc49cb93576448ba82e631688c51a40bf03d6a889d78fddf57917d198de10f@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Philip Azar <pazar@squareup.com>
A gateway turn runs on a generation of its own, in a dedicated goose-acp
started on the generation's URL with session naming off: never the
shared goose, never retried, never run unforwarded. A conversation's
turn holds the conversation, resumes its saved session (no fallback to
a new one) with the binding saved with it, and saves the pair before its
first prompt. It names its session to the gateway and registers the
children goose delegates, whose failures fail the turn.

Every exit retires it in order: seal, interrupt, strict close, stop;
the generation is closed only once the process has exited, and the
conversation waits on that retirement. A route change wins as the
outcome, then a cancel; a goose that did not stop or a close native
refused fails the turn visibly.

Compaction of a saved forwarded session runs as such a turn under the
control's context, or is refused visibly. Watch and timer turns run on a
standalone binding, saving nothing; retiring the agent waits for them.
Protected agents and providers other than Databricks are refused before
anything spawns. GATEWAY stays false.

controlTurn, compactRefusal and FORWARDING_PROTECTED move unchanged from
the Claude runtime to the shared opener.

Co-authored-by: coder2 <4acc49cb93576448ba82e631688c51a40bf03d6a889d78fddf57917d198de10f@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Philip Azar <pazar@squareup.com>
… and pool

- Register every forwarded turn on its agent from before its first await,
  with the agent's lifetime combined into its signal; retiring the agent
  waits for each to end and refuses while a turn's goose has yet to exit
  or native has not closed its generation, keeping the stopped entry and
  its barrier for a later clean retirement. A failed close stays recorded.
- Give `exclusive` a deadline that ends only its wait for the
  conversation, as `deliver`'s does; a forwarded message past it is late.
- Note what a forwarded turn's session was shown at goose's delivered
  receipt, keeping the turn's activity callbacks.
- Count each forwarded Buzz tools server in the shared pool, busy from
  before it starts until it closes.

Co-authored-by: coder2 <4acc49cb93576448ba82e631688c51a40bf03d6a889d78fddf57917d198de10f@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Philip Azar <pazar@squareup.com>
A forwarded turn's new session, once kept, is recorded through the
existing session-prompts owner, as an ordinary new session is; a resumed
session or one that could not be kept records nothing.

Co-authored-by: coder2 <4acc49cb93576448ba82e631688c51a40bf03d6a889d78fddf57917d198de10f@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Philip Azar <pazar@squareup.com>
Goose v1.57.0 gives Buzz no id for a prompt it sends, so a wake run
without the agent gateway ends its prompt with a Buzz-owned mark naming
one random turn id, reused however `refresh` rebuilds it. The goose
transcript reads only that trailing mark (or the one just before a
steer's own tail), strips it from the shown prompt even once the notice
is gone, and joins the notice by goose's native prompt id through the
shared `withNotices`, now in the Activity transcript module. A mark a
wake's own text ends with is escaped before any real one is added.
Notices are kept per agent while the app runs, capped at 100, and
dropped when the agent retires; a reload or compaction loses them.

Co-authored-by: coder2 <4acc49cb93576448ba82e631688c51a40bf03d6a889d78fddf57917d198de10f@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Philip Azar <pazar@squareup.com>
A mark followed by a steer's tail, or by part of one that an in-place
steer completes, passed the trailing-only escape and was read back as
Buzz's mark. Conversation and standalone input now escapes every
`<buzz-turn` tag, so no steer applied later can make one readable.
Agent help notes the Activity notice's lifetime.

Co-authored-by: coder2 <4acc49cb93576448ba82e631688c51a40bf03d6a889d78fddf57917d198de10f@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Philip Azar <pazar@squareup.com>
@philazar
philazar requested a review from a team as a code owner November 6, 2026 23:38
matt2e added a commit that referenced this pull request Nov 9, 2026
A Claude turn that fails for a missing or rejected key replies with a
sentence that links that provider's section of Agent setup. Codex and
Goose posted the raw provider error with no link, so the owner had to
know the fix lives under Settings -> Agent setup -> that provider. And
Claude's sentence replaced the provider's words, which then survived
only on the Activity tab.

Now all three reply the same way: the sentence and its Settings link
first, then, on lines of their own, what the provider said, quoted and
attributed. An error none of them recognises posts exactly as before,
and so does Buzz's own refusal to start Claude without a key, which
has no provider's words to quote.

Reproduction (desktop only; traced in code, with the error text taken
from the real binaries on this workstation, codex-cli 0.166.0 and goose
1.57.0, pointed at a local server returning OpenAI's and Anthropic's
401 bodies):

- Codex signed out (empty CODEX_HOME): the turn fails with
  "unexpected status 401 Unauthorized: Missing bearer or basic
  authentication in header, url: .../v1/responses".
- Codex with a refused API key: "unexpected status 401 Unauthorized:
  Incorrect API key provided: sk-...".
- Codex with a ChatGPT sign-in that stopped working: one of the
  binary's six refresh messages, all "... could not be refreshed[ ...].
  Please [log out and ]sign in again.": "Your access token could not be
  refreshed because your refresh token has expired / was already used /
  was revoked. Please log out and sign in again.", "Your access token
  could not be refreshed. Please log out and sign in again.", "...
  because you have since logged out or signed in to another account.
  Please sign in again." and "Your authentication session could not be
  refreshed automatically. Please log out and sign in again."
  (auth_recovery_unavailable).
- Goose without ANTHROPIC_API_KEY: session/prompt fails with
  {code: -32603, message: "Internal error", data: "Error getting agent
  reply: Configuration value not found: ANTHROPIC_API_KEY"}.
- Goose with a refused key: {code: -32000, message: "Authentication
  required"}. The thread reads it bare from session/prompt, as "goose
  could not start: Authentication required" from session/new, and as
  "Error: Authentication required" from a deferred handover.
- Claude with no key: Buzz refuses to start it with "Claude needs an
  Anthropic API key. ..."; with a refused key, the turn ends with
  "Invalid API key · Fix external API key".

Reply in the thread, before -> after (links are
buzz://settings?section=agents&provider=<type key>, shortened here):

Codex, signed out
  before: Codex could not finish: unexpected status 401 Unauthorized:
          Missing bearer or basic authentication in header, url:
          https://api.openai.com/v1/responses.
  after:  Codex isn't signed in. Head to [Settings](…buzz.codex%2Fcodex)
          to sign in.

          Codex said:
          > unexpected status 401 Unauthorized: Missing bearer or basic
          > authentication in header, url:
          > https://api.openai.com/v1/responses
Codex, refused API key or any refresh message above
  before: Codex could not finish: Your authentication session could not
          be refreshed automatically. Please log out and sign in again..
  after:  OpenAI rejected Codex's sign-in. It may have expired or been
          revoked. Head to [Settings](…buzz.codex%2Fcodex) to sign in
          again.

          Codex said:
          > Your authentication session could not be refreshed
          > automatically. Please log out and sign in again.
Goose, missing key
  before: ⚠️ I couldn't finish that: Internal error: Error getting agent
          reply: Configuration value not found: ANTHROPIC_API_KEY
  after:  ⚠️ I couldn't finish that: Goose is missing an API key
          (ANTHROPIC_API_KEY). Head to [Settings](…buzz.goose%2Fgoose)
          to add it.

          goose said:
          > Internal error: Error getting agent reply: Configuration
          > value not found: ANTHROPIC_API_KEY
Goose, refused key or sign-in
  before: ⚠️ I couldn't finish that: goose could not start:
          Authentication required
  after:  ⚠️ I couldn't finish that: Goose's provider rejected its API
          key or sign-in. Head to [Settings](…buzz.goose%2Fgoose) to
          check its provider.

          goose said:
          > goose could not start: Authentication required
Claude, refused key
  before: ⚠️ I couldn't finish that: Anthropic rejected Claude's API
          key. It may have been revoked or run out of credit. Head to
          [Settings](…buzz.claude-code%2Fclaude-code) to replace it.
  after:  (the same sentence)

          Claude said:
          > Invalid API key · Fix external API key
Claude, missing key (unchanged: Buzz refused to start Claude)
  before: ⚠️ I couldn't finish that: Claude is missing an API key. Head
          to [Settings](…buzz.claude-code%2Fclaude-code) to add one.
  after:  the same
Any other failure: unchanged, e.g. "Codex could not finish: model
busy.", "Codex could not finish: MCP OAuth refresh token was rejected;
reauthorization required.", "⚠️ I couldn't finish that: Internal
error: Error getting agent reply: Request failed: connection refused"
and "⚠️ I couldn't finish that: Error: overloaded".

The quotes above are wrapped to fit here. In the reply, a blank line
follows the sentence, the quote starts on the line right after the
label, each provider message is one "> " line, and only line breaks of
its own add more, each quoted.

Format. Thread messages render as Markdown (MessageMarkdown:
react-markdown with remark-gfm and remark-breaks), and styled
blockquotes are part of it, so the provider's words go in a "> " quote.
A short "Codex said:" / "goose said:" / "Claude said:" line above the
quote says whose words they are; without it, "Authentication required"
reads as Buzz's own. The reply is
"<sentence>\n\n<who> said:\n> <words>": the blank line makes the
sentence and the label paragraphs of their own, and the quote starts on
the label's next line, which ends its paragraph. Each publish path
(Codex's `publish`, goose's and Claude's `report()`) hands the content
to the agent's publish, which native code signs verbatim
(app_agent_publish), so the newlines reach the thread.

Spacing. In the thread's styles (Messages.module.css), each paragraph
and blockquote in a message has 1rem (--space-4) margins above and
below, which collapse between neighbours, and a blockquote pads its
bar by 2px (--space-half). Measured in headless Chromium with those
styles and the 14px/22px message type: the label sits 16px below the
sentence, the quote's bar 16px below the label, and its first line
18px. So the quote doesn't sit tighter under the label than the label
does under the sentence. Tightening it would take a thread-wide rule
like the one lists already have: --space-1 margins on both
.text > p:has(+ blockquote) and .text > p + blockquote (both, since
collapsed margins take the larger), for 4px to the bar and 6px to the
text. That changes every message whose paragraph runs into a quote, so
this commit leaves the styles alone.

The shared features/agents/failure-words.ts `withProviderWords`
formats it: it trims the provider's words, quotes at most 500
characters of them (as goose's and Claude's reports already slice any
error to 500), ends words it cut short with "…", and quotes every line.
With no words to quote, the sentence stands alone. The sentence isn't
capped. Goose's and Claude's reports slice the error to 500 before the
helper sees it, so only a Codex quote can carry the "…". Codex
quotes its words without the notice's "Codex could not finish: " or
"could not start: ", and without the period that notice adds, so the
quote ends as Codex's message did (no "again.."). Other notices, such
as a failed handover's, are quoted whole.

Each plugin recognises its own error shapes in its setup module
(Codex's sign-in, goose's provider keys), as claude-code/auth.tsx does;
nothing moves into the Agents service. Codex's and goose's
`explainTurn` return the whole reply. Claude's `explainTurn` keeps its
sentences, and claude-code/index.ts quotes Claude after whatever it
explains: #2995 adds a signed-out branch inside that method, so editing
it would conflict, and wrapping it quotes #2995's new sentence too. It
doesn't quote Buzz's own refusal to start Claude without a key
(auth.tsx's `refusedByBuzz`, which reads NO_KEY): those aren't Claude's
words, and they would repeat the sentence's advice with an older menu
path.

Codex matches "Missing bearer or basic authentication" for signed out,
and "Incorrect API key provided" or /could not be refreshed\b[^.\n]*\.
Please (?:log out and )?sign in again/i for a refused sign-in. The
refresh match stays within one sentence and the next, so it can't join
two unrelated errors: "configured values could not be refreshed:
enterprise IdP session has no OIDC ID token; sign in again." stays raw,
as does a "could not be refreshed" line followed by a "Please sign in
again" line. The space before "Please" is literal rather than \s+, which
all six messages fit, so a line break never joins the two. An MCP
server's OAuth failure ("MCP OAuth refresh token was rejected; ...",
"MCP OAuth access token is expired and refresh failed; ...") is not
Codex's sign-in, so it doesn't match either.

Goose matches "Configuration value not found: <NAME>API_KEY" for a
missing key, and "Authentication required" at the start or after ": "
for a refusal, which leaves the phrase inside other text alone. A turn
doesn't say whether the provider refused a key or a sign-in, and a
sign-in is redone outside Buzz, so the sentence asks to check the
provider rather than to update a key.

Codex applies its hook in `publish`, which every thread notice goes
through, so no call site changes. A sign-in Codex refuses at
thread/start or turn/start lands in execute()'s catch and reads "Codex
could not finish: ...". Only the protection preflight and `entry.after`
produce "Codex could not start: ...", and neither fails for a sign-in.
Goose applies its hook in `report()`, as Claude does. The Activity
tab's status keeps Codex's own notice.

Codex's Settings section offers Sign in / Sign in again, not a key
field, so its sentences say sign in. The Codex sign-in sentence stays
as it is. The sign-in dialog's framing ("Security reviews need a
ChatGPT sign-in made in Buzz") is a follow-up copy fix, not part of
this change. Remote execution runtimes keep the pass-through `explain`,
as Claude's do, since their credentials live on the target.

The link parses (buzzLinkTarget) to {kind: "settings", section:
"agents", provider: "buzz.codex/codex" | "buzz.goose/goose"}, and Agent
setup opens the provider dialog whose type key equals `provider`
(AgentDefaults.tsx), the path Claude's link takes.

Tests:
- features/agents/failure-words.test.ts: "follows the explanation with
  the provider's words, quoted and attributed", "quotes every line of
  the provider's words, without stray whitespace", "quotes at most 500
  characters of the provider's words, marking the cut", "lets the
  explanation stand alone when the provider said nothing".
- codex/runtime.test.ts, through the plugin's apply: "links Codex's
  Settings and quotes Codex when a turn fails because it is signed out"
  / "... OpenAI rejects its API key" / "... its ChatGPT sign-in
  expired" / "... it cannot recover its sign-in"; "reports a turn that
  failed otherwise (model busy) as Codex said it" / "... (MCP OAuth
  refresh token was rejected; reauthorization required) ..." / "...
  (MCP OAuth access token is expired and refresh failed;
  reauthorization required) ..." / "... (configured values could not be
  refreshed: x) ..." / "... (configured values could not be refreshed:
  enterprise IdP session has no OIDC ID token; sign in again.) ...".
- codex/runtime.test.ts, with the fake app-server refusing the method
  with the revoked-refresh-token message: "links Codex's Settings and
  quotes Codex when Codex refuses thread/start for its sign-in, keeping
  Codex's words for the Activity tab" / "... turn/start ...".
- codex/setup.test.ts: "quotes Codex's words after the link, without
  the notice around them", "quotes at most 500 characters of Codex's
  words, marking the cut", "leaves a notice it can't explain exactly as
  Codex's runtime wrote it".
- goose/runtime.test.ts: "reports a turn whose goose lacks its
  provider's key, linking Goose's Settings and quoting goose" / "...
  had its key refused, linking Goose's Settings and quoting goose" /
  "... had its key refused opening the session, linking Goose's
  Settings and quoting goose" / "... failed otherwise, in goose's
  words".
- goose/setup.test.ts: "explainTurn links Settings and quotes goose for
  a refusal reported as "Authentication required"" / "... "goose could
  not start: Authentication required"" / "... "Error: Authentication
  required""; "explainTurn leaves a mention of authentication inside
  other text as goose said it".
- goose/index.test.ts: "links a failed turn's refused key to Goose's
  own Agent setup, quoting goose".
- claude-code/index.test.ts: "links a failed turn's key to Claude's own
  Agent setup, quoting only what Claude said" (missing key: the
  sentence alone; refused key: quoted; "Error: overloaded" unchanged).

Verification (base 7444eb1, origin/main when this was branched):

| Check                                                  | Result                                   |
| ------------------------------------------------------ | ---------------------------------------- |
| New tests, production files at 7444eb1              | 18 fail; the other-error guards pass     |
| New tests, production files at the Settings-link-only  | 16 fail (every quote); guards pass       |
| version of this change                                 |                                          |
| New tests, helper with 8f515d2d37's single newline     | 19 fail (every quote)                    |
| before "X said:"                                       |                                          |
| Replies rendered through MessageMarkdown (throwaway)   | <p> sentence + link, <p> "X said:", then |
|                                                        | <blockquote>; line breaks kept inside it |
| Thread spacing, headless Chromium with the real        | sentence to label 16px; label to quote   |
| tokens and Messages.module.css                         | bar 16px, to its first line 18px         |
| failure-words, codex runtime/setup/index, goose        | 12 files, 629 tests pass                 |
| runtime/index/setup, claude-code auth/index/runtime/   |                                          |
| setup, navigation buzz-links test files                |                                          |
| bin/pnpm exec tsc --noEmit                             | clean                                    |
| biome check --error-on-warnings on changed files       | clean                                    |
| git diff --check                                       | clean                                    |
| git merge-tree with #2959, #2960, #2995, #2951, #2961, | clean, as with 7444eb1                |
| #2944, #2938, #2859                                    |                                          |
| git merge-tree with #2995: claude-code/index.ts and    | clean; biome clean on the merged files   |
| auth.tsx keep both #2995's signed-out branch and       |                                          |
| refusedByBuzz                                          |                                          |
| git merge-tree with #2921                              | same 22 files as with 7444eb1; its    |
|                                                        | hunks in claude-code/index.ts and        |
|                                                        | goose/runtime(.test).ts byte-identical   |

Not verified: not run in the desktop app, and the spacing was measured
in Chromium rather than the app's webview. The error text came from the
real binaries against a local 401 server, not from OpenAI or Anthropic
themselves. Goose's "Error: Authentication required" shape is tested
only through explainTurn: SessionControls swallows a control action's
error, so no runtime path delivers it today. No docs change:
docs/buzz-help.md and docs/agents.md don't describe failure replies for
any harness.

Overlap: #2959 (draft) replaces the "could not finish" publish with
`notify(reason)`, which calls `this.publish`, and keeps the notice's
wording, so its notices get explained and quoted with no change to its
lines. This commit edits neither that hunk nor #2951's, and leaves
execute()'s local refusal `explain` named as it is, since #2959
rewrites that area (`this.explain` doesn't clash with it). #2995 edits
Claude's `explainTurn`, its doc comment, the lines around NO_KEY and
claude-code/index.ts's imports and setup; this commit leaves all of
those lines alone, adds `refusedByBuzz` after the ClaudeAuth class, and
imports it and the helper after ./reads. In goose/runtime.ts the new
field stays clear of the class-head and constructor hunks in both #2944
and #2921.
In goose/runtime.test.ts the explainTurn import sits after the
./runtime import, clear of the import lines #2921, #2944 and #2960
change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: mtoohey <mtoohey@squareup.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
matt2e added a commit that referenced this pull request Nov 9, 2026
…3081)

## Problem

When a provider refuses an agent's key or sign-in, the turn's reply in
the thread gave the owner no route to the fix:

- **Codex** posted OpenAI's raw error: `Codex could not finish:
unexpected status 401 Unauthorized: Missing bearer or basic
authentication in header, url: https://api.openai.com/v1/responses.`
- **Goose** posted the ACP error bare: `⚠️ I couldn't finish that:
Authentication required`, or `Internal error: Error getting agent reply:
Configuration value not found: ANTHROPIC_API_KEY`.
- **Claude** already replied with a sentence linking its section of
Settings → Agent setup, but that sentence replaced Claude's own words,
which then survived only on the Activity tab.

The owner had to know that the fix lives under Settings → Agent setup →
that provider.

## Change

All three now reply the same way: a sentence with a **Settings** link to
that provider's dialog, then, on lines of their own, what the provider
said, quoted and attributed:

```
<sentence with Settings link>

<Provider> said:
> <quoted provider text>
```

The link is `buzz://settings?section=agents&provider=<type key>`
(`buzz.codex%2Fcodex`, `buzz.goose%2Fgoose`,
`buzz.claude-code%2Fclaude-code`). It parses to `{kind: "settings",
section: "agents", provider}` and Agent setup opens the dialog whose
type key matches, the path Claude's link already took.

An error none of them recognises posts byte-for-byte as before. So does
Buzz's own refusal to start Claude without a key, which has no provider
words to quote.

### Before → after

The **after** column shows the sentence. Each reply then continues with
a blank line, `Codex said:` / `goose said:` / `Claude said:`, and the
provider's words quoted. For Codex the quote is the message without the
`Codex could not finish: ` wrapper and its trailing period; for Goose
and Claude it is the error as reported (sliced to 500 characters, as
those reports already did).

| Provider | Case | Before | After (sentence; quote follows) |
| --- | --- | --- | --- |
| Codex | signed out (`Missing bearer or basic authentication`) | `Codex
could not finish: unexpected status 401 Unauthorized: Missing bearer or
basic authentication in header, url:
https://api.openai.com/v1/responses.` | Codex isn't signed in. Head to
**Settings** to sign in. |
| Codex | refused API key (`Incorrect API key provided`) or any of the
binary's six `… could not be refreshed[…]. Please [log out and ]sign in
again.` messages | `Codex could not finish: Your authentication session
could not be refreshed automatically. Please log out and sign in
again..` | OpenAI rejected Codex's sign-in. It may have expired or been
revoked. Head to **Settings** to sign in again. |
| Goose | missing key (`Configuration value not found: <NAME>API_KEY`) |
`⚠️ I couldn't finish that: Internal error: Error getting agent reply:
Configuration value not found: ANTHROPIC_API_KEY` | ⚠️ I couldn't finish
that: Goose is missing an API key (ANTHROPIC_API_KEY). Head to
**Settings** to add it. |
| Goose | refused key or sign-in (`Authentication required` at the start
or after `: `) | `⚠️ I couldn't finish that: goose could not start:
Authentication required` | ⚠️ I couldn't finish that: Goose's provider
rejected its API key or sign-in. Head to **Settings** to check its
provider. |
| Claude | refused key | `⚠️ I couldn't finish that: Anthropic rejected
Claude's API key. It may have been revoked or run out of credit. Head to
**Settings** to replace it.` | the same sentence, now followed by
`Claude said:` and `> Invalid API key · Fix external API key` |
| Claude | missing key (Buzz refused to start Claude) | `⚠️ I couldn't
finish that: Claude is missing an API key. Head to **Settings** to add
one.` | unchanged: the sentence alone |
| any | other failure | e.g. `Codex could not finish: model busy.`,
`Codex could not finish: MCP OAuth refresh token was rejected;
reauthorization required.`, `⚠️ I couldn't finish that: Error:
overloaded` | unchanged |

### Design

- **Each plugin recognises its own error shapes** in its setup module,
as `claude-code/auth.tsx` already does: `codex/setup.ts` `explainTurn`
(sign-in) and `goose/setup.ts` `explainTurn` (provider keys). Nothing
moves into the Agents service.
- **Shared formatter:** `src/features/agents/failure-words.ts`
`withProviderWords(explanation, who, words)` trims the provider's words,
quotes at most 500 characters (ending a cut with `…`), quotes every
line, and returns `<explanation>\n\n<who> said:\n> …`. With no words it
returns the sentence alone. The explanation itself is not capped.
- **Codex applies the hook inside `publish`**, which every thread notice
goes through, so no call site changes. Draft #2959 replaces the "could
not finish" publish with `notify(reason)`, which calls `this.publish`,
so its notices are explained and quoted with no change to its lines. A
sign-in Codex refuses at `thread/start` or `turn/start` lands in
`execute()`'s catch and is covered. The Activity tab's status keeps
Codex's own notice.
- **Goose applies the hook in `report()`**, as Claude does.
- **Claude keeps its `explainTurn` sentences**; `claude-code/index.ts`
quotes Claude after whatever it explains, except when `refusedByBuzz`
(Buzz's own NO_KEY refusal) produced the error. #2995 adds a signed-out
branch inside `explainTurn`, so this change stays out of that method and
merges cleanly with it (verified with `git merge-tree`).
- **Codex's refresh match** `/could not be refreshed\b[^.\n]*\. Please
(?:log out and )?sign in again/i` stays within one sentence and the
next, so `configured values could not be refreshed: enterprise IdP
session has no OIDC ID token; sign in again.` and the MCP OAuth refresh
errors stay raw.
- **Remote execution runtimes** keep their pass-through `explain`, as
Claude's do; their credentials live on the target.
- **No style change.** In the thread, paragraphs and blockquotes have
`--space-4` (16 px) margins that collapse, so the label sits 16 px below
the sentence and the quote's bar 16 px below the label (first quoted
line 18 px, after the bar's padding). Tightening it would be a
thread-wide rule for every paragraph that runs into a quote, so this PR
leaves `Messages.module.css` alone.

## Testing

Tests added in `failure-words.test.ts`, `codex/runtime.test.ts` (through
the plugin's `apply`, and with the fake app-server refusing
`thread/start` / `turn/start`), `codex/setup.test.ts`,
`goose/runtime.test.ts`, `goose/setup.test.ts`, `goose/index.test.ts`
and `claude-code/index.test.ts`. They cover each matched shape, the
500-character cut, multi-line quoting, and guards that the unmatched
errors above (model busy, both MCP OAuth messages, the two `configured
values could not be refreshed` forms, `Error: overloaded`) post exactly
as before.

Base: `7444eb1299` (origin/main when branched).

| Check | Result |
| --- | --- |
| New tests, production files at `7444eb1299` | 18 fail; the other-error
guards pass |
| New tests, production files at the Settings-link-only version of this
change | 16 fail (every quote); guards pass |
| New tests, helper with a single newline before `X said:` | 19 fail
(every quote) |
| Replies rendered through `MessageMarkdown` (throwaway) | `<p>`
sentence + link, `<p>` "X said:", then `<blockquote>`; line breaks kept
inside it |
| Thread spacing, headless Chromium with the real tokens and
`Messages.module.css` | sentence → label 16 px; label → quote bar 16 px,
→ its first line 18 px |
| failure-words, codex runtime/setup/index, goose runtime/index/setup,
claude-code auth/index/runtime/setup, navigation buzz-links test files |
12 files, 629 tests pass |
| `bin/pnpm exec tsc --noEmit` | clean |
| `biome check --error-on-warnings` on changed files | clean |
| `git diff --check` | clean |
| `git merge-tree` with #2959, #2960, #2995, #2951, #2961, #2944, #2938,
#2859 | clean, as with `7444eb1299` |
| `git merge-tree` with #2995 | clean; `claude-code/index.ts` and
`auth.tsx` keep both #2995's signed-out branch and `refusedByBuzz`;
biome clean on the merged files |
| `git merge-tree` with #2921 | same 22 conflicting files as
`7444eb1299` already has with it; its hunks in `claude-code/index.ts`
and `goose/runtime(.test).ts` byte-identical, so none added |

The error text came from the real binaries on a workstation (codex-cli
0.166.0, goose 1.57.0) pointed at a local server returning OpenAI's and
Anthropic's 401 bodies, not from OpenAI or Anthropic themselves.

## In-app check

**This is a browser-build fallback recording, not the desktop app.** The
recording workstation had no relay to connect to (no `.env.local`, no
local relay), no display server or WebKitGTK, and native identity there
needs a real community sign-in; posting from a debug identity into a
real community would send messages to an outside service. The fixture
stands in for the two agents and the turn: on a mention it posts the
reply that each build's own `codex/setup.ts` / `goose/setup.ts`
`explainTurn` composes from the raw error strings captured from the real
binaries. Real in the recording: each worktree's Buzz frontend, the
Playwright fixture relay and production broker, the Agents service
delivering the mention, `MessageMarkdown` with the real
`Messages.module.css`, `buzz://` link routing, and Settings → Agent
setup with the real Codex and Goose plugins bridged to the real
binaries. Spacing measured in the real thread matched the headless
numbers above (16 / 16 / 18 px).

Left is **BEFORE** = origin/main `7444eb1299`; right is **AFTER** =
`13b8178396`. Both halves run the same script; only the right half then
follows the Settings links.


https://github.com/user-attachments/assets/b9ed7624-410c-4dc4-b61e-88a3e20e944f

| BEFORE (`7444eb1299`) | AFTER (`13b8178396`) |
| --- | --- |
| ![BEFORE: Codex replies with OpenAI's raw 401 ending in two OpenAI
links; Goose replies "Authentication required". Neither links Settings
or
quotes.](https://github.com/user-attachments/assets/24b92943-9d1b-4fce-bea5-cdd9f55b6226)
| ![AFTER: each reply has its sentence with a Settings link, a blank
line, "Codex said:" or "goose said:", then the
quote.](https://github.com/user-attachments/assets/4a84598d-ab00-45e7-ab70-db45c8ebb914)
|

AFTER (`13b8178396`), close-up of the thread column: sentence, 16 px
gap, "X said:", 16 px gap, then the quote's bar.

![AFTER close-up of both replies, showing the even spacing around "Codex
said:" and "goose
said:"](https://github.com/user-attachments/assets/52e6010d-d7e9-44c9-8196-6bbf1a3df6c1)

AFTER (`13b8178396`): clicking Codex's Settings link opens Settings →
Agent setup with the Codex dialog.

![AFTER: Settings → Agent setup with the Codex dialog, "Codex needs you
to sign in." and a Sign in
button](https://github.com/user-attachments/assets/e3ca7e6b-0576-4f19-8ca8-36016487df30)

## Overlap

- **#2959 (draft):** its `notify(reason)` calls `this.publish`, so its
notices are quoted with no change to its lines. This PR edits neither
that hunk nor #2951's.
- **#2995:** edits Claude's `explainTurn`, its doc comment, the NO_KEY
lines and `claude-code/index.ts`'s imports and setup. This PR leaves
those lines alone and adds `refusedByBuzz` after the `ClaudeAuth` class.
- **#2921:** already conflicts with main in 22 files; this PR adds no
new conflict (its hunks in the shared files are byte-identical).

## Follow-ups (not in this PR)

- The rendered gap above and below "X said:" is 16 px each because
paragraph and blockquote margins collapse. Tightening the
paragraph-into-quote gap is a thread-wide CSS change.
- The Codex Settings dialog's framing ("Security reviews need a ChatGPT
sign-in made in Buzz") is a separate copy fix.
- Goose's bundled hf-hub prints the same "Authentication required"
words, so a Hugging Face refusal would get the provider sentence too.
The sentence says "check its provider" rather than naming a key, which
still fits, but it is worth a look.
- Goose's `Error: Authentication required` shape is tested only through
`explainTurn`: `SessionControls` swallows a control action's error, so
no runtime path delivers it today.
- A Goose agent whose provider override differs from goose's default has
no way in Buzz to repair that provider's key alone: the Goose dialog's
only save action writes device defaults (provider and model) and
reconfigures every inheriting agent. The Settings link therefore opens
the dialog on the default provider, and the sentence says "check its
provider" without naming one. A credentials-only save for a non-default
provider is its own change. (A commit that named the provider and
preselected it in the dialog was tried on this branch and reverted after
review, for this reason.)
- No docs change: `docs/buzz-help.md` and `docs/agents.md` do not
describe failure replies for any harness.
- Not run in the native desktop app; human check pending.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: mtoohey <mtoohey@squareup.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
philazar and others added 2 commits November 13, 2026 14:30
A forwarded goose's delegates run on the turn's provider and model:
GOOSE_SUBAGENT_PROVIDER and GOOSE_SUBAGENT_MODEL are pinned in its env,
so neither the model's `delegate` arguments nor goose config can pick
another provider (a recipe's own goose_provider still wins, accepted).
A resumed ACP session starts on no known provider, so its first turn
reapplies the choice's with set_config_option, failing visibly when goose
refuses: goose 1.57.0 keeps the provider a session was saved with.
Delegates are recognised by `_meta.goose.toolCall.toolName`, not the
title goose decorates with a source or generates.

A forwarded conversation waits only for its goose to exit: a generation
native could not close still fails that turn and the agent's
retirement, but no longer refuses the conversation's later turns.

New session saves its marker before dropping the live session, so a
failed save leaves the session in place. A throwing `observe` is logged
instead of escaping the stdout reader, and a `null` session/load answer
reads as no settings. Docs scope the goose fallback notice to gateway
goose wakes in a conversation; comments cover the child rendezvous,
registerForwardChild's refusals and forwarded resume behavior.

Co-authored-by: coder2 <4acc49cb93576448ba82e631688c51a40bf03d6a889d78fddf57917d198de10f@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Philip Azar <pazar@squareup.com>
A dispatched gateway turn now goes through the gateway only when the
agent's effective provider is Databricks: the provider it names, else
goose's own default (read through GooseSetup.providers()). Any other
provider, or a default that can't be read, runs the turn as it would
without the gateway, with no notice and no Settings change asked of the
user. Compaction makes the same choice. The PROVIDER_ONLY refusal, which
told users to choose Databricks in the agent's settings and also refused
a blank provider whose goose default is Databricks, is removed.

Co-authored-by: coder2 <4acc49cb93576448ba82e631688c51a40bf03d6a889d78fddf57917d198de10f@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Philip Azar <pazar@squareup.com>

This branch has not been deployed

No deployments
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.

1 participant