Skip to content

fix: place steering by its durable runtime event, not the Host queue echo - #5675

Merged
Astro-Han merged 6 commits into
apache:mainfrom
Astro-Han:fix/5669-steering-durable-order
Sep 24, 2026
Merged

Astro-Han merged 6 commits into
apache:mainfrom
Astro-Han:fix/5669-steering-durable-order

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The Runtime pulls steering only at a provider step boundary (ai-sdk-turn.ts drains before startStream and after a tool-free step, both after waitUntilConsumedThroughCurrent). The durable ledger therefore always reads assistant(N) → steer → assistant(N+1), and the settled transcript never splits a step around a steer row.

The live projection could. The Host synthesized a steering_message when a queue entry went in_flight, and that echo could reach a client ahead of step N's tail: the fake backend streams the same message after draining steering, and a reconnecting subscriber's catch-up let session events overtake the unpaid assistant prefix. #5541 handled that order by cutting the step into slices at the boundary. #5658 then sliced completions at per-slice source offsets and needed the Host-relative replaced flag to decide when not to. A projector rebuilt on resubscription cannot know that flag, so a reseeded completion rendered AAAA / "" (#5669).

This PR removes that representation instead of feeding it more facts. The steering row is placed only by the runtime's own steering_message, which is emitted after the durable write, in the same FIFO as the step's content:

  • Runtime Host projector: delete the in-flight steering synthesis (newlyInFlight, rootQueueInFlight, the seedActive loop, #renderedSteeringMessageIds). A steering event already durable in the bootstrap is still suppressed (fix(runtime-host): forward the durable steering echo to session subscribers #3316).
  • Runtime Host coordinator: forwarded session events join the subscriber's existing #deliverInOrder barrier. A catch-up still paying an earlier assistant prefix can no longer be overtaken by a steering or tool event, which is what the deferred field's own contract already stated. Deferred work now spends the same slow-consumer byte budget as the queue, so a subscriber stuck behind an unpaid prefix is evicted instead of holding unbounded tool output.
  • Renderer live projection: one item per step. Delete boundary slicing, sliceStart, completion slicing, and the finalize-every-slice pass. A completion replaces its step's text whole. A live steering row hands off to the transcript by its durable user id, or when the transcript reaches the terminal state, replacing the nack/ghost special cases.
  • @maka/core/events: delete replaced from TextCompleteEvent / ThinkingCompleteEvent. The projector keeps its internal flag that withholds deltas after a Host reset.
  • Queue surfaces (main, Side Conversation, WorkHub): keep showing a pulled in_flight entry until the runtime event places it. The composer already disables editing and reordering for non-queued entries. Previously the echo covered this window. The projector drops a steering entry from every queue_update once it has forwarded the runtime event or the message is durable, and emits that filtered queue alongside the row, so no surface (including the CLI, which mirrors only queue_update) shows it twice before the lease ack.
  • Fake backend: drain steering only at step boundaries, like the real Runtime (before the first step and after each completed one). A later steer is answered in a new assistant message instead of inside the one already streaming. The hold-open scenario likewise completes its waiting message before the steer and acknowledges it in a new one; the workhub-layout and streaming-remount locators that assumed one live bubble now select the acknowledgement bubble.

Fixes #5669

User-visible behavior

  • A steer row renders after the content the model produced before it read the steer, matching the settled transcript. It no longer jumps at handoff.
  • After a subscription recovery, a reseeded completion lands on its step instead of rendering empty or wrong text until the transcript takes over.
  • The steer row enters the timeline once the runtime has written it, not when the Host leased it. Until then it stays in the queue.

Not changed

  • FAKE_HOLD_OPEN_REWRITE_PROMPT still continues the same message after a steer: streaming-remount needs one secret split across a remount inside one bubble, and steering is its only trigger. The renderer keeps such output on its step above the steer row.
  • materialize.ts's ts-based splice for a settled steering row the live stream missed (deferredSettled) stays: a recovery transcript bootstrap is a byte-bounded tail page, so a settled steer can appear without the earlier step it follows.

Residual window

  • A terminal reseed can re-add a completion for a step the renderer already handed to the transcript. It is appended at the end until reconciliation, which settles every durable assistant message of a terminal Turn. This happens today as well.

Verification

  • New regression tests. Each fails on main and passes here:
    • runtime-host-session-observer.test.ts › lands a reseeded completion whole on a live step a steering row followed: drives the real observer and projector through a slow_consumer recovery into a terminal reseed, feeds the target's events into applyLiveTurnBufferEvent, and asserts the overlayLiveTurn timeline is thinking:ABC, user:steer. On main it is thinking:AAAA, user:steer (the empty post-steer slice).
    • session-continuity-coordinator.test.ts › a steering message cannot overtake the prefix a subscriber is still being paid, and › events held behind an unpaid prefix spend the slow-consumer budget (verified to fail with the budget check disabled).
    • chat-turn-steering-order.test.ts › keeps a step whole above a steering row that its later output arrives after, and › lands a completion after a steering row whole on its step.
    • live-turn-projection.test.ts › hands a running live steering row to the transcript once its user row is durable.
    • session-projector.test.ts › leaves an in-flight steering message in the queue until the runtime event places it: the runtime event carries an emptied queue, and a queue revision before the ack lists no steering entry.
  • Updated to the new contract: the observer queue test, the message-queue, transient-projection and WorkHub queue tests, the projector steering tests, and the fake backend steering test (steer, step, complete, steer, step, complete). Four fix(ui): keep steering a timeline boundary when its step continues #5541/fix(ui): apply Runtime Host stream slices by offset, not size #5658 slice tests were deleted because they pinned the removed representation.
  • Test runs:
    • @maka/ui: 641/641
    • @maka/runtime: 3538 pass, 0 fail
    • @maka/runtime-host: 2090 pass, 0 fail
    • @maka/desktop main: 2806/2806
    • maka-agent (cli): 1161 pass, 0 fail
  • npm --workspace @maka/desktop run typecheck, npm run format, npm run lint: clean.
  • E2E not run locally; left to CI.
  • No screenshot: the change is timeline ordering and queue timing, with no static visual change.

AI use

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

Tool(s) and scope: Claude Code (analysis, implementation, tests). Codex was consulted read-only for a risk review of the design.

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

…echo

The Runtime only pulls steering at a provider step boundary, so the durable ledger always reads assistant(N), steer, assistant(N+1) and the settled transcript never splits a step around a steer row. The live projection did: the Host synthesized a steering_message when a queue entry went in_flight, and that echo could reach a client ahead of step N's tail (the fake backend and a reconnecting subscriber's catch-up both produce this order). The renderer then cut the step into slices at the boundary, sliced completions at per-slice source offsets, and needed a Host-relative `replaced` flag to decide when not to. A projector rebuilt on resubscription cannot know that flag, so a reseeded completion showed `AAAA / ""` (apache#5669).

The steering row is now placed only by the runtime's own steering_message, which is emitted after the durable write in the same FIFO as the step's content:

- Runtime Host projector: delete the in-flight steering synthesis (`newlyInFlight`, `rootQueueInFlight`, the `seedActive` loop, `#renderedSteeringMessageIds`). A steering event already durable in the bootstrap is still suppressed.
- Runtime Host coordinator: forwarded session events join the subscriber's delivery order, so a catch-up that is still paying an earlier assistant prefix can no longer be overtaken by a steering or tool event.
- Renderer live projection: one item per step. Delete boundary slicing, `sliceStart`, completion slicing and the finalize-every-slice pass; a completion replaces its step's text whole. A live steering row hands off to the transcript by its durable user id, or when the transcript reaches the terminal state.
- `@maka/core/events`: delete `replaced` from TextCompleteEvent / ThinkingCompleteEvent. The projector keeps its internal flag that withholds deltas after a Host reset.
- Queue surfaces (main, Side Conversation, WorkHub) keep showing a pulled `in_flight` entry until the runtime event places it; previously the echo covered that window.

The fake backend still streams the same message after draining steering; the renderer now keeps such output on its step above the steer row, so its E2E uses need no change.

Compatibility: `replaced?: true` leaves the SessionEvent contract; no consumer outside the renderer read it. The steer row enters the timeline when the runtime has written it rather than when the Host leased it, and stays in the queue until then.

Generated-by: Claude Code
@github-actions github-actions Bot added the effort/L Under 1000 readable lines label Sep 24, 2026
The runtime writes a steering message before the Host acknowledges its lease. A queue revision in that window still listed the entry as in_flight, so it showed in the queue and the timeline at once. The projector now omits steering entries it has already forwarded or that are durable in the bootstrap, for every queue_update it projects.

Generated-by: Claude Code
The real Runtime pulls steering before the first provider step and after each completed one, so a steer always sits between two assistant messages. The fake backend drained between chunks of one message, which produced an order the product never sees. It now drains at the same boundaries and answers a later steer in a new assistant message. The hold-open scenarios keep steering as their stream trigger.

Generated-by: Claude Code
The hold-open scenario continued its waiting message after draining a steer, an order the real Runtime never produces. It now completes the waiting message, emits the steer, and acknowledges it in a new message while the Turn keeps running. The E2E locators that assumed one live bubble now select the acknowledgement bubble.

The rewrite variant still continues the same message: streaming-remount needs one secret split across a remount inside one bubble, and steering is its only trigger.

Generated-by: Claude Code
Work deferred behind a subscriber's catch-up backlog was never counted against the slow-consumer budget. After session events joined that barrier, a subscriber stuck on an old prefix could hold an unbounded run of tool output in memory without being evicted; on main those events went straight to the bounded queue. Deferred work now spends the same byte budget as the queue, eviction drops it, and a closed subscriber defers nothing.

Generated-by: Claude Code
When the runtime placed a steer, the projector forwarded the row but left the last queue_update standing until the Host's next queue revision. The CLI mirrors only queue_update, so it showed the steer both in the transcript and in the pending bar. The projector now emits the filtered queue alongside the row whenever the Host queue still lists that message.

Generated-by: Claude Code
@Astro-Han
Astro-Han marked this pull request as ready for review September 24, 2026 06:48

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

Reviewed exact head 49cf7be0f827dd8e79f56df3763bc5aa4e55a7ca. I found no P0-P2 issues.

The change now derives steering placement from the runtime's durable steering_message boundary instead of the Host queue lease echo. I traced the production path from persist-before-include steering (packages/runtime/src/ai-sdk-turn.ts:2965-3040), through ordered subscriber delivery and bounded deferred work (packages/runtime-host/src/server/session-continuity-coordinator.ts:739-849,1549-1632,1721-1768), queue removal after placement (packages/runtime-host/src/adapter/session-projector.ts:380-407,491-499), and live-to-durable UI reconciliation (packages/ui/src/live-turn-projection.ts:220-242,302-379,593-640). The resulting authority and ordering model is coherent: the queue remains visible until the runtime event is durable, the event cannot overtake an unpaid assistant prefix, and the renderer keeps one whole projection per provider step.

Verification on this head:

  • Focused Runtime Host coordinator/projector tests: 74/74; focused Desktop observer/queue/WorkHub tests: 99/99; focused UI/Runtime fake-backend tests: 58/58.
  • Full UI: 641/641; Desktop: 2690/2690; Runtime: 3524 passed / 13 skipped; CLI: 1161 passed / 3 skipped.
  • Build, typecheck, lint, format, ASF headers, renderer build/staleness, renderer architecture, E2E budget, and git diff --check passed.
  • Hosted test is successful, and the merge tree against current main (4d1e35898fa27bb32408a2bee7746e09ad2f85a9) is clean.

The full Runtime Host suite had two local-only failures: the managed Bash sandbox test remains blocked by this runner's denied unshare/bwrap, and one real health-probe timeout passed immediately when rerun alone. I did not run packaged Electron E2E or native Windows/macOS execution. No schema or migration change is involved.

Review notice: This review was prepared by an automated review agent operated by hqhq1025 and is published at the direction of AstroHan, who has read these findings and is the human accountable for them.

@Astro-Han
Astro-Han merged commit 0a5b9dc into apache:main Sep 24, 2026
2 checks passed
@Astro-Han
Astro-Han deleted the fix/5669-steering-durable-order branch September 24, 2026 07:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/L Under 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Live turn projection survives Runtime Host resubscription, so a reseeded completion is sliced against stale offsets

2 participants