Skip to content

Commit 4693292

Browse files
committed
fix(chat,sdk): only treat channel records as replayed when the run is resuming
The previous commit took the replay window from the channel's tail so a stop that no turn boundary covered could not be applied twice. On a first boot that is wrong: nothing has been applied by anyone yet, so anything already on the channel was treated as replayed and dropped. That broke head starts. The client can signal a handover before the agent run has booted, which is the whole point of the flow, and the signal was discarded as already-applied. The agent then waited out its idle window and lost the warm partial. A replay window now only exists for a run that is resuming, which is either a run whose predecessor left a turn boundary or one the wire marks as a continuation or a retried attempt. A first boot has no window and applies what it finds.
1 parent 89b5eb1 commit 4693292

1 file changed

Lines changed: 21 additions & 9 deletions

File tree

  • packages/trigger-sdk/src/v3

packages/trigger-sdk/src/v3/ai.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1970,7 +1970,7 @@ async function findSessionInReplayWindowEnd(
19701970
*/
19711971
async function installChatInputRouter(
19721972
chatId: string,
1973-
options?: { fallbackResumeFrom?: number }
1973+
options?: { fallbackResumeFrom?: number; resuming?: boolean }
19741974
): Promise<SessionChannelRouter> {
19751975
const entry = chatInputRouterEntry(chatId);
19761976
if (entry.attached) return entry.router;
@@ -1986,12 +1986,19 @@ async function installChatInputRouter(
19861986
if (checkpoint.resumeFrom === undefined && options?.fallbackResumeFrom !== undefined) {
19871987
checkpoint.resumeFrom = options.fallbackResumeFrom;
19881988
}
1989-
const replayWindowEnd = await findSessionInReplayWindowEnd(chatId, checkpoint.resumeFrom);
1990-
if (replayWindowEnd !== undefined) {
1991-
checkpoint.appliedThrough = Math.max(
1992-
checkpoint.appliedThrough ?? replayWindowEnd,
1993-
replayWindowEnd
1994-
);
1989+
// Only a resuming run has a replay window. On a first boot nothing has been
1990+
// applied by anyone, so treating what is already on the channel as replayed
1991+
// would discard a signal that arrived before the agent got here, which is
1992+
// exactly how a head-start handover reaches a cold run.
1993+
const resuming = checkpoint.resumeFrom !== undefined || options?.resuming === true;
1994+
if (resuming) {
1995+
const replayWindowEnd = await findSessionInReplayWindowEnd(chatId, checkpoint.resumeFrom);
1996+
if (replayWindowEnd !== undefined) {
1997+
checkpoint.appliedThrough = Math.max(
1998+
checkpoint.appliedThrough ?? replayWindowEnd,
1999+
replayWindowEnd
2000+
);
2001+
}
19952002
}
19962003

19972004
const router = entry.router;
@@ -5590,7 +5597,9 @@ function chatCustomAgent<
55905597
markChatAgentRunForStreamsWarning();
55915598
taskContext.setConversationId(payload.chatId);
55925599
stampConversationIdOnActiveSpan(payload.chatId);
5593-
await installChatInputRouter(payload.chatId);
5600+
await installChatInputRouter(payload.chatId, {
5601+
resuming: Boolean(payload.continuation),
5602+
});
55945603
return userRun(payload, runOptions);
55955604
},
55965605
});
@@ -5943,6 +5952,7 @@ function chatAgent<
59435952
// carries none.
59445953
await installChatInputRouter(payload.chatId, {
59455954
fallbackResumeFrom: bootInCursorResolved ? bootInCursor : undefined,
5955+
resuming: Boolean(payload.continuation) || ctx.attempt.number > 1,
59465956
});
59475957

59485958
// ── Recovery boot + chain reconstruction ────────────────────────
@@ -9773,7 +9783,9 @@ function createChatSession(
97739783
activeMsgSub = undefined;
97749784
if (!booted) {
97759785
booted = true;
9776-
await installChatInputRouter(currentPayload.chatId);
9786+
await installChatInputRouter(currentPayload.chatId, {
9787+
resuming: Boolean(currentPayload.continuation),
9788+
});
97779789
stop = createStopSignal();
97789790
}
97799791
turn++;

0 commit comments

Comments
 (0)