Skip to content

Commit e896f46

Browse files
committed
fix(side-chat): replay durable steering admission
Generated-by: Codex
1 parent e72ed85 commit e896f46

2 files changed

Lines changed: 56 additions & 8 deletions

File tree

packages/runtime-host/src/__tests__/session-projector.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,44 @@ test('reseeds the Host admission fact for a message already in the active Turn',
307307
);
308308
});
309309

310+
test('reseeds the Host admission fact after an active Turn message leaves the queue', () => {
311+
const current = snapshot();
312+
const projector = new RuntimeHostSessionProjector(
313+
current,
314+
createRuntimeHostSessionProjectionSeed(
315+
[
316+
{
317+
type: 'user',
318+
id: 'ticket-1',
319+
turnId: 'turn-1',
320+
ts: 1,
321+
text: 'continue here',
322+
steeringEventId: 'steering-event-1',
323+
},
324+
],
325+
current,
326+
),
327+
() => 10,
328+
[],
329+
true,
330+
);
331+
332+
assert.deepEqual(
333+
projector
334+
.seedActive(false)
335+
.filter(
336+
(event): event is Extract<SessionEvent, { type: 'message_admission' }> =>
337+
event.type === 'message_admission',
338+
)
339+
.map((event) => ({
340+
outcome: event.outcome,
341+
turnId: event.turnId,
342+
messageId: event.messageId,
343+
})),
344+
[{ outcome: 'admitted', turnId: 'turn-1', messageId: 'ticket-1' }],
345+
);
346+
});
347+
310348
test('reseeds the latest provider retry when the active Turn still carries one', () => {
311349
const retry = {
312350
phase: 'scheduled' as const,

packages/runtime-host/src/adapter/session-projector.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,24 @@ interface AssistantAccumulator {
4242
}
4343

4444
export interface RuntimeHostSessionProjectionSeed {
45-
readonly durableInFlightMessageIds: readonly string[];
45+
readonly durableSteeringMessages: readonly {
46+
readonly messageId: string;
47+
readonly turnId: string;
48+
}[];
4649
readonly activeAssistantMessages: readonly Extract<StoredMessage, { type: 'assistant' }>[];
4750
}
4851

4952
export function createRuntimeHostSessionProjectionSeed(
5053
transcript: readonly StoredMessage[],
5154
snapshot: SessionContinuitySnapshot,
5255
): RuntimeHostSessionProjectionSeed {
53-
const inFlightMessageIds = new Set(
54-
rootQueueInFlight(snapshot.queue).map((entry) => entry.messageId),
55-
);
5656
return {
57-
durableInFlightMessageIds: transcript
58-
.filter((message) => inFlightMessageIds.has(message.id))
59-
.map((message) => message.id),
57+
durableSteeringMessages: transcript
58+
.filter(
59+
(message): message is Extract<StoredMessage, { type: 'user' }> =>
60+
message.type === 'user' && message.steeringEventId !== undefined,
61+
)
62+
.map((message) => ({ messageId: message.id, turnId: message.turnId })),
6063
activeAssistantMessages:
6164
snapshot.rootTurn === null
6265
? []
@@ -84,6 +87,7 @@ export class RuntimeHostSessionProjector {
8487
#snapshot: SessionContinuitySnapshot;
8588
readonly #now: () => number;
8689
readonly #transcriptIds: Set<string>;
90+
readonly #durableSteeringTurnByMessage: ReadonlyMap<string, string>;
8791
readonly #accumulators = new Map<string, AssistantAccumulator>();
8892
#projectMessageAdmissions: boolean;
8993

@@ -96,7 +100,10 @@ export class RuntimeHostSessionProjector {
96100
) {
97101
this.#snapshot = structuredClone(snapshot);
98102
this.#now = now;
99-
this.#transcriptIds = new Set(seed.durableInFlightMessageIds);
103+
this.#durableSteeringTurnByMessage = new Map(
104+
seed.durableSteeringMessages.map(({ messageId, turnId }) => [messageId, turnId]),
105+
);
106+
this.#transcriptIds = new Set(this.#durableSteeringTurnByMessage.keys());
100107
this.#projectMessageAdmissions = projectMessageAdmissions;
101108
const root = snapshot.rootTurn;
102109
if (!root) return;
@@ -158,6 +165,9 @@ export class RuntimeHostSessionProjector {
158165
...new Set([
159166
...this.#snapshot.rootTurnSourceMessageIds,
160167
...rootQueueInFlight(this.#snapshot.queue).map((entry) => entry.messageId),
168+
...[...this.#durableSteeringTurnByMessage]
169+
.filter(([, turnId]) => turnId === root.turnId)
170+
.map(([messageId]) => messageId),
161171
]),
162172
],
163173
this.#now(),

0 commit comments

Comments
 (0)