Skip to content

fix(desktop): preserve live turns across session refreshes - #3189

Merged
M4n5ter merged 1 commit into
mainfrom
fix/desktop-live-turn-reconcile-race
Aug 18, 2026
Merged

fix(desktop): preserve live turns across session refreshes#3189
M4n5ter merged 1 commit into
mainfrom
fix/desktop-live-turn-reconcile-race

Conversation

@M4n5ter

@M4n5ter M4n5ter commented Aug 18, 2026

Copy link
Copy Markdown
Member

Summary

  • bind transient settlement to the accepted authoritative session-list read and the exact live projections observed before that read began
  • clear only an unchanged per-session projection with an identity compare-and-swap, so a concurrent confirmation, replacement, or content advance cannot be retired by an older catalog snapshot
  • merge observer-owned nonterminal root Turn identities into Desktop session-list summaries without persisting them or changing Runtime Host wire data

Fixes #3177

Root cause

Settlement previously combined two independently versioned inputs: a React sessions state snapshot and whatever live-Turn projection happened to be current when an effect ran. Changing the effect phase could narrow one scheduling window, but it could not establish causality. A list result could already be queued, a sessions:changed notification could then confirm or replace the Turn, and React could commit the older list afterward; reconciliation would treat that older catalog state as evidence about the newer projection and clear the Stop witness.

Runtime Host-backed Desktop lists also omitted the observer's current running Turn identity, so later catalog refreshes could not preserve a live Turn when the durable header lagged or was stale.

Design

createSessionListRefresher now captures renderer context immediately before each authority read and passes it only with the accepted generation. Settlement runs at that acceptance boundary, before the list enters React state. The state controller conditionally clears a projection only when it is still the same object captured for that read. Superseded responses discard both their catalog result and their context, while a trailing read captures a fresh context.

This removes the React effect and adds no persisted state, protocol field, or renderer epoch. SessionSummary.runningTurnIds remains an ephemeral list projection sourced from the existing Runtime Host observer.

Verification

  • compiled Desktop test suite — 904/904 passed
  • focused causal reconciliation, request-context, and Runtime Host observer tests — 45/45 passed
  • npm run lint
  • npm run format:check
  • npm run typecheck
  • npm --workspace @maka/desktop run build
  • npx knip --workspace apps/desktop
  • npx knip --workspace packages/ui
  • independent correctness and design reviews found no actionable findings
  • scoped simplification audit found no deletion candidate, deletion probe, or architecture decision gate

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: OpenAI Codex investigated the race, implemented the fix, and added regression coverage under M4n5ter's direction and review. M4n5ter accepts responsibility for the result.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 63472916-3241-4eb9-90e6-095c1e6b407b

📥 Commits

Reviewing files that changed from the base of the PR and between ce6a90d and b88b41b.

📒 Files selected for processing (8)
  • apps/desktop/src/main/__tests__/session-read-state.test.ts
  • apps/desktop/src/main/__tests__/settled-session-transient-reconcile.test.ts
  • apps/desktop/src/renderer/app-shell-session-ui-state.ts
  • apps/desktop/src/renderer/app-shell.tsx
  • apps/desktop/src/renderer/session-read-state.ts
  • apps/desktop/src/renderer/settled-session-transients.ts
  • apps/desktop/src/renderer/use-app-shell-session-list.ts
  • apps/desktop/src/renderer/use-app-shell-session-workspace.ts
💤 Files with no reviewable changes (1)
  • apps/desktop/src/renderer/app-shell.tsx

Included review availability: Your plan includes up to 3 reviews per rolling hour; 0 remain after this review.


📝 Walkthrough

What this PR solves

This PR preserves the active Desktop Turn when a session refresh follows interruption, replacement, or conversation remount. It prevents an older session snapshot from clearing a newer confirmed Turn. It also exposes observed nonterminal root Turns in Desktop session-list summaries so the Stop control remains available.

Source of truth

The PR extends the existing sources of truth. The runtime observer supplies live Turn identity, and the session catalog remains the session-list authority. runningTurnIds is derived transiently and is not persisted or added to Runtime Host wire data. Renderer reconciliation uses captured live projections and authoritative refresh results.

The PR does not create a separate persisted state path.

Scope and complexity

The solution is coherent with the race condition:

  • Reconciliation now runs during session-list commit, after React layout state is available.
  • Cleanup checks the expected LiveTurnProjection before clearing state.
  • Refreshes capture renderer-owned request context to prevent stale responses from applying state.
  • Runtime Host session summaries include observed running root Turn IDs only when active.
  • The previous session-status cleanup effect and its wiring were removed.

No further deletion is evident without weakening the new regression coverage. The removed effect and cleanup path are replaced by refresh-time reconciliation, which is required to avoid stale cleanup.

Complexity delta

Added:

  • One transient observer-derived runningTurnIds projection.
  • One renderer request-context type and capture/commit branch.
  • Projection-matching cleanup logic.
  • Internal method and hook contract changes.
  • Regression tests for refresh races and running Turn projection.

