Skip to content

CL-6495: say what actually failed when a workbench can't be created - #227

Merged
TheGreatAxios merged 4 commits into
mainfrom
cl-6495-create-error
Aug 21, 2026
Merged

TheGreatAxios merged 4 commits into
mainfrom
cl-6495-create-error

Conversation

@TheGreatAxios

Copy link
Copy Markdown
Contributor

Summary

new-workbench-picker.tsx's create-workbench catch bound nothing:

} catch {
  toast("Couldn't create the workbench — try again.");
  setCreating(false);
}

Whatever createWorkbenchFromTemplate threw was discarded, so every
failure looked identical and there was no signal to debug the owner's
intermittent repro from.

  • Bind + log: the cause is now logged (getLogger("web.new-workbench-picker"))
    with its message, and status/path when it's an ApiQueryError — path
    alone names which step in createWorkbenchFromTemplate fired (manifest
    read vs. block deploy vs. POST /chat/workbenches, etc.), so the next
    occurrence should identify the failing step immediately.
  • Honest toast copy: describeWorkbenchCreateFailure shows a
    precondition Error's message verbatim (e.g. "A code-review workbench
    isn't available here yet.") instead of flattening it into a lying
    "try again," and runs anything else through describeApiError — the
    same treatment global-routines.ts, home-page.tsx, and
    library-page.tsx already use for this class of failure.

Toast system finding (priority 2)

Confirmed there is exactly one toast system: @corbits/react-ui's
toast(message: string) re-export, used by every call site in the repo.
But at the pinned SHA (3b12281), that function has no error/destructive
variant at all — it always renders the same neutral, unstyled treatment
regardless of content. The "doesn't match the design system" complaint
isn't a misuse of the toast system in this repo; it's a capability gap
in corbitsdev/react-ui itself (no toast.error/destructive styling
exists to opt into). Not fixed here per "core UI lives in react-ui" —
flagging for a follow-up there.

Underlying-failure candidates (priority 3)

Traced createWorkbenchFromTemplate's awaits for what could produce the
owner's intermittent failure, now nameable via the logged path:

  • fetchWorkbenchTemplateManifest (GET .../library/templates/:id) —
    CL-6458 made library seeding lazy-on-read; a first-read race before the
    shelf has converged is plausible and returns null → the "isn't
    available here yet" precondition Error.
  • deployWorkbenchTemplateBlock (POST .../template-blocks/:asset/deploy)
    — the block-workflow deploy step.
  • createWorkbench (POST .../chat/workbenches) and
    patchWorkbenchSettings — the mint + pending-connections write.
  • listAgentDefinitions/findMyraDefinition — a bench with no default
    setup agent throws the same class of precondition Error.
  • The GitHub repo-picker step (getConnectGithubState,
    startReviewingGithubRepos) only runs when GitHub is already connected.

Could not reproduce the intermittent case in this pass, but the next
occurrence's logged path should say which of these it was.

Sibling sweep

Grepped apps/web/src and packages/*/src for other bare catch {
blocks. 87 matches — most are legitimate best-effort fallbacks
(localStorage/sessionStorage reads/writes, JSON.parse defaults). A
smaller cluster mirrors this exact bug's class (user action → generic
toast, real error discarded): shell/workbench-list.tsx:271,
shell/context-menu/items.tsx:54, connect-service-actions.ts:122,
chat-ui/timeline.tsx:784, chat-ui/use-thread-navigation.ts:128,
chat-ui/composer.tsx:559, settings-ui/account-section.tsx:98. Not
fixed here — worth its own ticket.

Test plan

  • bun test ./src/pages/new-workbench-picker.test.ts — new unit
    tests for describeWorkbenchCreateFailure
  • bun test ./test/new-workbench-picker.test.tsx — existing DOM
    suite still green
  • bunx tsc --noEmit -p apps/web
  • bun run lint from repo root

Does not touch layout (CL-6489 owns that in this same file).

Fixes CL-6495

Pins the wording describeWorkbenchCreateFailure should pick for each
shape of cause: a precondition Error shown verbatim, an ApiQueryError
routed through describeApiError, and a status-less/network failure
still landing on the generic try-again.
The create-workbench catch bound nothing, so whatever
createWorkbenchFromTemplate threw was discarded and every failure
showed the same 'try again' toast regardless of whether retrying could
help. Now the cause is logged with its status/path so the next
occurrence names which step fired, and the toast copy is honest: a
precondition failure (no setup agent, an unavailable template) shows
its own message instead of a retry prompt that would be a lie, and an
ApiQueryError runs through describeApiError so the status drives the
wording, matching the treatment other pages already use.
@TheGreatAxios

Copy link
Copy Markdown
Contributor Author

Peer review: items 2-4 hold (tests/tsc/lint green, toast-styling gap confirmed real and upstream-only), but item 1 (raw-Error-verbatim safety) does not hold — found a live leak, not a hypothetical one. Blocking merge until fixed.

The leak

describeWorkbenchCreateFailure excludes only ApiQueryError:

if (cause instanceof Error && !(cause instanceof ApiQueryError)) {
  return cause.message;
}

But createWorkbenchFromTemplate directly awaits four @corbits/chat-ui calls — createWorkbench, patchWorkbenchSettings, getConnectGithubState, startReviewingGithubRepos — that throw ChatApiError (packages/chat-ui/src/api.ts:198-207), a separate class that also extends Error directly. It is not an ApiQueryError, so it passes the guard and its raw .message goes straight into the toast verbatim:

  • `The server answered ${response.status} for ${path}.` (api.ts:257) — leaks the internal /api/tenants/<tenantId>/... path to the end user
  • `Unexpected response shape from ${path}: ${parsed.summary}` (api.ts:265) — leaks path + raw arktype validation summary
  • a bare fetch-failure message on network errors (api.ts:249)

chat-ui already ships describeChatError for exactly this reason — its doc comment literally says "never error.message ... which for a ChatApiError embeds the raw request path." This PR doesn't use it and guards against the wrong class.

Two more plain-Error leaks on paths this same flow calls:

  • listPluginsForTenant → resolveOne (packages/connections/src/plugins.ts:78-97) throws plain Error embedding descriptor.displayName + raw status, or an arktype .summary on schema mismatch.
  • instantiateWorkbenchTemplate (packages/workflow-catalog/src/instantiate.ts:133-136) throws plain Error embedding manifest.id + participant.handle for an unmapped template participant.

Net effect: for these steps, a failure now shows the person an internal path/tenant id or a schema-validation dump instead of the old generic "try again" — worse than the bug being fixed, even though the two curated precondition throws in instant-agent-create.ts are handled correctly.

Secondary: logged path/status also blind for the same 4 steps

Since ChatApiError has no path field and the log line only reads .status/.path off ApiQueryError, a createWorkbench/patchWorkbenchSettings/getConnectGithubState/startReviewingGithubRepos failure logs status: undefined, path: undefined — the exact steps the PR description calls out as the point of this change. The path is still recoverable from the free-text message on 2 of 3 ChatApiError throw shapes, but not on network failures, and not structurally.

Suggested fix

Don't try to exhaustively enumerate every non-curated Error subclass (there are at least 3 in this codebase already: ApiQueryError, ChatApiError, plain Error from connections/workflow-catalog). Either:

  • mark the two curated precondition throws in instant-agent-create.ts with a dedicated class (e.g. WorkbenchPreconditionError) and only pass that through verbatim, or
  • explicitly exclude ChatApiError too and route it through describeChatError, and fix listPluginsForTenant/instantiateWorkbenchTemplate to throw typed errors rather than plain Error.

Everything else in the PR (test coverage, toast-styling research, sibling-catch sweep) looks solid — this is the one thing to fix before merge.

The previous fix denylisted ApiQueryError before showing an Error's
message verbatim, which failed open: ChatApiError (thrown by
createWorkbench, patchWorkbenchSettings, and the GitHub-connect steps)
slipped straight through and put raw request paths and schema
summaries in the toast — the exact leak class this ticket exists to
close. Same for plain Errors from listPluginsForTenant and
instantiateWorkbenchTemplate.

Inverted to an allow-list: only WorkbenchPreconditionError (a new
marker for the two intentionally user-facing precondition messages —
no setup agent, an unavailable template) is shown verbatim.
ApiQueryError and ChatApiError each go through their own describer
(describeApiError, describeChatError, the latter now exported from
chat-ui), and anything else falls to one generic message. A future
error type therefore fails safe by default instead of leaking.

Also reads status off ChatApiError for the create-failure log, since
four of the five awaited steps throw that type and previously logged
status: undefined regardless of what actually failed.
The stub serves an empty definitions list, so the create fails its
precondition rather than the request. The generic toast used to hide that;
now that preconditions are shown verbatim, the assertion names the failure
the test actually exercises.
@TheGreatAxios
TheGreatAxios merged commit f70ad59 into main Aug 21, 2026
2 checks passed
@TheGreatAxios
TheGreatAxios deleted the cl-6495-create-error branch August 25, 2026 15:29
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