Skip to content

fix(console): stop blaming the data API for chat-route failures - #155

Merged
Travis-Gilbert merged 1 commit into
mainfrom
fix/console-chat-failure-labelling
Aug 2, 2026
Merged

fix(console): stop blaming the data API for chat-route failures#155
Travis-Gilbert merged 1 commit into
mainfrom
fix/console-chat-failure-labelling

Conversation

@Travis-Gilbert

Copy link
Copy Markdown
Owner

Travis hit "The data API is unreachable." on /workspace/{id}/chat while the data API was answering normally.

Measured at the time:

Probe Result
commonplace-api…/healthz 200
commonplace-api…/graphql 401, fail-closed auth
console proxy /api/objects/views 401 principal_resolution=unauthenticated
CONSOLE_DATA_API_URL http://commonplace-api.railway.internal:8080, correct

Nothing was unreachable. The config was right. The banner was wrong.

Where it came from

fetchChatCatalog dials /api/chat/projects — a console route, not the data API. ChatPage mapped every rejection from it to console_data_api_unreachable:

loadError === 'workspace_object_scope_unenforced'
  ? 'workspace_object_scope_unenforced'
  : 'console_data_api_unreachable'   // ← everything else

A 500, a timeout, or a JSON parse error on the console's own chat route announced an outage on a different service entirely.

This is the same defect the degraded-state work existed to remove, left standing on the path users hit most. #152/#154 did not fix it — and worse, that change removed the (uncorrelated) origin from this branch, so it now renders a bare sentence with no evidence at all. That is precisely the reported screenshot: the sentence, twice, and nothing else.

The fix

readJson threw new Error(message), discarding response.status and the wire code the route returned, so the caller had nothing to classify with. It now throws a typed error:

export class ChatWireError extends Error {
  readonly door: string;
  readonly status: number | null;   // null when the request never landed
  readonly wireCode: string | null;
}

All six call sites in catalog-client.ts pass the door they dialed. ChatPage reports the code the route actually named, falling back to console_chat_wire_failed when it named none, with that request's own door and status.

workspace_object_scope_unenforced needs no special case any more: it is a mapped code and resolves on its own.

The disconnected branch is untouched and still carries transportOrigin, because connection is derived from onTransport and that origin genuinely is its outcome.

What the banner says now

Situation Before After
500 from /api/chat/projects The data API is unreachable. The chat wire could not complete this turn. — /api/chat/projects answered 500.
chat fetch never landed The data API is unreachable. ...did not answer. That is DNS, the network, or a blocked origin, not a status code.
data API genuinely down The data API is unreachable. unchanged, with door/host/status

Verification

Command Result
vitest run src/lib/degradation.test.ts 14 passed (+2)
pnpm --filter @commonplace/console run build:railway exit 0

The two new tests pin the thing that went wrong: a chat failure must not contain "data api", and it names the chat wire when the caller supplies no door.

Note

/api/version now serves on v2.theoremharness.com reporting sha 267600cc, so #154 is deployed and the version endpoint works. This PR fixes the labelling that #154 left behind.

Travis hit "The data API is unreachable." on
/workspace/{id}/chat while the data API was answering normally:
commonplace-api returned 200 on /healthz and a fail-closed 401 on
/graphql, and the console's own /api/objects/views proxy returned a
correct 401. Nothing was unreachable.

The banner came from the chat catalog fetch. fetchChatCatalog dials
/api/chat/projects, a console route, and ChatPage mapped EVERY rejection
from it to `console_data_api_unreachable`:

    loadError === 'workspace_object_scope_unenforced'
      ? 'workspace_object_scope_unenforced'
      : 'console_data_api_unreachable'   // everything else

So a 500, a timeout, or a parse error on the console's own chat route
announced an outage on a different service. This is the same defect the
degraded-state work set out to remove, left standing on the path users
hit most, and #152/#154 did not touch it. Worse, that change removed the
(uncorrelated) origin from this branch, guaranteeing it renders a bare
sentence with no evidence at all. That is exactly the screenshot.

readJson threw `new Error(message)`, discarding the status and the wire
code the route returned, so the caller had nothing to classify with. It
now throws a typed ChatWireError carrying door, status and wireCode, and
every one of the six call sites passes the door it dialed.

ChatPage reports the code the route actually named, falling back to
`console_chat_wire_failed` when it named none, with that request's own
door and status. `workspace_object_scope_unenforced` needs no special
case now: it is a mapped code and resolves on its own.

The disconnected branch is unchanged and still carries transportOrigin,
because `connection` is derived from onTransport and that origin really
is its outcome.

  vitest run src/lib/degradation.test.ts   14 passed
    (+2: a chat failure must not say "data api", and names the chat wire
     when the caller supplies no door)
  pnpm --filter @commonplace/console run build:railway   exit 0
Copilot AI review requested due to automatic review settings August 2, 2026 06:38
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@Travis-Gilbert, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 8 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 56e1d72e-b6a8-47e3-8d26-0f64dfa1c6f1

📥 Commits

Reviewing files that changed from the base of the PR and between 267600c and 29fbfa0.

📒 Files selected for processing (4)
  • apps/console/src/components/chat/ChatPage.tsx
  • apps/console/src/lib/chat/catalog-client.ts
  • apps/console/src/lib/degradation.test.ts
  • apps/console/src/lib/degradation.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ecc-tools

ecc-tools Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes mislabelled degraded-state messaging on the Console chat route (/workspace/{id}/chat) by preserving and reporting evidence (door + status + route wire code) from failures of the console’s own /api/chat/* routes, instead of incorrectly mapping them to “The data API is unreachable.”

Changes:

  • Introduces a typed ChatWireError that preserves route door, HTTP status, and wire code when /api/chat/* requests fail.
  • Updates ChatPage to classify chat failures using the route’s named wire code (or console_chat_wire_failed) and attach the failing request’s door/status to the degradation banner.
  • Adds degradation mapping/test coverage so chat-route failures describe the chat wire (and no longer mention “data api”).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
apps/console/src/lib/degradation.ts Adds a door default for console_chat_wire_failed so evidence can fall back to “The chat wire” when no door is provided.
apps/console/src/lib/degradation.test.ts Adds tests pinning that chat-route failures do not blame the data API and that the chat wire is named when no door is supplied.
apps/console/src/lib/chat/catalog-client.ts Adds ChatWireError and updates JSON reading to throw typed errors with door/status/wireCode instead of flattening to Error(message).
apps/console/src/components/chat/ChatPage.tsx Switches loadError to structured ChatFailure and uses it to drive correct degradation codes and evidence for chat-route failures.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 365 to 369
} catch (error) {
if (active) {
setLoadError(error instanceof Error ? error.message : 'thread_unreachable');
setLoadError(chatFailure(error, '/api/chat/threads'));
}
}

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 29fbfa0649

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

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

message: body.message ?? body.error ?? `chat request failed: ${response.status}`,
door,
status: response.status,
wireCode: body.error ?? null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve known inner wire codes

When the object seam returns a known code without a message, responseError turns that code into the thrown error message, and /api/chat/projects wraps it as { error: 'project_catalog_failed', message: '<inner code>' }. Selecting only body.error here therefore discards codes such as workspace_object_scope_unenforced; ChatPage passes the unmapped wrapper to degradationFor and shows the generic unavailable sentence instead of the existing scope-specific guidance. The previous path deliberately recognized that inner code, so preserve recognized message codes or return the underlying code as a separate structured field.

Useful? React with 👍 / 👎.

Comment on lines +71 to +72
// fetch itself rejected, so there is no status: the request never landed.
return { code: 'console_chat_wire_failed', door };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Distinguish response decoding failures from network failures

This fallback handles every non-ChatWireError, not only a rejected fetch. If /api/chat/projects or a thread route answers successfully but its JSON is malformed or truncated, response.json() throws a SyntaxError and this branch records no status; degradationFor then tells the user that the route did not answer and attributes it to DNS, the network, or a blocked origin even though the route did answer. Wrap successful-response decoding failures with the response status, or otherwise classify them separately from transport rejection.

Useful? React with 👍 / 👎.

@Travis-Gilbert
Travis-Gilbert merged commit e93a8cb into main Aug 2, 2026
8 of 10 checks passed
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.

2 participants