@@ -2319,7 +2319,11 @@ async function findSessionInReplayWindowEnd(
23192319 */
23202320async function installChatInputRouter(
23212321 chatId: string,
2322- options?: { fallbackResumeFrom?: number; recoveredThrough?: number; resuming?: boolean }
2322+ options?: {
2323+ fallbackResumeFrom?: number;
2324+ recoveredSeqNums?: readonly number[];
2325+ resuming?: boolean;
2326+ }
23232327): Promise<SessionChannelRouter> {
23242328 const entry = chatInputRouterEntry(chatId);
23252329 if (entry.attached) return entry.router;
@@ -2350,20 +2354,13 @@ async function installChatInputRouter(
23502354 }
23512355 }
23522356
2353- // A boot that replayed `.in` itself has already answered everything up to
2354- // `recoveredThrough`, so the floor has to cover it before the tail opens.
2355- if (options?.recoveredThrough !== undefined) {
2356- const recovered = options.recoveredThrough;
2357- checkpoint.resumeFrom = Math.max(checkpoint.resumeFrom ?? recovered, recovered);
2358- checkpoint.appliedThrough = Math.max(
2359- checkpoint.appliedThrough ?? checkpoint.resumeFrom,
2360- checkpoint.resumeFrom
2361- );
2362- }
2363-
23642357 const router = entry.router;
23652358 router.restore(checkpoint);
23662359
2360+ if (options?.recoveredSeqNums && options.recoveredSeqNums.length > 0) {
2361+ router.markRecovered(options.recoveredSeqNums);
2362+ }
2363+
23672364 const floor = router.resumeFrom();
23682365 if (floor !== undefined) {
23692366 sessionStreams.setLastSeqNum(chatId, "in", floor);
@@ -7270,6 +7267,19 @@ function chatAgent<
72707267 // `messagesInput.waitWithIdleTimeout` so recovered turns fire first.
72717268 const bootInjectedQueue: ChatTaskWirePayload<TUIMessage, inferSchemaIn<TClientDataSchema>>[] =
72727269 [];
7270+ const recoveredSeqByPayload = new WeakMap<
7271+ ChatTaskWirePayload<TUIMessage, inferSchemaIn<TClientDataSchema>>,
7272+ number
7273+ >();
7274+ const dispatchBootInjected = (): ChatTaskWirePayload<
7275+ TUIMessage,
7276+ inferSchemaIn<TClientDataSchema>
7277+ > => {
7278+ const injected = bootInjectedQueue.shift()!;
7279+ const settledSeq = recoveredSeqByPayload.get(injected);
7280+ if (settledSeq !== undefined) chatInputRouter().settleRecovered(settledSeq);
7281+ return injected;
7282+ };
72737283 const couldHavePriorState = payload.continuation === true || ctx.attempt.number > 1;
72747284
72757285 // `.in` resume cursor, computed at most once per boot. The boot
@@ -7435,18 +7445,11 @@ function chatAgent<
74357445
74367446 // ── session.in router ──────────────────────────────────────────
74377447 //
7438- // Reads the turn boundary and subscribes in one call. `bootInCursor` is
7439- // only a fallback: the boot block above may already have resolved a
7440- // cursor from the snapshot, which is used when the boundary itself
7441- // carries none. Everything the boot replayed off `.in` is dispatched from
7442- // `bootInjectedQueue` below, so it goes into the floor here — folded in
7443- // after the subscription opens, the live tail re-delivers it as a turn.
7444- const lastRecoveredInSeq =
7445- replayedInTail.length > 0 ? replayedInTail[replayedInTail.length - 1]!.seqNum : undefined;
7448+ const recoveredSeqNums = replayedInTail.map((r) => r.seqNum);
74467449
74477450 await installChatInputRouter(payload.chatId, {
74487451 fallbackResumeFrom: bootInCursorResolved ? bootInCursor : undefined,
7449- recoveredThrough: lastRecoveredInSeq ,
7452+ recoveredSeqNums ,
74507453 resuming: Boolean(payload.continuation) || ctx.attempt.number > 1,
74517454 });
74527455
@@ -7536,7 +7539,7 @@ function chatAgent<
75367539 // branches: at n=1 the orphan partial is dropped and the interrupted
75377540 // user is re-dispatched as a fresh turn instead.
75387541 let seedChain: TUIMessage[];
7539- let recoveredTurns: TUIMessage[];
7542+ let recoveredEntries: { message: TUIMessage; seqNum: number | undefined } [];
75407543 if (hookChain !== undefined) {
75417544 seedChain = hookChain;
75427545 } else if (partialAssistant !== undefined && inFlightUsers.length > 1) {
@@ -7545,11 +7548,20 @@ function chatAgent<
75457548 seedChain = settledMessages;
75467549 }
75477550 if (hookRecoveredTurns !== undefined) {
7548- recoveredTurns = hookRecoveredTurns;
7551+ const seqNumByRecoveredId = new Map<string, number>();
7552+ for (const entry of replayedInTail) {
7553+ seqNumByRecoveredId.set(entry.message.id, entry.seqNum);
7554+ }
7555+ recoveredEntries = hookRecoveredTurns.map((message) => ({
7556+ message,
7557+ seqNum: seqNumByRecoveredId.get(message.id),
7558+ }));
75497559 } else if (partialAssistant !== undefined && inFlightUsers.length > 1) {
7550- recoveredTurns = inFlightUsers.slice(1);
7560+ recoveredEntries = replayedInTail
7561+ .slice(1)
7562+ .map((r) => ({ message: r.message, seqNum: r.seqNum }));
75517563 } else {
7552- recoveredTurns = inFlightUsers ;
7564+ recoveredEntries = replayedInTail.map((r) => ({ message: r.message, seqNum: r.seqNum })) ;
75537565 }
75547566 // `beforeBoot` errors bubble — the customer opted into blocking
75557567 // persistence and a failure there should fail the run rather than
@@ -7580,12 +7592,13 @@ function chatAgent<
75807592 for (const entry of replayedInTail) {
75817593 metadataById.set(entry.message.id, entry.metadata);
75827594 }
7583- for (const msg of recoveredTurns) {
7595+ const dispatchedRecoveredSeqs = new Set<number>();
7596+ for (const { message: msg, seqNum } of recoveredEntries) {
75847597 if (wireMessageId && msg.id === wireMessageId) continue;
75857598 const recoveredMetadata = metadataById.has(msg.id)
75867599 ? metadataById.get(msg.id)
75877600 : payload.metadata;
7588- bootInjectedQueue.push( {
7601+ const injectedPayload = {
75897602 chatId: payload.chatId,
75907603 sessionId: payload.sessionId,
75917604 metadata: recoveredMetadata,
@@ -7594,7 +7607,17 @@ function chatAgent<
75947607 messageId: msg.id,
75957608 continuation: payload.continuation,
75967609 previousRunId: payload.previousRunId,
7597- } as ChatTaskWirePayload<TUIMessage, inferSchemaIn<TClientDataSchema>>);
7610+ } as ChatTaskWirePayload<TUIMessage, inferSchemaIn<TClientDataSchema>>;
7611+ bootInjectedQueue.push(injectedPayload);
7612+ if (seqNum !== undefined) {
7613+ recoveredSeqByPayload.set(injectedPayload, seqNum);
7614+ dispatchedRecoveredSeqs.add(seqNum);
7615+ }
7616+ }
7617+ for (const entry of replayedInTail) {
7618+ if (!dispatchedRecoveredSeqs.has(entry.seqNum)) {
7619+ chatInputRouter().settleRecovered(entry.seqNum);
7620+ }
75987621 }
75997622
76007623 accumulatedUIMessages = seedChain;
@@ -7778,7 +7801,7 @@ function chatAgent<
77787801 */
77797802 let dispatchedRecoveredFirstTurn = false;
77807803 if (preloaded && bootInjectedQueue.length > 0) {
7781- currentWirePayload = bootInjectedQueue.shift()! ;
7804+ currentWirePayload = dispatchBootInjected() ;
77827805 dispatchedRecoveredFirstTurn = true;
77837806 }
77847807
@@ -8029,7 +8052,7 @@ function chatAgent<
80298052 // waiting on the live session.in. Subsequent recovered turns
80308053 // get drained by the end-of-turn picker below.
80318054 if (bootInjectedQueue.length > 0) {
8032- currentWirePayload = bootInjectedQueue.shift()! ;
8055+ currentWirePayload = dispatchBootInjected() ;
80338056 } else {
80348057 const effectiveIdleTimeout = idleTimeoutInSeconds ?? payload.idleTimeoutInSeconds;
80358058 const effectiveTurnTimeout =
@@ -9610,7 +9633,7 @@ function chatAgent<
96109633 // produced these from in-flight user messages on session.in
96119634 // that the dead predecessor never acknowledged.
96129635 if (bootInjectedQueue.length > 0) {
9613- currentWirePayload = bootInjectedQueue.shift()! ;
9636+ currentWirePayload = dispatchBootInjected() ;
96149637 return "continue";
96159638 }
96169639
@@ -9986,7 +10009,7 @@ function chatAgent<
998610009 // recovered turn shouldn't strand the rest of the boot queue
998710010 // until an unrelated live message arrives.
998810011 if (bootInjectedQueue.length > 0) {
9989- currentWirePayload = bootInjectedQueue.shift()! ;
10012+ currentWirePayload = dispatchBootInjected() ;
999010013 continue;
999110014 }
999210015
0 commit comments