Skip to content

fix(desktop): mid-turn send opens a parallel root turn on the embedded runtime — should route through the steering queue #1954

Description

@Astro-Han

Symptom

In Maka Desktop, sending a new message while the current session turn is still running:

  1. The chat view jitters (the live-turn projection flips between two turns).
  2. The session reports an error (conversation error toast).

Root cause

Production Desktop still runs the embedded SessionManager path, which lacks the submit semantics established by #1357:

  • sessions:sendruntimeKernel.sendMessagestartTurn opens a new top-level turn unconditionally (no active-root guard).
  • ensureActive reuses the same backend generation; AiSdkBackend is a single mutable-state instance (aborted, currentTurnId, currentRunId, abortController, injectedSteeringMessages, ...). Two concurrent send() calls stomp these fields, so stop / respond / sandbox-boundary route to the wrong turn and one run fails → a streamed error event → session error.
  • The renderer arms liveTurn for the new turn (T_B), but every text_delta from the still-running T_A flips the projection back; persisted message order interleaves (T_B's user message lands before T_A's; assistant texts land in completion order), so full-list refreshes reorder content and scroll jumps.

CLI (#1087) and Desktop voice (sessions:steer) already route mid-turn text through the steering queue; only Desktop text input opens a parallel turn.

The final shape already exists

#1357 (merged) defines turn.message.submit: "An idle submission starts exactly one root Turn; active submissions become steering or follow-up entries." This bug is the embedded production path not yet applying that semantics. #853 (RFC) / #1357 (Message authority) / #1167 (M3 tracking) remain the long-term home; the M5 production cutover deletes the embedded path.

Proposed fix (short term, aligns with #1357)

  1. Desktop main sessions:send asks the embedded runtime steering queue first (runtimeKernel.steer — the embedded counterpart of feat(runtime-host): establish message authority foundation #1357's queue): { kind: 'queued' } injects into the running turn; { kind: 'fallback' } falls through to the existing new-turn sendMessage. Converge this decision in main so chat / bot / voice-fallback / goal entrypoints share one routing point (and return steered so the renderer skips new-turn bookkeeping: armTurnActive, optimistic status, refreshMessagesUntilTurn).
  2. Renderer handleEvent learns steering_messagerefreshMessages (currently falls into the default branch), so the injected text appears in the transcript (it is already persisted as a role: 'user' RuntimeEvent with steering: true).

Verification

  • Runtime repro (fake backend + MemoryAgentRunStore): two concurrent sendMessage calls on one session must not parallel-run or interleave message order.
  • Desktop E2E (fake backend): send while running → no second turn, steering text visible, no error.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions