fix: place steering by its durable runtime event, not the Host queue echo - #5675
Conversation
…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
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
hqhq1025
left a comment
There was a problem hiding this comment.
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 --checkpassed. - Hosted
testis successful, and the merge tree against currentmain(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.
Summary
The Runtime pulls steering only at a provider step boundary (
ai-sdk-turn.tsdrains beforestartStreamand after a tool-free step, both afterwaitUntilConsumedThroughCurrent). The durable ledger therefore always readsassistant(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_messagewhen a queue entry wentin_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-relativereplacedflag to decide when not to. A projector rebuilt on resubscription cannot know that flag, so a reseeded completion renderedAAAA / ""(#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:newlyInFlight,rootQueueInFlight, theseedActiveloop,#renderedSteeringMessageIds). A steering event already durable in the bootstrap is still suppressed (fix(runtime-host): forward the durable steering echo to session subscribers #3316).#deliverInOrderbarrier. A catch-up still paying an earlier assistant prefix can no longer be overtaken by a steering or tool event, which is what thedeferredfield'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.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: deletereplacedfromTextCompleteEvent/ThinkingCompleteEvent. The projector keeps its internal flag that withholds deltas after a Host reset.in_flightentry until the runtime event places it. The composer already disables editing and reordering for non-queuedentries. Previously the echo covered this window. The projector drops a steering entry from everyqueue_updateonce 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 onlyqueue_update) shows it twice before the lease ack.workhub-layoutandstreaming-remountlocators that assumed one live bubble now select the acknowledgement bubble.Fixes #5669
User-visible behavior
Not changed
FAKE_HOLD_OPEN_REWRITE_PROMPTstill continues the same message after a steer:streaming-remountneeds 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
Verification
mainand 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 aslow_consumerrecovery into a terminal reseed, feeds the target's events intoapplyLiveTurnBufferEvent, and asserts theoverlayLiveTurntimeline isthinking:ABC, user:steer. Onmainit isthinking: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.@maka/ui: 641/641@maka/runtime: 3538 pass, 0 fail@maka/runtime-host: 2090 pass, 0 fail@maka/desktopmain: 2806/2806maka-agent(cli): 1161 pass, 0 failnpm --workspace @maka/desktop run typecheck,npm run format,npm run lint: clean.AI use
Tool(s) and scope: Claude Code (analysis, implementation, tests). Codex was consulted read-only for a risk review of the design.
Checklist
Does this PR entail a change in behavior?