@@ -2472,7 +2472,11 @@ async function findSessionInReplayWindowEnd(
24722472 */
24732473async function installChatInputRouter(
24742474 chatId: string,
2475- options?: { fallbackResumeFrom?: number; recoveredThrough?: number; resuming?: boolean }
2475+ options?: {
2476+ fallbackResumeFrom?: number;
2477+ recoveredSeqNums?: readonly number[];
2478+ resuming?: boolean;
2479+ }
24762480): Promise<SessionChannelRouter> {
24772481 const entry = chatInputRouterEntry(chatId);
24782482 if (entry.attached) return entry.router;
@@ -2503,20 +2507,13 @@ async function installChatInputRouter(
25032507 }
25042508 }
25052509
2506- // A boot that replayed `.in` itself has already answered everything up to
2507- // `recoveredThrough`, so the floor has to cover it before the tail opens.
2508- if (options?.recoveredThrough !== undefined) {
2509- const recovered = options.recoveredThrough;
2510- checkpoint.resumeFrom = Math.max(checkpoint.resumeFrom ?? recovered, recovered);
2511- checkpoint.appliedThrough = Math.max(
2512- checkpoint.appliedThrough ?? checkpoint.resumeFrom,
2513- checkpoint.resumeFrom
2514- );
2515- }
2516-
25172510 const router = entry.router;
25182511 router.restore(checkpoint);
25192512
2513+ if (options?.recoveredSeqNums && options.recoveredSeqNums.length > 0) {
2514+ router.markRecovered(options.recoveredSeqNums);
2515+ }
2516+
25202517 const floor = router.resumeFrom();
25212518 if (floor !== undefined) {
25222519 sessionStreams.setLastSeqNum(chatId, "in", floor);
@@ -7211,6 +7208,19 @@ function chatAgent<
72117208 // `messagesInput.waitWithIdleTimeout` so recovered turns fire first.
72127209 const bootInjectedQueue: ChatTaskWirePayload<TUIMessage, inferSchemaIn<TClientDataSchema>>[] =
72137210 [];
7211+ const recoveredSeqByPayload = new WeakMap<
7212+ ChatTaskWirePayload<TUIMessage, inferSchemaIn<TClientDataSchema>>,
7213+ number
7214+ >();
7215+ const dispatchBootInjected = (): ChatTaskWirePayload<
7216+ TUIMessage,
7217+ inferSchemaIn<TClientDataSchema>
7218+ > => {
7219+ const injected = bootInjectedQueue.shift()!;
7220+ const settledSeq = recoveredSeqByPayload.get(injected);
7221+ if (settledSeq !== undefined) chatInputRouter().settleRecovered(settledSeq);
7222+ return injected;
7223+ };
72147224 const couldHavePriorState = payload.continuation === true || ctx.attempt.number > 1;
72157225
72167226 // `.in` resume cursor, computed at most once per boot. The boot
@@ -7355,18 +7365,11 @@ function chatAgent<
73557365
73567366 // ── session.in router ──────────────────────────────────────────
73577367 //
7358- // Reads the turn boundary and subscribes in one call. `bootInCursor` is
7359- // only a fallback: the boot block above may already have resolved a
7360- // cursor from the snapshot, which is used when the boundary itself
7361- // carries none. Everything the boot replayed off `.in` is dispatched from
7362- // `bootInjectedQueue` below, so it goes into the floor here — folded in
7363- // after the subscription opens, the live tail re-delivers it as a turn.
7364- const lastRecoveredInSeq =
7365- replayedInTail.length > 0 ? replayedInTail[replayedInTail.length - 1]!.seqNum : undefined;
7368+ const recoveredSeqNums = replayedInTail.map((r) => r.seqNum);
73667369
73677370 await installChatInputRouter(payload.chatId, {
73687371 fallbackResumeFrom: bootInCursorResolved ? bootInCursor : undefined,
7369- recoveredThrough: lastRecoveredInSeq ,
7372+ recoveredSeqNums ,
73707373 resuming: Boolean(payload.continuation) || ctx.attempt.number > 1,
73717374 });
73727375
@@ -7456,7 +7459,7 @@ function chatAgent<
74567459 // branches: at n=1 the orphan partial is dropped and the interrupted
74577460 // user is re-dispatched as a fresh turn instead.
74587461 let seedChain: TUIMessage[];
7459- let recoveredTurns: TUIMessage[];
7462+ let recoveredEntries: { message: TUIMessage; seqNum: number | undefined } [];
74607463 if (hookChain !== undefined) {
74617464 seedChain = hookChain;
74627465 } else if (partialAssistant !== undefined && inFlightUsers.length > 1) {
@@ -7465,11 +7468,20 @@ function chatAgent<
74657468 seedChain = settledMessages;
74667469 }
74677470 if (hookRecoveredTurns !== undefined) {
7468- recoveredTurns = hookRecoveredTurns;
7471+ const seqNumByRecoveredId = new Map<string, number>();
7472+ for (const entry of replayedInTail) {
7473+ seqNumByRecoveredId.set(entry.message.id, entry.seqNum);
7474+ }
7475+ recoveredEntries = hookRecoveredTurns.map((message) => ({
7476+ message,
7477+ seqNum: seqNumByRecoveredId.get(message.id),
7478+ }));
74697479 } else if (partialAssistant !== undefined && inFlightUsers.length > 1) {
7470- recoveredTurns = inFlightUsers.slice(1);
7480+ recoveredEntries = replayedInTail
7481+ .slice(1)
7482+ .map((r) => ({ message: r.message, seqNum: r.seqNum }));
74717483 } else {
7472- recoveredTurns = inFlightUsers ;
7484+ recoveredEntries = replayedInTail.map((r) => ({ message: r.message, seqNum: r.seqNum })) ;
74737485 }
74747486 // `beforeBoot` errors bubble — the customer opted into blocking
74757487 // persistence and a failure there should fail the run rather than
@@ -7500,12 +7512,13 @@ function chatAgent<
75007512 for (const entry of replayedInTail) {
75017513 metadataById.set(entry.message.id, entry.metadata);
75027514 }
7503- for (const msg of recoveredTurns) {
7515+ const dispatchedRecoveredSeqs = new Set<number>();
7516+ for (const { message: msg, seqNum } of recoveredEntries) {
75047517 if (wireMessageId && msg.id === wireMessageId) continue;
75057518 const recoveredMetadata = metadataById.has(msg.id)
75067519 ? metadataById.get(msg.id)
75077520 : payload.metadata;
7508- bootInjectedQueue.push( {
7521+ const injectedPayload = {
75097522 chatId: payload.chatId,
75107523 sessionId: payload.sessionId,
75117524 metadata: recoveredMetadata,
@@ -7514,7 +7527,17 @@ function chatAgent<
75147527 messageId: msg.id,
75157528 continuation: payload.continuation,
75167529 previousRunId: payload.previousRunId,
7517- } as ChatTaskWirePayload<TUIMessage, inferSchemaIn<TClientDataSchema>>);
7530+ } as ChatTaskWirePayload<TUIMessage, inferSchemaIn<TClientDataSchema>>;
7531+ bootInjectedQueue.push(injectedPayload);
7532+ if (seqNum !== undefined) {
7533+ recoveredSeqByPayload.set(injectedPayload, seqNum);
7534+ dispatchedRecoveredSeqs.add(seqNum);
7535+ }
7536+ }
7537+ for (const entry of replayedInTail) {
7538+ if (!dispatchedRecoveredSeqs.has(entry.seqNum)) {
7539+ chatInputRouter().settleRecovered(entry.seqNum);
7540+ }
75187541 }
75197542
75207543 accumulatedUIMessages = seedChain;
@@ -7683,7 +7706,7 @@ function chatAgent<
76837706 */
76847707 let dispatchedRecoveredFirstTurn = false;
76857708 if (preloaded && bootInjectedQueue.length > 0) {
7686- currentWirePayload = bootInjectedQueue.shift()! ;
7709+ currentWirePayload = dispatchBootInjected() ;
76877710 dispatchedRecoveredFirstTurn = true;
76887711 }
76897712
@@ -7934,7 +7957,7 @@ function chatAgent<
79347957 // waiting on the live session.in. Subsequent recovered turns
79357958 // get drained by the end-of-turn picker below.
79367959 if (bootInjectedQueue.length > 0) {
7937- currentWirePayload = bootInjectedQueue.shift()! ;
7960+ currentWirePayload = dispatchBootInjected() ;
79387961 } else {
79397962 const effectiveIdleTimeout = idleTimeoutInSeconds ?? payload.idleTimeoutInSeconds;
79407963 const effectiveTurnTimeout =
@@ -9478,7 +9501,7 @@ function chatAgent<
94789501 // produced these from in-flight user messages on session.in
94799502 // that the dead predecessor never acknowledged.
94809503 if (bootInjectedQueue.length > 0) {
9481- currentWirePayload = bootInjectedQueue.shift()! ;
9504+ currentWirePayload = dispatchBootInjected() ;
94829505 return "continue";
94839506 }
94849507
@@ -9849,7 +9872,7 @@ function chatAgent<
98499872 // recovered turn shouldn't strand the rest of the boot queue
98509873 // until an unrelated live message arrives.
98519874 if (bootInjectedQueue.length > 0) {
9852- currentWirePayload = bootInjectedQueue.shift()! ;
9875+ currentWirePayload = dispatchBootInjected() ;
98539876 continue;
98549877 }
98559878
0 commit comments