Skip to content

feat(chat): continue an interrupted turn from the tool call it stopped at - #9189

Open
tamimbinhakim wants to merge 3 commits into
pingdotgg:mainfrom
tamimbinhakim:feat/continue-interrupted-turn
Open

feat(chat): continue an interrupted turn from the tool call it stopped at#9189
tamimbinhakim wants to merge 3 commits into
pingdotgg:mainfrom
tamimbinhakim:feat/continue-interrupted-turn

Conversation

@tamimbinhakim

@tamimbinhakim tamimbinhakim commented Sep 2, 2026

Copy link
Copy Markdown

Problem

When you stop a turn, or it dies with an error, the only way to get the agent moving again is to type another message. Two things are wrong with that: you have to retype, and the agent has no idea which step it was on when it got cut off, so it tends to re-run everything or wander.

None of the five providers can resume a half-finished tool call, so "Continue" has to be a new turn under the hood. This PR makes that turn invisible in the transcript and precise about where to pick up.

What this does

Messages get an optional origin. thread.turn.start accepts message.origin: "continuation". The decider stamps it on the thread.message-sent event, the projection stores it (migration 045 adds a nullable origin column on projection_thread_messages), and the in-memory projector and client reducer carry it through. Nothing else in the pipeline changes: the provider still receives it as a normal user prompt on the resumed session, so it works for Codex, Claude, Cursor, Grok and OpenCode alike.

Continue banner. When the latest turn is interrupted or error, nothing is running and no approval is pending, the composer stack shows "Pick up where it stopped" (or "Pick up after the error") with a Continue button. The client builds the prompt from the thread's work log: the last in-flight tool call of that turn, its command with any leading cd split off into "in <dir>", or the tool label for non-command calls. The words come from one builder in client-runtime so web and mobile send the same prompt. The turn is sent with the thread's current model, runtime mode and interaction mode.

Marker, not bubble. Web renders a continuation message as a thin "Continued from the interrupted step" divider with the timestamp; mobile renders the same divider. No optimistic bubble is pushed.

Reverse state. The banner is dismissable per turn and disappears once a message is sent or a turn is running.

Mobile gets the marker in this PR; the mobile Continue button can follow once the composer banner pattern exists there.

Testing

  • decider.continue.test.ts: origin stamped on the recorded message, absent otherwise.
  • 045_ProjectionThreadMessageOrigin.test.ts: migration adds the nullable column.
  • threadReducer.test.ts: origin kept on the appended message.
  • continuation-prompt.test.ts: prompt wording with command + directory, tool label, and bare fallback; cd splitting.
  • Existing ProjectionThreadMessages, ProjectionPipeline, decider and timeline suites pass.
  • Targeted typecheck: server, web, client-runtime, contracts, mobile.

Related: #9186 (tool rows) gives the interrupted row its visual state; this PR does not depend on it.


Note

Medium Risk
Touches turn-start events, message projection upserts, and a new DB column; incorrect origin handling could mislabel transcripts or drop metadata on streaming completion, but changes are additive and optional for older clients.

Overview
Adds an optional origin: "continuation" on user messages so Continue after an interrupted or errored turn is stored and projected like a normal turn start, but shown in the transcript as a divider instead of a user bubble (web and mobile). Contracts, decider, in-memory projector, client reducer, and projection_thread_messages (migration 045, nullable origin) all carry the field through streaming and upsert paths, preserving origin when later writes omit it.

On web, when the latest turn is interrupted or error and nothing else is blocking work, the composer shows a Continue banner. It builds a shared buildContinuationPrompt (plus splitLeadingCdForPrompt) from the cut-off tool/work-log entry and calls thread.turn.start with that origin. The timeline minimap skips continuation messages. User docs describe the flow.

Medium scope: new user-facing orchestration path and DB column, but backward-compatible optional metadata and targeted tests (decider, migration, reducer, prompt builder).

Reviewed by Cursor Bugbot for commit f6e158d. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add Continue banner to resume interrupted chat turns with origin=continuation

  • Adds OrchestrationMessageOrigin to contracts and propagates the optional origin=continuation through thread.turn.start commands, thread.message-sent events, the decider, projector, persistence layer, and the client thread reducer.
  • Adds migration 045, which adds a nullable origin column to projection_thread_messages; subsequent upserts preserve an existing origin when the incoming row omits one.
  • Adds a shared buildContinuationPrompt helper in client-runtime that produces a reason-specific prompt using the interrupted tool call, command, or directory context and instructs the agent not to repeat completed work.
  • Adds a Continue composer banner in ChatView.tsx for interrupted or errored latest server turns; pressing Continue dispatches a new user turn carrying the generated prompt and origin=continuation.
  • Web and mobile timelines render continuation-origin user messages as a labeled marker with a play icon instead of a user bubble, and the web minimap excludes them.
  • Behavioral Change: the new origin column is nullable and defaults to absent; readers in ProjectionSnapshotQuery.ts and ProjectionThreadMessages.ts are updated, but any out-of-tree consumers of projection_thread_messages must tolerate the new column.

Macroscope summarized f6e158d.

…d at

Stopping a turn, or having it fail, leaves the user retyping a message to
get the agent moving again, and the agent has no idea which step it was
on. None of the providers can resume a half-finished tool call, so
Continue is a new turn under the hood, made invisible and precise.

Messages gain an optional origin. A continuation turn is a normal
thread.turn.start whose message carries origin: continuation; the
decider, projection (new nullable column), in-memory projector and client
reducer pass it through. Web and mobile render such messages as a small
marker instead of a user bubble. The composer shows a Pick up where it
stopped notice when the latest turn is interrupted or errored; Continue
builds a prompt naming the command (with its directory) or file change
that was in flight and sends it with the thread's current model and
modes. The prompt builder lives in client-runtime so both surfaces send
the same words. Works for every provider since it rides the normal
resume path.
@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Sep 2, 2026
Comment thread apps/web/src/components/ChatView.tsx
Comment thread apps/web/src/components/ChatView.tsx

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

Two consistency gaps in the new Continue flow; both are in apps/web and both stem from the continuation message being a real user message that the timeline deliberately hides.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/chat/MessagesTimeline.tsx
Comment thread apps/web/src/components/ChatView.tsx
@macroscopeapp

macroscopeapp Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR introduces a new Continue workflow that starts agent turns from interrupted or failed tool calls and adds cross-cutting UI, orchestration, and persistence behavior. The additive metadata and nullable migration reduce compatibility risk, but the new workflow and potential tool execution are substantial enough to require human review.

You can add or adjust custom eligibility rules. Learn more.

- Continue runs the same optimistic path as a send: sendInFlightRef is
  held, local dispatch begins, the view follows to the end, and the state
  is reset if the turn fails to start, so a second send cannot race it.
- The cut-off step prefers a call that never finished (stopped, failed or
  still in progress), then the last tool call of the turn, so the prompt
  names the right step.
- Continuation messages are excluded from the timeline minimap.
Comment thread apps/web/src/components/ChatView.tsx Outdated

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e785e1e. Configure here.

Comment thread apps/web/src/components/ChatView.tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant