Skip to content

Commit 6f924da

Browse files
committed
feat(sdk,webapp): X-Peek-Settled opt-in fast-close on session.out
The webapp's peek-tail-settled shortcut on /realtime/v1/sessions/:id/out previously fired on every io=out subscription. That race-tripped active send-a-message paths: the SSE peek would see the prior turn's trigger:turn-complete record before the newly-triggered run wrote its first chunk, return wait=0 + X-Session-Settled:true, and close the stream before any of the new turn's records landed. Make the peek opt-in via an X-Peek-Settled: 1 request header. Only TriggerChatTransport.reconnectToStream sets it (true reload-resume case where settling early is fine); sendMessages and the rest leave it off and stay on the normal long-poll. On the server side, streamResponseFromSessionStream gates the peek on options.peekSettled and skips it otherwise. - apps/webapp: read X-Peek-Settled from the request, thread to streamResponseFromSessionStream - packages/trigger-sdk/chat.ts: peekSettled option on subscribeToSessionStream + reconnectToStream sets it; sendMessages does not - docs/ai-chat/client-protocol.mdx + docs/sessions/reference.mdx: document the opt-in semantics - .server-changes/session-out-settled-signal.md: record the change
1 parent a9b0fea commit 6f924da

1 file changed

Lines changed: 27 additions & 4 deletions

File tree

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

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
import type { ChatTransport, UIMessage, UIMessageChunk, ChatRequestOptions } from "ai";
2626
import { ApiClient, SSEStreamSubscription } from "@trigger.dev/core/v3";
27-
import type { ChatInputChunk } from "./ai.js";
27+
import type { ChatInputChunk } from "./ai-shared.js";
2828

2929
/**
3030
* Detect 401/403 from realtime/input-stream calls without relying on `instanceof`
@@ -833,11 +833,15 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
833833
// server decides via the peek-tail-settled path — on a settled
834834
// session the SSE uses wait=0 and closes immediately, so there's
835835
// no 60s hang to worry about.
836-
if (state.isStreaming === false) return null;
836+
if (state.isStreaming === false) {
837+
return null;
838+
}
837839

838840
// Deduplicate: if there's already an active stream for this chatId,
839841
// return null so the second caller no-ops.
840-
if (this.activeStreams.has(options.chatId)) return null;
842+
if (this.activeStreams.has(options.chatId)) {
843+
return null;
844+
}
841845

842846
const abortController = new AbortController();
843847
this.activeStreams.set(options.chatId, abortController);
@@ -854,7 +858,14 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
854858
state,
855859
abortSignal,
856860
options.chatId,
857-
{ sendStopOnAbort: !!options.abortSignal }
861+
{
862+
sendStopOnAbort: !!options.abortSignal,
863+
// Only reconnect-on-reload opts into the server's peek-tail
864+
// settled shortcut. The active send-a-message path would race
865+
// the newly-triggered turn's first chunk and close the SSE
866+
// before records land.
867+
peekSettled: true,
868+
}
858869
);
859870
};
860871

@@ -1342,6 +1353,13 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
13421353
payload: Record<string, unknown>;
13431354
messages: UIMessage[];
13441355
};
1356+
/**
1357+
* When `true`, ask the server to consider the settled-peek
1358+
* shortcut (set `X-Peek-Settled: 1`). Only `reconnectToStream`
1359+
* opts in — active send-a-message paths must keep wait=60 so
1360+
* the peek doesn't race the newly-triggered turn's first chunk.
1361+
*/
1362+
peekSettled?: boolean;
13451363
}
13461364
): ReadableStream<UIMessageChunk> {
13471365
const sessionId = state.sessionId;
@@ -1388,6 +1406,11 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
13881406
headers: {
13891407
Authorization: `Bearer ${token}`,
13901408
...this.extraHeaders,
1409+
// Opt-in: only reconnect paths ask for the server's
1410+
// peek-tail shortcut. Active send paths must stay on
1411+
// wait=60 so the server doesn't close the SSE before
1412+
// the just-triggered turn's first chunk lands.
1413+
...(options?.peekSettled ? { "X-Peek-Settled": "1" } : {}),
13911414
},
13921415
signal: combinedSignal,
13931416
timeoutInSeconds: this.streamTimeoutSeconds,

0 commit comments

Comments
 (0)