@trigger.dev/sdk 4.5.15 → 4.6.0
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/config/extensions/custom.mdx +14 -1
- 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/github-integration.mdx +1 -1
- 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/docs/troubleshooting.mdx +1 -1
- 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/esm/v3/chat.js
CHANGED
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
* }
|
|
23
23
|
* ```
|
|
24
24
|
*/
|
|
25
|
-
import { controlSubtype, headerValue, PUBLIC_ACCESS_TOKEN_HEADER, SESSION_IN_EVENT_ID_HEADER, SSEStreamSubscription, TRIGGER_CONTROL_SUBTYPE, } from "@trigger.dev/core/v3";
|
|
25
|
+
import { controlSubtype, headerValue, SESSION_CLOSED_HEADER, SESSION_CLOSED_REASON_HEADER, PUBLIC_ACCESS_TOKEN_HEADER, SESSION_IN_EVENT_ID_HEADER, SSEStreamSubscription, TRIGGER_CONTROL_SUBTYPE, } from "@trigger.dev/core/v3";
|
|
26
26
|
function byteLength(body) {
|
|
27
27
|
return new TextEncoder().encode(body).byteLength;
|
|
28
28
|
}
|
|
29
29
|
import { ChatTabCoordinator } from "./chat-tab-coordinator.js";
|
|
30
|
-
import { slimSubmitMessageForWire } from "./ai-shared.js";
|
|
30
|
+
import { MAX_EOF_RESUBSCRIBES, slimSubmitMessageForWire, waitBeforeEofResubscribe, } from "./ai-shared.js";
|
|
31
31
|
const DEFAULT_BASE_URL = "https://api.trigger.dev";
|
|
32
32
|
const DEFAULT_STREAM_TIMEOUT_SECONDS = 120;
|
|
33
33
|
/**
|
|
@@ -40,6 +40,32 @@ function isAuthError(error) {
|
|
|
40
40
|
const e = error;
|
|
41
41
|
return e.name === "TriggerApiError" && (e.status === 401 || e.status === 403);
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Detect the closed-session refusal from `.in/append`: HTTP 409 with the
|
|
45
|
+
* stable `session_closed` code. Terminal — never retried.
|
|
46
|
+
*/
|
|
47
|
+
function isSessionClosedError(error) {
|
|
48
|
+
if (error === null || typeof error !== "object")
|
|
49
|
+
return false;
|
|
50
|
+
const e = error;
|
|
51
|
+
return e.name === "TriggerApiError" && e.status === 409 && e.code === "session_closed";
|
|
52
|
+
}
|
|
53
|
+
function sessionClosedReasonOf(error) {
|
|
54
|
+
if (error === null || typeof error !== "object")
|
|
55
|
+
return undefined;
|
|
56
|
+
const reason = error.closedReason;
|
|
57
|
+
return typeof reason === "string" ? reason : undefined;
|
|
58
|
+
}
|
|
59
|
+
/** The local form of the refusal, for sends the transport rejects itself. */
|
|
60
|
+
function sessionClosedError(chatId, reason) {
|
|
61
|
+
const err = new Error(reason ? `Chat session ${chatId} is closed: ${reason}` : `Chat session ${chatId} is closed`);
|
|
62
|
+
err.name = "TriggerApiError";
|
|
63
|
+
err.status = 409;
|
|
64
|
+
err.code = "session_closed";
|
|
65
|
+
if (reason)
|
|
66
|
+
err.closedReason = reason;
|
|
67
|
+
return err;
|
|
68
|
+
}
|
|
43
69
|
/**
|
|
44
70
|
* Detect a 404 from a session-PAT-authed call — i.e. the session doesn't
|
|
45
71
|
* exist in the current environment. Happens when hydrated session state
|
|
@@ -145,6 +171,7 @@ export class TriggerChatTransport {
|
|
|
145
171
|
_onSessionChange;
|
|
146
172
|
_onEvent;
|
|
147
173
|
sessions = new Map();
|
|
174
|
+
pendingResumeCursors = new Map();
|
|
148
175
|
activeStreams = new Map();
|
|
149
176
|
pendingStarts = new Map();
|
|
150
177
|
// Last turn-producing send per chat — attribution source for the
|
|
@@ -184,6 +211,8 @@ export class TriggerChatTransport {
|
|
|
184
211
|
lastEventId: session.lastEventId,
|
|
185
212
|
activeInputSeq: session.activeInputSeq,
|
|
186
213
|
isStreaming: session.isStreaming,
|
|
214
|
+
closed: session.closed,
|
|
215
|
+
closedReason: session.closedReason,
|
|
187
216
|
});
|
|
188
217
|
}
|
|
189
218
|
}
|
|
@@ -233,6 +262,10 @@ export class TriggerChatTransport {
|
|
|
233
262
|
*/
|
|
234
263
|
sendMessages = async (options) => {
|
|
235
264
|
const { trigger, chatId, messageId, messages, abortSignal, body, metadata } = options;
|
|
265
|
+
const closedState = this.sessions.get(chatId);
|
|
266
|
+
if (closedState?.closed) {
|
|
267
|
+
throw sessionClosedError(chatId, closedState.closedReason);
|
|
268
|
+
}
|
|
236
269
|
if (this.coordinator) {
|
|
237
270
|
if (this.coordinator.isReadOnly(chatId)) {
|
|
238
271
|
throw new Error("This chat is active in another tab");
|
|
@@ -242,6 +275,14 @@ export class TriggerChatTransport {
|
|
|
242
275
|
const mergedMetadata = this.defaultMetadata || metadata
|
|
243
276
|
? { ...(this.defaultMetadata ?? {}), ...(metadata ?? {}) }
|
|
244
277
|
: undefined;
|
|
278
|
+
// An action sent through `useChat`. `useChatActions` (or any caller) puts
|
|
279
|
+
// it in `body.action`; sending it here rather than through
|
|
280
|
+
// `transport.sendAction` means `useChat` owns the response stream, so an
|
|
281
|
+
// action that becomes a turn renders the way a message turn does.
|
|
282
|
+
const actionInBody = body?.action;
|
|
283
|
+
if (actionInBody !== undefined) {
|
|
284
|
+
return this.sendAction(chatId, actionInBody, { abortSignal, metadata: mergedMetadata });
|
|
285
|
+
}
|
|
245
286
|
// First-turn handover routing — when `headStart` is set AND no
|
|
246
287
|
// session state exists yet for this chatId, POST the wire payload
|
|
247
288
|
// to the customer's `chat.handover` route handler. The handler
|
|
@@ -304,11 +345,9 @@ export class TriggerChatTransport {
|
|
|
304
345
|
activeStream.abort();
|
|
305
346
|
this.activeStreams.delete(chatId);
|
|
306
347
|
}
|
|
307
|
-
// A stop that never saw its TURN_COMPLETE leaves the flag set, and the new
|
|
308
|
-
// turn would be skipped record by record.
|
|
309
|
-
state.skipToTurnComplete = false;
|
|
310
348
|
state.activeInputSeq = inSeq;
|
|
311
349
|
state.isStreaming = true;
|
|
350
|
+
state.outstandingTurnAbandoned = false;
|
|
312
351
|
this.notifySessionChange(chatId, state);
|
|
313
352
|
// Owning turn: aborting this live send stops the turn the user drives.
|
|
314
353
|
return this.subscribeToSessionStream(state, abortSignal, chatId, {
|
|
@@ -380,6 +419,16 @@ export class TriggerChatTransport {
|
|
|
380
419
|
};
|
|
381
420
|
this.sessions.set(chatId, state);
|
|
382
421
|
this.notifySessionChange(chatId, state);
|
|
422
|
+
// Step 1 streams from the warm server either way; this says the agent run that owes step 2
|
|
423
|
+
// is parked on an undeployed external deployment id.
|
|
424
|
+
if (response.headers.get("X-Trigger-Chat-Pending-Version") === "1") {
|
|
425
|
+
this.emitEvent({
|
|
426
|
+
type: "run-pending-version",
|
|
427
|
+
chatId,
|
|
428
|
+
timestamp: Date.now(),
|
|
429
|
+
source: "head-start",
|
|
430
|
+
});
|
|
431
|
+
}
|
|
383
432
|
// Filter the parsed UIMessage stream:
|
|
384
433
|
// - Drop control chunks (`trigger:turn-complete`,
|
|
385
434
|
// `trigger:session-state`) before they reach AI SDK — they
|
|
@@ -546,6 +595,9 @@ export class TriggerChatTransport {
|
|
|
546
595
|
const state = this.sessions.get(options.chatId);
|
|
547
596
|
if (!state)
|
|
548
597
|
return null;
|
|
598
|
+
// A closed session has no further turns to resume.
|
|
599
|
+
if (state.closed)
|
|
600
|
+
return null;
|
|
549
601
|
// Watch is a standing subscription: a settled session is exactly the
|
|
550
602
|
// state it waits in, so a completed last turn must not block the resume.
|
|
551
603
|
if (state.isStreaming === false && !this.watchMode)
|
|
@@ -590,7 +642,13 @@ export class TriggerChatTransport {
|
|
|
590
642
|
catch {
|
|
591
643
|
return false;
|
|
592
644
|
}
|
|
593
|
-
|
|
645
|
+
// Only gate when a sent turn is still outstanding. A stop at a boundary has
|
|
646
|
+
// nothing to supersede, and gating it would swallow the next turn.
|
|
647
|
+
if (!state.outstandingTurnAbandoned &&
|
|
648
|
+
(state.isStreaming || state.activeInputSeq !== undefined)) {
|
|
649
|
+
state.skipToTurnComplete = true;
|
|
650
|
+
state.supersededInputSeq = state.activeInputSeq;
|
|
651
|
+
}
|
|
594
652
|
const activeStream = this.activeStreams.get(chatId);
|
|
595
653
|
if (activeStream) {
|
|
596
654
|
activeStream.abort();
|
|
@@ -609,15 +667,28 @@ export class TriggerChatTransport {
|
|
|
609
667
|
this.notifySessionChange(chatId, state);
|
|
610
668
|
return true;
|
|
611
669
|
};
|
|
670
|
+
/**
|
|
671
|
+
* Clear the supersede gate armed by a stop. Call this when the stopped
|
|
672
|
+
* turn died without ever writing its `trigger:turn-complete` boundary,
|
|
673
|
+
* otherwise the next turn stays gated forever.
|
|
674
|
+
*/
|
|
675
|
+
clearSupersedeGate = (chatId) => {
|
|
676
|
+
const state = this.sessions.get(chatId);
|
|
677
|
+
if (!state)
|
|
678
|
+
return;
|
|
679
|
+
state.skipToTurnComplete = false;
|
|
680
|
+
state.supersededInputSeq = undefined;
|
|
681
|
+
state.outstandingTurnAbandoned = true;
|
|
682
|
+
};
|
|
612
683
|
/**
|
|
613
684
|
* Send a custom action chunk (for `chat.agent`'s `actionSchema` /
|
|
614
|
-
* `onAction` hook).
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
* `
|
|
618
|
-
*
|
|
685
|
+
* `onAction` hook). An action is an edit: only `hydrateMessages` and
|
|
686
|
+
* `onAction` fire on the agent side, and the returned stream completes
|
|
687
|
+
* with `trigger:turn-complete` once the edit is persisted. When `onAction`
|
|
688
|
+
* returns `chat.turn()` the turn's response follows on the same stream.
|
|
689
|
+
* Per-action `metadata` is merged over the transport's `clientData`.
|
|
619
690
|
*/
|
|
620
|
-
sendAction = async (chatId, action) => {
|
|
691
|
+
sendAction = async (chatId, action, options) => {
|
|
621
692
|
if (this.coordinator) {
|
|
622
693
|
if (this.coordinator.isReadOnly(chatId)) {
|
|
623
694
|
throw new Error("This chat is active in another tab");
|
|
@@ -629,7 +700,9 @@ export class TriggerChatTransport {
|
|
|
629
700
|
chatId,
|
|
630
701
|
trigger: "action",
|
|
631
702
|
action,
|
|
632
|
-
metadata: this.defaultMetadata
|
|
703
|
+
metadata: this.defaultMetadata || options?.metadata
|
|
704
|
+
? { ...(this.defaultMetadata ?? {}), ...(options?.metadata ?? {}) }
|
|
705
|
+
: undefined,
|
|
633
706
|
};
|
|
634
707
|
const body = this.serializeInputChunk({ kind: "message", payload: wirePayload });
|
|
635
708
|
const partId = crypto.randomUUID();
|
|
@@ -644,16 +717,14 @@ export class TriggerChatTransport {
|
|
|
644
717
|
activeStream.abort();
|
|
645
718
|
this.activeStreams.delete(chatId);
|
|
646
719
|
}
|
|
647
|
-
// A stop that never saw its TURN_COMPLETE leaves the flag set, and the new
|
|
648
|
-
// turn would be skipped record by record.
|
|
649
|
-
state.skipToTurnComplete = false;
|
|
650
720
|
// Mark streaming + persist so a reload mid-action resumes (reconnectToStream
|
|
651
721
|
// no-ops when the persisted session says isStreaming: false).
|
|
652
722
|
state.activeInputSeq = inSeq;
|
|
653
723
|
state.isStreaming = true;
|
|
724
|
+
state.outstandingTurnAbandoned = false;
|
|
654
725
|
this.notifySessionChange(chatId, state);
|
|
655
726
|
// Owning action: aborting this send stops the turn the user drives.
|
|
656
|
-
return this.subscribeToSessionStream(state,
|
|
727
|
+
return this.subscribeToSessionStream(state, options?.abortSignal, chatId, {
|
|
657
728
|
sinceInSeq: inSeq,
|
|
658
729
|
sendStopOnAbort: true,
|
|
659
730
|
});
|
|
@@ -668,14 +739,42 @@ export class TriggerChatTransport {
|
|
|
668
739
|
return this.toPersisted(state);
|
|
669
740
|
};
|
|
670
741
|
setSession(chatId, session) {
|
|
671
|
-
this.sessions.set(chatId, {
|
|
742
|
+
this.sessions.set(chatId, this.applyPendingResumeCursor(chatId, {
|
|
672
743
|
publicAccessToken: session.publicAccessToken,
|
|
673
744
|
lastEventId: session.lastEventId,
|
|
674
745
|
activeInputSeq: session.activeInputSeq,
|
|
675
746
|
isStreaming: session.isStreaming,
|
|
676
|
-
});
|
|
747
|
+
}));
|
|
677
748
|
this.notifySessionChange(chatId, this.toPersisted(this.sessions.get(chatId)));
|
|
678
749
|
}
|
|
750
|
+
/**
|
|
751
|
+
* Seed the `.out` resume cursor from a loaded transcript. Applied to the
|
|
752
|
+
* session now if it exists, otherwise held until the session is created so
|
|
753
|
+
* the first live subscription opens past the persisted history instead of
|
|
754
|
+
* replaying it. Never moves an existing cursor backward: the transcript load
|
|
755
|
+
* is async, so a live `.out` record can already have advanced the session
|
|
756
|
+
* past the snapshot, and overwriting it would replay those records.
|
|
757
|
+
*/
|
|
758
|
+
seedResumeCursor = (chatId, lastEventId) => {
|
|
759
|
+
const existing = this.sessions.get(chatId);
|
|
760
|
+
if (existing?.publicAccessToken) {
|
|
761
|
+
if (existing.lastEventId === undefined) {
|
|
762
|
+
existing.lastEventId = lastEventId;
|
|
763
|
+
this.notifySessionChange(chatId, this.toPersisted(existing));
|
|
764
|
+
}
|
|
765
|
+
this.pendingResumeCursors.delete(chatId);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
this.pendingResumeCursors.set(chatId, lastEventId);
|
|
769
|
+
};
|
|
770
|
+
applyPendingResumeCursor(chatId, state) {
|
|
771
|
+
const pending = this.pendingResumeCursors.get(chatId);
|
|
772
|
+
if (pending !== undefined && state.lastEventId === undefined) {
|
|
773
|
+
state.lastEventId = pending;
|
|
774
|
+
}
|
|
775
|
+
this.pendingResumeCursors.delete(chatId);
|
|
776
|
+
return state;
|
|
777
|
+
}
|
|
679
778
|
setOnSessionChange(callback) {
|
|
680
779
|
this._onSessionChange = callback;
|
|
681
780
|
}
|
|
@@ -746,6 +845,22 @@ export class TriggerChatTransport {
|
|
|
746
845
|
setClientData(clientData) {
|
|
747
846
|
this.defaultMetadata = clientData;
|
|
748
847
|
}
|
|
848
|
+
/**
|
|
849
|
+
* Update the request-time callbacks. Same reason as `setClientData`:
|
|
850
|
+
* a host that rebuilds these per render (a dashboard navigating between
|
|
851
|
+
* projects) would otherwise keep hitting the endpoint captured at
|
|
852
|
+
* construction. Every read goes through the live field, so the next
|
|
853
|
+
* request uses the latest callback.
|
|
854
|
+
*/
|
|
855
|
+
setAccessToken(accessToken) {
|
|
856
|
+
this.resolveAccessToken = accessToken;
|
|
857
|
+
}
|
|
858
|
+
setStartSession(startSession) {
|
|
859
|
+
this.resolveStartSession = startSession;
|
|
860
|
+
}
|
|
861
|
+
setFetch(fetchOverride) {
|
|
862
|
+
this.fetchOverride = fetchOverride;
|
|
863
|
+
}
|
|
749
864
|
// -------------------------------------------------------------------------
|
|
750
865
|
// Multi-tab coordination passthrough
|
|
751
866
|
// -------------------------------------------------------------------------
|
|
@@ -793,7 +908,42 @@ export class TriggerChatTransport {
|
|
|
793
908
|
lastEventId: state.lastEventId,
|
|
794
909
|
activeInputSeq: state.activeInputSeq,
|
|
795
910
|
isStreaming: state.isStreaming,
|
|
911
|
+
closed: state.closed,
|
|
912
|
+
closedReason: state.closedReason,
|
|
796
913
|
});
|
|
914
|
+
/**
|
|
915
|
+
* Flip a session to closed and tell listeners once. Terminal: sends and
|
|
916
|
+
* reconnects stop from here on, and there is no reopening.
|
|
917
|
+
*/
|
|
918
|
+
markSessionClosed(chatId, state, reason, source) {
|
|
919
|
+
if (state.closed)
|
|
920
|
+
return;
|
|
921
|
+
state.closed = true;
|
|
922
|
+
if (reason)
|
|
923
|
+
state.closedReason = reason;
|
|
924
|
+
state.isStreaming = false;
|
|
925
|
+
state.activeInputSeq = undefined;
|
|
926
|
+
state.skipToTurnComplete = false;
|
|
927
|
+
state.supersededInputSeq = undefined;
|
|
928
|
+
this.emitEvent({
|
|
929
|
+
type: "session-closed",
|
|
930
|
+
chatId,
|
|
931
|
+
timestamp: Date.now(),
|
|
932
|
+
...(reason ? { reason } : {}),
|
|
933
|
+
source,
|
|
934
|
+
});
|
|
935
|
+
this.notifySessionChange(chatId, state);
|
|
936
|
+
this.coordinator?.release(chatId);
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Whether the conversation has been closed — by the agent via
|
|
940
|
+
* `chat.close()`, or from outside via `sessions.close()`. Closed is
|
|
941
|
+
* one-way: the transport refuses further sends and reconnects, and the
|
|
942
|
+
* transcript stays readable.
|
|
943
|
+
*/
|
|
944
|
+
sessionStatus = (chatId) => this.sessions.get(chatId)?.closed ? "closed" : "open";
|
|
945
|
+
/** The reason the session was closed, when one was given. */
|
|
946
|
+
sessionClosedReason = (chatId) => this.sessions.get(chatId)?.closedReason;
|
|
797
947
|
notifySessionChange(chatId, session) {
|
|
798
948
|
if (!this._onSessionChange)
|
|
799
949
|
return;
|
|
@@ -828,7 +978,7 @@ export class TriggerChatTransport {
|
|
|
828
978
|
// `sessions: { ... }` already, or the very first `accessToken` call
|
|
829
979
|
// returns a PAT for an out-of-band-created session.
|
|
830
980
|
const token = await this.resolveAccessToken({ chatId });
|
|
831
|
-
const state = { publicAccessToken: token };
|
|
981
|
+
const state = this.applyPendingResumeCursor(chatId, { publicAccessToken: token });
|
|
832
982
|
this.sessions.set(chatId, state);
|
|
833
983
|
this.notifySessionChange(chatId, state);
|
|
834
984
|
return state;
|
|
@@ -837,15 +987,23 @@ export class TriggerChatTransport {
|
|
|
837
987
|
if (!this.resolveStartSession) {
|
|
838
988
|
throw new Error("TriggerChatTransport: `startSession` is required to call `start()` / `preload()`. Either provide it or pre-hydrate the session via `sessions: { ... }`.");
|
|
839
989
|
}
|
|
840
|
-
const { publicAccessToken } = await this.resolveStartSession({
|
|
990
|
+
const { publicAccessToken, pendingVersion } = await this.resolveStartSession({
|
|
841
991
|
taskId: this.taskId,
|
|
842
992
|
chatId,
|
|
843
993
|
clientData: (this.defaultMetadata ?? {}),
|
|
844
994
|
});
|
|
845
|
-
|
|
995
|
+
if (pendingVersion) {
|
|
996
|
+
this.emitEvent({
|
|
997
|
+
type: "run-pending-version",
|
|
998
|
+
chatId,
|
|
999
|
+
timestamp: Date.now(),
|
|
1000
|
+
source: "start",
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
const state = this.applyPendingResumeCursor(chatId, {
|
|
846
1004
|
publicAccessToken,
|
|
847
1005
|
isStreaming: false,
|
|
848
|
-
};
|
|
1006
|
+
});
|
|
849
1007
|
this.sessions.set(chatId, state);
|
|
850
1008
|
this.notifySessionChange(chatId, state);
|
|
851
1009
|
return state;
|
|
@@ -858,6 +1016,33 @@ export class TriggerChatTransport {
|
|
|
858
1016
|
const raw = this.resolveBaseURLFn(ctx);
|
|
859
1017
|
return raw.replace(/\/$/, "");
|
|
860
1018
|
}
|
|
1019
|
+
/**
|
|
1020
|
+
* `fetchClient` for an `out` subscription. Reads `this.fetchOverride` on
|
|
1021
|
+
* every call, not once per subscription: a watch-mode subscription
|
|
1022
|
+
* reconnects for the lifetime of the page, and `setFetch` may have swapped
|
|
1023
|
+
* the override in between.
|
|
1024
|
+
*/
|
|
1025
|
+
sseFetchClient(ctx) {
|
|
1026
|
+
return ((input, init) => {
|
|
1027
|
+
const override = this.fetchOverride;
|
|
1028
|
+
if (!override)
|
|
1029
|
+
return fetch(input, init);
|
|
1030
|
+
if (typeof input === "string") {
|
|
1031
|
+
return override(input, init ?? {}, ctx);
|
|
1032
|
+
}
|
|
1033
|
+
if (input instanceof URL) {
|
|
1034
|
+
return override(input.toString(), init ?? {}, ctx);
|
|
1035
|
+
}
|
|
1036
|
+
// Request — preserve its url + intrinsic init, let any provided init
|
|
1037
|
+
// override on top (matches fetch(Request, init) semantics).
|
|
1038
|
+
return override(input.url, {
|
|
1039
|
+
method: input.method,
|
|
1040
|
+
headers: input.headers,
|
|
1041
|
+
signal: input.signal,
|
|
1042
|
+
...(init ?? {}),
|
|
1043
|
+
}, ctx);
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
861
1046
|
async doFetch(ctx, url, init) {
|
|
862
1047
|
return this.fetchOverride ? this.fetchOverride(url, init, ctx) : fetch(url, init);
|
|
863
1048
|
}
|
|
@@ -879,24 +1064,65 @@ export class TriggerChatTransport {
|
|
|
879
1064
|
const err = new Error(`appendToSessionStream failed: ${response.status} ${text}`);
|
|
880
1065
|
err.name = "TriggerApiError";
|
|
881
1066
|
err.status = response.status;
|
|
1067
|
+
let parsed;
|
|
1068
|
+
try {
|
|
1069
|
+
parsed = JSON.parse(text);
|
|
1070
|
+
}
|
|
1071
|
+
catch {
|
|
1072
|
+
parsed = undefined;
|
|
1073
|
+
}
|
|
1074
|
+
if (typeof parsed?.code === "string")
|
|
1075
|
+
err.code = parsed.code;
|
|
1076
|
+
if (typeof parsed?.closedReason === "string")
|
|
1077
|
+
err.closedReason = parsed.closedReason;
|
|
882
1078
|
throw err;
|
|
883
1079
|
}
|
|
884
1080
|
// The appended record's `.in` seq, for correlating the response stream to
|
|
885
1081
|
// this send. Omitted by older webapps / a lost idempotency claim.
|
|
886
1082
|
const data = (await response.json().catch(() => undefined));
|
|
1083
|
+
if (data?.pendingVersion === true) {
|
|
1084
|
+
this.emitEvent({
|
|
1085
|
+
type: "run-pending-version",
|
|
1086
|
+
chatId,
|
|
1087
|
+
timestamp: Date.now(),
|
|
1088
|
+
source: "send",
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
887
1091
|
return typeof data?.seq === "number" ? data.seq : undefined;
|
|
888
1092
|
}
|
|
889
1093
|
async callWithAuthRetry(chatId, state, op) {
|
|
1094
|
+
if (state.closed) {
|
|
1095
|
+
throw sessionClosedError(chatId, state.closedReason);
|
|
1096
|
+
}
|
|
1097
|
+
// Every attempt goes through here, so a closed-session refusal is terminal
|
|
1098
|
+
// on whichever one hits it. Reaching a 409 from a later attempt and not
|
|
1099
|
+
// marking it would leave local status open on a conversation the server
|
|
1100
|
+
// has already ended.
|
|
1101
|
+
const attempt = async (token) => {
|
|
1102
|
+
try {
|
|
1103
|
+
return await op(token);
|
|
1104
|
+
}
|
|
1105
|
+
catch (err) {
|
|
1106
|
+
if (isSessionClosedError(err)) {
|
|
1107
|
+
// Terminal. Retrying, refreshing the PAT, or recreating the session
|
|
1108
|
+
// would all be wrong: the conversation is over.
|
|
1109
|
+
this.markSessionClosed(chatId, state, sessionClosedReasonOf(err), "append");
|
|
1110
|
+
}
|
|
1111
|
+
throw err;
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
890
1114
|
// 1) Try with the current PAT.
|
|
891
1115
|
try {
|
|
892
|
-
return await
|
|
1116
|
+
return await attempt(state.publicAccessToken);
|
|
893
1117
|
}
|
|
894
1118
|
catch (err) {
|
|
1119
|
+
if (isSessionClosedError(err))
|
|
1120
|
+
throw err;
|
|
895
1121
|
if (isSessionNotFoundError(err)) {
|
|
896
1122
|
// The cached PAT authenticated but the session doesn't exist here —
|
|
897
1123
|
// recreate it and retry.
|
|
898
1124
|
await this.recreateSession(chatId, state);
|
|
899
|
-
return await
|
|
1125
|
+
return await attempt(state.publicAccessToken);
|
|
900
1126
|
}
|
|
901
1127
|
if (!isAuthError(err))
|
|
902
1128
|
throw err;
|
|
@@ -907,7 +1133,7 @@ export class TriggerChatTransport {
|
|
|
907
1133
|
state.publicAccessToken = fresh;
|
|
908
1134
|
this.notifySessionChange(chatId, state);
|
|
909
1135
|
try {
|
|
910
|
-
return await
|
|
1136
|
+
return await attempt(fresh);
|
|
911
1137
|
}
|
|
912
1138
|
catch (err) {
|
|
913
1139
|
if (!isSessionNotFoundError(err))
|
|
@@ -917,7 +1143,7 @@ export class TriggerChatTransport {
|
|
|
917
1143
|
// state is stale (created in a different environment, or before the
|
|
918
1144
|
// sessions upgrade). Recreate the session and retry once.
|
|
919
1145
|
await this.recreateSession(chatId, state);
|
|
920
|
-
return await
|
|
1146
|
+
return await attempt(state.publicAccessToken);
|
|
921
1147
|
}
|
|
922
1148
|
/**
|
|
923
1149
|
* Repair a stale/phantom hydrated session. The cached state pointed at a
|
|
@@ -933,11 +1159,19 @@ export class TriggerChatTransport {
|
|
|
933
1159
|
if (!this.resolveStartSession) {
|
|
934
1160
|
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.");
|
|
935
1161
|
}
|
|
936
|
-
const { publicAccessToken } = await this.resolveStartSession({
|
|
1162
|
+
const { publicAccessToken, pendingVersion } = await this.resolveStartSession({
|
|
937
1163
|
taskId: this.taskId,
|
|
938
1164
|
chatId,
|
|
939
1165
|
clientData: (this.defaultMetadata ?? {}),
|
|
940
1166
|
});
|
|
1167
|
+
if (pendingVersion) {
|
|
1168
|
+
this.emitEvent({
|
|
1169
|
+
type: "run-pending-version",
|
|
1170
|
+
chatId,
|
|
1171
|
+
timestamp: Date.now(),
|
|
1172
|
+
source: "start",
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
941
1175
|
state.publicAccessToken = publicAccessToken;
|
|
942
1176
|
state.lastEventId = undefined;
|
|
943
1177
|
state.isStreaming = false;
|
|
@@ -961,8 +1195,13 @@ export class TriggerChatTransport {
|
|
|
961
1195
|
: internalAbort.signal;
|
|
962
1196
|
if (abortSignal) {
|
|
963
1197
|
abortSignal.addEventListener("abort", () => {
|
|
964
|
-
|
|
1198
|
+
// A late abort (unmount, or the consumer dropping a drained stream)
|
|
1199
|
+
// has no turn to stop: don't gate the next one, don't write a stop.
|
|
1200
|
+
const outstanding = !state.outstandingTurnAbandoned &&
|
|
1201
|
+
(state.isStreaming || state.activeInputSeq !== undefined);
|
|
1202
|
+
if (options?.sendStopOnAbort !== false && outstanding && !internalAbort.signal.aborted) {
|
|
965
1203
|
state.skipToTurnComplete = true;
|
|
1204
|
+
state.supersededInputSeq = state.activeInputSeq;
|
|
966
1205
|
this.appendInputChunk(chatId, state.publicAccessToken, this.serializeInputChunk({ kind: "stop" })).catch(() => { });
|
|
967
1206
|
}
|
|
968
1207
|
internalAbort.abort();
|
|
@@ -1022,26 +1261,7 @@ export class TriggerChatTransport {
|
|
|
1022
1261
|
})()
|
|
1023
1262
|
: () => { };
|
|
1024
1263
|
const sseCtx = { endpoint: "out", chatId };
|
|
1025
|
-
const
|
|
1026
|
-
const sseFetchClient = fetchOverride
|
|
1027
|
-
? ((input, init) => {
|
|
1028
|
-
if (typeof input === "string") {
|
|
1029
|
-
return fetchOverride(input, init ?? {}, sseCtx);
|
|
1030
|
-
}
|
|
1031
|
-
if (input instanceof URL) {
|
|
1032
|
-
return fetchOverride(input.toString(), init ?? {}, sseCtx);
|
|
1033
|
-
}
|
|
1034
|
-
// Request — preserve its url + intrinsic init, let any
|
|
1035
|
-
// provided init override on top (matches fetch(Request, init)
|
|
1036
|
-
// semantics).
|
|
1037
|
-
return fetchOverride(input.url, {
|
|
1038
|
-
method: input.method,
|
|
1039
|
-
headers: input.headers,
|
|
1040
|
-
signal: input.signal,
|
|
1041
|
-
...(init ?? {}),
|
|
1042
|
-
}, sseCtx);
|
|
1043
|
-
})
|
|
1044
|
-
: undefined;
|
|
1264
|
+
const sseFetchClient = this.sseFetchClient(sseCtx);
|
|
1045
1265
|
let sawFirstChunk = false;
|
|
1046
1266
|
let sinceInSeq = options?.sinceInSeq;
|
|
1047
1267
|
const connectSseOnce = async (token) => {
|
|
@@ -1089,12 +1309,6 @@ export class TriggerChatTransport {
|
|
|
1089
1309
|
return await connectSseOnce(fresh);
|
|
1090
1310
|
}
|
|
1091
1311
|
};
|
|
1092
|
-
// A body that ends without a turn-complete is only terminal when the
|
|
1093
|
-
// server says the session settled — otherwise the turn is still
|
|
1094
|
-
// running and we lost the connection (long-poll window closed, proxy
|
|
1095
|
-
// restarted). Resubscribe from `state.lastEventId`, bounded so a
|
|
1096
|
-
// permanently empty stream can't spin.
|
|
1097
|
-
const MAX_EOF_RESUBSCRIBES = 5;
|
|
1098
1312
|
let eofResubscribes = 0;
|
|
1099
1313
|
const resumeAfterEof = async () => {
|
|
1100
1314
|
// Watch mode is a standing subscription: it outlives turn-complete
|
|
@@ -1104,21 +1318,7 @@ export class TriggerChatTransport {
|
|
|
1104
1318
|
!currentSubscription?.sessionSettled &&
|
|
1105
1319
|
!combinedSignal.aborted) {
|
|
1106
1320
|
eofResubscribes++;
|
|
1107
|
-
|
|
1108
|
-
// mid-backoff and the stream stays open for the rest of it.
|
|
1109
|
-
await new Promise((resolve) => {
|
|
1110
|
-
let timer;
|
|
1111
|
-
const done = () => {
|
|
1112
|
-
clearTimeout(timer);
|
|
1113
|
-
combinedSignal.removeEventListener("abort", done);
|
|
1114
|
-
resolve();
|
|
1115
|
-
};
|
|
1116
|
-
// Jitter the backoff so many clients reconnecting after the same
|
|
1117
|
-
// dropped window don't resubscribe in lockstep.
|
|
1118
|
-
const backoff = Math.min(100 * 2 ** (eofResubscribes - 1), 5_000);
|
|
1119
|
-
timer = setTimeout(done, backoff * (0.5 + Math.random() * 0.5));
|
|
1120
|
-
combinedSignal.addEventListener("abort", done);
|
|
1121
|
-
});
|
|
1321
|
+
await waitBeforeEofResubscribe(eofResubscribes, combinedSignal);
|
|
1122
1322
|
if (combinedSignal.aborted)
|
|
1123
1323
|
break;
|
|
1124
1324
|
const opened = await openWithAuthRetry();
|
|
@@ -1242,11 +1442,41 @@ export class TriggerChatTransport {
|
|
|
1242
1442
|
continue;
|
|
1243
1443
|
}
|
|
1244
1444
|
}
|
|
1245
|
-
if (state.skipToTurnComplete
|
|
1246
|
-
|
|
1247
|
-
|
|
1445
|
+
if (state.skipToTurnComplete &&
|
|
1446
|
+
controlValue !== TRIGGER_CONTROL_SUBTYPE.SESSION_CLOSED &&
|
|
1447
|
+
controlValue !== TRIGGER_CONTROL_SUBTYPE.PENDING_VERSION) {
|
|
1448
|
+
// Fall through on a boundary so the `sinceInSeq` check and the
|
|
1449
|
+
// turn-completed emit below still run; content records are skipped.
|
|
1450
|
+
// A close is terminal and a pending version is the gated turn's own
|
|
1451
|
+
// handover status, so neither is ever gated away.
|
|
1452
|
+
if (controlValue !== TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE)
|
|
1453
|
+
continue;
|
|
1454
|
+
// A boundary older than the superseded turn is not its boundary —
|
|
1455
|
+
// keep gating. A header-less boundary (legacy wire) still clears.
|
|
1456
|
+
const gateCursorRaw = headerValue(value.headers, SESSION_IN_EVENT_ID_HEADER);
|
|
1457
|
+
const gateCursor = gateCursorRaw !== undefined ? Number.parseInt(gateCursorRaw, 10) : NaN;
|
|
1458
|
+
if (state.supersededInputSeq !== undefined &&
|
|
1459
|
+
!Number.isNaN(gateCursor) &&
|
|
1460
|
+
gateCursor < state.supersededInputSeq) {
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
state.skipToTurnComplete = false;
|
|
1464
|
+
state.supersededInputSeq = undefined;
|
|
1465
|
+
// This boundary is the new turn's own, so the gate swallowed its
|
|
1466
|
+
// output: fail the turn instead of completing an empty answer, and
|
|
1467
|
+
// leave nothing armed for the retry.
|
|
1468
|
+
if (sinceInSeq !== undefined &&
|
|
1469
|
+
!Number.isNaN(gateCursor) &&
|
|
1470
|
+
gateCursor >= sinceInSeq) {
|
|
1471
|
+
// The lost boundary may also carry the session close.
|
|
1472
|
+
if (headerValue(value.headers, SESSION_CLOSED_HEADER) === "true") {
|
|
1473
|
+
this.markSessionClosed(chatId, state, headerValue(value.headers, SESSION_CLOSED_REASON_HEADER), "stream");
|
|
1474
|
+
}
|
|
1475
|
+
state.activeInputSeq = undefined;
|
|
1476
|
+
state.isStreaming = false;
|
|
1477
|
+
this.notifySessionChange(chatId, state);
|
|
1478
|
+
throw new Error("The previous turn's output was lost. Try again.");
|
|
1248
1479
|
}
|
|
1249
|
-
continue;
|
|
1250
1480
|
}
|
|
1251
1481
|
if (controlValue === TRIGGER_CONTROL_SUBTYPE.UPGRADE_REQUIRED) {
|
|
1252
1482
|
// Server has already triggered the new run via
|
|
@@ -1255,6 +1485,26 @@ export class TriggerChatTransport {
|
|
|
1255
1485
|
// and keep reading.
|
|
1256
1486
|
continue;
|
|
1257
1487
|
}
|
|
1488
|
+
if (controlValue === TRIGGER_CONTROL_SUBTYPE.SESSION_CLOSED) {
|
|
1489
|
+
this.markSessionClosed(chatId, state, headerValue(value.headers, SESSION_CLOSED_REASON_HEADER), "stream");
|
|
1490
|
+
internalAbort.abort();
|
|
1491
|
+
try {
|
|
1492
|
+
controller.close();
|
|
1493
|
+
}
|
|
1494
|
+
catch {
|
|
1495
|
+
/* already closed */
|
|
1496
|
+
}
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
if (controlValue === TRIGGER_CONTROL_SUBTYPE.PENDING_VERSION) {
|
|
1500
|
+
this.emitEvent({
|
|
1501
|
+
type: "run-pending-version",
|
|
1502
|
+
chatId,
|
|
1503
|
+
timestamp: Date.now(),
|
|
1504
|
+
source: "upgrade",
|
|
1505
|
+
});
|
|
1506
|
+
continue;
|
|
1507
|
+
}
|
|
1258
1508
|
if (controlValue === TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE) {
|
|
1259
1509
|
// Skip a turn-complete from an earlier turn (committed `.in` cursor
|
|
1260
1510
|
// below this send's seq), e.g. an undo action that raced this send.
|
|
@@ -1265,6 +1515,11 @@ export class TriggerChatTransport {
|
|
|
1265
1515
|
continue;
|
|
1266
1516
|
}
|
|
1267
1517
|
}
|
|
1518
|
+
// A close decided during this turn rides out on turn-complete —
|
|
1519
|
+
// the last record a reader sees before it ends the stream.
|
|
1520
|
+
if (headerValue(value.headers, SESSION_CLOSED_HEADER) === "true") {
|
|
1521
|
+
this.markSessionClosed(chatId, state, headerValue(value.headers, SESSION_CLOSED_REASON_HEADER), "stream");
|
|
1522
|
+
}
|
|
1268
1523
|
const refreshedToken = headerValue(value.headers, PUBLIC_ACCESS_TOKEN_HEADER) ??
|
|
1269
1524
|
legacyChunk?.publicAccessToken;
|
|
1270
1525
|
if (refreshedToken) {
|