Skip to content

Commit d1b7738

Browse files
committed
fix(sdk): stop chat.createSession wedging on stop and erroring on continuation boots
turn.complete() bare-awaited the AI SDK's totalUsage promise, which never settles after a stop-abort: the run wedged inside the stopped turn and the chat could never take another message. Now raced with a 2s timeout, the same guard chat.agent's turn loop uses. createSession's first turn only waited for a message on preload boots. Continuation runs (spawned after a cancel, crash, or upgrade) arrive with the boot payload stripped, so the loop invoked the model with an empty prompt and the turn errored. Message-less continuation boots now wait for the next session input, and the continuation flag is preserved so user code can seed stored history off turn.continuation.
1 parent 954ee5c commit d1b7738

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)