@@ -2322,7 +2322,11 @@ async function findSessionInReplayWindowEnd(
23222322 */
23232323async function installChatInputRouter(
23242324 chatId: string,
2325- options?: { fallbackResumeFrom?: number; recoveredThrough?: number; resuming?: boolean }
2325+ options?: {
2326+ fallbackResumeFrom?: number;
2327+ recoveredSeqNums?: readonly number[];
2328+ resuming?: boolean;
2329+ }
23262330): Promise<SessionChannelRouter> {
23272331 const entry = chatInputRouterEntry(chatId);
23282332 if (entry.attached) return entry.router;
@@ -2353,20 +2357,13 @@ async function installChatInputRouter(
23532357 }
23542358 }
23552359
2356- // A boot that replayed `.in` itself has already answered everything up to
2357- // `recoveredThrough`, so the floor has to cover it before the tail opens.
2358- if (options?.recoveredThrough !== undefined) {
2359- const recovered = options.recoveredThrough;
2360- checkpoint.resumeFrom = Math.max(checkpoint.resumeFrom ?? recovered, recovered);
2361- checkpoint.appliedThrough = Math.max(
2362- checkpoint.appliedThrough ?? checkpoint.resumeFrom,
2363- checkpoint.resumeFrom
2364- );
2365- }
2366-
23672360 const router = entry.router;
23682361 router.restore(checkpoint);
23692362
2363+ if (options?.recoveredSeqNums && options.recoveredSeqNums.length > 0) {
2364+ router.markRecovered(options.recoveredSeqNums);
2365+ }
2366+
23702367 const floor = router.resumeFrom();
23712368 if (floor !== undefined) {
23722369 sessionStreams.setLastSeqNum(chatId, "in", floor);
@@ -7273,6 +7270,20 @@ function chatAgent<
72737270 // `messagesInput.waitWithIdleTimeout` so recovered turns fire first.
72747271 const bootInjectedQueue: ChatTaskWirePayload<TUIMessage, inferSchemaIn<TClientDataSchema>>[] =
72757272 [];
7273+ const recoveredSeqByPayload = new WeakMap<
7274+ ChatTaskWirePayload<TUIMessage, inferSchemaIn<TClientDataSchema>>,
7275+ number
7276+ >();
7277+ const dispatchBootInjected = (): ChatTaskWirePayload<
7278+ TUIMessage,
7279+ inferSchemaIn<TClientDataSchema>
7280+ > => bootInjectedQueue.shift()!;
7281+ const settleRecoveredTurn = (
7282+ wirePayload: ChatTaskWirePayload<TUIMessage, inferSchemaIn<TClientDataSchema>>
7283+ ) => {
7284+ const settledSeq = recoveredSeqByPayload.get(wirePayload);
7285+ if (settledSeq !== undefined) chatInputRouter().settleRecovered(settledSeq);
7286+ };
72767287 const couldHavePriorState = payload.continuation === true || ctx.attempt.number > 1;
72777288
72787289 // `.in` resume cursor, computed at most once per boot. The boot
@@ -7438,18 +7449,11 @@ function chatAgent<
74387449
74397450 // ── session.in router ──────────────────────────────────────────
74407451 //
7441- // Reads the turn boundary and subscribes in one call. `bootInCursor` is
7442- // only a fallback: the boot block above may already have resolved a
7443- // cursor from the snapshot, which is used when the boundary itself
7444- // carries none. Everything the boot replayed off `.in` is dispatched from
7445- // `bootInjectedQueue` below, so it goes into the floor here — folded in
7446- // after the subscription opens, the live tail re-delivers it as a turn.
7447- const lastRecoveredInSeq =
7448- replayedInTail.length > 0 ? replayedInTail[replayedInTail.length - 1]!.seqNum : undefined;
7452+ const recoveredSeqNums = replayedInTail.map((r) => r.seqNum);
74497453
74507454 await installChatInputRouter(payload.chatId, {
74517455 fallbackResumeFrom: bootInCursorResolved ? bootInCursor : undefined,
7452- recoveredThrough: lastRecoveredInSeq ,
7456+ recoveredSeqNums ,
74537457 resuming: Boolean(payload.continuation) || ctx.attempt.number > 1,
74547458 });
74557459
@@ -7539,7 +7543,7 @@ function chatAgent<
75397543 // branches: at n=1 the orphan partial is dropped and the interrupted
75407544 // user is re-dispatched as a fresh turn instead.
75417545 let seedChain: TUIMessage[];
7542- let recoveredTurns: TUIMessage[];
7546+ let recoveredEntries: { message: TUIMessage; seqNum: number | undefined } [];
75437547 if (hookChain !== undefined) {
75447548 seedChain = hookChain;
75457549 } else if (partialAssistant !== undefined && inFlightUsers.length > 1) {
@@ -7548,11 +7552,22 @@ function chatAgent<
75487552 seedChain = settledMessages;
75497553 }
75507554 if (hookRecoveredTurns !== undefined) {
7551- recoveredTurns = hookRecoveredTurns;
7555+ const seqNumsByRecoveredId = new Map<string, number[]>();
7556+ for (const entry of replayedInTail) {
7557+ const existing = seqNumsByRecoveredId.get(entry.message.id);
7558+ if (existing) existing.push(entry.seqNum);
7559+ else seqNumsByRecoveredId.set(entry.message.id, [entry.seqNum]);
7560+ }
7561+ recoveredEntries = hookRecoveredTurns.map((message) => ({
7562+ message,
7563+ seqNum: seqNumsByRecoveredId.get(message.id)?.shift(),
7564+ }));
75527565 } else if (partialAssistant !== undefined && inFlightUsers.length > 1) {
7553- recoveredTurns = inFlightUsers.slice(1);
7566+ recoveredEntries = replayedInTail
7567+ .slice(1)
7568+ .map((r) => ({ message: r.message, seqNum: r.seqNum }));
75547569 } else {
7555- recoveredTurns = inFlightUsers ;
7570+ recoveredEntries = replayedInTail.map((r) => ({ message: r.message, seqNum: r.seqNum })) ;
75567571 }
75577572 // `beforeBoot` errors bubble — the customer opted into blocking
75587573 // persistence and a failure there should fail the run rather than
@@ -7583,12 +7598,13 @@ function chatAgent<
75837598 for (const entry of replayedInTail) {
75847599 metadataById.set(entry.message.id, entry.metadata);
75857600 }
7586- for (const msg of recoveredTurns) {
7601+ const dispatchedRecoveredSeqs = new Set<number>();
7602+ for (const { message: msg, seqNum } of recoveredEntries) {
75877603 if (wireMessageId && msg.id === wireMessageId) continue;
75887604 const recoveredMetadata = metadataById.has(msg.id)
75897605 ? metadataById.get(msg.id)
75907606 : payload.metadata;
7591- bootInjectedQueue.push( {
7607+ const injectedPayload = {
75927608 chatId: payload.chatId,
75937609 sessionId: payload.sessionId,
75947610 metadata: recoveredMetadata,
@@ -7597,7 +7613,17 @@ function chatAgent<
75977613 messageId: msg.id,
75987614 continuation: payload.continuation,
75997615 previousRunId: payload.previousRunId,
7600- } as ChatTaskWirePayload<TUIMessage, inferSchemaIn<TClientDataSchema>>);
7616+ } as ChatTaskWirePayload<TUIMessage, inferSchemaIn<TClientDataSchema>>;
7617+ bootInjectedQueue.push(injectedPayload);
7618+ if (seqNum !== undefined) {
7619+ recoveredSeqByPayload.set(injectedPayload, seqNum);
7620+ dispatchedRecoveredSeqs.add(seqNum);
7621+ }
7622+ }
7623+ for (const entry of replayedInTail) {
7624+ if (!dispatchedRecoveredSeqs.has(entry.seqNum)) {
7625+ chatInputRouter().settleRecovered(entry.seqNum);
7626+ }
76017627 }
76027628
76037629 accumulatedUIMessages = seedChain;
@@ -7781,7 +7807,7 @@ function chatAgent<
77817807 */
77827808 let dispatchedRecoveredFirstTurn = false;
77837809 if (preloaded && bootInjectedQueue.length > 0) {
7784- currentWirePayload = bootInjectedQueue.shift()! ;
7810+ currentWirePayload = dispatchBootInjected() ;
77857811 dispatchedRecoveredFirstTurn = true;
77867812 }
77877813
@@ -8032,7 +8058,7 @@ function chatAgent<
80328058 // waiting on the live session.in. Subsequent recovered turns
80338059 // get drained by the end-of-turn picker below.
80348060 if (bootInjectedQueue.length > 0) {
8035- currentWirePayload = bootInjectedQueue.shift()! ;
8061+ currentWirePayload = dispatchBootInjected() ;
80368062 } else {
80378063 const effectiveIdleTimeout = idleTimeoutInSeconds ?? payload.idleTimeoutInSeconds;
80388064 const effectiveTurnTimeout =
@@ -8687,6 +8713,7 @@ function chatAgent<
86878713 chatId: currentWirePayload.chatId,
86888714 messageId: currentWirePayload.messageId,
86898715 });
8716+ settleRecoveredTurn(currentWirePayload);
86908717 await writeTurnCompleteChunk(currentWirePayload.chatId);
86918718 // Not a turn — don't consume an iteration.
86928719 turn--;
@@ -9483,6 +9510,8 @@ function chatAgent<
94839510 locals.set(chatResponsePartsKey, []);
94849511 }
94859512
9513+ settleRecoveredTurn(currentWirePayload);
9514+
94869515 // Write turn-complete control chunk — closes the frontend stream.
94879516 const turnCompleteResult = await writeTurnCompleteChunk(
94889517 currentWirePayload.chatId,
@@ -9613,7 +9642,7 @@ function chatAgent<
96139642 // produced these from in-flight user messages on session.in
96149643 // that the dead predecessor never acknowledged.
96159644 if (bootInjectedQueue.length > 0) {
9616- currentWirePayload = bootInjectedQueue.shift()! ;
9645+ currentWirePayload = dispatchBootInjected() ;
96179646 return "continue";
96189647 }
96199648
@@ -9989,7 +10018,7 @@ function chatAgent<
998910018 // recovered turn shouldn't strand the rest of the boot queue
999010019 // until an unrelated live message arrives.
999110020 if (bootInjectedQueue.length > 0) {
9992- currentWirePayload = bootInjectedQueue.shift()! ;
10021+ currentWirePayload = dispatchBootInjected() ;
999310022 continue;
999410023 }
999510024
0 commit comments