Skip to content

Commit 174be6c

Browse files
committed
fix(chat): avoid settled peek for active reconnects
1 parent 89a5f66 commit 174be6c

2 files changed

Lines changed: 44 additions & 5 deletions

File tree

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,12 +1207,12 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
12071207
sendStopOnAbort: options.stopOnAbort ?? false,
12081208
sinceInSeq: state.activeInputSeq,
12091209
// Reconnect-on-reload opts into the server's settled-peek shortcut
1210-
// so the SSE doesn't hang for 60s when no turn is in flight. Active
1211-
// send-a-message paths must keep wait=60 to avoid racing the
1212-
// freshly-triggered turn's first chunk. Watch mode must NOT peek: a
1213-
// settled peek between turns sets sessionSettled and closes the
1210+
// so the SSE doesn't hang for 60s when no turn is in flight. A known
1211+
// active input must not peek because the previous turn's completion
1212+
// can remain at the tail until the current turn writes its first chunk.
1213+
// Watch mode must NOT peek: a settled peek between turns closes the
12141214
// standing subscription, so the viewer never sees the next turn.
1215-
peekSettled: !this.watchMode,
1215+
peekSettled: !this.watchMode && state.activeInputSeq === undefined,
12161216
});
12171217
};
12181218

packages/trigger-sdk/test/chat-turn-correlation.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,45 @@ describe("transport turn correlation", () => {
165165
expect(transport.getSession("c1")?.activeInputSeq).toBeUndefined();
166166
});
167167

168+
it("does not request a settled peek while reconnecting a known active input", async () => {
169+
vi.useFakeTimers();
170+
try {
171+
const subscribeHeaders: Headers[] = [];
172+
const transport = new TriggerChatTransport({
173+
task: "test-task",
174+
accessToken: async () => "tok_test",
175+
sessions: {
176+
c1: { publicAccessToken: "tok_test", isStreaming: true, activeInputSeq: 5 },
177+
},
178+
fetch: async (_url, init) => {
179+
const headers = new Headers(init?.headers);
180+
subscribeHeaders.push(headers);
181+
182+
if (subscribeHeaders.length === 1) {
183+
// Match the server shortcut: a peek sees the previous turn's
184+
// boundary at the tail and marks this otherwise-normal EOF settled.
185+
return batchResponse([turnComplete(10, 4)], headers.has("X-Peek-Settled"));
186+
}
187+
188+
return batchResponse([textDelta(11, "current"), turnComplete(12, 5)]);
189+
},
190+
});
191+
192+
const stream = await transport.reconnectToStream({ chatId: "c1" });
193+
194+
expect(stream).not.toBeNull();
195+
const deltas = readDeltas(stream!);
196+
await vi.advanceTimersByTimeAsync(1_000);
197+
await expect(deltas).resolves.toEqual(["current"]);
198+
expect(subscribeHeaders).toHaveLength(2);
199+
expect(subscribeHeaders[0]?.get("X-Peek-Settled")).toBeNull();
200+
expect(transport.getSession("c1")?.isStreaming).toBe(false);
201+
expect(transport.getSession("c1")?.activeInputSeq).toBeUndefined();
202+
} finally {
203+
vi.useRealTimers();
204+
}
205+
});
206+
168207
it("keeps turn correlation through a lost append response, retry, and reload", async () => {
169208
const committed = new Map<string, number>();
170209
const appendPartIds: string[] = [];

0 commit comments

Comments
 (0)