@trigger.dev/sdk 0.0.0-prerelease-20260909071038 → 0.0.0-prerelease-streamfix-20260909094302

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.
Files changed (142) hide show
  1. package/dist/commonjs/imports/ai-runtime-cjs.cjs.map +1 -1
  2. package/dist/commonjs/imports/ai-runtime.js +0 -2
  3. package/dist/commonjs/v3/ai-shared.d.ts +0 -15
  4. package/dist/commonjs/v3/ai-shared.js +1 -35
  5. package/dist/commonjs/v3/ai-shared.js.map +1 -1
  6. package/dist/commonjs/v3/ai.d.ts +25 -275
  7. package/dist/commonjs/v3/ai.js +308 -1283
  8. package/dist/commonjs/v3/ai.js.map +1 -1
  9. package/dist/commonjs/v3/chat-client.d.ts +2 -11
  10. package/dist/commonjs/v3/chat-client.js +111 -227
  11. package/dist/commonjs/v3/chat-client.js.map +1 -1
  12. package/dist/commonjs/v3/chat-react.d.ts +0 -88
  13. package/dist/commonjs/v3/chat-react.js +1 -130
  14. package/dist/commonjs/v3/chat-react.js.map +1 -1
  15. package/dist/commonjs/v3/chat-server.d.ts +6 -42
  16. package/dist/commonjs/v3/chat-server.js +7 -52
  17. package/dist/commonjs/v3/chat-server.js.map +1 -1
  18. package/dist/commonjs/v3/chat.d.ts +10 -92
  19. package/dist/commonjs/v3/chat.js +73 -328
  20. package/dist/commonjs/v3/chat.js.map +1 -1
  21. package/dist/commonjs/v3/sessions.d.ts +2 -15
  22. package/dist/commonjs/v3/sessions.js +1 -12
  23. package/dist/commonjs/v3/sessions.js.map +1 -1
  24. package/dist/commonjs/v3/shared.js +36 -30
  25. package/dist/commonjs/v3/shared.js.map +1 -1
  26. package/dist/commonjs/v3/test/index.d.ts +0 -1
  27. package/dist/commonjs/v3/test/index.js +1 -3
  28. package/dist/commonjs/v3/test/index.js.map +1 -1
  29. package/dist/commonjs/v3/test/mock-chat-agent.d.ts +4 -47
  30. package/dist/commonjs/v3/test/mock-chat-agent.js +4 -115
  31. package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
  32. package/dist/commonjs/v3/test/test-session-handle.js +0 -6
  33. package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
  34. package/dist/commonjs/version.js +1 -1
  35. package/dist/esm/imports/ai-runtime.d.ts +2 -2
  36. package/dist/esm/imports/ai-runtime.js +2 -2
  37. package/dist/esm/imports/ai-runtime.js.map +1 -1
  38. package/dist/esm/v3/ai-shared.d.ts +0 -15
  39. package/dist/esm/v3/ai-shared.js +0 -33
  40. package/dist/esm/v3/ai-shared.js.map +1 -1
  41. package/dist/esm/v3/ai.d.ts +25 -275
  42. package/dist/esm/v3/ai.js +305 -1275
  43. package/dist/esm/v3/ai.js.map +1 -1
  44. package/dist/esm/v3/chat-client.d.ts +2 -11
  45. package/dist/esm/v3/chat-client.js +112 -228
  46. package/dist/esm/v3/chat-client.js.map +1 -1
  47. package/dist/esm/v3/chat-react.d.ts +0 -88
  48. package/dist/esm/v3/chat-react.js +1 -127
  49. package/dist/esm/v3/chat-react.js.map +1 -1
  50. package/dist/esm/v3/chat-server.d.ts +6 -42
  51. package/dist/esm/v3/chat-server.js +8 -53
  52. package/dist/esm/v3/chat-server.js.map +1 -1
  53. package/dist/esm/v3/chat.d.ts +10 -92
  54. package/dist/esm/v3/chat.js +74 -329
  55. package/dist/esm/v3/chat.js.map +1 -1
  56. package/dist/esm/v3/sessions.d.ts +2 -15
  57. package/dist/esm/v3/sessions.js +1 -11
  58. package/dist/esm/v3/sessions.js.map +1 -1
  59. package/dist/esm/v3/shared.js +23 -17
  60. package/dist/esm/v3/shared.js.map +1 -1
  61. package/dist/esm/v3/test/index.d.ts +0 -1
  62. package/dist/esm/v3/test/index.js +0 -1
  63. package/dist/esm/v3/test/index.js.map +1 -1
  64. package/dist/esm/v3/test/mock-chat-agent.d.ts +4 -47
  65. package/dist/esm/v3/test/mock-chat-agent.js +6 -117
  66. package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
  67. package/dist/esm/v3/test/test-session-handle.js +0 -6
  68. package/dist/esm/v3/test/test-session-handle.js.map +1 -1
  69. package/dist/esm/version.js +1 -1
  70. package/docs/ai-chat/actions.mdx +23 -55
  71. package/docs/ai-chat/anatomy.mdx +3 -3
  72. package/docs/ai-chat/backend.mdx +48 -125
  73. package/docs/ai-chat/background-injection.mdx +19 -69
  74. package/docs/ai-chat/client-protocol.mdx +4 -5
  75. package/docs/ai-chat/compaction.mdx +7 -13
  76. package/docs/ai-chat/custom-agents.mdx +0 -23
  77. package/docs/ai-chat/fast-starts.mdx +20 -27
  78. package/docs/ai-chat/frontend.mdx +14 -19
  79. package/docs/ai-chat/lifecycle-hooks.mdx +2 -6
  80. package/docs/ai-chat/migrating-from-a-route-handler.mdx +14 -16
  81. package/docs/ai-chat/patterns/database-persistence.mdx +2 -6
  82. package/docs/ai-chat/patterns/persistence-and-replay.mdx +23 -34
  83. package/docs/ai-chat/patterns/skills.mdx +10 -7
  84. package/docs/ai-chat/patterns/version-upgrades.mdx +6 -79
  85. package/docs/ai-chat/pending-messages.mdx +3 -3
  86. package/docs/ai-chat/prompt-caching.mdx +25 -23
  87. package/docs/ai-chat/quick-start.mdx +11 -11
  88. package/docs/ai-chat/reference.mdx +9 -67
  89. package/docs/ai-chat/sessions.mdx +1 -6
  90. package/docs/ai-chat/testing.mdx +1 -2
  91. package/docs/ai-chat/tools.mdx +13 -18
  92. package/docs/ai-chat/upgrade-guide.mdx +2 -2
  93. package/docs/apikeys.mdx +45 -27
  94. package/docs/config/extensions/syncEnvVars.mdx +0 -6
  95. package/docs/deploy-environment-variables.mdx +2 -23
  96. package/docs/deployment/overview.mdx +8 -4
  97. package/docs/deployment/preview-branches.mdx +4 -4
  98. package/docs/deployment/version-skew-protection.mdx +0 -62
  99. package/docs/manual-setup.mdx +7 -7
  100. package/docs/mcp-tools.mdx +0 -9
  101. package/docs/quick-start.mdx +3 -3
  102. package/docs/realtime/auth.mdx +1 -1
  103. package/docs/self-hosting/security.mdx +0 -5
  104. package/docs/tasks/scheduled.mdx +0 -24
  105. package/docs/triggering.mdx +1 -1
  106. package/package.json +4 -4
  107. package/skills/trigger-authoring-chat-agent/SKILL.md +29 -41
  108. package/skills/trigger-chat-agent-advanced/SKILL.md +12 -31
  109. package/dist/commonjs/v3/chatSnapshotIo.d.ts +0 -2
  110. package/dist/commonjs/v3/chatSnapshotIo.js +0 -174
  111. package/dist/commonjs/v3/chatSnapshotIo.js.map +0 -1
  112. package/dist/commonjs/v3/chatVersionSkew.d.ts +0 -12
  113. package/dist/commonjs/v3/chatVersionSkew.js +0 -30
  114. package/dist/commonjs/v3/chatVersionSkew.js.map +0 -1
  115. package/dist/commonjs/v3/externalDeploymentId.d.ts +0 -23
  116. package/dist/commonjs/v3/externalDeploymentId.js +0 -43
  117. package/dist/commonjs/v3/externalDeploymentId.js.map +0 -1
  118. package/dist/commonjs/v3/test/transcript-storage-tests.d.ts +0 -40
  119. package/dist/commonjs/v3/test/transcript-storage-tests.js +0 -258
  120. package/dist/commonjs/v3/test/transcript-storage-tests.js.map +0 -1
  121. package/dist/commonjs/v3/transcriptStorage.d.ts +0 -229
  122. package/dist/commonjs/v3/transcriptStorage.js +0 -321
  123. package/dist/commonjs/v3/transcriptStorage.js.map +0 -1
  124. package/dist/esm/v3/chatSnapshotIo.d.ts +0 -2
  125. package/dist/esm/v3/chatSnapshotIo.js +0 -166
  126. package/dist/esm/v3/chatSnapshotIo.js.map +0 -1
  127. package/dist/esm/v3/chatVersionSkew.d.ts +0 -12
  128. package/dist/esm/v3/chatVersionSkew.js +0 -27
  129. package/dist/esm/v3/chatVersionSkew.js.map +0 -1
  130. package/dist/esm/v3/externalDeploymentId.d.ts +0 -23
  131. package/dist/esm/v3/externalDeploymentId.js +0 -38
  132. package/dist/esm/v3/externalDeploymentId.js.map +0 -1
  133. package/dist/esm/v3/test/transcript-storage-tests.d.ts +0 -40
  134. package/dist/esm/v3/test/transcript-storage-tests.js +0 -255
  135. package/dist/esm/v3/test/transcript-storage-tests.js.map +0 -1
  136. package/dist/esm/v3/transcriptStorage.d.ts +0 -229
  137. package/dist/esm/v3/transcriptStorage.js +0 -309
  138. package/dist/esm/v3/transcriptStorage.js.map +0 -1
  139. package/docs/ai-chat/patterns/native-compaction.mdx +0 -310
  140. package/docs/ai-chat/transcript-storage.mdx +0 -233
  141. package/docs/reports.mdx +0 -157
  142. package/docs/troubleshooting-zod.mdx +0 -158
@@ -22,12 +22,12 @@
22
22
  * }
23
23
  * ```
24
24
  */
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";
25
+ import { controlSubtype, headerValue, 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 { MAX_EOF_RESUBSCRIBES, slimSubmitMessageForWire, waitBeforeEofResubscribe, } from "./ai-shared.js";
30
+ import { slimSubmitMessageForWire } 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,32 +40,6 @@ 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
- }
69
43
  /**
70
44
  * Detect a 404 from a session-PAT-authed call — i.e. the session doesn't
71
45
  * exist in the current environment. Happens when hydrated session state
@@ -171,7 +145,6 @@ export class TriggerChatTransport {
171
145
  _onSessionChange;
172
146
  _onEvent;
173
147
  sessions = new Map();
174
- pendingResumeCursors = new Map();
175
148
  activeStreams = new Map();
176
149
  pendingStarts = new Map();
177
150
  // Last turn-producing send per chat — attribution source for the
@@ -211,8 +184,6 @@ export class TriggerChatTransport {
211
184
  lastEventId: session.lastEventId,
212
185
  activeInputSeq: session.activeInputSeq,
213
186
  isStreaming: session.isStreaming,
214
- closed: session.closed,
215
- closedReason: session.closedReason,
216
187
  });
217
188
  }
218
189
  }
@@ -262,10 +233,6 @@ export class TriggerChatTransport {
262
233
  */
263
234
  sendMessages = async (options) => {
264
235
  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
- }
269
236
  if (this.coordinator) {
270
237
  if (this.coordinator.isReadOnly(chatId)) {
271
238
  throw new Error("This chat is active in another tab");
@@ -275,14 +242,6 @@ export class TriggerChatTransport {
275
242
  const mergedMetadata = this.defaultMetadata || metadata
276
243
  ? { ...(this.defaultMetadata ?? {}), ...(metadata ?? {}) }
277
244
  : 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
- }
286
245
  // First-turn handover routing — when `headStart` is set AND no
287
246
  // session state exists yet for this chatId, POST the wire payload
288
247
  // to the customer's `chat.handover` route handler. The handler
@@ -345,9 +304,11 @@ export class TriggerChatTransport {
345
304
  activeStream.abort();
346
305
  this.activeStreams.delete(chatId);
347
306
  }
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;
348
310
  state.activeInputSeq = inSeq;
349
311
  state.isStreaming = true;
350
- state.outstandingTurnAbandoned = false;
351
312
  this.notifySessionChange(chatId, state);
352
313
  // Owning turn: aborting this live send stops the turn the user drives.
353
314
  return this.subscribeToSessionStream(state, abortSignal, chatId, {
@@ -419,16 +380,6 @@ export class TriggerChatTransport {
419
380
  };
420
381
  this.sessions.set(chatId, state);
421
382
  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
- }
432
383
  // Filter the parsed UIMessage stream:
433
384
  // - Drop control chunks (`trigger:turn-complete`,
434
385
  // `trigger:session-state`) before they reach AI SDK — they
@@ -595,9 +546,6 @@ export class TriggerChatTransport {
595
546
  const state = this.sessions.get(options.chatId);
596
547
  if (!state)
597
548
  return null;
598
- // A closed session has no further turns to resume.
599
- if (state.closed)
600
- return null;
601
549
  // Watch is a standing subscription: a settled session is exactly the
602
550
  // state it waits in, so a completed last turn must not block the resume.
603
551
  if (state.isStreaming === false && !this.watchMode)
@@ -642,13 +590,7 @@ export class TriggerChatTransport {
642
590
  catch {
643
591
  return false;
644
592
  }
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
- }
593
+ state.skipToTurnComplete = true;
652
594
  const activeStream = this.activeStreams.get(chatId);
653
595
  if (activeStream) {
654
596
  activeStream.abort();
@@ -667,28 +609,15 @@ export class TriggerChatTransport {
667
609
  this.notifySessionChange(chatId, state);
668
610
  return true;
669
611
  };
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
- };
683
612
  /**
684
613
  * Send a custom action chunk (for `chat.agent`'s `actionSchema` /
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`.
614
+ * `onAction` hook). Actions are not turns — only `hydrateMessages`
615
+ * and `onAction` fire on the agent side. The returned stream
616
+ * carries any model response `onAction` produced (when it returns a
617
+ * `StreamTextResult`); for `void`-returning side-effect-only actions
618
+ * the stream completes immediately with `trigger:turn-complete`.
690
619
  */
