@trigger.dev/sdk 4.5.16 → 4.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commonjs/imports/ai-runtime-cjs.cjs.map +1 -1
- package/dist/commonjs/imports/ai-runtime.js +2 -0
- package/dist/commonjs/v3/ai-shared.d.ts +15 -0
- package/dist/commonjs/v3/ai-shared.js +35 -1
- package/dist/commonjs/v3/ai-shared.js.map +1 -1
- package/dist/commonjs/v3/ai.d.ts +311 -25
- package/dist/commonjs/v3/ai.js +1633 -380
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/auth.d.ts +10 -4
- package/dist/commonjs/v3/auth.js.map +1 -1
- package/dist/commonjs/v3/chat-client.d.ts +11 -2
- package/dist/commonjs/v3/chat-client.js +227 -111
- package/dist/commonjs/v3/chat-client.js.map +1 -1
- package/dist/commonjs/v3/chat-react.d.ts +88 -0
- package/dist/commonjs/v3/chat-react.js +130 -1
- package/dist/commonjs/v3/chat-react.js.map +1 -1
- package/dist/commonjs/v3/chat-server.d.ts +42 -6
- package/dist/commonjs/v3/chat-server.js +52 -7
- package/dist/commonjs/v3/chat-server.js.map +1 -1
- package/dist/commonjs/v3/chat.d.ts +92 -10
- package/dist/commonjs/v3/chat.js +328 -73
- package/dist/commonjs/v3/chat.js.map +1 -1
- package/dist/commonjs/v3/chatSnapshotIo.d.ts +2 -0
- package/dist/commonjs/v3/chatSnapshotIo.js +173 -0
- package/dist/commonjs/v3/chatSnapshotIo.js.map +1 -0
- package/dist/commonjs/v3/chatVersionSkew.d.ts +12 -0
- package/dist/commonjs/v3/chatVersionSkew.js +30 -0
- package/dist/commonjs/v3/chatVersionSkew.js.map +1 -0
- package/dist/commonjs/v3/externalDeploymentId.d.ts +23 -0
- package/dist/commonjs/v3/externalDeploymentId.js +43 -0
- package/dist/commonjs/v3/externalDeploymentId.js.map +1 -0
- package/dist/commonjs/v3/retry.d.ts +4 -0
- package/dist/commonjs/v3/retry.js +22 -13
- package/dist/commonjs/v3/retry.js.map +1 -1
- package/dist/commonjs/v3/sessions.d.ts +15 -2
- package/dist/commonjs/v3/sessions.js +12 -1
- package/dist/commonjs/v3/sessions.js.map +1 -1
- package/dist/commonjs/v3/shared.js +30 -36
- package/dist/commonjs/v3/shared.js.map +1 -1
- package/dist/commonjs/v3/test/index.d.ts +1 -0
- package/dist/commonjs/v3/test/index.js +3 -1
- package/dist/commonjs/v3/test/index.js.map +1 -1
- package/dist/commonjs/v3/test/mock-chat-agent.d.ts +47 -4
- package/dist/commonjs/v3/test/mock-chat-agent.js +116 -10
- package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/commonjs/v3/test/test-session-handle.js +6 -0
- package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
- package/dist/commonjs/v3/test/transcript-storage-tests.d.ts +40 -0
- package/dist/commonjs/v3/test/transcript-storage-tests.js +258 -0
- package/dist/commonjs/v3/test/transcript-storage-tests.js.map +1 -0
- package/dist/commonjs/v3/transcriptStorage.d.ts +261 -0
- package/dist/commonjs/v3/transcriptStorage.js +374 -0
- package/dist/commonjs/v3/transcriptStorage.js.map +1 -0
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/imports/ai-runtime.d.ts +2 -2
- package/dist/esm/imports/ai-runtime.js +2 -2
- package/dist/esm/imports/ai-runtime.js.map +1 -1
- package/dist/esm/v3/ai-shared.d.ts +15 -0
- package/dist/esm/v3/ai-shared.js +33 -0
- package/dist/esm/v3/ai-shared.js.map +1 -1
- package/dist/esm/v3/ai.d.ts +311 -25
- package/dist/esm/v3/ai.js +1625 -377
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/auth.d.ts +10 -4
- package/dist/esm/v3/auth.js.map +1 -1
- package/dist/esm/v3/chat-client.d.ts +11 -2
- package/dist/esm/v3/chat-client.js +228 -112
- package/dist/esm/v3/chat-client.js.map +1 -1
- package/dist/esm/v3/chat-react.d.ts +88 -0
- package/dist/esm/v3/chat-react.js +127 -1
- package/dist/esm/v3/chat-react.js.map +1 -1
- package/dist/esm/v3/chat-server.d.ts +42 -6
- package/dist/esm/v3/chat-server.js +53 -8
- package/dist/esm/v3/chat-server.js.map +1 -1
- package/dist/esm/v3/chat.d.ts +92 -10
- package/dist/esm/v3/chat.js +329 -74
- package/dist/esm/v3/chat.js.map +1 -1
- package/dist/esm/v3/chatSnapshotIo.d.ts +2 -0
- package/dist/esm/v3/chatSnapshotIo.js +165 -0
- package/dist/esm/v3/chatSnapshotIo.js.map +1 -0
- package/dist/esm/v3/chatVersionSkew.d.ts +12 -0
- package/dist/esm/v3/chatVersionSkew.js +27 -0
- package/dist/esm/v3/chatVersionSkew.js.map +1 -0
- package/dist/esm/v3/externalDeploymentId.d.ts +23 -0
- package/dist/esm/v3/externalDeploymentId.js +38 -0
- package/dist/esm/v3/externalDeploymentId.js.map +1 -0
- package/dist/esm/v3/retry.d.ts +4 -0
- package/dist/esm/v3/retry.js +14 -8
- package/dist/esm/v3/retry.js.map +1 -1
- package/dist/esm/v3/sessions.d.ts +15 -2
- package/dist/esm/v3/sessions.js +11 -1
- package/dist/esm/v3/sessions.js.map +1 -1
- package/dist/esm/v3/shared.js +17 -23
- package/dist/esm/v3/shared.js.map +1 -1
- package/dist/esm/v3/test/index.d.ts +1 -0
- package/dist/esm/v3/test/index.js +1 -0
- package/dist/esm/v3/test/index.js.map +1 -1
- package/dist/esm/v3/test/mock-chat-agent.d.ts +47 -4
- package/dist/esm/v3/test/mock-chat-agent.js +118 -12
- package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/esm/v3/test/test-session-handle.js +6 -0
- package/dist/esm/v3/test/test-session-handle.js.map +1 -1
- package/dist/esm/v3/test/transcript-storage-tests.d.ts +40 -0
- package/dist/esm/v3/test/transcript-storage-tests.js +255 -0
- package/dist/esm/v3/test/transcript-storage-tests.js.map +1 -0
- package/dist/esm/v3/transcriptStorage.d.ts +261 -0
- package/dist/esm/v3/transcriptStorage.js +360 -0
- package/dist/esm/v3/transcriptStorage.js.map +1 -0
- package/dist/esm/version.js +1 -1
- package/docs/ai-chat/actions.mdx +55 -23
- package/docs/ai-chat/anatomy.mdx +47 -27
- package/docs/ai-chat/backend.mdx +125 -48
- package/docs/ai-chat/background-injection.mdx +122 -19
- package/docs/ai-chat/client-protocol.mdx +5 -4
- package/docs/ai-chat/compaction.mdx +13 -7
- package/docs/ai-chat/custom-agents.mdx +23 -0
- package/docs/ai-chat/fast-starts.mdx +27 -20
- package/docs/ai-chat/frontend.mdx +19 -14
- package/docs/ai-chat/how-it-works.mdx +1 -1
- package/docs/ai-chat/lifecycle-hooks.mdx +6 -2
- package/docs/ai-chat/migrating-from-a-route-handler.mdx +16 -14
- package/docs/ai-chat/migrating-from-hydrate-messages.mdx +299 -0
- package/docs/ai-chat/patterns/database-persistence.mdx +24 -9
- package/docs/ai-chat/patterns/native-compaction.mdx +310 -0
- package/docs/ai-chat/patterns/persistence-and-replay.mdx +34 -23
- package/docs/ai-chat/patterns/skills.mdx +7 -10
- package/docs/ai-chat/patterns/version-upgrades.mdx +79 -6
- package/docs/ai-chat/pending-messages.mdx +3 -3
- package/docs/ai-chat/prompt-caching.mdx +23 -25
- package/docs/ai-chat/quick-start.mdx +11 -11
- package/docs/ai-chat/reference.mdx +69 -10
- package/docs/ai-chat/sessions.mdx +6 -1
- package/docs/ai-chat/side-channels.mdx +1 -1
- package/docs/ai-chat/testing.mdx +2 -1
- package/docs/ai-chat/tools.mdx +18 -13
- package/docs/ai-chat/transcript-storage.mdx +247 -0
- package/docs/ai-chat/upgrade-guide.mdx +2 -2
- package/docs/apikeys.mdx +27 -45
- package/docs/cli-env-commands.mdx +114 -0
- package/docs/cli-projects-commands.mdx +62 -0
- package/docs/cli-runs-commands.mdx +94 -0
- package/docs/config/extensions/syncEnvVars.mdx +6 -0
- package/docs/deploy-environment-variables.mdx +23 -2
- package/docs/deployment/overview.mdx +4 -8
- package/docs/deployment/preview-branches.mdx +4 -4
- package/docs/deployment/version-skew-protection.mdx +62 -0
- package/docs/introduction.mdx +58 -152
- package/docs/manual-setup.mdx +7 -7
- package/docs/mcp-tools.mdx +9 -0
- package/docs/quick-start.mdx +3 -3
- package/docs/realtime/auth.mdx +1 -1
- package/docs/reports.mdx +157 -0
- package/docs/self-hosting/security.mdx +17 -0
- package/docs/tasks/scheduled.mdx +24 -0
- package/docs/triggering.mdx +1 -1
- package/docs/troubleshooting-zod.mdx +158 -0
- package/package.json +4 -4
- package/skills/trigger-authoring-chat-agent/SKILL.md +41 -29
- package/skills/trigger-chat-agent-advanced/SKILL.md +54 -19
package/dist/commonjs/v3/chat.js
CHANGED
|
@@ -44,6 +44,32 @@ function isAuthError(error) {
|
|
|
44
44
|
const e = error;
|
|
45
45
|
return e.name === "TriggerApiError" && (e.status === 401 || e.status === 403);
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Detect the closed-session refusal from `.in/append`: HTTP 409 with the
|
|
49
|
+
* stable `session_closed` code. Terminal — never retried.
|
|
50
|
+
*/
|
|
51
|
+
function isSessionClosedError(error) {
|
|
52
|
+
if (error === null || typeof error !== "object")
|
|
53
|
+
return false;
|
|
54
|
+
const e = error;
|
|
55
|
+
return e.name === "TriggerApiError" && e.status === 409 && e.code === "session_closed";
|
|
56
|
+
}
|
|
57
|
+
function sessionClosedReasonOf(error) {
|
|
58
|
+
if (error === null || typeof error !== "object")
|
|
59
|
+
return undefined;
|
|
60
|
+
const reason = error.closedReason;
|
|
61
|
+
return typeof reason === "string" ? reason : undefined;
|
|
62
|
+
}
|
|
63
|
+
/** The local form of the refusal, for sends the transport rejects itself. */
|
|
64
|
+
function sessionClosedError(chatId, reason) {
|
|
65
|
+
const err = new Error(reason ? `Chat session ${chatId} is closed: ${reason}` : `Chat session ${chatId} is closed`);
|
|
66
|
+
err.name = "TriggerApiError";
|
|
67
|
+
err.status = 409;
|
|
68
|
+
err.code = "session_closed";
|
|
69
|
+
if (reason)
|
|
70
|
+
err.closedReason = reason;
|
|
71
|
+
return err;
|
|
72
|
+
}
|
|
47
73
|
/**
|
|
48
74
|
* Detect a 404 from a session-PAT-authed call — i.e. the session doesn't
|
|
49
75
|
* exist in the current environment. Happens when hydrated session state
|
|
@@ -149,6 +175,7 @@ class TriggerChatTransport {
|
|
|
149
175
|
_onSessionChange;
|
|
150
176
|
_onEvent;
|
|
151
177
|
sessions = new Map();
|
|
178
|
+
pendingResumeCursors = new Map();
|
|
152
179
|
activeStreams = new Map();
|
|
153
180
|
pendingStarts = new Map();
|
|
154
181
|
// Last turn-producing send per chat — attribution source for the
|
|
@@ -188,6 +215,8 @@ class TriggerChatTransport {
|
|
|
188
215
|
lastEventId: session.lastEventId,
|
|
189
216
|
activeInputSeq: session.activeInputSeq,
|
|
190
217
|
isStreaming: session.isStreaming,
|
|
218
|
+
closed: session.closed,
|
|
219
|
+
closedReason: session.closedReason,
|
|
191
220
|
});
|
|
192
221
|
}
|
|
193
222
|
}
|
|
@@ -237,6 +266,10 @@ class TriggerChatTransport {
|
|
|
237
266
|
*/
|
|
238
267
|
sendMessages = async (options) => {
|
|
239
268
|
const { trigger, chatId, messageId, messages, abortSignal, body, metadata } = options;
|
|
269
|
+
const closedState = this.sessions.get(chatId);
|
|
270
|
+
if (closedState?.closed) {
|
|
271
|
+
throw sessionClosedError(chatId, closedState.closedReason);
|
|
272
|
+
}
|
|
240
273
|
if (this.coordinator) {
|
|
241
274
|
if (this.coordinator.isReadOnly(chatId)) {
|
|
242
275
|
throw new Error("This chat is active in another tab");
|
|
@@ -246,6 +279,14 @@ class TriggerChatTransport {
|
|
|
246
279
|
const mergedMetadata = this.defaultMetadata || metadata
|
|
247
280
|
? { ...(this.defaultMetadata ?? {}), ...(metadata ?? {}) }
|
|
248
281
|
: undefined;
|
|
282
|
+
// An action sent through `useChat`. `useChatActions` (or any caller) puts
|
|
283
|
+
// it in `body.action`; sending it here rather than through
|
|
284
|
+
// `transport.sendAction` means `useChat` owns the response stream, so an
|
|
285
|
+
// action that becomes a turn renders the way a message turn does.
|
|
286
|
+
const actionInBody = body?.action;
|
|
287
|
+
if (actionInBody !== undefined) {
|
|
288
|
+
return this.sendAction(chatId, actionInBody, { abortSignal, metadata: mergedMetadata });
|
|
289
|
+
}
|
|
249
290
|
// First-turn handover routing — when `headStart` is set AND no
|
|
250
291
|
// session state exists yet for this chatId, POST the wire payload
|
|
251
292
|
// to the customer's `chat.handover` route handler. The handler
|
|
@@ -308,11 +349,9 @@ class TriggerChatTransport {
|
|
|
308
349
|
activeStream.abort();
|
|
309
350
|
this.activeStreams.delete(chatId);
|
|
310
351
|
}
|
|
311
|
-
// A stop that never saw its TURN_COMPLETE leaves the flag set, and the new
|
|
312
|
-
// turn would be skipped record by record.
|
|
313
|
-
state.skipToTurnComplete = false;
|
|
314
352
|
state.activeInputSeq = inSeq;
|
|
315
353
|
state.isStreaming = true;
|
|
354
|
+
state.outstandingTurnAbandoned = false;
|
|
316
355
|
this.notifySessionChange(chatId, state);
|
|
317
356
|
// Owning turn: aborting this live send stops the turn the user drives.
|
|
318
357
|
return this.subscribeToSessionStream(state, abortSignal, chatId, {
|
|
@@ -384,6 +423,16 @@ class TriggerChatTransport {
|
|
|
384
423
|
};
|
|
385
424
|
this.sessions.set(chatId, state);
|
|
386
425
|
this.notifySessionChange(chatId, state);
|
|
426
|
+
// Step 1 streams from the warm server either way; this says the agent run that owes step 2
|
|
427
|
+
// is parked on an undeployed external deployment id.
|
|
428
|
+
if (response.headers.get("X-Trigger-Chat-Pending-Version") === "1") {
|
|
429
|
+
this.emitEvent({
|
|
430
|
+
type: "run-pending-version",
|
|
431
|
+
chatId,
|
|
432
|
+
timestamp: Date.now(),
|
|
433
|
+
source: "head-start",
|
|
434
|
+
});
|
|
435
|
+
}
|
|
387
436
|
// Filter the parsed UIMessage stream:
|
|
388
437
|
// - Drop control chunks (`trigger:turn-complete`,
|
|
389
438
|
// `trigger:session-state`) before they reach AI SDK — they
|
|
@@ -550,6 +599,9 @@ class TriggerChatTransport {
|
|
|
550
599
|
const state = this.sessions.get(options.chatId);
|
|
551
600
|
if (!state)
|
|
552
601
|
return null;
|
|
602
|
+
// A closed session has no further turns to resume.
|
|
603
|
+
if (state.closed)
|
|
604
|
+
return null;
|
|
553
605
|
// Watch is a standing subscription: a settled session is exactly the
|
|
554
606
|
// state it waits in, so a completed last turn must not block the resume.
|
|
555
607
|
if (state.isStreaming === false && !this.watchMode)
|
|
@@ -594,7 +646,13 @@ class TriggerChatTransport {
|
|
|
594
646
|
catch {
|
|
595
647
|
return false;
|
|
596
648
|
}
|
|
597
|
-
|
|
649
|
+
// Only gate when a sent turn is still outstanding. A stop at a boundary has
|
|
650
|
+
// nothing to supersede, and gating it would swallow the next turn.
|
|
651
|
+
if (!state.outstandingTurnAbandoned &&
|
|
652
|
+
(state.isStreaming || state.activeInputSeq !== undefined)) {
|
|
653
|
+
state.skipToTurnComplete = true;
|
|
654
|
+
state.supersededInputSeq = state.activeInputSeq;
|
|
655
|
+
}
|
|
598
656
|
const activeStream = this.activeStreams.get(chatId);
|
|
599
657
|
if (activeStream) {
|
|
600
658
|
activeStream.abort();
|
|
@@ -613,15 +671,28 @@ class TriggerChatTransport {
|
|
|
613
671
|
this.notifySessionChange(chatId, state);
|
|
614
672
|
return true;
|
|
615
673
|
};
|
|
674
|
+
/**
|
|
675
|
+
* Clear the supersede gate armed by a stop. Call this when the stopped
|
|
676
|
+
* turn died without ever writing its `trigger:turn-complete` boundary,
|
|
677
|
+
* otherwise the next turn stays gated forever.
|
|
678
|
+
*/
|
|
679
|
+
clearSupersedeGate = (chatId) => {
|
|
680
|
+
const state = this.sessions.get(chatId);
|
|
681
|
+
if (!state)
|
|
682
|
+
return;
|
|
683
|
+
state.skipToTurnComplete = false;
|
|
684
|
+
state.supersededInputSeq = undefined;
|
|
685
|
+
state.outstandingTurnAbandoned = true;
|
|
686
|
+
};
|
|
616
687
|
/**
|
|
617
688
|
* Send a custom action chunk (for `chat.agent`'s `actionSchema` /
|
|
618
|
-
* `onAction` hook).
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
* `
|
|
622
|
-
*
|
|
689
|
+
* `onAction` hook). An action is an edit: only `hydrateMessages` and
|
|
690
|
+
* `onAction` fire on the agent side, and the returned stream completes
|
|
691
|
+
* with `trigger:turn-complete` once the edit is persisted. When `onAction`
|
|
692
|
+
* returns `chat.turn()` the turn's response follows on the same stream.
|
|
693
|
+
* Per-action `metadata` is merged over the transport's `clientData`.
|
|
623
694
|
*/
|
|
624
|
-
sendAction = async (chatId, action) => {
|
|
695
|
+
sendAction = async (chatId, action, options) => {
|
|
625
696
|
if (this.coordinator) {
|
|
626
697
|
if (this.coordinator.isReadOnly(chatId)) {
|
|
627
698
|
throw new Error("This chat is active in another tab");
|
|
@@ -633,7 +704,9 @@ class TriggerChatTransport {
|
|
|
633
704
|
chatId,
|
|
634
705
|
trigger: "action",
|
|
635
706
|
action,
|
|
636
|
-
metadata: this.defaultMetadata
|
|
707
|
+
metadata: this.defaultMetadata || options?.metadata
|
|
708
|
+
? { ...(this.defaultMetadata ?? {}), ...(options?.metadata ?? {}) }
|
|
709
|
+
: undefined,
|
|
637
710
|
};
|
|
638
711
|
const body = this.serializeInputChunk({ kind: "message", payload: wirePayload });
|
|
639
712
|
const partId = crypto.randomUUID();
|
|
@@ -648,16 +721,14 @@ class TriggerChatTransport {
|
|
|
648
721
|
activeStream.abort();
|
|
649
722
|
this.activeStreams.delete(chatId);
|
|
650
723
|
}
|
|
651
|
-
// A stop that never saw its TURN_COMPLETE leaves the flag set, and the new
|
|
652
|
-
// turn would be skipped record by record.
|
|
653
|
-
state.skipToTurnComplete = false;
|
|
654
724
|
// Mark streaming + persist so a reload mid-action resumes (reconnectToStream
|
|
655
725
|
// no-ops when the persisted session says isStreaming: false).
|
|
656
726
|
state.activeInputSeq = inSeq;
|
|
657
727
|
state.isStreaming = true;
|
|
728
|
+
state.outstandingTurnAbandoned = false;
|
|
658
729
|
this.notifySessionChange(chatId, state);
|
|
659
730
|
// Owning action: aborting this send stops the turn the user drives.
|
|
660
|
-
return this.subscribeToSessionStream(state,
|
|
731
|
+
return this.subscribeToSessionStream(state, options?.abortSignal, chatId, {
|
|
661
732
|
sinceInSeq: inSeq,
|
|
662
733
|
sendStopOnAbort: true,
|
|
663
734
|
});
|
|
@@ -672,14 +743,42 @@ class TriggerChatTransport {
|
|
|
672
743
|
return this.toPersisted(state);
|
|
673
744
|
};
|
|
674
745
|
setSession(chatId, session) {
|
|
675
|
-
this.sessions.set(chatId, {
|
|
746
|
+
this.sessions.set(chatId, this.applyPendingResumeCursor(chatId, {
|
|
676
747
|
publicAccessToken: session.publicAccessToken,
|
|
677
748
|
lastEventId: session.lastEventId,
|
|
678
749
|
activeInputSeq: session.activeInputSeq,
|
|
679
750
|
isStreaming: session.isStreaming,
|
|
680
|
-
});
|
|
751
|
+
}));
|
|
681
752
|
this.notifySessionChange(chatId, this.toPersisted(this.sessions.get(chatId)));
|
|
682
753
|
}
|
|
754
|
+
/**
|
|
755
|
+
* Seed the `.out` resume cursor from a loaded transcript. Applied to the
|
|
756
|
+
* session now if it exists, otherwise held until the session is created so
|
|
757
|
+
* the first live subscription opens past the persisted history instead of
|
|
758
|
+
* replaying it. Never moves an existing cursor backward: the transcript load
|
|
759
|
+
* is async, so a live `.out` record can already have advanced the session
|
|
760
|
+
* past the snapshot, and overwriting it would replay those records.
|
|
761
|
+
*/
|
|
762
|
+
seedResumeCursor = (chatId, lastEventId) => {
|
|
763
|
+
const existing = this.sessions.get(chatId);
|
|
764
|
+
if (existing?.publicAccessToken) {
|
|
765
|
+
if (existing.lastEventId === undefined) {
|
|
766
|
+
existing.lastEventId = lastEventId;
|
|
767
|
+
this.notifySessionChange(chatId, this.toPersisted(existing));
|
|
768
|
+
}
|
|
769
|
+
this.pendingResumeCursors.delete(chatId);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
this.pendingResumeCursors.set(chatId, lastEventId);
|
|
773
|
+
};
|
|
774
|
+
applyPendingResumeCursor(chatId, state) {
|
|
775
|
+
const pending = this.pendingResumeCursors.get(chatId);
|
|
776
|
+
if (pending !== undefined && state.lastEventId === undefined) {
|
|
777
|
+
state.lastEventId = pending;
|
|
778
|
+
}
|
|
779
|
+
this.pendingResumeCursors.delete(chatId);
|
|
780
|
+
return state;
|
|
781
|
+
}
|
|
683
782
|
setOnSessionChange(callback) {
|
|
684
783
|
this._onSessionChange = callback;
|
|
685
784
|
}
|
|
@@ -750,6 +849,22 @@ class TriggerChatTransport {
|
|
|
750
849
|
setClientData(clientData) {
|
|
751
850
|
this.defaultMetadata = clientData;
|
|
752
851
|
}
|
|
852
|
+
/**
|
|
853
|
+
* Update the request-time callbacks. Same reason as `setClientData`:
|
|
854
|
+
* a host that rebuilds these per render (a dashboard navigating between
|
|
855
|
+
* projects) would otherwise keep hitting the endpoint captured at
|
|
856
|
+
* construction. Every read goes through the live field, so the next
|
|
857
|
+
* request uses the latest callback.
|
|
858
|
+
*/
|
|
859
|
+
setAccessToken(accessToken) {
|
|
860
|
+
this.resolveAccessToken = accessToken;
|
|
861
|
+
}
|
|
862
|
+
setStartSession(startSession) {
|
|
863
|
+
this.resolveStartSession = startSession;
|
|
864
|
+
}
|
|
865
|
+
setFetch(fetchOverride) {
|
|
866
|
+
this.fetchOverride = fetchOverride;
|
|
867
|
+
}
|
|
753
868
|
// -------------------------------------------------------------------------
|
|
754
869
|
// Multi-tab coordination passthrough
|
|
755
870
|
// -------------------------------------------------------------------------
|
|
@@ -797,7 +912,42 @@ class TriggerChatTransport {
|
|
|
797
912
|
lastEventId: state.lastEventId,
|
|
798
913
|
activeInputSeq: state.activeInputSeq,
|
|
799
914
|
isStreaming: state.isStreaming,
|
|
915
|
+
closed: state.closed,
|
|
916
|
+
closedReason: state.closedReason,
|
|
800
917
|
});
|
|
918
|
+
/**
|
|
919
|
+
* Flip a session to closed and tell listeners once. Terminal: sends and
|
|
920
|
+
* reconnects stop from here on, and there is no reopening.
|
|
921
|
+
*/
|
|
922
|
+
markSessionClosed(chatId, state, reason, source) {
|
|
923
|
+
if (state.closed)
|
|
924
|
+
return;
|
|
925
|
+
state.closed = true;
|
|
926
|
+
if (reason)
|
|
927
|
+
state.closedReason = reason;
|
|
928
|
+
state.isStreaming = false;
|
|
929
|
+
state.activeInputSeq = undefined;
|
|
930
|
+
state.skipToTurnComplete = false;
|
|
931
|
+
state.supersededInputSeq = undefined;
|
|
932
|
+
this.emitEvent({
|
|
933
|
+
type: "session-closed",
|
|
934
|
+
chatId,
|
|
935
|
+
timestamp: Date.now(),
|
|
936
|
+
...(reason ? { reason } : {}),
|
|
937
|
+
source,
|
|
938
|
+
});
|
|
939
|
+
this.notifySessionChange(chatId, state);
|
|
940
|
+
this.coordinator?.release(chatId);
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Whether the conversation has been closed — by the agent via
|
|
944
|
+
* `chat.close()`, or from outside via `sessions.close()`. Closed is
|
|
945
|
+
* one-way: the transport refuses further sends and reconnects, and the
|
|
946
|
+
* transcript stays readable.
|
|
947
|
+
*/
|
|
948
|
+
sessionStatus = (chatId) => this.sessions.get(chatId)?.closed ? "closed" : "open";
|
|
949
|
+
/** The reason the session was closed, when one was given. */
|
|
950
|
+
sessionClosedReason = (chatId) => this.sessions.get(chatId)?.closedReason;
|
|
801
951
|
notifySessionChange(chatId, session) {
|
|
802
952
|
if (!this._onSessionChange)
|
|
803
953
|
return;
|
|
@@ -832,7 +982,7 @@ class TriggerChatTransport {
|
|
|
832
982
|
// `sessions: { ... }` already, or the very first `accessToken` call
|
|
833
983
|
// returns a PAT for an out-of-band-created session.
|
|
834
984
|
const token = await this.resolveAccessToken({ chatId });
|
|
835
|
-
const state = { publicAccessToken: token };
|
|
985
|
+
const state = this.applyPendingResumeCursor(chatId, { publicAccessToken: token });
|
|
836
986
|
this.sessions.set(chatId, state);
|
|
837
987
|
this.notifySessionChange(chatId, state);
|
|
838
988
|
return state;
|
|
@@ -841,15 +991,23 @@ class TriggerChatTransport {
|
|
|
841
991
|
if (!this.resolveStartSession) {
|
|
842
992
|
throw new Error("TriggerChatTransport: `startSession` is required to call `start()` / `preload()`. Either provide it or pre-hydrate the session via `sessions: { ... }`.");
|
|
843
993
|
}
|
|
844
|
-
const { publicAccessToken } = await this.resolveStartSession({
|
|
994
|
+
const { publicAccessToken, pendingVersion } = await this.resolveStartSession({
|
|
845
995
|
taskId: this.taskId,
|
|
846
996
|
chatId,
|
|
847
997
|
clientData: (this.defaultMetadata ?? {}),
|
|
848
998
|
});
|
|
849
|
-
|
|
999
|
+
if (pendingVersion) {
|
|
1000
|
+
this.emitEvent({
|
|
1001
|
+
type: "run-pending-version",
|
|
1002
|
+
chatId,
|
|
1003
|
+
timestamp: Date.now(),
|
|
1004
|
+
source: "start",
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
const state = this.applyPendingResumeCursor(chatId, {
|
|
850
1008
|
publicAccessToken,
|
|
851
1009
|
isStreaming: false,
|
|
852
|
-
};
|
|
1010
|
+
});
|
|
853
1011
|
this.sessions.set(chatId, state);
|
|
854
1012
|
this.notifySessionChange(chatId, state);
|
|
855
1013
|
return state;
|
|
@@ -862,6 +1020,33 @@ class TriggerChatTransport {
|
|
|
862
1020
|
const raw = this.resolveBaseURLFn(ctx);
|
|
863
1021
|
return raw.replace(/\/$/, "");
|
|
864
1022
|
}
|
|
1023
|
+
/**
|
|
1024
|
+
* `fetchClient` for an `out` subscription. Reads `this.fetchOverride` on
|
|
1025
|
+
* every call, not once per subscription: a watch-mode subscription
|
|
1026
|
+
* reconnects for the lifetime of the page, and `setFetch` may have swapped
|
|
1027
|
+
* the override in between.
|
|
1028
|
+
*/
|
|
1029
|
+
sseFetchClient(ctx) {
|
|
1030
|
+
return ((input, init) => {
|
|
1031
|
+
const override = this.fetchOverride;
|
|
1032
|
+
if (!override)
|
|
1033
|
+
return fetch(input, init);
|
|
1034
|
+
if (typeof input === "string") {
|
|
1035
|
+
return override(input, init ?? {}, ctx);
|
|
1036
|
+
}
|
|
1037
|
+
if (input instanceof URL) {
|
|
1038
|
+
return override(input.toString(), init ?? {}, ctx);
|
|
1039
|
+
}
|
|
1040
|
+
// Request — preserve its url + intrinsic init, let any provided init
|
|
1041
|
+
// override on top (matches fetch(Request, init) semantics).
|
|
1042
|
+
return override(input.url, {
|
|
1043
|
+
method: input.method,
|
|
1044
|
+
headers: input.headers,
|
|
1045
|
+
signal: input.signal,
|
|
1046
|
+
...(init ?? {}),
|
|
1047
|
+
}, ctx);
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
865
1050
|
async doFetch(ctx, url, init) {
|
|
866
1051
|
return this.fetchOverride ? this.fetchOverride(url, init, ctx) : fetch(url, init);
|
|
867
1052
|
}
|
|
@@ -883,24 +1068,65 @@ class TriggerChatTransport {
|
|
|
883
1068
|
const err = new Error(`appendToSessionStream failed: ${response.status} ${text}`);
|
|
884
1069
|
err.name = "TriggerApiError";
|
|
885
1070
|
err.status = response.status;
|
|
1071
|
+
let parsed;
|
|
1072
|
+
try {
|
|
1073
|
+
parsed = JSON.parse(text);
|
|
1074
|
+
}
|
|
1075
|
+
catch {
|
|
1076
|
+
parsed = undefined;
|
|
1077
|
+
}
|
|
1078
|
+
if (typeof parsed?.code === "string")
|
|
1079
|
+
err.code = parsed.code;
|
|
1080
|
+
if (typeof parsed?.closedReason === "string")
|
|
1081
|
+
err.closedReason = parsed.closedReason;
|
|
886
1082
|
throw err;
|
|
887
1083
|
}
|
|
888
1084
|
// The appended record's `.in` seq, for correlating the response stream to
|
|
889
1085
|
// this send. Omitted by older webapps / a lost idempotency claim.
|
|
890
1086
|
const data = (await response.json().catch(() => undefined));
|
|
1087
|
+
if (data?.pendingVersion === true) {
|
|
1088
|
+
this.emitEvent({
|
|
1089
|
+
type: "run-pending-version",
|
|
1090
|
+
chatId,
|
|
1091
|
+
timestamp: Date.now(),
|
|
1092
|
+
source: "send",
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
891
1095
|
return typeof data?.seq === "number" ? data.seq : undefined;
|
|
892
1096
|
}
|
|
893
1097
|
async callWithAuthRetry(chatId, state, op) {
|
|
1098
|
+
if (state.closed) {
|
|
1099
|
+
throw sessionClosedError(chatId, state.closedReason);
|
|
1100
|
+
}
|
|
1101
|
+
// Every attempt goes through here, so a closed-session refusal is terminal
|
|
1102
|
+
// on whichever one hits it. Reaching a 409 from a later attempt and not
|
|
1103
|
+
// marking it would leave local status open on a conversation the server
|
|
1104
|
+
// has already ended.
|
|
1105
|
+
const attempt = async (token) => {
|
|
1106
|
+
try {
|
|
1107
|
+
return await op(token);
|
|
1108
|
+
}
|
|
1109
|
+
catch (err) {
|
|
1110
|
+
if (isSessionClosedError(err)) {
|
|
1111
|
+
// Terminal. Retrying, refreshing the PAT, or recreating the session
|
|
1112
|
+
// would all be wrong: the conversation is over.
|
|
1113
|
+
this.markSessionClosed(chatId, state, sessionClosedReasonOf(err), "append");
|
|
1114
|
+
}
|
|
1115
|
+
throw err;
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
894
1118
|
// 1) Try with the current PAT.
|
|
895
1119
|
try {
|
|
896
|
-
return await
|
|
1120
|
+
return await attempt(state.publicAccessToken);
|
|
897
1121
|
}
|
|
898
1122
|
catch (err) {
|
|
1123
|
+
if (isSessionClosedError(err))
|
|
1124
|
+
throw err;
|
|
899
1125
|
if (isSessionNotFoundError(err)) {
|
|
900
1126
|
// The cached PAT authenticated but the session doesn't exist here —
|
|
901
1127
|
// recreate it and retry.
|
|
902
1128
|
await this.recreateSession(chatId, state);
|
|
903
|
-
return await
|
|
1129
|
+
return await attempt(state.publicAccessToken);
|
|
904
1130
|
}
|
|
905
1131
|
if (!isAuthError(err))
|
|
906
1132
|
throw err;
|
|
@@ -911,7 +1137,7 @@ class TriggerChatTransport {
|
|
|
911
1137
|
state.publicAccessToken = fresh;
|
|
912
1138
|
this.notifySessionChange(chatId, state);
|
|
913
1139
|
try {
|
|
914
|
-
return await
|
|
1140
|
+
return await attempt(fresh);
|
|
915
1141
|
}
|
|
916
1142
|
catch (err) {
|
|
917
1143
|
if (!isSessionNotFoundError(err))
|
|
@@ -921,7 +1147,7 @@ class TriggerChatTransport {
|
|
|
921
1147
|
// state is stale (created in a different environment, or before the
|
|
922
1148
|
// sessions upgrade). Recreate the session and retry once.
|
|
923
1149
|
await this.recreateSession(chatId, state);
|
|
924
|
-
return await
|
|
1150
|
+
return await attempt(state.publicAccessToken);
|
|
925
1151
|
}
|
|
926
1152
|
/**
|
|
927
1153
|
* Repair a stale/phantom hydrated session. The cached state pointed at a
|
|
@@ -937,11 +1163,19 @@ class TriggerChatTransport {
|
|
|
937
1163
|
if (!this.resolveStartSession) {
|
|
938
1164
|
throw new Error("TriggerChatTransport: session not found and no `startSession` configured to recreate it. The stored session state for this chat may be stale (e.g. created in a different environment) — provide `startSession` or clear the stored session so a fresh one can be created.");
|
|
939
1165
|
}
|
|
940
|
-
const { publicAccessToken } = await this.resolveStartSession({
|
|
1166
|
+
const { publicAccessToken, pendingVersion } = await this.resolveStartSession({
|
|
941
1167
|
taskId: this.taskId,
|
|
942
1168
|
chatId,
|
|
943
1169
|
clientData: (this.defaultMetadata ?? {}),
|
|
944
1170
|
});
|
|
1171
|
+
if (pendingVersion) {
|
|
1172
|
+
this.emitEvent({
|
|
1173
|
+
type: "run-pending-version",
|
|
1174
|
+
chatId,
|
|
1175
|
+
timestamp: Date.now(),
|
|
1176
|
+
source: "start",
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
945
1179
|
state.publicAccessToken = publicAccessToken;
|
|
946
1180
|
state.lastEventId = undefined;
|
|
947
1181
|
state.isStreaming = false;
|
|
@@ -965,8 +1199,13 @@ class TriggerChatTransport {
|
|
|
965
1199
|
: internalAbort.signal;
|
|
966
1200
|
if (abortSignal) {
|
|
967
1201
|
abortSignal.addEventListener("abort", () => {
|
|
968
|
-
|
|
1202
|
+
// A late abort (unmount, or the consumer dropping a drained stream)
|
|
1203
|
+
// has no turn to stop: don't gate the next one, don't write a stop.
|
|
1204
|
+
const outstanding = !state.outstandingTurnAbandoned &&
|
|
1205
|
+
(state.isStreaming || state.activeInputSeq !== undefined);
|
|
1206
|
+
if (options?.sendStopOnAbort !== false && outstanding && !internalAbort.signal.aborted) {
|
|
969
1207
|
state.skipToTurnComplete = true;
|
|
1208
|
+
state.supersededInputSeq = state.activeInputSeq;
|
|
970
1209
|
this.appendInputChunk(chatId, state.publicAccessToken, this.serializeInputChunk({ kind: "stop" })).catch(() => { });
|
|
971
1210
|
}
|
|
972
1211
|
internalAbort.abort();
|
|
@@ -1026,26 +1265,7 @@ class TriggerChatTransport {
|
|
|
1026
1265
|
})()
|
|
1027
1266
|
: () => { };
|
|
1028
1267
|
const sseCtx = { endpoint: "out", chatId };
|
|
1029
|
-
const
|
|
1030
|
-
const sseFetchClient = fetchOverride
|
|
1031
|
-
? ((input, init) => {
|
|
1032
|
-
if (typeof input === "string") {
|
|
1033
|
-
return fetchOverride(input, init ?? {}, sseCtx);
|
|
1034
|
-
}
|
|
1035
|
-
if (input instanceof URL) {
|
|
1036
|
-
return fetchOverride(input.toString(), init ?? {}, sseCtx);
|
|
1037
|
-
}
|
|
1038
|
-
// Request — preserve its url + intrinsic init, let any
|
|
1039
|
-
// provided init override on top (matches fetch(Request, init)
|
|
1040
|
-
// semantics).
|
|
1041
|
-
return fetchOverride(input.url, {
|
|
1042
|
-
method: input.method,
|
|
1043
|
-
headers: input.headers,
|
|
1044
|
-
signal: input.signal,
|
|
1045
|
-
...(init ?? {}),
|
|
1046
|
-
}, sseCtx);
|
|
1047
|
-
})
|
|
1048
|
-
: undefined;
|
|
1268
|
+
const sseFetchClient = this.sseFetchClient(sseCtx);
|
|
1049
1269
|
let sawFirstChunk = false;
|
|
1050
1270
|
let sinceInSeq = options?.sinceInSeq;
|
|
1051
1271
|
const connectSseOnce = async (token) => {
|
|
@@ -1093,36 +1313,16 @@ class TriggerChatTransport {
|
|
|
1093
1313
|
return await connectSseOnce(fresh);
|
|
1094
1314
|
}
|
|
1095
1315
|
};
|
|
1096
|
-
// A body that ends without a turn-complete is only terminal when the
|
|
1097
|
-
// server says the session settled — otherwise the turn is still
|
|
1098
|
-
// running and we lost the connection (long-poll window closed, proxy
|
|
1099
|
-
// restarted). Resubscribe from `state.lastEventId`, bounded so a
|
|
1100
|
-
// permanently empty stream can't spin.
|
|
1101
|
-
const MAX_EOF_RESUBSCRIBES = 5;
|
|
1102
1316
|
let eofResubscribes = 0;
|
|
1103
1317
|
const resumeAfterEof = async () => {
|
|
1104
1318
|
// Watch mode is a standing subscription: it outlives turn-complete
|
|
1105
1319
|
// (which clears `isStreaming`) and idle windows EOF by design, so the
|
|
1106
1320
|
// give-up budget doesn't apply. Only abort or a settled session ends it.
|
|
1107
|
-
while ((this.watchMode || (state.isStreaming && eofResubscribes < MAX_EOF_RESUBSCRIBES)) &&
|
|
1321
|
+
while ((this.watchMode || (state.isStreaming && eofResubscribes < ai_shared_js_1.MAX_EOF_RESUBSCRIBES)) &&
|
|
1108
1322
|
!currentSubscription?.sessionSettled &&
|
|
1109
1323
|
!combinedSignal.aborted) {
|
|
1110
1324
|
eofResubscribes++;
|
|
1111
|
-
|
|
1112
|
-
// mid-backoff and the stream stays open for the rest of it.
|
|
1113
|
-
await new Promise((resolve) => {
|
|
1114
|
-
let timer;
|
|
1115
|
-
const done = () => {
|
|
1116
|
-
clearTimeout(timer);
|
|
1117
|
-
combinedSignal.removeEventListener("abort", done);
|
|
1118
|
-
resolve();
|
|
1119
|
-
};
|
|
1120
|
-
// Jitter the backoff so many clients reconnecting after the same
|
|
1121
|
-
// dropped window don't resubscribe in lockstep.
|
|
1122
|
-
const backoff = Math.min(100 * 2 ** (eofResubscribes - 1), 5_000);
|
|
1123
|
-
timer = setTimeout(done, backoff * (0.5 + Math.random() * 0.5));
|
|
1124
|
-
combinedSignal.addEventListener("abort", done);
|
|
1125
|
-
});
|
|
1325
|
+
await (0, ai_shared_js_1.waitBeforeEofResubscribe)(eofResubscribes, combinedSignal);
|
|
1126
1326
|
if (combinedSignal.aborted)
|
|
1127
1327
|
break;
|
|
1128
1328
|
const opened = await openWithAuthRetry();
|
|
@@ -1246,11 +1446,41 @@ class TriggerChatTransport {
|
|
|
1246
1446
|
continue;
|
|
1247
1447
|
}
|
|
1248
1448
|
}
|
|
1249
|
-
if (state.skipToTurnComplete
|
|
1250
|
-
|
|
1251
|
-
|
|
1449
|
+
if (state.skipToTurnComplete &&
|
|
1450
|
+
controlValue !== v3_1.TRIGGER_CONTROL_SUBTYPE.SESSION_CLOSED &&
|
|
1451
|
+
controlValue !== v3_1.TRIGGER_CONTROL_SUBTYPE.PENDING_VERSION) {
|
|
1452
|
+
// Fall through on a boundary so the `sinceInSeq` check and the
|
|
1453
|
+
// turn-completed emit below still run; content records are skipped.
|
|
1454
|
+
// A close is terminal and a pending version is the gated turn's own
|
|
1455
|
+
// handover status, so neither is ever gated away.
|
|
1456
|
+
if (controlValue !== v3_1.TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE)
|
|
1457
|
+
continue;
|
|
1458
|
+
// A boundary older than the superseded turn is not its boundary —
|
|
1459
|
+
// keep gating. A header-less boundary (legacy wire) still clears.
|
|
1460
|
+
const gateCursorRaw = (0, v3_1.headerValue)(value.headers, v3_1.SESSION_IN_EVENT_ID_HEADER);
|
|
1461
|
+
const gateCursor = gateCursorRaw !== undefined ? Number.parseInt(gateCursorRaw, 10) : NaN;
|
|
1462
|
+
if (state.supersededInputSeq !== undefined &&
|
|
1463
|
+
!Number.isNaN(gateCursor) &&
|
|
1464
|
+
gateCursor < state.supersededInputSeq) {
|
|
1465
|
+
continue;
|
|
1466
|
+
}
|
|
1467
|
+
state.skipToTurnComplete = false;
|
|
1468
|
+
state.supersededInputSeq = undefined;
|
|
1469
|
+
// This boundary is the new turn's own, so the gate swallowed its
|
|
1470
|
+
// output: fail the turn instead of completing an empty answer, and
|
|
1471
|
+
// leave nothing armed for the retry.
|
|
1472
|
+
if (sinceInSeq !== undefined &&
|
|
1473
|
+
!Number.isNaN(gateCursor) &&
|
|
1474
|
+
gateCursor >= sinceInSeq) {
|
|
1475
|
+
// The lost boundary may also carry the session close.
|
|
1476
|
+
if ((0, v3_1.headerValue)(value.headers, v3_1.SESSION_CLOSED_HEADER) === "true") {
|
|
1477
|
+
this.markSessionClosed(chatId, state, (0, v3_1.headerValue)(value.headers, v3_1.SESSION_CLOSED_REASON_HEADER), "stream");
|
|
1478
|
+
}
|
|
1479
|
+
state.activeInputSeq = undefined;
|
|
1480
|
+
state.isStreaming = false;
|
|
1481
|
+
this.notifySessionChange(chatId, state);
|
|
1482
|
+
throw new Error("The previous turn's output was lost. Try again.");
|
|
1252
1483
|
}
|
|
1253
|
-
continue;
|
|
1254
1484
|
}
|
|
1255
1485
|
if (controlValue === v3_1.TRIGGER_CONTROL_SUBTYPE.UPGRADE_REQUIRED) {
|
|
1256
1486
|
// Server has already triggered the new run via
|
|
@@ -1259,6 +1489,26 @@ class TriggerChatTransport {
|
|
|
1259
1489
|
// and keep reading.
|
|
1260
1490
|
continue;
|
|
1261
1491
|
}
|
|
1492
|
+
if (controlValue === v3_1.TRIGGER_CONTROL_SUBTYPE.SESSION_CLOSED) {
|
|
1493
|
+
this.markSessionClosed(chatId, state, (0, v3_1.headerValue)(value.headers, v3_1.SESSION_CLOSED_REASON_HEADER), "stream");
|
|
1494
|
+
internalAbort.abort();
|
|
1495
|
+
try {
|
|
1496
|
+
controller.close();
|
|
1497
|
+
}
|
|
1498
|
+
catch {
|
|
1499
|
+
/* already closed */
|
|
1500
|
+
}
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
if (controlValue === v3_1.TRIGGER_CONTROL_SUBTYPE.PENDING_VERSION) {
|
|
1504
|
+
this.emitEvent({
|
|
1505
|
+
type: "run-pending-version",
|
|
1506
|
+
chatId,
|
|
1507
|
+
timestamp: Date.now(),
|
|
1508
|
+
source: "upgrade",
|
|
1509
|
+
});
|
|
1510
|
+
continue;
|
|
1511
|
+
}
|
|
1262
1512
|
if (controlValue === v3_1.TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE) {
|
|
1263
1513
|
// Skip a turn-complete from an earlier turn (committed `.in` cursor
|
|
1264
1514
|
// below this send's seq), e.g. an undo action that raced this send.
|
|
@@ -1269,6 +1519,11 @@ class TriggerChatTransport {
|
|
|
1269
1519
|
continue;
|
|
1270
1520
|
}
|
|
1271
1521
|
}
|
|
1522
|
+
// A close decided during this turn rides out on turn-complete —
|
|
1523
|
+
// the last record a reader sees before it ends the stream.
|
|
1524
|
+
if ((0, v3_1.headerValue)(value.headers, v3_1.SESSION_CLOSED_HEADER) === "true") {
|
|
1525
|
+
this.markSessionClosed(chatId, state, (0, v3_1.headerValue)(value.headers, v3_1.SESSION_CLOSED_REASON_HEADER), "stream");
|
|
1526
|
+
}
|
|
1272
1527
|
const refreshedToken = (0, v3_1.headerValue)(value.headers, v3_1.PUBLIC_ACCESS_TOKEN_HEADER) ??
|
|
1273
1528
|
legacyChunk?.publicAccessToken;
|
|
1274
1529
|
if (refreshedToken) {
|