Removed:

  • The session-status-based useSettledSessionTransientReconcile effect.
  • Its app-shell wiring and direct cleanup exposure.
  • Cleanup that could act without matching the current projection.
  • The need for session summaries to arbitrate Desktop live streaming state.

The PR adds a small amount of explicit state and branching, but removes a competing cleanup path. Total maintenance complexity decreases or remains justified by the race-specific behavior and focused tests.

Validation

The reported validation includes Desktop tests, focused and full E2E runs, lint, formatting, build, typecheck, and Knip checks. Direct evidence for the final status of these checks is not available in the supplied diff context.

Review-relevant risks

The diff affects user-visible Desktop live Turn behavior and the Stop-control flow. Material changes in this area require independent human review under repository policy.

The diff changes internal TypeScript interfaces and hook contracts, including RuntimeHostSessionCatalogIpcDeps, SessionListRefresherOptions, and app-shell session hooks. Material contract changes require independent human review under repository policy.

No security, licensing, release, or governance effect was identified in the current diff.

The person performing the merge must review the final diff. A maintainer makes the final determination.

Walkthrough

Changes

The Runtime Host now projects active root turn IDs into session listings. Renderer session refreshes capture live-turn context before authority reads and conditionally clear settled transient state. The previous shell-level reconciliation effect was removed.

Live turn projection

Layer / File(s) Summary
Runtime running-turn projection
apps/desktop/src/main/runtime-host-session-observer.ts, apps/desktop/src/main/runtime-host-session-catalog-ipc-main.ts, apps/desktop/src/main/runtime-host-desktop-candidate.ts, apps/desktop/src/main/__tests__/runtime-host-session-catalog-running-turns.test.ts, packages/ui/src/session-history-list.tsx
Session listings now include non-terminal root turn IDs when active. Tests cover running and idle projections.
Refresh context and transient reconciliation
apps/desktop/src/renderer/session-read-state.ts, apps/desktop/src/renderer/settled-session-transients.ts, apps/desktop/src/renderer/app-shell-session-ui-state.ts, apps/desktop/src/renderer/use-app-shell-session-list.ts, apps/desktop/src/renderer/use-app-shell-session-workspace.ts, apps/desktop/src/main/__tests__/session-read-state.test.ts, apps/desktop/src/main/__tests__/settled-session-transient-reconcile.test.ts
Refreshes preserve context captured before asynchronous reads. Reconciliation clears state only when the expected live-turn projection remains current.
Shell reconciliation wiring removal
apps/desktop/src/renderer/app-shell-effects.ts, apps/desktop/src/renderer/app-shell.tsx
The former settled-session reconciliation hook and its shell invocation were removed.

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

Merge Risk: ⚪ Minimal · up to b88b4

The change preserves live turns across session refreshes and is supported by passing tests and validation checks; no actionable merge-blocking risk remains.

Sequence Diagram(s)

sequenceDiagram
  participant useAppShellSessionList
  participant createSessionListRefresher
  participant SessionCatalog
  participant reconcileSettledSessionTransients
  participant clearTurnTransientStateIfCurrent
  useAppShellSessionList->>createSessionListRefresher: capture live-turn request context
  createSessionListRefresher->>SessionCatalog: request session list
  SessionCatalog-->>createSessionListRefresher: return session summaries
  createSessionListRefresher->>reconcileSettledSessionTransients: reconcile captured projections
  reconcileSettledSessionTransients->>clearTurnTransientStateIfCurrent: clear matching settled projection
  createSessionListRefresher->>useAppShellSessionList: commit sessions with captured context
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address issue #3177 by preserving active Turn identity across session refreshes and adding targeted regression coverage.
Out of Scope Changes check ✅ Passed The tests, session-list changes, transient reconciliation, and documentation updates directly support the linked issue and PR objectives.
Ai Use Disclosure ✅ Passed The PR selects generative tooling, names OpenAI Codex and its implementation/testing scope, and its sole introduced commit has the valid trailer Generated-by: Codex.
Title check ✅ Passed The title clearly summarizes the primary change: preserving live turns across Desktop session refreshes.
Description check ✅ Passed The description includes the required summary, issue reference, verification, AI-use selection, and checklist with concrete implementation details.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/desktop-live-turn-reconcile-race

Comment @coderabbitai help to get the list of available commands.

@M4n5ter
M4n5ter marked this pull request as ready for review August 18, 2026 05:33
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Preserve live turns across Desktop session refreshes

🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Reconcile session transients during layout to prevent stale snapshots clearing confirmed Turns.
• Project observer-owned running root Turn IDs into renderer session-list summaries.
• Add regressions for reconciliation timing and Runtime Host session projection.
Diagram

