Skip to content

Commit 4783419

Browse files
authored
fix(sdk): stop chat.createSession wedging on stop and erroring on continuation boots (#3920)
## Summary Two `chat.createSession()` bugs that break chats at its abstraction level: 1. **Stopping a generation wedged the run forever.** `turn.complete()` bare-awaited the AI SDK's `totalUsage` promise, which never settles after a stop-abort. The run stayed stuck inside the stopped turn (trace shows a permanently partial `ai.streamText` span and no further `waiting for next message`), so the chat could never take another message. Fixed with the same 2s `Promise.race` guard `chat.agent`'s turn loop already uses. 2. **Continuation runs invoked the model with an empty prompt.** The first turn only waited for a message on `preload` boots. A continuation run (spawned after a cancel, crash, or version upgrade) arrives with the boot payload stripped, so the loop ran a turn with zero messages and errored with `AI_InvalidPromptError: messages must not be empty`. Message-less continuation boots now wait for the next session input ("waiting for first message (continuation)"), and `turn.continuation` is preserved across the wait so user code can seed stored history off it. Both reproduced and verified end-to-end against a live environment (stop followed by a next turn; cancel followed by a continuation turn with seeded history), plus the existing unit suite.
1 parent a04cdff commit 4783419

2 files changed

Lines changed: 37 additions & 8 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@trigger.dev/sdk": patch
3+
---
4+
5+
Fix two `chat.createSession()` bugs: stopping a generation no longer wedges the run (the turn loop raced a `totalUsage` promise that never settles after a stop-abort), and continuation runs now wait for the next message instead of invoking the model with an empty prompt.

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

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8988,19 +8988,35 @@ function createChatSession(
89888988
async next(): Promise<IteratorResult<ChatTurn>> {
89898989
turn++;
89908990

8991-
// First turn: handle preload — wait for the first real message
8992-
if (turn === 0 && currentPayload.trigger === "preload") {
8991+
// First turn: wait when the boot payload carries no message.
8992+
// Preload boots wait for the first real message; continuation
8993+
// boots (fresh run via `ensureRunForSession` / end-and-continue)
8994+
// arrive with the sticky boot-payload fields stripped, so running
8995+
// a turn immediately would invoke the model with no user input.
8996+
const isMessagelessContinuationBoot =
8997+
currentPayload.continuation === true && !currentPayload.message;
8998+
if (turn === 0 && (currentPayload.trigger === "preload" || isMessagelessContinuationBoot)) {
89938999
const result = await messagesInput.waitWithIdleTimeout({
89949000
idleTimeoutInSeconds:
89959001
sessionIdleTimeoutOpt ?? currentPayload.idleTimeoutInSeconds ?? 30,
89969002
timeout,
8997-
spanName: "waiting for first message",
9003+
spanName:
9004+
currentPayload.trigger === "preload"
9005+
? "waiting for first message"
9006+
: "waiting for first message (continuation)",
89989007
});
89999008
if (!result.ok || runSignal.aborted) {
90009009
stop.cleanup();
90019010
return { done: true, value: undefined };
90029011
}
9012+
const continuationBoot = isMessagelessContinuationBoot;
90039013
currentPayload = result.output;
9014+
// Preserve the continuation flag — the wire payload of the next
9015+
// message doesn't carry it, and `turn.continuation` is how the
9016+
// user knows to seed history (e.g. `turn.setMessages(stored)`).
9017+
if (continuationBoot && currentPayload.continuation === undefined) {
9018+
currentPayload = { ...currentPayload, continuation: true };
9019+
}
90049020
}
90059021

90069022
// Subsequent turns: wait for the next message
@@ -9170,14 +9186,22 @@ function createChatSession(
91709186
}
91719187
}
91729188

9173-
// Capture token usage from the streamText result
9189+
// Capture token usage from the streamText result. Race with a 2s
9190+
// timeout — on stop-abort the AI SDK's totalUsage promise can hang
9191+
// indefinitely, which would wedge the turn loop (same guard as
9192+
// chat.agent's turn loop).
91749193
let turnUsage: LanguageModelUsage | undefined;
91759194
if (typeof (source as any).totalUsage?.then === "function") {
91769195
try {
9177-
const usage: LanguageModelUsage = await (source as any).totalUsage;
9178-
turnUsage = usage;
9179-
previousTurnUsage = usage;
9180-
cumulativeUsage = addUsage(cumulativeUsage, usage);
9196+
const usage = (await Promise.race([
9197+
(source as any).totalUsage,
9198+
new Promise<undefined>((r) => setTimeout(() => r(undefined), 2_000)),
9199+
])) as LanguageModelUsage | undefined;
9200+
if (usage) {
9201+
turnUsage = usage;
9202+
previousTurnUsage = usage;
9203+
cumulativeUsage = addUsage(cumulativeUsage, usage);
9204+
}
91819205
} catch {
91829206
/* non-fatal */
91839207
}

0 commit comments

Comments
 (0)