Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions packages/runtime-host/src/__tests__/session-projector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,38 @@ test('reseeds an empty queue after queued successors completed while disconnecte
assert.deepEqual(queue.followupEntries, []);
});

test('projects a queue drain that lands while no root Turn is live', () => {
// apache/maka#5520: a drain observed after the root Turn is gone must still
// reach the renderer, or a phantom queued card survives whose retract fails
// with not_found forever. Seeding stays silent for rootless snapshots — the
// Desktop observer pins an empty seed there — because a client that never
// observed the session has no stale card to clear.
const projector = new RuntimeHostSessionProjector(
snapshot({ queue: queue(2, [steeringEntry('queued')]) }),
createRuntimeHostSessionProjectionSeed([], snapshot()),
() => 10,
);

const drained = projector.accept({
kind: 'subscription.session_projection',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: 1,
snapshot: snapshot({ projectionRevision: 2, rootTurn: null, queue: queue(3, []) }),
});
assert.deepEqual(
drained.events.map((event) => event.type),
['queue_update'],
);
const update = drained.events.find(
(event): event is Extract<SessionEvent, { type: 'queue_update' }> =>
event.type === 'queue_update',
);
assert.ok(update, 'the drained queue must be projected');
assert.deepEqual(update.steering, []);
assert.deepEqual(update.followup, []);
});

test('reseeds the latest provider retry when the active Turn still carries one', () => {
const retry = {
phase: 'scheduled' as const,
Expand Down
47 changes: 27 additions & 20 deletions packages/runtime-host/src/adapter/session-projector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,28 +424,35 @@ export class RuntimeHostSessionProjector {
for (const interaction of newlyPendingInteractions(previousSnapshot, next)) {
events.push(...projectRuntimeHostInteractionRequest(interaction, this.#now()));
}
const queueChangedNow = queueChanged(previousSnapshot.queue, next.queue);
const enteredActiveTurn =
root && queueChanged(previousSnapshot.queue, next.queue)
? newlyInFlight(previousSnapshot.queue, next.queue)
: [];
if (root && queueChanged(previousSnapshot.queue, next.queue)) {
for (const entry of enteredActiveTurn) {
if (
this.#durableTurnByMessage.has(entry.messageId) ||
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}`,
turnId: root.turnId,
messageId: entry.messageId,
ts: this.#now(),
content: structuredClone(entry.content),
});
root && queueChangedNow ? newlyInFlight(previousSnapshot.queue, next.queue) : [];
if (queueChangedNow) {
if (root) {
for (const entry of enteredActiveTurn) {
if (
this.#durableTurnByMessage.has(entry.messageId) ||
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}`,
turnId: root.turnId,
messageId: entry.messageId,
ts: this.#now(),
content: structuredClone(entry.content),
});
}
}
events.push(projectQueueUpdate(next.queue, root.turnId, this.#now()));
// Project the authoritative queue even with no live Turn: a drain that
// lands after the root Turn is gone must still reach observers, or a
// queued card survives as a phantom whose retract fails with not_found
// (apache/maka#5520).
events.push(
projectQueueUpdate(next.queue, root?.turnId ?? previousRoot?.turnId ?? '', this.#now()),
);
}
if (startedTurn) this.#accumulators.clear();
// Emit the presentation-only compaction-started event when the root Turn
Expand Down