sequenceDiagram
participant RH as Runtime Host
participant OBS as Session Observer
participant IPC as Catalog IPC
participant SHELL as Desktop Shell
participant REC as Layout Reconciler
participant STORE as Live Turn Store
RH->>OBS: Publish root Turn
SHELL->>IPC: Refresh sessions
IPC->>RH: Read catalog
RH-->>IPC: Session snapshots
IPC->>OBS: Read running IDs
OBS-->>IPC: Nonterminal Turn ID
IPC-->>SHELL: Enriched summaries
SHELL->>REC: Commit session list
REC->>STORE: Reconcile in layout
Note over REC,STORE: Before passive effects
OBS-->>STORE: Confirm live Turn
Loading
High-Level Assessment

The PR's approach is appropriate: live execution identity remains observer-owned, is projected only into renderer-facing summaries, and reconciliation moves to the React phase matching the committed snapshot. Persisting running IDs would blur durable and transient ownership, while passive-effect guards would add versioning complexity without eliminating the timing window as directly.

Files changed (9) +188 / -56

Enhancement (1) +5 / -0
runtime-host-session-observer.tsExpose observed nonterminal root Turn identity +5/-0

Expose observed nonterminal root Turn identity

• Adds a query returning the current root Turn ID when the observed Turn is nonterminal. Terminal or unavailable snapshots return no running IDs.

apps/desktop/src/main/runtime-host-session-observer.ts

Bug fix (4) +54 / -2
runtime-host-desktop-candidate.tsConnect session observer running IDs to catalog IPC +1/-0

Connect session observer running IDs to catalog IPC

• Wires the Runtime Host session observer into session-list projection so renderer summaries can include current running root Turn identities.

apps/desktop/src/main/runtime-host-desktop-candidate.ts

runtime-host-session-catalog-ipc-main.tsEnrich Desktop session summaries with live Turn IDs +15/-1

Enrich Desktop session summaries with live Turn IDs

• Extends catalog IPC dependencies with an observer-owned running Turn lookup. Session-list responses copy nonempty Turn IDs into renderer summaries without changing durable Runtime Host catalog data.

apps/desktop/src/main/runtime-host-session-catalog-ipc-main.ts

app-shell.tsxUse layout-phase session transient reconciler +1/-1

Use layout-phase session transient reconciler

• Switches the app shell to import the dedicated layout-phase reconciliation hook.

apps/desktop/src/renderer/app-shell.tsx

use-settled-session-transient-reconcile.tsReconcile settled Turn state during layout +37/-0

Reconcile settled Turn state during layout

• Introduces a dedicated hook that calculates settled session transients in 'useLayoutEffect'. This ensures reconciliation runs against the live-state projection associated with the committed session list before passive effects or later notifications intervene.

apps/desktop/src/renderer/use-settled-session-transient-reconcile.ts

Refactor (1) +0 / -48
app-shell-effects.tsRemove passive transient reconciliation effect +0/-48

Remove passive transient reconciliation effect

• Removes the passive-effect implementation that could combine an older committed session snapshot with newer live Turn state.

apps/desktop/src/renderer/app-shell-effects.ts

Tests (2) +125 / -0
runtime-host-session-catalog-running-turns.test.tsVerify running Turn projection in session lists +65/-0

Verify running Turn projection in session lists

• Adds an IPC-level regression test proving observed running Turn IDs are attached to active session summaries. It also verifies idle summaries omit the optional field.

apps/desktop/src/main/tests/runtime-host-session-catalog-running-turns.test.ts

settled-session-transient-reconcile.test.tsCover stale snapshot and Turn confirmation ordering +60/-0

Cover stale snapshot and Turn confirmation ordering

• Adds a React regression test where a session list commits before a matching Turn confirmation. It verifies layout-phase reconciliation preserves the newly confirmed live Turn.

apps/desktop/src/main/tests/settled-session-transient-reconcile.test.ts

Documentation (1) +4 / -6
session-history-list.tsxDocument Desktop live-run signal ownership +4/-6

Document Desktop live-run signal ownership

• Updates session-row documentation to acknowledge observer-projected running Turn IDs while clarifying that the shared presentation component does not arbitrate live-run sources.

packages/ui/src/session-history-list.tsx

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

A session-list result could be committed against a newer live Turn projection and clear the Stop witness. Runtime Host-backed Desktop lists also omitted observer-owned running Turn identities.

Capture renderer live state at each authoritative list read, reconcile only accepted results, and clear transient state with a projection-identity compare-and-swap. Project observed nonterminal root Turn identities only into renderer list summaries.

Generated-by: Codex
@M4n5ter
M4n5ter force-pushed the fix/desktop-live-turn-reconcile-race branch from ce6a90d to b88b41b Compare August 18, 2026 06:25
@M4n5ter

M4n5ter commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

fast path: The current changes are minor and reversible

@M4n5ter
M4n5ter merged commit 26c60c0 into main Aug 18, 2026
12 checks passed
@M4n5ter
M4n5ter deleted the fix/desktop-live-turn-reconcile-race branch August 18, 2026 06:36
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.

test(desktop): streaming-remount E2E intermittently misses the active Turn

1 participant