Skip to content

improvement(mothership): abort path race preventing persistence#4647

Merged
icecrasher321 merged 3 commits into
stagingfrom
improvement/copilot-stream
May 17, 2026
Merged

improvement(mothership): abort path race preventing persistence#4647
icecrasher321 merged 3 commits into
stagingfrom
improvement/copilot-stream

Conversation

@icecrasher321
Copy link
Copy Markdown
Collaborator

Summary

Fixes stopped copilot streams so partial assistant responses are persisted reliably even if the abort path clears the active stream marker first.

Type of Change

  • Bug fix

Testing

Tested manually

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel
Copy link
Copy Markdown

vercel Bot commented May 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped May 17, 2026 10:23pm

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented May 17, 2026

PR Summary

Medium Risk
Touches chat persistence/finalization logic and introduces row-locking + new cancellation paths, which could affect chat message ordering and stream-marker clearing under concurrency.

Overview
Ensures stopped / cancelled Copilot streams reliably persist partial assistant output and emit the completed status even when the chat’s conversationId stream marker was cleared before the stop handler runs.

Refactors stop/finalization to use a transactional, FOR UPDATE-locked finalizeAssistantTurn that can optionally operate when the marker is already cleared (streamMarkerPolicy: 'active-or-cleared') and returns a structured outcome used to drive publishing and tracing.

Adds withStoppedContentBlock to consistently append a canonical complete: cancelled block, updates unified chat onComplete to persist cancelled results (instead of delegating solely to /chat/stop), and hardens the lifecycle runner to invoke onComplete(cancelled) even when abort-triggered exceptions occur; tests were expanded accordingly.

Reviewed by Cursor Bugbot for commit 05830df. Configure here.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 17, 2026

Greptile Summary

This PR fixes a race condition where stopping a copilot stream could silently drop partial assistant responses. Previously, the abort-exception catch path called onError (which cleared conversationId), racing with the /chat/stop endpoint's UPDATE that filtered on conversationId = streamId, so whichever ran first could invalidate the other's write.

  • terminal-state.ts wraps the SELECT + UPDATE in a db.transaction with SELECT … FOR UPDATE, adds a streamMarkerPolicy parameter (active-only | active-or-cleared) so the cleared-marker scenario can still own the turn, and returns a structured FinalizeAssistantTurnResult for downstream idempotency decisions.
  • run.ts adds an onCompleteStarted guard and routes throw-after-abort through onComplete(cancelled) instead of onError, forwarding accumulated content and blocks so the finaliser has something to persist.
  • post.ts removes the early return on result.cancelled and instead calls finalizeAssistantTurn with the new policy; route.ts delegates its own inline SELECT/UPDATE to the same finaliser, passing userId for row-level ownership.

Confidence Score: 5/5

Safe to merge; the core race is correctly closed by the FOR UPDATE transaction and the active-or-cleared policy handles both orderings of concurrent writes idempotently.

The transaction + row-lock approach is sound for PostgreSQL, the two writer paths (stop route and cancelled lifecycle) are both serialised through the same finaliser, and the AssistantAlreadyPersisted outcome prevents double-writes. New tests cover the key race scenarios. The only gap is a defence-in-depth omission (no userId on the cancelled path in post.ts), which is not immediately exploitable given session-scoped chatIds.

apps/sim/lib/copilot/chat/post.ts — the buildOnComplete cancelled path omits userId when calling finalizeAssistantTurn, while the stop route passes it; worth aligning for consistency.

Important Files Changed

Filename Overview
apps/sim/lib/copilot/chat/terminal-state.ts Core fix: adds db.transaction + SELECT ... FOR UPDATE row lock so concurrent stop calls are serialised; introduces streamMarkerPolicy to allow writes when the marker was already cleared by a racing finaliser; returns a structured FinalizeAssistantTurnResult instead of void
apps/sim/lib/copilot/request/lifecycle/run.ts Adds onCompleteStarted guard to prevent double-invocation of onComplete; cancelled-path catch block now calls onComplete (not onError) with accumulated partial content, forwarding content blocks and tool call summaries correctly
apps/sim/lib/copilot/chat/post.ts Cancelled path now calls finalizeAssistantTurn with active-or-cleared instead of returning early; userId is not forwarded to finalizeAssistantTurn, asymmetric with the stop route which does pass userId
apps/sim/app/api/copilot/chat/stop/route.ts Delegates to finalizeAssistantTurn with active-or-cleared; correctly passes userId: session.user.id; stop outcome and pub-sub notification logic updated to handle AssistantAlreadyPersisted idempotency
apps/sim/lib/copilot/chat/persisted-message.ts New withStoppedContentBlock helper appends a cancelled completion block to a message, guarding against double-adding one if already present; logic is correct

Sequence Diagram

sequenceDiagram
    participant Client
    participant StopRoute as StopRoute
    participant Lifecycle as Lifecycle
    participant Finalizer as Finalizer
    participant DB as PostgreSQL

    Client->>StopRoute: POST /chat/stop
    Client->>Lifecycle: AbortSignal fires

    Lifecycle->>Lifecycle: runStreamLoop throws
    Lifecycle->>Finalizer: finalizeAssistantTurn active-or-cleared
    Finalizer->>DB: BEGIN SELECT FOR UPDATE
    DB-->>Finalizer: row locked
    Finalizer->>DB: UPDATE append assistant clear marker
    Finalizer-->>Lifecycle: "updated=true AppendedAssistant COMMIT"

    StopRoute->>Finalizer: finalizeAssistantTurn active-or-cleared with userId
    Finalizer->>DB: BEGIN SELECT FOR UPDATE
    DB-->>Finalizer: row assistant already present marker null
    Finalizer-->>StopRoute: "updated=false AssistantAlreadyPersisted"
    StopRoute->>StopRoute: publishStatusChanged completed idempotent
Loading

Reviews (3): Last reviewed commit: "address bugbot comment" | Re-trigger Greptile

Comment thread apps/sim/app/api/copilot/chat/stop/route.ts Outdated
Comment thread apps/sim/app/api/copilot/chat/stop/route.test.ts
@icecrasher321
Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321
Copy link
Copy Markdown
Collaborator Author

bugbot run

Comment thread apps/sim/lib/copilot/request/lifecycle/run.ts Outdated
@icecrasher321
Copy link
Copy Markdown
Collaborator Author

bugbot run

@icecrasher321
Copy link
Copy Markdown
Collaborator Author

@greptile

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 05830df. Configure here.

@icecrasher321 icecrasher321 merged commit 42bbb8a into staging May 17, 2026
14 checks passed
@waleedlatif1 waleedlatif1 deleted the improvement/copilot-stream branch May 18, 2026 20:21
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