Skip to content

feat(ai-chat): close the error and abort edge-case gaps - #791

Merged
urjitc merged 11 commits into
mainfrom
worktree-chat-error-edge-cases
Aug 16, 2026
Merged

feat(ai-chat): close the error and abort edge-case gaps#791
urjitc merged 11 commits into
mainfrom
worktree-chat-error-edge-cases

Conversation

@urjitc

@urjitc urjitc commented Aug 16, 2026

Copy link
Copy Markdown
Member

Problem

When a chat run fails or is stopped, the UI often left the user with nothing actionable — and a deep dive into the framework (cloudflare/agents + Think source, their issue tracker, and this repo's local patches) surfaced deeper problems: deleting a thread while viewing it leaves a zombie transcript that silently resurrects the thread, switching threads always suspends on a redundant fetch, and regeneration secretly behaves like "continue" instead of a fresh answer.

Changes

Error/abort affordances

  • Stopped-run notice — a run aborted before the first token shows an inline notice with Try again; mid-stream stops keep their partial reply and stay quiet.
  • Start new chat on overflow — the context-overflow banner gets a real "Start new chat" button that creates and selects a fresh thread.
  • Stored error detail in the banner — unclassified failures show the persisted server message (usage-limit reset date, recovery outcome) instead of burying it in a tooltip.
  • Refresh action on the connection banner; toast on new-chat failure; hide unrendered data-* parts from old transcripts.

Thread lifecycle (verified against framework source)

  • Clean deletes — the thread's socket lives on the directory DO, so deleting never closes it; a mounted view kept a zombie transcript whose next frame resurrected the deleted thread (upstream deleteSubAgent doesn't stick while a client is still connected cloudflare/agents#2003, fix PR #2024 open). Now: switch to a surviving thread before deleting; when none survives, remount so the view reconnects to a fresh default thread.
  • Instant thread switching — an in-session settled-transcript cache served via a pre-fulfilled thenable (React 19 unwraps synchronously — zero fallback frames); the server's connect-time broadcast replaces the seed with authoritative state. Streaming threads are never cached (resume handshake is their fast path; a seeded partial risks a duplicate bubble on id drift).

Regeneration correctness

  • Regenerate from the last user message — Think assembles the prompt from the stored path, so regenerated turns carried the old answer plus a "continue your previous response" instruction (worst on retries of partial failed responses). Fixed app-side: flag regenerate turns in the body, trim the prompt to the last user message in beforeTurn (non-continuation only). Workaround for Think regeneration includes the previous response in the model prompt cloudflare/agents#2028; remove once upstream #2038 ships.

Upstream

Deliberately skipped

A deploy version-skew "refresh to update" toast remains the biggest untouched gap; it needs build/deploy plumbing that deserves its own PR.

Verification

tsc --noEmit clean; all 62 ai-chat vitest tests pass. Framework claims verified against agents@0.19.0 / @cloudflare/think@0.15.0 dist (with this repo's patches applied) and the upstream repo at main.

https://claude.ai/code/session_01S9YxTH6xRfC6bq3vbgmTQD

urjitc added 5 commits August 15, 2026 20:19
Old transcripts can carry data parts from retired features. Counting them
as displayable made such messages render as a blank bubble instead of
falling through to the empty-response affordance.

Claude-Session: https://claude.ai/code/session_01S9YxTH6xRfC6bq3vbgmTQD
Stopping a response while it was still pending left the user message as
the transcript tail with no indicator and no way to retry short of
retyping. Surface an inline notice with the existing Try again action;
mid-stream stops keep their partial reply and stay quiet.

Claude-Session: https://claude.ai/code/session_01S9YxTH6xRfC6bq3vbgmTQD
Creation failures only hit the console, so the New chat button appeared
to do nothing. Match the delete-thread failure toast.

Claude-Session: https://claude.ai/code/session_01S9YxTH6xRfC6bq3vbgmTQD
The overflow banner told users to start a new chat but offered no way to
do it. Add the action next to Try again; it creates the thread and
carries the failed prompt over as a queued direct prompt (text only).

Claude-Session: https://claude.ai/code/session_01S9YxTH6xRfC6bq3vbgmTQD
Unclassified failures rendered only generic copy while the real reason
(usage-limit reset date, recovery outcome) sat in a thread-list tooltip;
surface it as a detail line under the banner headline. The terminal
connection banner also told users to refresh with no way to do it — give
it a button.

Claude-Session: https://claude.ai/code/session_01S9YxTH6xRfC6bq3vbgmTQD
@cursor

cursor Bot commented Aug 16, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 5 minutes

Limit details: You’ve used all 1 included review currently available under your plan.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

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: 8f5c99fa-45d7-4f2e-a5f5-f2336381d039

📥 Commits

Reviewing files that changed from the base of the PR and between 2562d16 and 00b8224.

📒 Files selected for processing (4)
  • src/features/workspaces/ai/ai-thread.ts
  • src/features/workspaces/components/AiChatPanel.tsx
  • src/features/workspaces/components/ai-chat/useAiChatPanelController.ts
  • src/features/workspaces/components/ai-chat/useWorkspaceAiChat.ts

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 15d2faa1-b4c6-4114-9315-145963140ae7

📥 Commits

Reviewing files that changed from the base of the PR and between bf2c990 and 2562d16.

📒 Files selected for processing (7)
  • src/features/workspaces/components/AiChatPanel.tsx
  • src/features/workspaces/components/ai-chat/AiChatMessageList.tsx
  • src/features/workspaces/components/ai-chat/AiChatThreadView.tsx
  • src/features/workspaces/components/ai-chat/ai-chat-display-state.ts
  • src/features/workspaces/components/ai-chat/ai-chat-error-state.test.ts
  • src/features/workspaces/components/ai-chat/ai-chat-error-state.ts
  • src/features/workspaces/components/ai-chat/useAiChatPanelController.ts

📝 Walkthrough

Walkthrough

The PR adds aborted-run error states, preserves server error details, and updates AI chat recovery actions. It wires context-overflow new-chat handling through the chat panel, excludes data-* parts from visible content, and shows new-chat creation failures in an error toast.

Changes

AI chat error recovery

Layer / File(s) Summary
Error state derivation and validation
src/features/workspaces/components/ai-chat/ai-chat-error-state.ts, src/features/workspaces/components/ai-chat/AiChatThreadView.tsx, src/features/workspaces/components/ai-chat/ai-chat-error-state.test.ts
The error-state helper preserves server messages and returns aborted when an aborted run ends after a user message. Tests cover pre-token and mid-stream aborts.
Error rendering and recovery actions
src/features/workspaces/components/ai-chat/AiChatMessageList.tsx, src/features/workspaces/components/ai-chat/AiChatThreadView.tsx, src/features/workspaces/components/AiChatPanel.tsx
The message list renders server details, refreshes connection failures, displays aborted responses, and exposes a context-overflow “Start new chat” action.
Transcript visibility and failure reporting
src/features/workspaces/components/ai-chat/ai-chat-display-state.ts, src/features/workspaces/components/ai-chat/useAiChatPanelController.ts
data-* parts no longer count as visible content. New-chat creation failures now display an error toast.

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

Merge Risk: ⚪ Minimal · up to 2562d

This change improves user-facing handling for stopped, failed, and disconnected chat runs without any actionable merge-blocking risk remaining.

Sequence Diagram(s)

sequenceDiagram
  participant AiChatThreadView
  participant deriveAiChatAssistantErrorState
  participant AiChatMessageList
  participant AiChatAssistantError
  participant useAiChatPanelController
  AiChatThreadView->>deriveAiChatAssistantErrorState: Pass latest message role
  deriveAiChatAssistantErrorState-->>AiChatThreadView: Return assistant error state
  AiChatThreadView->>AiChatMessageList: Pass error state and onStartNewChat
  AiChatMessageList->>AiChatAssistantError: Render error actions
  AiChatAssistantError->>useAiChatPanelController: Invoke onStartNewChat
Loading

Possibly related PRs

Suggested reviewers: cubic-dev-ai

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary changes to AI chat error and abort edge cases.
✨ 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 worktree-chat-error-edge-cases

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.

@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit 00b8224.

@cubic-dev-ai cubic-dev-ai Bot 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.

2 issues found across 8 files

You’re at about 95% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/features/workspaces/components/ai-chat/useAiChatPanelController.ts">

<violation number="1" location="src/features/workspaces/components/ai-chat/useAiChatPanelController.ts:56">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The `handleNewChat` changes add two new behaviors — a `carryPrompt` path that queues a direct prompt after thread creation, and a toast error path on creation failure. Neither is exercised by tests, and there are no tests for `useAiChatPanelController` at all. Add regression assertions covering these paths so the behavior-change is validated.</violation>
</file>

<file name="src/features/workspaces/components/ai-chat/AiChatMessageList.tsx">

<violation number="1" location="src/features/workspaces/components/ai-chat/AiChatMessageList.tsx:307">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

This behavior-change diff introduces several new error-rendering branches in the component (aborted notice, error-detail line, Refresh page button, Start new chat button), but none are exercised by tests. The existing `ai-chat-error-state.test.ts` validates the derivation of the error state objects, not the UI rendering in this file. Add component-level tests that assert the correct buttons and messages render for each `errorState.kind` and `classification` combination.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic


try {
const thread = await createThread();
if (carryPrompt) {

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.

P2: Custom agent: Flag AI Slop and Fabricated Changes

The handleNewChat changes add two new behaviors — a carryPrompt path that queues a direct prompt after thread creation, and a toast error path on creation failure. Neither is exercised by tests, and there are no tests for useAiChatPanelController at all. Add regression assertions covering these paths so the behavior-change is validated.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/ai-chat/useAiChatPanelController.ts, line 56:

<comment>The `handleNewChat` changes add two new behaviors — a `carryPrompt` path that queues a direct prompt after thread creation, and a toast error path on creation failure. Neither is exercised by tests, and there are no tests for `useAiChatPanelController` at all. Add regression assertions covering these paths so the behavior-change is validated.</comment>

<file context>
@@ -45,16 +46,19 @@ export function useAiChatPanelController({ workspaceId }: UseAiChatPanelControll
 
 		try {
 			const thread = await createThread();
+			if (carryPrompt) {
+				useWorkspaceAiComposerDraftStore.getState().queueDirectPrompt(thread.id, carryPrompt);
+			}
</file context>

}) {
// An overflowed chat can rarely be retried into success, so the escape
// hatch the copy suggests gets its own action.
const canStartNewChat =

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.

P2: Custom agent: Flag AI Slop and Fabricated Changes

This behavior-change diff introduces several new error-rendering branches in the component (aborted notice, error-detail line, Refresh page button, Start new chat button), but none are exercised by tests. The existing ai-chat-error-state.test.ts validates the derivation of the error state objects, not the UI rendering in this file. Add component-level tests that assert the correct buttons and messages render for each errorState.kind and classification combination.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/ai-chat/AiChatMessageList.tsx, line 307:

<comment>This behavior-change diff introduces several new error-rendering branches in the component (aborted notice, error-detail line, Refresh page button, Start new chat button), but none are exercised by tests. The existing `ai-chat-error-state.test.ts` validates the derivation of the error state objects, not the UI rendering in this file. Add component-level tests that assert the correct buttons and messages render for each `errorState.kind` and `classification` combination.</comment>

<file context>
@@ -284,12 +294,25 @@ function AiChatAssistantError({
 }) {
+	// An overflowed chat can rarely be retried into success, so the escape
+	// hatch the copy suggests gets its own action.
+	const canStartNewChat =
+		errorState.kind === "assistant" &&
+		errorState.classification === "context_overflow" &&
</file context>

The overflow action now just creates and selects a fresh thread; the
queued-direct-prompt plumbing wasn't worth its wiring.

Claude-Session: https://claude.ai/code/session_01S9YxTH6xRfC6bq3vbgmTQD
@urjitc
urjitc force-pushed the worktree-chat-error-edge-cases branch from 9931e73 to 2562d16 Compare August 16, 2026 00:31
urjitc added 3 commits August 15, 2026 20:57
Switching threads suspended on the /get-messages fetch every time, even
for a thread visited seconds ago. Serve the last settled transcript from
an in-session cache via a pre-fulfilled thenable (React 19 unwraps it
synchronously, so no fallback frame); the server's connect-time broadcast
replaces it with authoritative state one round trip later. Streaming
threads are never cached — the resume handshake is their fast path, and a
seeded partial risks a duplicate bubble on message-id drift.

Claude-Session: https://claude.ai/code/session_01S9YxTH6xRfC6bq3vbgmTQD
Deleting a thread never closes the viewer's socket (it lives on the
directory DO), so a mounted view kept a zombie transcript whose next
frame silently resurrected the deleted thread (cloudflare/agents#2003).
Switch to the most recent surviving thread before deleting; when none
survives, remount the view after the delete so it reconnects to a fresh
default thread. Also evict the deleted thread's transcript cache.

Claude-Session: https://claude.ai/code/session_01S9YxTH6xRfC6bq3vbgmTQD
Think assembles the model prompt from the stored path, so a regenerated
turn still carried the old assistant answer plus a provider-prefill
"continue your previous response" instruction — regenerate behaved like
continue, worst on retries of partial failed responses. Flag regenerate
turns in the request body and trim the prompt back to the last user
message in beforeTurn (non-continuation turns only, so tool round-trips
keep their in-flight assistant). Branch storage was already correct.
Workaround for cloudflare/agents#2028; remove once #2038 ships.

Claude-Session: https://claude.ai/code/session_01S9YxTH6xRfC6bq3vbgmTQD

@cubic-dev-ai cubic-dev-ai Bot 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.

1 issue found across 4 files (changes from recent commits).

You’re at about 95% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/features/workspaces/components/ai-chat/useAiChatPanelController.ts">

<violation number="1" location="src/features/workspaces/components/ai-chat/useAiChatPanelController.ts:73">
P1: When deleting the sole active default thread, `selectThread(undefined)` still resolves to the same default ID, so the view stays mounted during deletion. Unmount or disable the thread view before issuing this delete, then remount a fresh default thread after success.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

const survivorId = wasActive ? threads.find((thread) => thread.id !== threadId)?.id : undefined;

if (wasActive) {
selectThread(survivorId);

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.

P1: When deleting the sole active default thread, selectThread(undefined) still resolves to the same default ID, so the view stays mounted during deletion. Unmount or disable the thread view before issuing this delete, then remount a fresh default thread after success.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/ai-chat/useAiChatPanelController.ts, line 73:

<comment>When deleting the sole active default thread, `selectThread(undefined)` still resolves to the same default ID, so the view stays mounted during deletion. Unmount or disable the thread view before issuing this delete, then remount a fresh default thread after success.</comment>

<file context>
@@ -59,19 +61,33 @@ export function useAiChatPanelController({ workspaceId }: UseAiChatPanelControll
+		const survivorId = wasActive ? threads.find((thread) => thread.id !== threadId)?.id : undefined;
+
+		if (wasActive) {
+			selectThread(survivorId);
+		}
+
</file context>

Comment thread src/features/workspaces/components/ai-chat/useWorkspaceAiChat.ts
Comment thread src/features/workspaces/components/ai-chat/useAiChatPanelController.ts Outdated
Cap the in-session transcript cache at 8 threads (LRU via Map order) so
long sessions don't retain every visited transcript. On a failed delete,
keep whatever thread the user is on instead of reselecting the errored
one — it's still in the list and the toast explains.

Claude-Session: https://claude.ai/code/session_01S9YxTH6xRfC6bq3vbgmTQD
@urjitc

urjitc commented Aug 16, 2026

Copy link
Copy Markdown
Member Author

Review triage: fixed the unbounded transcript cache (now LRU-capped at 8 threads) and removed the failed-delete reselect instead of guarding it. Not addressing: the sole-default-thread delete race needs a send within the delete RPC window and the post-delete remount heals it (root fix is upstream cloudflare/agents#2024); the carry-prompt test comment refers to code already removed; component-render tests for the new banner branches would only assert trivial conditionals — the error-state derivation they render is unit-tested in ai-chat-error-state.test.ts.

@cubic-dev-ai cubic-dev-ai Bot 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.

1 issue found across 2 files (changes from recent commits).

You’re at about 95% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/features/workspaces/components/ai-chat/useWorkspaceAiChat.ts">

<violation number="1" location="src/features/workspaces/components/ai-chat/useWorkspaceAiChat.ts:38">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The new `cacheTranscript` LRU eviction logic is untested. Add at least a regression test asserting that the transcript cache caps at `TRANSCRIPT_CACHE_MAX_THREADS`, evicts the oldest entry when over capacity, and refreshes recency on re-insertion.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

const TRANSCRIPT_CACHE_MAX_THREADS = 8;
const transcriptCache = new Map<string, AiChatMessage[]>();

function cacheTranscript(threadId: string, messages: AiChatMessage[]) {

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.

P2: Custom agent: Flag AI Slop and Fabricated Changes

The new cacheTranscript LRU eviction logic is untested. Add at least a regression test asserting that the transcript cache caps at TRANSCRIPT_CACHE_MAX_THREADS, evicts the oldest entry when over capacity, and refreshes recency on re-insertion.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/ai-chat/useWorkspaceAiChat.ts, line 38:

<comment>The new `cacheTranscript` LRU eviction logic is untested. Add at least a regression test asserting that the transcript cache caps at `TRANSCRIPT_CACHE_MAX_THREADS`, evicts the oldest entry when over capacity, and refreshes recency on re-insertion.</comment>

<file context>
@@ -30,8 +30,22 @@ const AI_CHAT_RENDER_THROTTLE_MS = 100;
+const TRANSCRIPT_CACHE_MAX_THREADS = 8;
 const transcriptCache = new Map<string, AiChatMessage[]>();
 
+function cacheTranscript(threadId: string, messages: AiChatMessage[]) {
+	transcriptCache.delete(threadId);
+	transcriptCache.set(threadId, messages);
</file context>

@urjitc
urjitc merged commit 3e81152 into main Aug 16, 2026
11 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Dev Board Aug 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant