diff --git a/packages/runtime-host/src/__tests__/execution-host-message.test.ts b/packages/runtime-host/src/__tests__/execution-host-message.test.ts index 9f69b0c2a5..9e1e84ae02 100644 --- a/packages/runtime-host/src/__tests__/execution-host-message.test.ts +++ b/packages/runtime-host/src/__tests__/execution-host-message.test.ts @@ -95,6 +95,64 @@ import { withTimeout, } from './fixtures/execution-host-suite.js'; +test('subscribed Clients receive the durable steering echo as a session event', async () => { + await withExecutionRoot(async (fixture) => { + const host = await fixture.startHost(); + const client = await connectClient(fixture.root); + const subscription = await client.openSessionSubscription({ + sessionId: fixture.sessionId, + transcript: { kind: 'none' }, + }); + const probe = new SubscriptionProbe(subscription); + + const turnId = randomUUID(); + await client.startTurn({ + sessionId: fixture.sessionId, + turnId, + content: { text: FAKE_WAIT_FOR_STEERING_PROMPT }, + }); + const steeringId = randomUUID(); + const steeringContent = { + text: 'steer mid-turn', + displayText: 'steer mid-turn', + }; + const submitted = await client.request('turn.message.submit', { + originHostEpoch: host.hostEpoch, + sessionId: fixture.sessionId, + messageId: steeringId, + content: steeringContent, + placement: 'current_turn', + }); + assert.equal(submitted.disposition, 'steering'); + + // apache/maka#3304: the steering render must not depend on observing the + // transient in-flight queue state; the durable echo is forwarded verbatim. + const echoed = await probe.waitFor( + (frame) => + frame.kind === 'subscription.session_event' && frame.event.type === 'steering_message', + 'continuity did not forward the durable steering echo', + ); + assert.equal(echoed.kind, 'subscription.session_event'); + if (echoed.kind === 'subscription.session_event') { + assert.equal(echoed.event.type, 'steering_message'); + if (echoed.event.type === 'steering_message') { + assert.equal(echoed.event.turnId, turnId); + assert.equal(echoed.event.messageId, steeringId); + assert.deepEqual(echoed.event.content, steeringContent); + } + } + + assert.equal( + (await waitForTerminalTurn(client, fixture.sessionId, turnId)).status, + 'completed', + ); + await subscription.close(); + await probe.done; + await client.close(); + await fixture.stopHost(host); + }); +}); + test('steering becomes durable and ordered followups automatically start the next root', async () => { await withExecutionRoot(async (fixture) => { const host = await fixture.startHost(); diff --git a/packages/runtime-host/src/__tests__/protocol.test.ts b/packages/runtime-host/src/__tests__/protocol.test.ts index a1a7b3810a..6fcbbcaa64 100644 --- a/packages/runtime-host/src/__tests__/protocol.test.ts +++ b/packages/runtime-host/src/__tests__/protocol.test.ts @@ -106,7 +106,7 @@ describe('Runtime Host bootstrap protocol', () => { }); test('keeps the subscription queue Epoch correlated', () => { - assert.equal(SESSION_CONTINUITY_SCHEMA_VERSION, 4); + assert.equal(SESSION_CONTINUITY_SCHEMA_VERSION, 5); const opened = { requestId: 'open-1', operation: 'subscription.open', @@ -305,6 +305,37 @@ describe('Runtime Host bootstrap protocol', () => { ]) { assert.throws(() => decodeHostFrame({ ...envelope, event }), isInvalidFrame); } + + // The durable steering echo shares the session-event frame without a + // toolUseId; unknown keys stay rejected. + const steering = { + type: 'steering_message' as const, + id: 'steering-event-1', + turnId: 'turn-1', + ts: 7, + messageId: 'steering-message-1', + content: { text: 'steer the turn' }, + }; + const decodedSteering = decodeHostFrame({ ...envelope, event: steering }); + assert.ok('kind' in decodedSteering); + if ('kind' in decodedSteering) { + assert.equal(decodedSteering.kind, 'subscription.session_event'); + if (decodedSteering.kind === 'subscription.session_event') { + assert.deepEqual(decodedSteering.event, steering); + } + } + assert.throws( + () => decodeHostFrame({ ...envelope, event: { ...steering, toolUseId: 'tool-1' } }), + isInvalidFrame, + ); + assert.throws( + () => + decodeHostFrame({ + ...envelope, + event: { ...steering, content: { text: 'x'.repeat(49 * 1024) } }, + }), + isInvalidFrame, + ); assert.throws( () => decodeHostFrame({ diff --git a/packages/runtime-host/src/__tests__/session-continuity-coordinator.test.ts b/packages/runtime-host/src/__tests__/session-continuity-coordinator.test.ts index 9dc5e30a56..120453b28a 100644 --- a/packages/runtime-host/src/__tests__/session-continuity-coordinator.test.ts +++ b/packages/runtime-host/src/__tests__/session-continuity-coordinator.test.ts @@ -69,6 +69,39 @@ test('open is an inactive publication barrier and live sequence starts at nextSe coordinator.close(); }); +test('forwards the durable steering echo to subscribers as a session event', async () => { + const sink = new RecordingSink(); + const coordinator = new SessionContinuityCoordinator( + HOST_EPOCH, + async () => canonical(), + new SessionAdmissionGate(), + ); + const connection = coordinator.attachConnection('connection-1', sink); + const opened = await open(coordinator, 'connection-1'); + connection.activate(opened.subscriptionId); + await delayImmediate(); + sink.frames.length = 0; + + const steering = { + type: 'steering_message' as const, + id: 'steering-event-1', + turnId: 'turn-1', + ts: 7, + messageId: 'steering-message-1', + content: { text: 'steer the turn' }, + }; + await coordinator.acceptRuntimeEvent(SESSION_ID, 'run-1', steering); + + assert.equal(sink.frames.length, 1); + const frame = sink.frames[0]; + assert.equal(frame?.kind, 'subscription.session_event'); + if (frame?.kind !== 'subscription.session_event') return; + assert.deepEqual(frame.event, steering); + + connection.abort(opened.subscriptionId); + coordinator.close(); +}); + test('open snapshot includes pending Interactions from the canonical projection', async () => { const pending = pendingInteraction(); const coordinator = new SessionContinuityCoordinator( diff --git a/packages/runtime-host/src/__tests__/session-projector.test.ts b/packages/runtime-host/src/__tests__/session-projector.test.ts index a3b0e462e6..d10729d041 100644 --- a/packages/runtime-host/src/__tests__/session-projector.test.ts +++ b/packages/runtime-host/src/__tests__/session-projector.test.ts @@ -8,6 +8,7 @@ import { import { SESSION_CONTINUITY_SCHEMA_VERSION, type SessionContinuitySnapshot, + type SteeringMessageSnapshot, type SubscriptionFrame, } from '../protocol/index.js'; @@ -243,6 +244,169 @@ test('does not replay settled transcript steps when the active step reaches term ); }); +test('projects the durable steering echo even when the in-flight queue state was never observed', () => { + // Regression for apache/maka#3304: the coalesced canonical refresh can jump + // the queue straight from queued to consumed, so the in-flight synthesis + // never fires. The forwarded steering_message event must render the message. + const projector = new RuntimeHostSessionProjector( + snapshot({ queue: queue(2, [steeringEntry('queued')]) }), + createRuntimeHostSessionProjectionSeed([], snapshot()), + () => 10, + ); + + const skipped = projector.accept({ + kind: 'subscription.session_projection', + hostEpoch: 'host-1', + subscriptionId: 'subscription-1', + sequence: 1, + snapshot: snapshot({ queue: queue(4, []) }), + }); + assert.deepEqual( + skipped.events.map((event) => event.type), + ['queue_update'], + ); + + const echoed = projector.accept(steeringFrame(2)).events; + assert.equal(echoed.length, 1); + assert.deepEqual(echoed[0], { + type: 'steering_message', + id: 'steering-event-1', + turnId: 'turn-1', + ts: 10, + messageId: 'steering-message-1', + content: { text: 'steer the turn' }, + }); +}); + +test('projects a steering message exactly once across both authoritative paths', () => { + // The queue in-flight synthesis and the durable session-event echo race; + // whichever projects the message first suppresses the other. + const inFlightFirst = new RuntimeHostSessionProjector( + snapshot({ queue: queue(2, [steeringEntry('queued')]) }), + createRuntimeHostSessionProjectionSeed([], snapshot()), + () => 10, + ); + const synthesized = inFlightFirst.accept({ + kind: 'subscription.session_projection', + hostEpoch: 'host-1', + subscriptionId: 'subscription-1', + sequence: 1, + snapshot: snapshot({ queue: queue(3, [steeringEntry('in_flight')]) }), + }); + assert.deepEqual( + synthesized.events.map((event) => event.type), + ['steering_message', 'queue_update'], + ); + assert.deepEqual(inFlightFirst.accept(steeringFrame(2)).events, []); + + const echoFirst = new RuntimeHostSessionProjector( + snapshot({ queue: queue(2, [steeringEntry('queued')]) }), + createRuntimeHostSessionProjectionSeed([], snapshot()), + () => 10, + ); + assert.equal(echoFirst.accept(steeringFrame(1)).events.length, 1); + const suppressed = echoFirst.accept({ + kind: 'subscription.session_projection', + hostEpoch: 'host-1', + subscriptionId: 'subscription-1', + sequence: 2, + snapshot: snapshot({ queue: queue(3, [steeringEntry('in_flight')]) }), + }); + assert.deepEqual( + suppressed.events.map((event) => event.type), + ['queue_update'], + ); +}); + +test('seeds an unrendered in-flight steering message once on rejoin', () => { + const projector = new RuntimeHostSessionProjector( + snapshot({ queue: queue(3, [steeringEntry('in_flight')]) }), + createRuntimeHostSessionProjectionSeed([], snapshot()), + () => 10, + ); + assert.deepEqual( + projector.seedActive(false).map((event) => event.type), + ['steering_message', 'queue_update'], + ); + // A live echo of the same message arriving after the seed is the duplicate. + assert.deepEqual(projector.accept(steeringFrame(1)).events, []); +}); + +test('suppresses the live echo for a steering message already durable in the bootstrap', () => { + // subscription.open can bootstrap the durable steering message and install + // the subscriber while the Host's forwarded echo for it is still pending: + // the bootstrapped render must stay the only one (apache/maka#3316 review). + const inFlight = snapshot({ queue: queue(3, [steeringEntry('in_flight')]) }); + const projector = new RuntimeHostSessionProjector( + inFlight, + createRuntimeHostSessionProjectionSeed( + [userSteering('steering-message-1', 'steering-event-1')], + inFlight, + ), + () => 10, + ); + + // Durable and in-flight: no synthesis seed… + assert.deepEqual( + projector.seedActive(false).map((event) => event.type), + ['queue_update'], + ); + // …and the late echo of the same message is the duplicate. + assert.deepEqual(projector.accept(steeringFrame(1)).events, []); + // A different steering message still renders normally. + assert.equal(projector.accept(steeringFrame(2, 'steering-message-2')).events.length, 1); +}); + +function steeringEntry(state: 'queued' | 'in_flight'): SteeringMessageSnapshot { + return { + entryId: 'entry-1', + messageId: 'steering-message-1', + content: { text: 'steer the turn' }, + placement: 'current_turn', + state, + }; +} + +function queue( + queueRevision: number, + steering: readonly SteeringMessageSnapshot[], +): SessionContinuitySnapshot['queue'] { + return { hostEpoch: 'host-1', queueRevision, steering, followup: [] }; +} + +function steeringFrame(sequence: number, messageId = 'steering-message-1'): SubscriptionFrame { + return { + kind: 'subscription.session_event', + hostEpoch: 'host-1', + subscriptionId: 'subscription-1', + sequence, + sessionId: 'session-1', + runId: 'run-1', + event: { + type: 'steering_message', + id: 'steering-event-1', + turnId: 'turn-1', + ts: 10, + messageId, + content: { text: 'steer the turn' }, + }, + }; +} + +function userSteering( + id: string, + steeringEventId: string, +): Extract { + return { + type: 'user', + id, + turnId: 'turn-1', + ts: 1, + text: 'steer the turn', + steeringEventId, + }; +} + function deltaFrame( sequence: number, startOffset: number, diff --git a/packages/runtime-host/src/adapter/session-projector.ts b/packages/runtime-host/src/adapter/session-projector.ts index 6a37dc2457..d979f79a83 100644 --- a/packages/runtime-host/src/adapter/session-projector.ts +++ b/packages/runtime-host/src/adapter/session-projector.ts @@ -24,6 +24,13 @@ interface AssistantAccumulator { export interface RuntimeHostSessionProjectionSeed { readonly durableInFlightMessageIds: readonly string[]; + /** + * messageIds of steering interjections already durable in the transcript. + * `subscription.open` can bootstrap message X and install the subscriber + * before the Host's live echo for X arrives; these ids suppress that late + * echo so the bootstrapped render stays the only one. + */ + readonly durableSteeringMessageIds: readonly string[]; readonly activeAssistantMessages: readonly Extract[]; } @@ -38,6 +45,9 @@ export function createRuntimeHostSessionProjectionSeed( durableInFlightMessageIds: transcript .filter((message) => inFlightMessageIds.has(message.id)) .map((message) => message.id), + durableSteeringMessageIds: transcript + .filter((message) => message.type === 'user' && message.steeringEventId !== undefined) + .map((message) => message.id), activeAssistantMessages: snapshot.rootTurn === null ? [] @@ -65,6 +75,13 @@ export class RuntimeHostSessionProjector { #snapshot: SessionContinuitySnapshot; readonly #now: () => number; readonly #transcriptIds: Set; + /** + * One render per steering message, whichever authoritative source projects + * it first: the transcript bootstrap (seeded here), the durable session-event + * echo (in stream order), or the queue in-flight synthesis (which can win the + * race or cover a rejoin window). + */ + readonly #renderedSteeringMessageIds: Set; readonly #accumulators = new Map(); constructor( @@ -76,6 +93,7 @@ export class RuntimeHostSessionProjector { this.#snapshot = structuredClone(snapshot); this.#now = now; this.#transcriptIds = new Set(seed.durableInFlightMessageIds); + this.#renderedSteeringMessageIds = new Set(seed.durableSteeringMessageIds); const root = snapshot.rootTurn; if (!root) return; for (const message of seed.activeAssistantMessages) { @@ -148,6 +166,8 @@ export class RuntimeHostSessionProjector { } for (const entry of rootQueueInFlight(this.#snapshot.queue)) { if (this.#transcriptIds.has(entry.messageId)) continue; + if (this.#renderedSteeringMessageIds.has(entry.messageId)) continue; + this.#renderedSteeringMessageIds.add(entry.messageId); events.push({ type: 'steering_message', id: `host-queue:${this.#snapshot.queue.hostEpoch}:${this.#snapshot.queue.queueRevision}:${entry.entryId}`, @@ -320,8 +340,16 @@ export class RuntimeHostSessionProjector { return emptyUpdate(events); } if (frame.kind === 'subscription.session_event') { - const event = projectToolEvent(frame); - if (event) events.push(event); + const event = projectSessionEvent(frame); + if (event) { + if (event.type === 'steering_message') { + if (this.#renderedSteeringMessageIds.has(event.messageId)) { + return emptyUpdate(events); + } + this.#renderedSteeringMessageIds.add(event.messageId); + } + events.push(event); + } return emptyUpdate(events); } if (frame.kind !== 'subscription.session_projection') return emptyUpdate(events); @@ -340,6 +368,8 @@ export class RuntimeHostSessionProjector { const root = next.rootTurn; if (root && queueChanged(previousSnapshot.queue, next.queue)) { for (const entry of newlyInFlight(previousSnapshot.queue, next.queue)) { + if (this.#renderedSteeringMessageIds.has(entry.messageId)) continue; + this.#renderedSteeringMessageIds.add(entry.messageId); events.push({ type: 'steering_message', id: `host-queue:${next.queue.hostEpoch}:${next.queue.queueRevision}:${entry.entryId}`, @@ -458,10 +488,20 @@ export function projectRuntimeHostInteractionRequest( return []; } -function projectToolEvent( +function projectSessionEvent( frame: Extract, ): SessionEvent | undefined { const event = frame.event; + if (event.type === 'steering_message') { + return { + type: 'steering_message', + id: event.id, + turnId: event.turnId, + ts: event.ts, + messageId: event.messageId, + content: structuredClone(event.content), + }; + } const base = { id: event.id, turnId: event.turnId, diff --git a/packages/runtime-host/src/protocol/session-continuity.ts b/packages/runtime-host/src/protocol/session-continuity.ts index cb713f3c88..d310cfb610 100644 --- a/packages/runtime-host/src/protocol/session-continuity.ts +++ b/packages/runtime-host/src/protocol/session-continuity.ts @@ -22,7 +22,12 @@ import { type SessionMessageQueueProjection, } from './message.js'; import { defineOperation } from './operation-spec.js'; -import { decodeTurnSnapshot, type TurnSnapshot } from './turn.js'; +import { + decodeMessageContent, + decodeTurnSnapshot, + type MessageContent, + type TurnSnapshot, +} from './turn.js'; import { decodeGoalProjection, type GoalProjection } from './goal.js'; import { decodeRuntimeResourceRef } from './runtime-resource.js'; import { @@ -31,7 +36,7 @@ import { type SessionTranscriptBootstrap, } from './session-transcript.js'; -export const SESSION_CONTINUITY_SCHEMA_VERSION = 4 as const; +export const SESSION_CONTINUITY_SCHEMA_VERSION = 5 as const; export const SESSION_CONTINUITY_SNAPSHOT_MAX_BYTES = 56 * 1024; // Leave transport headroom for the response envelope and request correlation. export const SUBSCRIPTION_OPEN_RESULT_MAX_BYTES = 92 * 1024; @@ -170,11 +175,26 @@ export type SessionToolEvent = content: ToolResultPreviewContent; }); +/** + * The durable mid-turn user interjection (steering), forwarded verbatim from the + * run's event stream. Unlike tool events it has no toolUseId; it shares the + * frame so subscribers render the interjection in place without depending on + * observing the transient in-flight queue state. + */ +export interface SessionSteeringEvent { + type: 'steering_message'; + id: string; + turnId: string; + ts: number; + messageId: string; + content: MessageContent; +} + export interface SessionEventFrame extends SubscriptionEnvelope { kind: 'subscription.session_event'; sessionId: string; runId: string; - event: SessionToolEvent; + event: SessionToolEvent | SessionSteeringEvent; } export interface SessionTranscriptAdvancedFrame extends SubscriptionEnvelope { @@ -339,7 +359,7 @@ export function decodeSubscriptionFrame(value: unknown): SubscriptionFrame { ...envelope, sessionId: requireEntityId(record.sessionId, 'sessionId'), runId: requireEntityId(record.runId, 'runId'), - event: decodeSessionToolEvent(record.event), + event: decodeSessionFrameEvent(record.event), }; } else if (record.kind === 'subscription.transcript_advanced') { assertExactKeys(record, 'Session transcript advanced frame', [ @@ -684,6 +704,31 @@ function decodeAssistantDelta(value: unknown): SessionAssistantDelta { }; } +function decodeSessionFrameEvent(value: unknown): SessionToolEvent | SessionSteeringEvent { + const record = requireRecord(value, 'Session event'); + if (record.type === 'steering_message') return decodeSessionSteeringEvent(record); + return decodeSessionToolEvent(record); +} + +function decodeSessionSteeringEvent(record: Record): SessionSteeringEvent { + assertExactKeys(record, 'Session steering event', [ + 'type', + 'id', + 'turnId', + 'ts', + 'messageId', + 'content', + ]); + return { + type: 'steering_message', + id: requireId(record.id, 'Session steering event id'), + turnId: requireEntityId(record.turnId, 'turnId'), + ts: requireCount(record.ts, 'Session steering event timestamp'), + messageId: requireEntityId(record.messageId, 'messageId'), + content: decodeMessageContent(record.content), + }; +} + function decodeSessionToolEvent(value: unknown): SessionToolEvent { const record = requireRecord(value, 'Session tool event'); const identity = { diff --git a/packages/runtime-host/src/server/root-turn-coordinator.ts b/packages/runtime-host/src/server/root-turn-coordinator.ts index b9afbd4fd0..11842217b2 100644 --- a/packages/runtime-host/src/server/root-turn-coordinator.ts +++ b/packages/runtime-host/src/server/root-turn-coordinator.ts @@ -73,7 +73,7 @@ import type { ConnectionContext, TurnOperationHandlerMap } from './operation-dis import { RootAdmissionOwner } from './root-admission-owner.js'; import { type SessionAdmissionLease, SessionAdmissionGate } from './session-admission-gate.js'; import { - type RuntimeSessionTransientEvent, + type RuntimeSessionForwardedEvent, SessionContinuityCoordinator, } from './session-continuity-coordinator.js'; import type { @@ -2096,7 +2096,7 @@ export class RootTurnCoordinator implements HostedExecutionAuthority { // Presentation observers do not participate in execution authority. } } - if (isRuntimeSessionTransientEvent(event)) { + if (isRuntimeSessionForwardedEvent(event)) { await this.continuity.acceptRuntimeEvent(input.sessionId, active.runId, event); } else if (isInteractionAnswerAck(event)) { await this.continuity.refreshCanonical(input.sessionId); @@ -2774,9 +2774,13 @@ function isStoppedInteractionAdmission( ); } -function isRuntimeSessionTransientEvent( +// Membership answers one question: forward this event live to subscribers via +// the continuity coordinator instead of letting the canonical refresh carry +// it. Persistence is orthogonal — it happens upstream in the run's own event +// stream, which is why the durable steering_message belongs here. +function isRuntimeSessionForwardedEvent( event: SessionEvent, -): event is RuntimeSessionTransientEvent { +): event is RuntimeSessionForwardedEvent { return ( event.type === 'text_delta' || event.type === 'text_complete' || @@ -2787,6 +2791,7 @@ function isRuntimeSessionTransientEvent( event.type === 'tool_progress' || event.type === 'tool_result_preview' || event.type === 'tool_result' || + event.type === 'steering_message' || event.type === 'provider_retry' ); } diff --git a/packages/runtime-host/src/server/session-continuity-coordinator.ts b/packages/runtime-host/src/server/session-continuity-coordinator.ts index 02d50c741b..d6664204d9 100644 --- a/packages/runtime-host/src/server/session-continuity-coordinator.ts +++ b/packages/runtime-host/src/server/session-continuity-coordinator.ts @@ -19,6 +19,7 @@ import { type SessionDomainChangedFrame, type SessionEventFrame, type SessionRuntimeResourcePtyDataFrame, + type SessionSteeringEvent, type SessionToolEvent, type SessionTranscriptAdvancedFrame, type SessionTranscriptPageInput, @@ -60,7 +61,7 @@ const MAX_SUBSCRIBER_QUEUED_BYTES = 256 * 1024; export type { CanonicalSessionProjection } from './canonical-session-projection.js'; -export type RuntimeSessionTransientEvent = Extract< +export type RuntimeSessionForwardedEvent = Extract< SessionEvent, { type: @@ -73,6 +74,7 @@ export type RuntimeSessionTransientEvent = Extract< | 'tool_progress' | 'tool_result_preview' | 'tool_result' + | 'steering_message' | 'provider_retry'; } >; @@ -94,7 +96,7 @@ interface SessionProjectionState { */ toolResultPreviews: Map< string, - Extract + Extract >; terminalPublicationFence?: TerminalPublicationFence; } @@ -597,7 +599,7 @@ export class SessionContinuityCoordinator implements SessionContinuityService { async acceptRuntimeEvent( sessionId: string, runId: string, - event: RuntimeSessionTransientEvent, + event: RuntimeSessionForwardedEvent, ): Promise { if ( (event.type === 'text_delta' || event.type === 'thinking_delta') && @@ -714,7 +716,7 @@ export class SessionContinuityCoordinator implements SessionContinuityService { } else if (event.type === 'tool_result') { state.toolResultPreviews.delete(event.toolUseId); } - const projected = projectToolEvent(event); + const projected = projectSessionEvent(event); for (const subscriber of state.subscribers.values()) { const frame: SessionEventFrame = { kind: 'subscription.session_event', @@ -948,7 +950,7 @@ export class SessionContinuityCoordinator implements SessionContinuityService { sequence: subscriber.nextSequence, sessionId, runId: rootTurn.runId, - event: projectToolEvent(preview), + event: projectSessionEvent(preview), }; this.#enqueue(subscriber, frame); } @@ -1430,7 +1432,7 @@ export class SessionContinuityCoordinator implements SessionContinuityService { subscriber: Subscriber, sessionId: string, runId: string, - event: Extract, + event: Extract, kind: SessionAssistantDelta['kind'], startOffset: number, ): void { @@ -1899,9 +1901,9 @@ function jsonStringContentBytes(value: string): number { return Buffer.byteLength(encoded.slice(1, -1), 'utf8'); } -function projectToolEvent( +function projectSessionEvent( event: Exclude< - RuntimeSessionTransientEvent, + RuntimeSessionForwardedEvent, { type: | 'text_delta' @@ -1911,7 +1913,20 @@ function projectToolEvent( | 'provider_retry'; } >, -): SessionToolEvent { +): SessionToolEvent | SessionSteeringEvent { + if (event.type === 'steering_message') { + // The durable steering echo: forwarded verbatim so subscribers render the + // interjection in place instead of depending on observing the transient + // in-flight queue state. + return { + type: 'steering_message', + id: event.id, + turnId: event.turnId, + ts: event.ts, + messageId: event.messageId, + content: structuredClone(event.content), + }; + } const identity = { id: event.id, turnId: event.turnId,