Skip to content

fix(console): say why a chat surface failed, not just that it did - #156

Merged
Travis-Gilbert merged 1 commit into
mainfrom
fix/console-surface-upstream-reason
Aug 2, 2026
Merged

fix(console): say why a chat surface failed, not just that it did#156
Travis-Gilbert merged 1 commit into
mainfrom
fix/console-surface-upstream-reason

Conversation

@Travis-Gilbert

@Travis-Gilbert Travis-Gilbert commented Aug 2, 2026

Copy link
Copy Markdown
Owner

The previous fix worked: the banner went from "The data API is unreachable." to

This surface cannot render right now.
/api/chat/projects answered 502.

That names the request. It does not name the cause — and the route knew the cause the whole time. Two gaps, both mine.

1. The reason was computed, sent, and thrown away

/api/chat/projects returns { error, message } where message holds the inner reason. chatFailure kept the code and dropped the message.

DegradationOrigin now carries reason, rendered after the door and status, never instead of them:

Detail line
before /api/chat/projects answered 502.
after /api/chat/projects answered 502. objects/query failed: 502

2. Thirteen of fifteen chat error codes had no sentence

Only console_chat_wire_failed and web_search_unavailable were mapped. These all fell through to the generic sentence:

project_catalog_failed   thread_catalog_failed   attachment_upload_failed
project_write_failed     thread_read_failed      file_required
project_select_failed    thread_create_failed    invalid_body
                         thread_update_failed    tenant_connector_unavailable
                         thread_not_found        web_search_requires_principal

That is why you saw "This surface cannot render right now" rather than something specific: project_catalog_failed was unmapped. An unmapped code is not a neutral default, it is a sentence nobody can act on.

A test now asserts every emitted code has a sentence distinct from the generic one, so a new route code cannot silently join them.

CS15 still holds

readableReason renders a bare wire code as its sentence, not the identifier:

reason: 'console_data_api_unreachable'
  → "...answered 502. The data API is unreachable."

An unmapped bare code passes through verbatim, deliberately: inventing a sentence would hide the only identifier the reader could search for.

A real hole its own test caught

The length bound was inside one branch. The code-shaped check /^[a-z][a-z0-9_]*$/ matches any run of lowercase, so a 500-character token took that path and returned before the cap — 533 characters into the banner. Bounding at the exit removes the class rather than the instance.

Verification

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

Two notes for reviewers

Rebased, not stashed. This keeps the onTransport refinement that arrived with #155lastTransport is cleared unless the connection is actually disconnected, which is a stronger version of the correlation guarantee than what I wrote. Verified present after the rebase.

Local build needs an install. #153 made build:railway run pnpm --filter twenty-ui run build first, and twenty-ui needs its own node_modules. A stale checkout fails with ERR_MODULE_NOT_FOUND: '@vitejs/plugin-react-swc' from packages/twenty-ui/vite.config.ts, which reads like a broken config and is not. Run pnpm install; the lockfile does not move.

Summary by CodeRabbit

  • Bug Fixes
    • Improved chat failure messages by displaying clearer, more specific explanations when upstream services or chat operations fail.
    • Added readable descriptions for project, thread, attachment, request, connector, and web search errors.
    • Prevented duplicate error details and shortened overly long failure messages for easier scanning.
    • Load-failure screens now include relevant failure reasons when available.

Travis' second screenshot showed the previous fix working: "/api/chat/projects
answered 502." That named the request. It did not name the cause, so it
still took a round trip to learn anything, and the route had known the
cause the whole time.

Two gaps, both mine.

The route returns { error, message } and the message holds the inner
reason. chatFailure kept the code and dropped the message, so the only
part naming what actually broke was computed, sent over the wire, and
discarded on arrival. DegradationOrigin now carries `reason` and it is
rendered after the door and status, never instead of them.

Second, 13 of the 15 error codes the /api/chat/* routes can emit were
missing from WIRE_MAP: project_catalog_failed, project_write_failed,
project_select_failed, thread_catalog_failed, thread_read_failed,
thread_create_failed, thread_update_failed, thread_not_found,
attachment_upload_failed, file_required, invalid_body,
tenant_connector_unavailable, web_search_requires_principal. Every one
fell through to "This surface cannot render right now." An unmapped code
is not a neutral default; it is a sentence nobody can act on. A test now
asserts each emitted code has a sentence of its own, so a new route code
cannot silently join them.

readableReason keeps CS15 intact: a bare wire code renders as its
sentence rather than the identifier, and anything else is prose already.
An unmapped bare code passes through, because inventing a sentence would
hide the only identifier the reader could search for.

The length bound had a real hole, caught by its own test. The
code-shaped check matches any run of lowercase, so a 500-character token
took that branch and returned before the cap. Bounding at the exit
instead of inside one branch removes the class, not the instance.

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

Rebased onto main rather than stashed, to keep the onTransport
refinement that came in with #155: lastTransport is cleared unless the
connection is actually disconnected, which is a stronger version of the
correlation guarantee than what I wrote.

Note for anyone building locally: #153 made build:railway run
`pnpm --filter twenty-ui run build` first, and twenty-ui needs its own
node_modules. A stale checkout fails with
ERR_MODULE_NOT_FOUND '@vitejs/plugin-react-swc' from
packages/twenty-ui/vite.config.ts. Run pnpm install; the lockfile does
not move.
Copilot AI review requested due to automatic review settings August 2, 2026 15:41
@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.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Chat failures now retain upstream reasons. The degradation system translates mapped wire codes, preserves prose, limits reason length, and appends reasons to rendered failure details. Tests cover these behaviors and chat-route mappings.

Changes

Chat failure reason rendering

Layer / File(s) Summary
Degradation contracts and chat failure mappings
apps/console/src/lib/degradation.ts
DegradationOrigin accepts an optional reason. Chat project, thread, attachment, request-body, connector, and web-search failure codes now have unavailable templates.
Reason sanitization and degradation descriptions
apps/console/src/lib/degradation.ts, apps/console/src/lib/degradation.test.ts
readableReason translates mapped codes, preserves prose, and limits output to 160 characters. describeOrigin appends reasons to status details. Tests cover translation, routes, statuses, truncation, and chat-code mappings.
Chat failure capture and rendering
apps/console/src/components/chat/ChatPage.tsx
Chat wire and fetch failures retain non-duplicate reasons. Load-failure rendering passes the reason to degradationFor.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ChatPage
  participant degradationFor
  participant describeOrigin
  ChatPage->>degradationFor: pass failure code and reason
  degradationFor->>describeOrigin: build degradation details
  describeOrigin-->>ChatPage: return bounded readable reason
Loading

Possibly related PRs

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding the reason for chat-surface failures in the console.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/console-surface-upstream-reason

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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/console/src/components/chat/ChatPage.tsx`:
- Around line 65-81: Sanitize the reason produced by the error-mapping flow
around the visible wire-error handling before exposing it to the UI: retain only
server-authored display-safe reasons or mapped wire-code text, never raw caught
Error.message values. At apps/console/src/components/chat/ChatPage.tsx lines
65-81, update the mapping to exclude arbitrary server exception text; at line
471, pass only that sanitized reason into degradationFor.

In `@apps/console/src/lib/degradation.test.ts`:
- Around line 180-194: Update the test named “every chat route error code has a
sentence of its own” to collect each emitted code’s cause and assert that the
set of causes has size equal to emitted.length. Keep the existing
generic-fallback assertion, but add the uniqueness check so duplicate sentences
among mapped codes fail.

In `@apps/console/src/lib/degradation.ts`:
- Around line 301-311: The fallback in readableReason for unmapped bare
identifiers must return safe generic prose instead of exposing trimmed. Update
readableReason while preserving mapped-code and sentence-like reason handling,
then add coverage for an unmapped bare origin.reason through describeOrigin or
the relevant public path, asserting the identifier is not rendered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 22224223-b8be-4b2e-a527-95b507358ea4

📥 Commits

Reviewing files that changed from the base of the PR and between e93a8cb and 469d8a9.

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

Comment on lines +65 to +81
const code = error.wireCode ?? 'console_chat_wire_failed';
return {
code: error.wireCode ?? 'console_chat_wire_failed',
code,
door: error.door,
status: error.status ?? undefined,
// These routes wrap an inner failure and put its reason in `message`.
// That reason is the only part naming what actually broke, so keep it
// unless it just repeats the code the sentence already covers.
reason: error.message && error.message !== code ? error.message : undefined,
};
}
// fetch itself rejected, so there is no status: the request never landed.
return { code: 'console_chat_wire_failed', door };
return {
code: 'console_chat_wire_failed',
door,
reason: error instanceof Error ? error.message : undefined,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Do not render arbitrary server exception text.

The route contract at apps/console/src/app/api/chat/projects/route.ts:12-20 returns raw caught Error.message. The new flow stores and displays it. The 160-character limit restricts size but does not remove internal or sensitive content.

  • apps/console/src/components/chat/ChatPage.tsx#L65-L81: retain only a server-authored, display-safe reason or a mapped wire code.
  • apps/console/src/components/chat/ChatPage.tsx#L471-L471: pass only the sanitized reason into degradationFor.
📍 Affects 1 file
  • apps/console/src/components/chat/ChatPage.tsx#L65-L81 (this comment)
  • apps/console/src/components/chat/ChatPage.tsx#L471-L471
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/src/components/chat/ChatPage.tsx` around lines 65 - 81, Sanitize
the reason produced by the error-mapping flow around the visible wire-error
handling before exposing it to the UI: retain only server-authored display-safe
reasons or mapped wire-code text, never raw caught Error.message values. At
apps/console/src/components/chat/ChatPage.tsx lines 65-81, update the mapping to
exclude arbitrary server exception text; at line 471, pass only that sanitized
reason into degradationFor.

Comment on lines +180 to +194
it('every chat route error code has a sentence of its own', () => {
const emitted = [
'project_catalog_failed', 'project_write_failed', 'project_select_failed',
'thread_catalog_failed', 'thread_read_failed', 'thread_create_failed',
'thread_update_failed', 'thread_not_found', 'attachment_upload_failed',
'file_required', 'invalid_body', 'tenant_connector_unavailable',
'web_search_requires_principal', 'console_chat_wire_failed',
'web_search_unavailable',
];
const generic = degradationFor('a_code_that_is_not_mapped_at_all').cause;
for (const code of emitted) {
expect(degradationFor(code).cause, `${code} fell through to the generic sentence`)
.not.toBe(generic);
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Test uniqueness between emitted chat failure sentences.

Line 190 compares each cause only with the unmapped fallback. Two emitted codes can share the same cause and this test still passes. Collect the emitted causes and assert that their set size equals emitted.length.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/src/lib/degradation.test.ts` around lines 180 - 194, Update the
test named “every chat route error code has a sentence of its own” to collect
each emitted code’s cause and assert that the set of causes has size equal to
emitted.length. Keep the existing generic-fallback assertion, but add the
uniqueness check so duplicate sentences among mapped codes fail.

Comment on lines +301 to +311
function readableReason(reason: string | undefined): string | undefined {
const trimmed = reason?.trim();
if (!trimmed) return undefined;
const rendered = /^[a-z][a-z0-9_]*$/.test(trimmed)
// An unmapped bare code has no sentence to show, and inventing one would
// hide the only identifier the reader could search for.
? (WIRE_MAP[trimmed]?.cause ?? trimmed)
: trimmed;
// Bound at the exit, not inside one branch. The code-shaped test matches any
// run of lowercase, so a long token took the other path and escaped the cap.
return rendered.length > 160 ? `${rendered.slice(0, 157)}...` : rendered;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep unknown wire identifiers out of the rendered detail.

Line 307 returns trimmed when a bare reason is not in WIRE_MAP. describeOrigin then displays that identifier to the user. This breaks the documented CS15 behavior and the PR requirement for unmapped codes.

Replace this fallback with safe generic prose. Add a test with an unmapped bare origin.reason.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/src/lib/degradation.ts` around lines 301 - 311, The fallback in
readableReason for unmapped bare identifiers must return safe generic prose
instead of exposing trimmed. Update readableReason while preserving mapped-code
and sentence-like reason handling, then add coverage for an unmapped bare
origin.reason through describeOrigin or the relevant public path, asserting the
identifier is not rendered.

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 improves console degraded-state messaging for chat surfaces by preserving and rendering the upstream failure reason (in addition to door + status), and by mapping previously-unmapped chat route error codes to actionable sentences. It also adds tests to prevent future regressions where new chat error codes silently fall back to the generic unavailable banner.

Changes:

  • Extend DegradationOrigin with reason and render it via a bounded readableReason helper.
  • Expand WIRE_MAP with per-route chat failure codes so chat failures produce specific, actionable sentences.
  • Add vitest coverage to ensure upstream reasons render and chat route codes don’t regress to the generic sentence; update ChatPage to propagate the reason field.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
apps/console/src/lib/degradation.ts Adds origin.reason, introduces readableReason, and expands WIRE_MAP for chat error codes so details include cause context.
apps/console/src/lib/degradation.test.ts Adds tests asserting upstream reasons surface, reasons are bounded, and chat codes don’t fall through to the generic sentence.
apps/console/src/components/chat/ChatPage.tsx Preserves route-provided reason text in ChatFailure and passes it into degradationFor.

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

Comment on lines +181 to +188
const emitted = [
'project_catalog_failed', 'project_write_failed', 'project_select_failed',
'thread_catalog_failed', 'thread_read_failed', 'thread_create_failed',
'thread_update_failed', 'thread_not_found', 'attachment_upload_failed',
'file_required', 'invalid_body', 'tenant_connector_unavailable',
'web_search_requires_principal', 'console_chat_wire_failed',
'web_search_unavailable',
];
'web_search_requires_principal', 'console_chat_wire_failed',
'web_search_unavailable',
];
const generic = degradationFor('a_code_that_is_not_mapped_at_all').cause;
Comment on lines +132 to +143
project_select_failed: {
level: 'unavailable',
cause: 'That project could not be made active.',
actionLabel: 'Retry',
door: 'The chat project catalog',
},
thread_catalog_failed: {
level: 'unavailable',
cause: 'The thread list could not be read.',
actionLabel: 'Retry',
door: 'The chat thread catalog',
},

@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: 469d8a9af6

ℹ️ 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".

Comment on lines +304 to +307
const rendered = /^[a-z][a-z0-9_]*$/.test(trimmed)
// An unmapped bare code has no sentence to show, and inventing one would
// hide the only identifier the reader could search for.
? (WIRE_MAP[trimmed]?.cause ?? trimmed)

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 Keep unmapped wire codes out of the banner

When an inner API returns a newly introduced or otherwise unmapped code as its reason, this fallback returns the identifier verbatim, and describeOrigin appends it directly to the user-facing banner. That contradicts this module's CS15 invariant that unmapped wire codes use generic prose and are reported only in development; fall back to the generic sentence or omit the reason instead of exposing trimmed.

Useful? React with 👍 / 👎.

Comment on lines +182 to +187
'project_catalog_failed', 'project_write_failed', 'project_select_failed',
'thread_catalog_failed', 'thread_read_failed', 'thread_create_failed',
'thread_update_failed', 'thread_not_found', 'attachment_upload_failed',
'file_required', 'invalid_body', 'tenant_connector_unavailable',
'web_search_requires_principal', 'console_chat_wire_failed',
'web_search_unavailable',

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 Include every emitted chat error in the coverage test

The asserted list omits activeProjectId_required, which /api/chat/projects emits at apps/console/src/app/api/chat/projects/route.ts:53 when the PUT body has no active project ID. That code is also absent from WIRE_MAP, so this case still falls through to the generic degradation sentence while the new test claiming to cover every chat route code passes; include this emitted code and its user-facing mapping, or derive the test inputs from a shared exhaustive source.

Useful? React with 👍 / 👎.

@Travis-Gilbert
Travis-Gilbert merged commit 4216f01 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