Skip to content

fix(orchestration): show follow-up assistant replies after Cursor session resume#3642

Open
davidmashburn wants to merge 3 commits into
pingdotgg:mainfrom
davidmashburn:fix/cursor-resume-assistant-message-order
Open

fix(orchestration): show follow-up assistant replies after Cursor session resume#3642
davidmashburn wants to merge 3 commits into
pingdotgg:mainfrom
davidmashburn:fix/cursor-resume-assistant-message-order

Conversation

@davidmashburn

@davidmashburn davidmashburn commented Jul 2, 2026

Copy link
Copy Markdown

Summary

  • Fixes a thread UI bug where follow-up assistant replies appeared missing after resuming a Cursor session.
  • Root cause: ACP reuses assistant segment IDs (segment:0, segment:1, …) across turns, so T3 projected later replies into the same message_id as turn 1 and kept the original created_at, sorting them above newer user messages.
  • Scopes assistant message IDs by turn (assistant:{turnId}:{baseKey}) and adds merge logic that resets text/created_at when a reused ID starts a new turn.

Changes

  • assistantMessageIds.ts — turn-scoped assistant message ID generation
  • threadMessageProjection.ts — shared merge helper for projection upserts
  • ProviderRuntimeIngestion.ts — use turn-scoped IDs for segments, completions, and diff checkpoints
  • ProjectionPipeline.ts / projector.ts — apply merge helper on thread.message-sent
  • Tests including regression for reused Cursor segment IDs across turns

Test plan

  • Unit tests for assistantMessageIds and threadMessageProjection
  • Updated ProviderRuntimeIngestion expectations for turn-scoped IDs
  • Regression test: resumed session reusing assistant:session-1:segment:0 across two turns creates distinct messages with correct timestamps
  • CI green on apps/server orchestration tests

Made with Cursor


Open in Devin Review

Note

Fix follow-up assistant replies not appearing after Cursor session resume

  • Scopes assistant message IDs by turn in ProviderRuntimeIngestion, so resumed sessions that reuse provider segment IDs produce distinct messages per turn instead of colliding.
  • Introduces threadMessageProjection.ts with canonical turn-aware merge logic: streaming deltas append within the same turn, non-streaming updates replace text unless empty, and a turn change resets text and createdAt.
  • Replaces ad-hoc merge logic in ProjectionPipeline and projector.ts with calls to mergeThreadMessageProjection.
  • Adds assistantMessageIds.ts to centralize ID construction in the format assistant:<turnId>:<baseKey>.
  • Behavioral Change: assistant message IDs are now turn-scoped, so any code relying on stable IDs across resumed turns will see new IDs per turn.

Macroscope summarized d02f77c.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a645fb98-8e09-4326-a0b5-0bc40a5adee0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jul 2, 2026

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.

🟠 High

When the first thread.message-sent event for an attachments-only message arrives (empty text, streaming: false, no existing row), mergeThreadMessageProjection evaluates previousMessage.text on the !previousMessage branch because the condition incoming.streaming || incoming.text.length > 0 is false, so nextText falls through to turnChanged ? "" : previousMessage.text, which dereferences undefined. This throws during projection instead of persisting the message. The guard should also cover the case where there is no previous message and the incoming text is empty.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration/Layers/ProjectionPipeline.ts around line 27:

When the first `thread.message-sent` event for an attachments-only message arrives (empty text, `streaming: false`, no existing row), `mergeThreadMessageProjection` evaluates `previousMessage.text` on the `!previousMessage` branch because the condition `incoming.streaming || incoming.text.length > 0` is false, so `nextText` falls through to `turnChanged ? "" : previousMessage.text`, which dereferences `undefined`. This throws during projection instead of persisting the message. The guard should also cover the case where there is no previous message and the incoming text is empty.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks — same issue flagged by Bugbot, and I agree it was worth fixing.

The failure mode is: no existing projection row, incoming thread.message-sent has empty text and streaming: false (attachments-only), and we incorrectly read previousMessage.text because turnChanged is false when previousMessage is undefined.

Addressed in 8c2d214 with optional chaining plus a unit test. The core Cursor resume behavior is unchanged.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

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

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3a401bf. Configure here.

Comment thread apps/server/src/orchestration/threadMessageProjection.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

2 blocking correctness issues found. This PR modifies runtime behavior in the orchestration pipeline (message ID generation and projection merging). Additionally, there are unresolved HIGH severity review comments identifying a potential null dereference issue that could cause exceptions during message projection.

You can customize Macroscope's approvability policy. Learn more.

@davidmashburn

Copy link
Copy Markdown
Author

Reviewed the bot feedback on this PR:

  • Cursor Bugbot and Macroscope both flagged the same edge case in mergeThreadMessageProjection: first upsert with empty non-streaming text could dereference previousMessage when undefined (attachments-only messages).
  • CodeRabbit skipped auto-review on this repo (no actionable findings).

I pushed 8c2d214 to address the empty-first-upsert case with (previousMessage?.text ?? "") and added a regression test. The Cursor session-resume fix itself is unchanged.

Happy to rebase onto latest main if helpful before merge.

return previousMessage.turnId !== nextTurnId;
}

export function mergeThreadMessageProjection(

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.

🟡 Medium orchestration/threadMessageProjection.ts:27

When an assistant message with attachments from an earlier turn is reused for a new turn, mergeThreadMessageProjection resets text and createdAt but does not signal the turn change, so projection callers that preserve previousMessage.attachments when the incoming event omits attachments keep stale attachments from the previous turn. The follow-up reply incorrectly shows files or images that belong to the earlier turn.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration/threadMessageProjection.ts around line 27:

When an assistant message with attachments from an earlier turn is reused for a new turn, `mergeThreadMessageProjection` resets `text` and `createdAt` but does not signal the turn change, so projection callers that preserve `previousMessage.attachments` when the incoming event omits attachments keep stale attachments from the previous turn. The follow-up reply incorrectly shows files or images that belong to the earlier turn.

davidmashburn and others added 2 commits July 7, 2026 18:11
…sion resume

When Cursor resumes a session, ACP reuses assistant segment IDs across turns.
Scope assistant message IDs by turn and reset projection state on turn change
so follow-up replies appear below the latest user message instead of updating
stale segments sorted above it.

Co-authored-by: Cursor <cursoragent@cursor.com>
Guard mergeThreadMessageProjection when there is no previous row and the
incoming upsert has empty non-streaming text, so attachments-only first
events persist instead of dereferencing undefined.

Co-authored-by: Cursor <cursoragent@cursor.com>
@davidmashburn

Copy link
Copy Markdown
Author

Rebased onto latest main (dad088976). Branch now has 2 commits:

  1. Cursor session-resume fix (turn-scoped assistant message IDs)
  2. Empty first-upsert guard in mergeThreadMessageProjection

No rebase conflicts.

@davidmashburn

Copy link
Copy Markdown
Author

@juliusmarminge — small orchestration fix when you have a minute.

#3642 fixes a Cursor-specific resume bug: after reconnecting a Cursor session, follow-up assistant replies were updating stale message segments above the latest user turn instead of appearing as new replies below it. Root cause is ACP reusing assistant segment IDs across turns; the fix turn-scopes those IDs and resets projection state on turn change. Macroscope/Bugbot also caught an edge case on the first empty non-streaming upsert — addressed in the second commit.

I also have a couple of other features that might be interesting if you're up for it:

No pressure on any of it — just thought I'd share in case any of it's useful. Thanks for maintaining this; really enjoying the project.

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