From d1b7738bee17312a32c636dd243061f239397154 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 12 Jun 2026 12:37:01 +0100 Subject: [PATCH] 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. --- .../create-session-stop-continuation.md | 5 +++ packages/trigger-sdk/src/v3/ai.ts | 40 +++++++++++++++---- 2 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 .changeset/create-session-stop-continuation.md diff --git a/.changeset/create-session-stop-continuation.md b/.changeset/create-session-stop-continuation.md new file mode 100644 index 0000000000..4ac472a0c0 --- /dev/null +++ b/.changeset/create-session-stop-continuation.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/sdk": patch +--- + +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. diff --git a/packages/trigger-sdk/src/v3/ai.ts b/packages/trigger-sdk/src/v3/ai.ts index 72e0cf1080..e3b3e60549 100644 --- a/packages/trigger-sdk/src/v3/ai.ts +++ b/packages/trigger-sdk/src/v3/ai.ts @@ -8988,19 +8988,35 @@ function createChatSession( async next(): Promise> { turn++; - // First turn: handle preload — wait for the first real message - if (turn === 0 && currentPayload.trigger === "preload") { + // First turn: wait when the boot payload carries no message. + // Preload boots wait for the first real message; continuation + // boots (fresh run via `ensureRunForSession` / end-and-continue) + // arrive with the sticky boot-payload fields stripped, so running + // a turn immediately would invoke the model with no user input. + const isMessagelessContinuationBoot = + currentPayload.continuation === true && !currentPayload.message; + if (turn === 0 && (currentPayload.trigger === "preload" || isMessagelessContinuationBoot)) { const result = await messagesInput.waitWithIdleTimeout({ idleTimeoutInSeconds: sessionIdleTimeoutOpt ?? currentPayload.idleTimeoutInSeconds ?? 30, timeout, - spanName: "waiting for first message", + spanName: + currentPayload.trigger === "preload" + ? "waiting for first message" + : "waiting for first message (continuation)", }); if (!result.ok || runSignal.aborted) { stop.cleanup(); return { done: true, value: undefined }; } + const continuationBoot = isMessagelessContinuationBoot; currentPayload = result.output; + // Preserve the continuation flag — the wire payload of the next + // message doesn't carry it, and `turn.continuation` is how the + // user knows to seed history (e.g. `turn.setMessages(stored)`). + if (continuationBoot && currentPayload.continuation === undefined) { + currentPayload = { ...currentPayload, continuation: true }; + } } // Subsequent turns: wait for the next message @@ -9170,14 +9186,22 @@ function createChatSession( } } - // Capture token usage from the streamText result + // Capture token usage from the streamText result. Race with a 2s + // timeout — on stop-abort the AI SDK's totalUsage promise can hang + // indefinitely, which would wedge the turn loop (same guard as + // chat.agent's turn loop). let turnUsage: LanguageModelUsage | undefined; if (typeof (source as any).totalUsage?.then === "function") { try { - const usage: LanguageModelUsage = await (source as any).totalUsage; - turnUsage = usage; - previousTurnUsage = usage; - cumulativeUsage = addUsage(cumulativeUsage, usage); + const usage = (await Promise.race([ + (source as any).totalUsage, + new Promise((r) => setTimeout(() => r(undefined), 2_000)), + ])) as LanguageModelUsage | undefined; + if (usage) { + turnUsage = usage; + previousTurnUsage = usage; + cumulativeUsage = addUsage(cumulativeUsage, usage); + } } catch { /* non-fatal */ }