@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
@@ -44,32 +44,6 @@ 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
- }
73
47
  /**
74
48
  * Detect a 404 from a session-PAT-authed call — i.e. the session doesn't
75
49
  * exist in the current environment. Happens when hydrated session state
@@ -175,7 +149,6 @@ class TriggerChatTransport {
175
149
  _onSessionChange;
176
150
  _onEvent;
177
151
  sessions = new Map();
178
- pendingResumeCursors = new Map();
179
152
  activeStreams = new Map();
180
153
  pendingStarts = new Map();
181
154
  // Last turn-producing send per chat — attribution source for the
@@ -215,8 +188,6 @@ class TriggerChatTransport {
215
188
  lastEventId: session.lastEventId,
216
189
  activeInputSeq: session.activeInputSeq,
217
190
  isStreaming: session.isStreaming,
218
- closed: session.closed,
219
- closedReason: session.closedReason,
220
191
  });
221
192
  }
222
193
  }
@@ -266,10 +237,6 @@ class TriggerChatTransport {
266
237
  */
267
238
  sendMessages = async (options) => {
268
239
  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
- }
273
240
  if (this.coordinator) {
274
241
  if (this.coordinator.isReadOnly(chatId)) {
275
242
  throw new Error("This chat is active in another tab");
@@ -279,14 +246,6 @@ class TriggerChatTransport {
279
246
  const mergedMetadata = this.defaultMetadata || metadata
280
247
  ? { ...(this.defaultMetadata ?? {}), ...(metadata ?? {}) }
281
248
  : 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
- }
290
249
  // First-turn handover routing — when `headStart` is set AND no
291
250
  // session state exists yet for this chatId, POST the wire payload
292
251
  // to the customer's `chat.handover` route handler. The handler
@@ -349,9 +308,11 @@ class TriggerChatTransport {
349
308
  activeStream.abort();
350
309
  this.activeStreams.delete(chatId);
351
310
  }
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;
352
314
  state.activeInputSeq = inSeq;
353
315
  state.isStreaming = true;
354
- state.outstandingTurnAbandoned = false;
355
316
  this.notifySessionChange(chatId, state);
356
317
  // Owning turn: aborting this live send stops the turn the user drives.
357
318
  return this.subscribeToSessionStream(state, abortSignal, chatId, {
@@ -423,16 +384,6 @@ class TriggerChatTransport {
423
384
  };
424
385
  this.sessions.set(chatId, state);
425
386
  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
- }
436
387
  // Filter the parsed UIMessage stream:
437
388
  // - Drop control chunks (`trigger:turn-complete`,
438
389
  // `trigger:session-state`) before they reach AI SDK — they
@@ -599,9 +550,6 @@ class TriggerChatTransport {
599
550
  const state = this.sessions.get(options.chatId);
600
551
  if (!state)
601
552
  return null;
602
- // A closed session has no further turns to resume.
603
- if (state.closed)
604
- return null;
605
553
  // Watch is a standing subscription: a settled session is exactly the
606
554
  // state it waits in, so a completed last turn must not block the resume.
607
555
  if (state.isStreaming === false && !this.watchMode)
@@ -646,13 +594,7 @@ class TriggerChatTransport {
646
594
  catch {
647
595
  return false;
648
596
  }
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
- }
597
+ state.skipToTurnComplete = true;
656
598
  const activeStream = this.activeStreams.get(chatId);
657
599
  if (activeStream) {
658
600
  activeStream.abort();
@@ -671,28 +613,15 @@ class TriggerChatTransport {
671
613
  this.notifySessionChange(chatId, state);
672
614
  return true;
673
615
  };
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
- };
687
616
  /**
688
617
  * Send a custom action chunk (for `chat.agent`'s `actionSchema` /
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`.
618
+ * `onAction` hook). Actions are not turns — only `hydrateMessages`
619
+ * and `onAction` fire on the agent side. The returned stream
620
+ * carries any model response `onAction` produced (when it returns a
621
+ * `StreamTextResult`); for `void`-returning side-effect-only actions
622
+ * the stream completes immediately with `trigger:turn-complete`.
694
623
  */
695
- sendAction = async (chatId, action, options) => {
624
+ sendAction = async (chatId, action) => {
696
625
  if (this.coordinator) {
697
626
  if (this.coordinator.isReadOnly(chatId)) {
698
627
  throw new Error("This chat is active in another tab");
@@ -704,9 +633,7 @@ class TriggerChatTransport {
704
633
  chatId,
705
634
  trigger: "action",
706
635
  action,
707
- metadata: this.defaultMetadata || options?.metadata
708
- ? { ...(this.defaultMetadata ?? {}), ...(options?.metadata ?? {}) }
709
- : undefined,
636
+ metadata: this.defaultMetadata ?? undefined,
710
637
  };
711
638
  const body = this.serializeInputChunk({ kind: "message", payload: wirePayload });
712
639
  const partId = crypto.randomUUID();
@@ -721,14 +648,16 @@ class TriggerChatTransport {
721
648
  activeStream.abort();
722
649
  this.activeStreams.delete(chatId);
723
650
  }
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;
724
654
  // Mark streaming + persist so a reload mid-action resumes (reconnectToStream
725
655
  // no-ops when the persisted session says isStreaming: false).
726
656
  state.activeInputSeq = inSeq;
727
657
  state.isStreaming = true;
728
- state.outstandingTurnAbandoned = false;
729
658
  this.notifySessionChange(chatId, state);
730
659
  // Owning action: aborting this send stops the turn the user drives.
731
- return this.subscribeToSessionStream(state, options?.abortSignal, chatId, {
660
+ return this.subscribeToSessionStream(state, undefined, chatId, {
732
661
  sinceInSeq: inSeq,
733
662
  sendStopOnAbort: true,
734
663
  });
@@ -743,42 +672,14 @@ class TriggerChatTransport {
743
672
  return this.toPersisted(state);
744
673
  };
745
674
  setSession(chatId, session) {
746
- this.sessions.set(chatId, this.applyPendingResumeCursor(chatId, {
675
+ this.sessions.set(chatId, {
747
676
  publicAccessToken: session.publicAccessToken,
748
677
  lastEventId: session.lastEventId,
749
678
  activeInputSeq: session.activeInputSeq,
750
679
  isStreaming: session.isStreaming,
751
- }));
680
+ });
752
681
  this.notifySessionChange(chatId, this.toPersisted(this.sessions.get(chatId)));
753
682
  }
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
- }
782
683
  setOnSessionChange(callback) {
783
684
  this._onSessionChange = callback;
784
685
  }
@@ -849,22 +750,6 @@ class TriggerChatTransport {
849
750
  setClientData(clientData) {
850
751
  this.defaultMetadata = clientData;
851
752
  }
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
- }
868
753
  // -------------------------------------------------------------------------
869
754
  // Multi-tab coordination passthrough
870
755
  // -------------------------------------------------------------------------
@@ -912,42 +797,7 @@ class TriggerChatTransport {
912
797
  lastEventId: state.lastEventId,
913
798
  activeInputSeq: state.activeInputSeq,
914
799
  isStreaming: state.isStreaming,
915
- closed: state.closed,
916
- closedReason: state.closedReason,
917
800
  });
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;
951
801
  notifySessionChange(chatId, session) {
952
802
  if (!this._onSessionChange)
953
803
  return;
@@ -982,7 +832,7 @@ class TriggerChatTransport {
982
832
  // `sessions: { ... }` already, or the very first `accessToken` call
983
833
  // returns a PAT for an out-of-band-created session.
984
834
  const token = await this.resolveAccessToken({ chatId });
985
- const state = this.applyPendingResumeCursor(chatId, { publicAccessToken: token });
835
+ const state = { publicAccessToken: token };
986
836
  this.sessions.set(chatId, state);
987
837
  this.notifySessionChange(chatId, state);
988
838
  return state;
@@ -991,23 +841,15 @@ class TriggerChatTransport {
991
841
  if (!this.resolveStartSession) {
992
842
  throw new Error("TriggerChatTransport: `startSession` is required to call `start()` / `preload()`. Either provide it or pre-hydrate the session via `sessions: { ... }`.");
993
843
  }
994
- const { publicAccessToken, pendingVersion } = await this.resolveStartSession({
844
+ const { publicAccessToken } = await this.resolveStartSession({
995
845
  taskId: this.taskId,
996
846
  chatId,
997
847
  clientData: (this.defaultMetadata ?? {}),
998
848
  });
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, {
849
+ const state = {
1008
850
  publicAccessToken,
1009
851
  isStreaming: false,
1010
- });
852
+ };
1011
853
  this.sessions.set(chatId, state);
1012
854
  this.notifySessionChange(chatId, state);
1013
855
  return state;
@@ -1020,33 +862,6 @@ class TriggerChatTransport {
1020
862
  const raw = this.resolveBaseURLFn(ctx);
1021
863
  return raw.replace(/\/$/, "");
1022
864
  }
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
- }
1050
865
  async doFetch(ctx, url, init) {
1051
866
  return this.fetchOverride ? this.fetchOverride(url, init, ctx) : fetch(url, init);
1052
867
  }
@@ -1068,65 +883,24 @@ class TriggerChatTransport {
1068
883
  const err = new Error(`appendToSessionStream failed: ${response.status} ${text}`);
1069
884
  err.name = "TriggerApiError";
1070
885
  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;
1082
886
  throw err;
1083
887
  }
1084
888
  // The appended record's `.in` seq, for correlating the response stream to
1085
889
  // this send. Omitted by older webapps / a lost idempotency claim.
1086
890
  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
- }
1095
891
  return typeof data?.seq === "number" ? data.seq : undefined;
1096
892
  }
1097
893
  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
- };
1118
894
  // 1) Try with the current PAT.
1119
895
  try {
1120
- return await attempt(state.publicAccessToken);
896
+ return await op(state.publicAccessToken);
1121
897
  }
1122
898
  catch (err) {
1123
- if (isSessionClosedError(err))
1124
- throw err;
1125
899
  if (isSessionNotFoundError(err)) {
1126
900
  // The cached PAT authenticated but the session doesn't exist here —
1127
901
  // recreate it and retry.
1128
902
  await this.recreateSession(chatId, state);
1129
- return await attempt(state.publicAccessToken);
903
+ return await op(state.publicAccessToken);
1130
904
  }
1131
905
  if (!isAuthError(err))
1132
906
  throw err;
@@ -1137,7 +911,7 @@ class TriggerChatTransport {
1137
911
  state.publicAccessToken = fresh;
1138
912
  this.notifySessionChange(chatId, state);
1139
913
  try {
1140
- return await attempt(fresh);
914
+ return await op(fresh);
1141
915
  }
1142
916
  catch (err) {
1143
917
  if (!isSessionNotFoundError(err))
@@ -1147,7 +921,7 @@ class TriggerChatTransport {
1147
921
  // state is stale (created in a different environment, or before the
1148
922
  // sessions upgrade). Recreate the session and retry once.
1149
923
  await this.recreateSession(chatId, state);
1150
- return await attempt(state.publicAccessToken);
924
+ return await op(state.publicAccessToken);
1151
925
  }
1152
926
  /**
1153
927
  * Repair a stale/phantom hydrated session. The cached state pointed at a
@@ -1163,19 +937,11 @@ class TriggerChatTransport {
1163
937
  if (!this.resolveStartSession) {
1164
938
  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.");
1165
939
  }
1166
- const { publicAccessToken, pendingVersion } = await this.resolveStartSession({
940
+ const { publicAccessToken } = await this.resolveStartSession({
1167
941
  taskId: this.taskId,
1168
942
  chatId,
1169
943
  clientData: (this.defaultMetadata ?? {}),
1170
944
  });
1171
- if (pendingVersion) {
1172
- this.emitEvent({
1173
- type: "run-pending-version",
1174
- chatId,
1175
- timestamp: Date.now(),
1176
- source: "start",
1177
- });
1178
- }
1179
945
  state.publicAccessToken = publicAccessToken;
1180
946
  state.lastEventId = undefined;
1181
947
  state.isStreaming = false;
@@ -1199,13 +965,8 @@ class TriggerChatTransport {
1199
965
  : internalAbort.signal;
1200
966
  if (abortSignal) {
1201
967
  abortSignal.addEventListener("abort", () => {
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) {
968
+ if (options?.sendStopOnAbort !== false) {
1207
969
  state.skipToTurnComplete = true;
1208
- state.supersededInputSeq = state.activeInputSeq;
1209
970
  this.appendInputChunk(chatId, state.publicAccessToken, this.serializeInputChunk({ kind: "stop" })).catch(() => { });
1210
971
  }
1211
972
  internalAbort.abort();
@@ -1265,7 +1026,26 @@ class TriggerChatTransport {
1265
1026
  })()
1266
1027
  : () => { };
1267
1028
  const sseCtx = { endpoint: "out", chatId };
1268
- const sseFetchClient = this.sseFetchClient(sseCtx);
1029
+ const fetchOverride = this.fetchOverride;
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;
1269
1049
  let sawFirstChunk = false;
1270
1050
  let sinceInSeq = options?.sinceInSeq;
1271
1051
  const connectSseOnce = async (token) => {
@@ -1313,16 +1093,36 @@ class TriggerChatTransport {
1313
1093
  return await connectSseOnce(fresh);
1314
1094
  }
1315
1095
  };
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;
1316
1102
  let eofResubscribes = 0;
1317
1103
  const resumeAfterEof = async () => {
1318
1104
  // Watch mode is a standing subscription: it outlives turn-complete
1319
1105
  // (which clears `isStreaming`) and idle windows EOF by design, so the
1320
1106
  // give-up budget doesn't apply. Only abort or a settled session ends it.
1321
- while ((this.watchMode || (state.isStreaming && eofResubscribes < ai_shared_js_1.MAX_EOF_RESUBSCRIBES)) &&
1107
+ while ((this.watchMode || (state.isStreaming && eofResubscribes < MAX_EOF_RESUBSCRIBES)) &&
1322
1108
  !currentSubscription?.sessionSettled &&
1323
1109
  !combinedSignal.aborted) {
1324
1110
  eofResubscribes++;
1325
- await (0, ai_shared_js_1.waitBeforeEofResubscribe)(eofResubscribes, combinedSignal);
1111
+ // Sleep, but wake immediately on abort — otherwise a stop lands
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
+ });
1326
1126
  if (combinedSignal.aborted)
1327
1127
  break;
1328
1128
  const opened = await openWithAuthRetry();
@@ -1446,41 +1246,11 @@ class TriggerChatTransport {
1446
1246
  continue;
1447
1247
  }
1448
1248
  }
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.");
1249
+ if (state.skipToTurnComplete) {
1250
+ if (controlValue === v3_1.TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE) {
1251
+ state.skipToTurnComplete = false;
1483
1252
  }
1253
+ continue;
1484
1254
  }
1485
1255
  if (controlValue === v3_1.TRIGGER_CONTROL_SUBTYPE.UPGRADE_REQUIRED) {
1486
1256
  // Server has already triggered the new run via
@@ -1489,26 +1259,6 @@ class TriggerChatTransport {
1489
1259
  // and keep reading.
1490
1260
  continue;
1491
1261
  }
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
- }
1512
1262
  if (controlValue === v3_1.TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE) {
1513
1263
  // Skip a turn-complete from an earlier turn (committed `.in` cursor
1514
1264
  // below this send's seq), e.g. an undo action that raced this send.
@@ -1519,11 +1269,6 @@ class TriggerChatTransport {
1519
1269
  continue;
1520
1270
  }
1521
1271
  }
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
- }
1527
1272
  const refreshedToken = (0, v3_1.headerValue)(value.headers, v3_1.PUBLIC_ACCESS_TOKEN_HEADER) ??
1528
1273
  legacyChunk?.publicAccessToken;
1529
1274
  if (refreshedToken) {