691
- sendAction = async (chatId, action, options) => {
620
+ sendAction = async (chatId, action) => {
692
621
  if (this.coordinator) {
693
622
  if (this.coordinator.isReadOnly(chatId)) {
694
623
  throw new Error("This chat is active in another tab");
@@ -700,9 +629,7 @@ export class TriggerChatTransport {
700
629
  chatId,
701
630
  trigger: "action",
702
631
  action,
703
- metadata: this.defaultMetadata || options?.metadata
704
- ? { ...(this.defaultMetadata ?? {}), ...(options?.metadata ?? {}) }
705
- : undefined,
632
+ metadata: this.defaultMetadata ?? undefined,
706
633
  };
707
634
  const body = this.serializeInputChunk({ kind: "message", payload: wirePayload });
708
635
  const partId = crypto.randomUUID();
@@ -717,14 +644,16 @@ export class TriggerChatTransport {
717
644
  activeStream.abort();
718
645
  this.activeStreams.delete(chatId);
719
646
  }
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;
720
650
  // Mark streaming + persist so a reload mid-action resumes (reconnectToStream
721
651
  // no-ops when the persisted session says isStreaming: false).
722
652
  state.activeInputSeq = inSeq;
723
653
  state.isStreaming = true;
724
- state.outstandingTurnAbandoned = false;
725
654
  this.notifySessionChange(chatId, state);
726
655
  // Owning action: aborting this send stops the turn the user drives.
727
- return this.subscribeToSessionStream(state, options?.abortSignal, chatId, {
656
+ return this.subscribeToSessionStream(state, undefined, chatId, {
728
657
  sinceInSeq: inSeq,
729
658
  sendStopOnAbort: true,
730
659
  });
@@ -739,42 +668,14 @@ export class TriggerChatTransport {
739
668
  return this.toPersisted(state);
740
669
  };
741
670
  setSession(chatId, session) {
742
- this.sessions.set(chatId, this.applyPendingResumeCursor(chatId, {
671
+ this.sessions.set(chatId, {
743
672
  publicAccessToken: session.publicAccessToken,
744
673
  lastEventId: session.lastEventId,
745
674
  activeInputSeq: session.activeInputSeq,
746
675
  isStreaming: session.isStreaming,
747
- }));
676
+ });
748
677
  this.notifySessionChange(chatId, this.toPersisted(this.sessions.get(chatId)));
749
678
  }
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
- }
778
679
  setOnSessionChange(callback) {
779
680
  this._onSessionChange = callback;
780
681
  }
@@ -845,22 +746,6 @@ export class TriggerChatTransport {
845
746
  setClientData(clientData) {
846
747
  this.defaultMetadata = clientData;
847
748
  }
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
- }
864
749
  // -------------------------------------------------------------------------
865
750
  // Multi-tab coordination passthrough
866
751
  // -------------------------------------------------------------------------
@@ -908,42 +793,7 @@ export class TriggerChatTransport {
908
793
  lastEventId: state.lastEventId,
909
794
  activeInputSeq: state.activeInputSeq,
910
795
  isStreaming: state.isStreaming,
911
- closed: state.closed,
912
- closedReason: state.closedReason,
913
796
  });
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;
947
797
  notifySessionChange(chatId, session) {
948
798
  if (!this._onSessionChange)
949
799
  return;
@@ -978,7 +828,7 @@ export class TriggerChatTransport {
978
828
  // `sessions: { ... }` already, or the very first `accessToken` call
979
829
  // returns a PAT for an out-of-band-created session.
980
830
  const token = await this.resolveAccessToken({ chatId });
981
- const state = this.applyPendingResumeCursor(chatId, { publicAccessToken: token });
831
+ const state = { publicAccessToken: token };
982
832
  this.sessions.set(chatId, state);
983
833
  this.notifySessionChange(chatId, state);
984
834
  return state;
@@ -987,23 +837,15 @@ export class TriggerChatTransport {
987
837
  if (!this.resolveStartSession) {
988
838
  throw new Error("TriggerChatTransport: `startSession` is required to call `start()` / `preload()`. Either provide it or pre-hydrate the session via `sessions: { ... }`.");
989
839
  }
990
- const { publicAccessToken, pendingVersion } = await this.resolveStartSession({
840
+ const { publicAccessToken } = await this.resolveStartSession({
991
841
  taskId: this.taskId,
992
842
  chatId,
993
843
  clientData: (this.defaultMetadata ?? {}),
994
844
  });
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, {
845
+ const state = {
1004
846
  publicAccessToken,
1005
847
  isStreaming: false,
1006
- });
848
+ };
1007
849
  this.sessions.set(chatId, state);
1008
850
  this.notifySessionChange(chatId, state);
1009
851
  return state;
@@ -1016,33 +858,6 @@ export class TriggerChatTransport {
1016
858
  const raw = this.resolveBaseURLFn(ctx);
1017
859
  return raw.replace(/\/$/, "");
1018
860
  }
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
- }
1046
861
  async doFetch(ctx, url, init) {
1047
862
  return this.fetchOverride ? this.fetchOverride(url, init, ctx) : fetch(url, init);
1048
863
  }
@@ -1064,65 +879,24 @@ export class TriggerChatTransport {
1064
879
  const err = new Error(`appendToSessionStream failed: ${response.status} ${text}`);
1065
880
  err.name = "TriggerApiError";
1066
881
  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;
1078
882
  throw err;
1079
883
  }
1080
884
  // The appended record's `.in` seq, for correlating the response stream to
1081
885
  // this send. Omitted by older webapps / a lost idempotency claim.
1082
886
  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
- }
1091
887
  return typeof data?.seq === "number" ? data.seq : undefined;
1092
888
  }
1093
889
  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
- };
1114
890
  // 1) Try with the current PAT.
1115
891
  try {
1116
- return await attempt(state.publicAccessToken);
892
+ return await op(state.publicAccessToken);
1117
893
  }
1118
894
  catch (err) {
1119
- if (isSessionClosedError(err))
1120
- throw err;
1121
895
  if (isSessionNotFoundError(err)) {
1122
896
  // The cached PAT authenticated but the session doesn't exist here —
1123
897
  // recreate it and retry.
1124
898
  await this.recreateSession(chatId, state);
1125
- return await attempt(state.publicAccessToken);
899
+ return await op(state.publicAccessToken);
1126
900
  }
1127
901
  if (!isAuthError(err))
1128
902
  throw err;
@@ -1133,7 +907,7 @@ export class TriggerChatTransport {
1133
907
  state.publicAccessToken = fresh;
1134
908
  this.notifySessionChange(chatId, state);
1135
909
  try {
1136
- return await attempt(fresh);
910
+ return await op(fresh);
1137
911
  }
1138
912
  catch (err) {
1139
913
  if (!isSessionNotFoundError(err))
@@ -1143,7 +917,7 @@ export class TriggerChatTransport {
1143
917
  // state is stale (created in a different environment, or before the
1144
918
  // sessions upgrade). Recreate the session and retry once.
1145
919
  await this.recreateSession(chatId, state);
1146
- return await attempt(state.publicAccessToken);
920
+ return await op(state.publicAccessToken);
1147
921
  }
1148
922
  /**
1149
923
  * Repair a stale/phantom hydrated session. The cached state pointed at a
@@ -1159,19 +933,11 @@ export class TriggerChatTransport {
1159
933
  if (!this.resolveStartSession) {
1160
934
  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.");
1161
935
  }
1162
- const { publicAccessToken, pendingVersion } = await this.resolveStartSession({
936
+ const { publicAccessToken } = await this.resolveStartSession({
1163
937
  taskId: this.taskId,
1164
938
  chatId,
1165
939
  clientData: (this.defaultMetadata ?? {}),
1166
940
  });
1167
- if (pendingVersion) {
1168
- this.emitEvent({
1169
- type: "run-pending-version",
1170
- chatId,
1171
- timestamp: Date.now(),
1172
- source: "start",
1173
- });
1174
- }
1175
941
  state.publicAccessToken = publicAccessToken;
1176
942
  state.lastEventId = undefined;
1177
943
  state.isStreaming = false;
@@ -1195,13 +961,8 @@ export class TriggerChatTransport {
1195
961
  : internalAbort.signal;
1196
962
  if (abortSignal) {
1197
963
  abortSignal.addEventListener("abort", () => {
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) {
964
+ if (options?.sendStopOnAbort !== false) {
1203
965
  state.skipToTurnComplete = true;
1204
- state.supersededInputSeq = state.activeInputSeq;
1205
966
  this.appendInputChunk(chatId, state.publicAccessToken, this.serializeInputChunk({ kind: "stop" })).catch(() => { });
1206
967
  }
1207
968
  internalAbort.abort();
@@ -1261,7 +1022,26 @@ export class TriggerChatTransport {
1261
1022
  })()
1262
1023
  : () => { };
1263
1024
  const sseCtx = { endpoint: "out", chatId };
1264
- const sseFetchClient = this.sseFetchClient(sseCtx);
1025
+ const fetchOverride = this.fetchOverride;
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;
1265
1045
  let sawFirstChunk = false;
1266
1046
  let sinceInSeq = options?.sinceInSeq;
1267
1047
  const connectSseOnce = async (token) => {
@@ -1309,6 +1089,12 @@ export class TriggerChatTransport {
1309
1089
  return await connectSseOnce(fresh);
1310
1090
  }
1311
1091
  };
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;
1312
1098
  let eofResubscribes = 0;
1313
1099
  const resumeAfterEof = async () => {
1314
1100
  // Watch mode is a standing subscription: it outlives turn-complete
@@ -1318,7 +1104,21 @@ export class TriggerChatTransport {
1318
1104
  !currentSubscription?.sessionSettled &&
1319
1105
  !combinedSignal.aborted) {
1320
1106
  eofResubscribes++;
1321
- await waitBeforeEofResubscribe(eofResubscribes, combinedSignal);
1107
+ // Sleep, but wake immediately on abort — otherwise a stop lands
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
+ });
1322
1122
  if (combinedSignal.aborted)
1323
1123
  break;
1324
1124
  const opened = await openWithAuthRetry();
@@ -1442,41 +1242,11 @@ export class TriggerChatTransport {
1442
1242
  continue;
1443
1243
  }
1444
1244
  }
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.");
1245
+ if (state.skipToTurnComplete) {
1246
+ if (controlValue === TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE) {
1247
+ state.skipToTurnComplete = false;
1479
1248
  }
1249
+ continue;
1480
1250
  }
1481
1251
  if (controlValue === TRIGGER_CONTROL_SUBTYPE.UPGRADE_REQUIRED) {
1482
1252
  // Server has already triggered the new run via
@@ -1485,26 +1255,6 @@ export class TriggerChatTransport {
1485
1255
  // and keep reading.
1486
1256
  continue;
1487
1257
  }
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
- }
1508
1258
  if (controlValue === TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE) {
1509
1259
  // Skip a turn-complete from an earlier turn (committed `.in` cursor
1510
1260
  // below this send's seq), e.g. an undo action that raced this send.
@@ -1515,11 +1265,6 @@ export class TriggerChatTransport {
1515
1265
  continue;
1516
1266
  }
1517
1267
  }
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
- }
1523
1268
  const refreshedToken = headerValue(value.headers, PUBLIC_ACCESS_TOKEN_HEADER) ??
1524
1269
  legacyChunk?.publicAccessToken;
1525
1270
  if (refreshedToken) {