Repository navigation
fix(agents): link Settings from key failures and quote the provider - #3081
Conversation
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source review via Wes’s account, with independent Rocket, Mantis and Groot coverage.
Three P2 findings: two inline, plus the Settings dismissal defect below.
[P2] Strip item from both outer Settings dismissal identities — src/app/navigation.ts:498–518. leaveSettings() removes provider but keeps the new item in both destructurings. targetKey() reparses the resulting target, so the new requirement that item needs provider throws. Open an item-bearing Goose link while Goose is disabled/unavailable and Close Settings fails immediately; alternatively, return with Back to a plain Settings visit and Close fails when the dismissal predicate reaches the retained item-bearing visit. The browser history driver evaluates all known entries before recording any dismissal, so the exception leaves Settings open. Strip item alongside provider in both places and cover Close directly from an unavailable item target and Close after Back, including Forward skipping the retired visit.
Reviewed head eb81fe497864ba4c94c5c7e1f01ed11f69b8fa01; target base 58116b826171d190a7376fc57b0509c32c507dde; merge base 7444eb12992a237eec786f5b81b94cdf48ff9c65.
Source analysis only: no tests, builds, app execution or code changes. The PR description and all seven screenshots were inspected; the two videos were not inspected. Native recovery, keyboard/focus and screen-reader behavior remain unexercised. One CI snapshot showed Rust/tool integration and CI required failing, two WebKit journey jobs cancelled and Windows skipped; those results were not diagnosed or attributed to this change. This is a COMMENT review, not approval.
eb81fe4 to
13b8178
Compare
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source follow-up via Wes’s account, with independent Rocket, Mantis and Groot coverage.
No new actionable findings in this bounded follow-up. Removing the provider-specific commit retires the three P2s from review 5827012733:
- The failure link no longer preselects an override provider in the device-defaults form.
- Settings targets no longer carry
item, removing the item-only dismissal key that made Close throw. - Goose reports generic provider guidance without reading mutable provider identity at failure completion.
The Codex/Claude hooks and shared quoting helper are unchanged from the reviewed revision. Goose retains its generic Settings link and quoted error. As the updated description notes, credentials-only repair for a non-default Goose provider remains outside this change; removing preselection does not implement that capability.
Reviewed head 13b8178396b3c084caff3cb53775b5892c888023, target base 3a1eb5601e997811009d01601786df8af9393a7f, merge base 7444eb12992a237eec786f5b81b94cdf48ff9c65; compared directly with previously reviewed eb81fe497864ba4c94c5c7e1f01ed11f69b8fa01.
Source-only: no tests, builds, apps or code changes. The current description and four screenshots were inspected (the images are byte-identical to the previously inspected copies); one video remains uninspected. Native behavior, keyboard/focus and screen-reader behavior remain unexercised. The single CI snapshot contains running jobs and recorded failures, so it does not establish a green gate; failures were not diagnosed or attributed to this change. This COMMENT is not approval.
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>
13b8178 to
015c793
Compare
kazemek21
left a comment
There was a problem hiding this comment.
🤖 Kyle reviewed this PR and the Nexus review-tracker notes and approved with no comments.
Posted by Codex (gpt-6.1-sol) on behalf of kylez.
## TL;DR Removes the six owner-session read tools that let a Buzz-hosted agent read with its owner's own access: channel, member and DM lists, channel details, messages and canvases. An agent's own reads and the channel archive, unarchive and delete changes are unchanged. The tools are gone from discovery on every harness, and a cached or direct call to one is refused before anything is read with the owner's session. Fixes NEXUS-4348 · Initiative: https://blockcell.sqprod.co/sites/kylez-buzz-mcp-initiative/ ## Scope Kyle's request was to remove "the channel/member/dm list/get" endpoints and leave the archive/delete channel changes for now. Removing `owner_read` and the two Canvas tools as well is the initiative lead's interpretation: they read through the same owner session and handle, and leaving them would keep the owner-read capability this removes. If that is wrong, the #2881 half can be restored on its own. ### Removed From #2745 (owner channels and members): - `owner_channel_list`, `owner_channel_get`, `owner_member_list`, their hint text and discovery on Claude Code, Codex, Pi, the session RPC and the harness kit. - `src/features/agents/owner-access.ts`, the run handle's `ownerAccess` field and the runner's host hook that built it for bundled-revision runs. - The `BUZZ_OWNER_MCP_COMMUNITIES` build setting (`vite.config.ts`). A distributor build that still sets it now gets nothing from it; `docs/configuration.md` lists it with the other retired inputs. - The owner-tool sections in `docs/agents.md`, `docs/configuration.md`, `docs/buzz-help.md` and `src/buzz-mcp/AGENTS.md`. From #2881 (owner messages and canvases): - `owner_read`, `owner_canvas_get`, `owner_canvas_history`, `src/buzz-mcp/owner.ts` and the `Owner*` client types. - The `docs/relay-queries.md` paragraph, and the exports of `tools.ts` helpers that only `owner.ts` used. `edited()` keeps its author-only edit selection, minus the owner parameter. ### Kept - From #2745: the Stop and cancellation fences in the Claude Code, Goose and ACP sessions (`workLifetime`, `current()`), which every Buzz tool call now depends on, and the channel-lifecycle changes (`channel-lifecycle.ts`, including its `records` parameter). - `addTool` still refuses plugin tool names starting with `owner_`, so a stale call to a retired name can never reach plugin code. There is no other owner-prefixed restriction and no new permission layer. - Ordinary agent-scoped reads, Codex per-thread/turn tool validation, plugin and external isolation. ## Alternate routes checked Nothing else reached these tools: no Rust, IPC, sidecar or RPC path, and Mesh and external agents use the same run handle that no longer carries owner access. Codex thread tool keys no longer carry the `:owner` suffix, so threads created with owner tools are replaced instead of reused with a stale tool list. ## Tests The new tests list the six names as a fixed fixture rather than reading them from the catalog: - `session-tools.test.ts`: `sessionTools()` and `sessionHint()`, which every harness lists from, name none of the six. - `service.test.ts`: a real bundled run handle has no `ownerAccess`. With the owner's session stubbed to return an `OWNER_ONLY` message for an owner-only channel, each retired name is refused with `No tool <name>` before any relay query. An ordinary `read` of that channel then runs with the agent's own key, filtered to that channel, and returns `No messages.`. The same client's `read` of the agent's own channel returns its message, and no read through the owner's session ever names the owner-only channel. External agents see none of the names and are refused the same way. - `codex/runtime.test.ts`: `dynamicTools` lists none of the names, each call fails Codex's validation, a planted owner-access spy is never called, and the thread's own `read` works. - `app-client.test.ts`: a handle that still carries an owner capability doesn't pass it to the tool client. - The Claude Code and Goose Stop/timer tests now hold an ordinary `mem_get` read instead of an owner tool. On a Blox box with these commits applied to main: `tsc` passes, and the 12 affected Vitest files pass (claude, codex, goose, pi runtimes, app-client, tools, session-tools, service, kit, ACP client, channel-lifecycle). Full CI covers the rest. `git push --no-verify` skipped the pre-push lanes (types-and-related-unit-tests, design-system, rust-clippy); CI's `pnpm check`, sharded Vitest and clippy jobs repeat all three. Fail-before check: the removal tests in `service.test.ts` and `codex/runtime.test.ts` were run against the pre-removal source (0a5c190). Three of them fail on real assertions: - Codex `dynamicTools` contains `owner_channel_list`. - An external agent's call returns "Owner tools are not available to this agent here; nothing was read." instead of `No tool owner_channel_list`. - The bundled handle has `ownerAccess`. All three pass on this branch. Vitest strips types, so this check doesn't cover `tsc`. The `session-tools.test.ts` discovery test also passes on the old source, because that source only lists the tools when the owner setting is on, so it doesn't count as a fail-before check. ## Not covered - No native app run or live relay check. The removal is exercised through the service, runner and harness tests above. - A Buzz process already running a build with these tools keeps them until it updates. This doesn't revoke anything remotely. ## Related open PRs - Human work in overlapping files keeps its behavior but may need a rebase: #2700, #2974, #3081, #2972 (cancel added tools on native Stop), #2935, #1634, #2622, #3095, #2882. - #3004, #3005, #3006, #3008 and #2980 build on the removed owner access and are not part of this change. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (claude-opus-5-5) on behalf of kylez --------- Signed-off-by: Kyle Zemek <kylez@squareup.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…restricted-communities * origin/main: ci: give WebKit shards headroom and keep fixture servers' idle connections open (#3074) fix(messages): offer Retry as soon as a send fails (#3080) fix(agents): link Settings from key failures and quote the provider (#3081) fix(identity): link Builderlab from Identity sync where its page holds the next step (#3082) fix(startup): show a recoverable screen when Buzz fails to open or crashes (#3085) fix(attention): ask before deleting a watch (#3092) fix(agents): remove the owner-access read tools (#3123) fix(channels): keep the side pane mounted under the image viewer (#3111) fix(solo): ask before Escape or Close drops a new workspace's typed fields (#3114) # Conflicts: # docs/configuration.md # vite.config.ts
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 could not finish: unexpected status 401 Unauthorized: Missing bearer or basic authentication in header, url: https://api.openai.com/v1/responses.⚠️ I couldn't finish that: Authentication required, orInternal error: Error getting agent reply: Configuration value not found: ANTHROPIC_API_KEY.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:
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 theCodex 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).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.Incorrect API key provided) or any of the binary's six… could not be refreshed[…]. Please [log out and ]sign in again.messagesCodex could not finish: Your authentication session could not be refreshed automatically. Please log out and sign in again..Configuration value not found: <NAME>API_KEY)⚠️ I couldn't finish that: Internal error: Error getting agent reply: Configuration value not found: ANTHROPIC_API_KEYAuthentication requiredat the start or after:)⚠️ I couldn't finish that: goose could not start: Authentication required⚠️ 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.Claude said:and> Invalid API key · Fix external API key⚠️ I couldn't finish that: Claude is missing an API key. Head to **Settings** to add one.Codex could not finish: model busy.,Codex could not finish: MCP OAuth refresh token was rejected; reauthorization required.,⚠️ I couldn't finish that: Error: overloadedDesign
claude-code/auth.tsxalready does:codex/setup.tsexplainTurn(sign-in) andgoose/setup.tsexplainTurn(provider keys). Nothing moves into the Agents service.src/features/agents/failure-words.tswithProviderWords(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.publish, which every thread notice goes through, so no call site changes. Draft fix(codex): tell every steered thread when a turn fails #2959 replaces the "could not finish" publish withnotify(reason), which callsthis.publish, so its notices are explained and quoted with no change to its lines. A sign-in Codex refuses atthread/startorturn/startlands inexecute()'s catch and is covered. The Activity tab's status keeps Codex's own notice.report(), as Claude does.explainTurnsentences;claude-code/index.tsquotes Claude after whatever it explains, except whenrefusedByBuzz(Buzz's own NO_KEY refusal) produced the error. fix(claude): sign in with a Claude account as well as an API key #2995 adds a signed-out branch insideexplainTurn, so this change stays out of that method and merges cleanly with it (verified withgit merge-tree)./could not be refreshed\b[^.\n]*\. Please (?:log out and )?sign in again/istays within one sentence and the next, soconfigured values could not be refreshed: enterprise IdP session has no OIDC ID token; sign in again.and the MCP OAuth refresh errors stay raw.explain, as Claude's do; their credentials live on the target.--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 leavesMessages.module.cssalone.Testing
Tests added in
failure-words.test.ts,codex/runtime.test.ts(through the plugin'sapply, and with the fake app-server refusingthread/start/turn/start),codex/setup.test.ts,goose/runtime.test.ts,goose/setup.test.ts,goose/index.test.tsandclaude-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 twoconfigured values could not be refreshedforms,Error: overloaded) post exactly as before.Base:
7444eb1299(origin/main when branched).7444eb1299X said:MessageMarkdown(throwaway)<p>sentence + link,<p>"X said:", then<blockquote>; line breaks kept inside itMessages.module.cssbin/pnpm exec tsc --noEmitbiome check --error-on-warningson changed filesgit diff --checkgit merge-treewith #2959, #2960, #2995, #2951, #2961, #2944, #2938, #28597444eb1299git merge-treewith #2995claude-code/index.tsandauth.tsxkeep both #2995's signed-out branch andrefusedByBuzz; biome clean on the merged filesgit merge-treewith #29217444eb1299already has with it; its hunks inclaude-code/index.tsandgoose/runtime(.test).tsbyte-identical, so none addedThe 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 owncodex/setup.ts/goose/setup.tsexplainTurncomposes 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,MessageMarkdownwith the realMessages.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.p3-codex-goose-settings-link-before-after.mp4
7444eb1299)13b8178396)AFTER (
13b8178396), close-up of the thread column: sentence, 16 px gap, "X said:", 16 px gap, then the quote's bar.AFTER (
13b8178396): clicking Codex's Settings link opens Settings → Agent setup with the Codex dialog.Overlap
notify(reason)callsthis.publish, so its notices are quoted with no change to its lines. This PR edits neither that hunk nor fix(codex): keep a running turn's results in its thread when another thread steers it #2951's.explainTurn, its doc comment, the NO_KEY lines andclaude-code/index.ts's imports and setup. This PR leaves those lines alone and addsrefusedByBuzzafter theClaudeAuthclass.Follow-ups (not in this PR)
Error: Authentication requiredshape is tested only throughexplainTurn:SessionControlsswallows a control action's error, so no runtime path delivers it today.docs/buzz-help.mdanddocs/agents.mddo not describe failure replies for any harness.🤖 Generated with Claude Code