Skip to content

Fix plan mode thread state desync#1186

Open
jamesx0416 wants to merge 1 commit intopingdotgg:mainfrom
jamesx0416:t3code/fix-plan-mode-thread-state
Open

Fix plan mode thread state desync#1186
jamesx0416 wants to merge 1 commit intopingdotgg:mainfrom
jamesx0416:t3code/fix-plan-mode-thread-state

Conversation

@jamesx0416
Copy link
Contributor

@jamesx0416 jamesx0416 commented Mar 18, 2026

What Changed

  • Mark projected thread sessions as starting as soon as thread.turn-start-requested is emitted so threads stop reading as completed during provider handoff.
  • Clear the composer send lock when the UI observes a latest-turn update after send start, even if the intermediate running state was never observed.
  • Add regression coverage for the projection path, session logic, and the browser flow that previously required clicking away and back into the thread.
  • Verified with bun fmt, bun lint, bun typecheck, plus targeted web and server Vitest coverage.

Why

Plan mode could leave an individual thread in a state-sync hole between projected session status and local composer state. In that case the thread row could still show the previous completed state while the next turn was starting, and once the turn finished the composer could remain artificially locked until the thread view remounted.

This change fixes both sides of that race. The server now projects a starting session immediately on turn request, and the web app now clears its local send latch as soon as it observes the turn that started after the current send.

Some of my threads that were running when my macbook crashed exhibit this behaviour.

UI Changes

None. This is a state-management and interaction fix only.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Note

Fix plan mode thread state desync by updating session to 'starting' on turn-start-requested

  • When a thread.turn-start-requested event is received, the projector and projection pipeline now immediately upsert a session row with status: 'starting', runtimeMode, and providerName before any thread.session-set event arrives.
  • ChatView exits the sending phase (re-enabling the send button) when any turn observation (requestedAt, startedAt, or completedAt) occurs at or after sendStartedAt, covering turns that complete without an observed running phase.
  • A new hasLatestTurnObservationSince utility in session-logic.ts detects whether the latest turn has any observation at or after a given timestamp.
  • Behavioral Change: The send button now unlocks as soon as a turn is observed rather than only when the session reaches the running phase.
📊 Macroscope summarized 2b0ffab. 8 files reviewed, 1 issue evaluated, 0 issues filtered, 1 comment posted

🗂️ Filtered Issues

- project `thread.turn-start-requested` into thread session state (`starting`) in server pipeline/read model
- add latest-turn timestamp observation guard so ChatView clears pending send even if `running` was never observed
- extend session-logic and projection tests for restart and fast-complete plan-mode flows
@github-actions github-actions bot added size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Mar 18, 2026
@coderabbitai
Copy link

coderabbitai bot commented Mar 18, 2026

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: 5fe0d9b9-1821-4a12-ba98-d1e1af40979f

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
📝 Coding Plan
  • Generate coding plan for human review comments

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 and usage tips.

Tip

You can disable the changed files summary in the walkthrough.

Disable the reviews.changed_files_summary setting to disable the changed files summary in the walkthrough.

Copy link
Contributor

Choose a reason for hiding this comment

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

🟢 Low

it("keeps projector forward-compatible for unhandled event types", async () => {

The test "keeps projector forward-compatible for unhandled event types" at line 125 uses thread.turn-start-requested, which now has a handler (added in this diff). The test passes only because the thread doesn't exist, causing the handler to return nextBase unchanged—not because it falls through to the default case. The test description is now misleading and no longer validates forward-compatibility behavior. Consider using an actually unhandled event type or updating the test description to reflect what it actually verifies.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/orchestration/projector.test.ts around line 125:

The test "keeps projector forward-compatible for unhandled event types" at line 125 uses `thread.turn-start-requested`, which now has a handler (added in this diff). The test passes only because the thread doesn't exist, causing the handler to return `nextBase` unchanged—not because it falls through to the `default` case. The test description is now misleading and no longer validates forward-compatibility behavior. Consider using an actually unhandled event type or updating the test description to reflect what it actually verifies.

Evidence trail:
- Test at apps/server/src/orchestration/projector.test.ts:125-151 - uses `thread.turn-start-requested` with an empty model, expects threads to remain empty
- New handler added at apps/server/src/orchestration/projector.ts:394-424 (see git_diff MERGE_BASE..REVIEWED_COMMIT) - contains `if (!thread) { return nextBase; }` early return
- Default case at apps/server/src/orchestration/projector.ts:657 - `default: return Effect.succeed(nextBase);` is the actual forward-compatibility mechanism

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fix it for me

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:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant