@trigger.dev/sdk 0.0.0-prerelease-20260911094758 → 0.0.0-prerelease-windows-bundle-fix-20260928100448

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 (133) hide show
  1. package/dist/commonjs/v3/ai.d.ts +73 -12
  2. package/dist/commonjs/v3/ai.js +542 -289
  3. package/dist/commonjs/v3/ai.js.map +1 -1
  4. package/dist/commonjs/v3/auth.d.ts +10 -4
  5. package/dist/commonjs/v3/auth.js +8 -2
  6. package/dist/commonjs/v3/auth.js.map +1 -1
  7. package/dist/commonjs/v3/chat-client.js +7 -0
  8. package/dist/commonjs/v3/chat-client.js.map +1 -1
  9. package/dist/commonjs/v3/chat-server.d.ts +1 -0
  10. package/dist/commonjs/v3/chat-server.js +8 -0
  11. package/dist/commonjs/v3/chat-server.js.map +1 -1
  12. package/dist/commonjs/v3/chat.js +13 -9
  13. package/dist/commonjs/v3/chat.js.map +1 -1
  14. package/dist/commonjs/v3/compactionResponse.d.ts +9 -0
  15. package/dist/commonjs/v3/compactionResponse.js +37 -0
  16. package/dist/commonjs/v3/compactionResponse.js.map +1 -0
  17. package/dist/commonjs/v3/concurrency-shared.d.ts +13 -0
  18. package/dist/commonjs/v3/concurrency-shared.js +35 -0
  19. package/dist/commonjs/v3/concurrency-shared.js.map +1 -0
  20. package/dist/commonjs/v3/concurrencyLimits.d.ts +73 -0
  21. package/dist/commonjs/v3/concurrencyLimits.js +166 -0
  22. package/dist/commonjs/v3/concurrencyLimits.js.map +1 -0
  23. package/dist/commonjs/v3/index.d.ts +2 -1
  24. package/dist/commonjs/v3/index.js +3 -1
  25. package/dist/commonjs/v3/index.js.map +1 -1
  26. package/dist/commonjs/v3/managedChatResponse.d.ts +44 -0
  27. package/dist/commonjs/v3/managedChatResponse.js +233 -0
  28. package/dist/commonjs/v3/managedChatResponse.js.map +1 -0
  29. package/dist/commonjs/v3/queues.d.ts +31 -0
  30. package/dist/commonjs/v3/queues.js +31 -0
  31. package/dist/commonjs/v3/queues.js.map +1 -1
  32. package/dist/commonjs/v3/runs.d.ts +5 -0
  33. package/dist/commonjs/v3/sessionTracing.d.ts +7 -0
  34. package/dist/commonjs/v3/sessionTracing.js +44 -0
  35. package/dist/commonjs/v3/sessionTracing.js.map +1 -0
  36. package/dist/commonjs/v3/sessions.js +31 -34
  37. package/dist/commonjs/v3/sessions.js.map +1 -1
  38. package/dist/commonjs/v3/shared.d.ts +18 -1
  39. package/dist/commonjs/v3/shared.js +145 -52
  40. package/dist/commonjs/v3/shared.js.map +1 -1
  41. package/dist/commonjs/v3/steeringContext.d.ts +41 -0
  42. package/dist/commonjs/v3/steeringContext.js +118 -0
  43. package/dist/commonjs/v3/steeringContext.js.map +1 -0
  44. package/dist/commonjs/v3/test/transcript-storage-tests.js.map +1 -1
  45. package/dist/commonjs/v3/transcriptStorage.d.ts +5 -2
  46. package/dist/commonjs/v3/transcriptStorage.js +55 -4
  47. package/dist/commonjs/v3/transcriptStorage.js.map +1 -1
  48. package/dist/commonjs/version.js +1 -1
  49. package/dist/esm/v3/ai.d.ts +73 -12
  50. package/dist/esm/v3/ai.js +543 -290
  51. package/dist/esm/v3/ai.js.map +1 -1
  52. package/dist/esm/v3/auth.d.ts +10 -4
  53. package/dist/esm/v3/auth.js +8 -2
  54. package/dist/esm/v3/auth.js.map +1 -1
  55. package/dist/esm/v3/chat-client.js +7 -0
  56. package/dist/esm/v3/chat-client.js.map +1 -1
  57. package/dist/esm/v3/chat-server.d.ts +1 -0
  58. package/dist/esm/v3/chat-server.js +8 -0
  59. package/dist/esm/v3/chat-server.js.map +1 -1
  60. package/dist/esm/v3/chat.js +13 -9
  61. package/dist/esm/v3/chat.js.map +1 -1
  62. package/dist/esm/v3/compactionResponse.d.ts +9 -0
  63. package/dist/esm/v3/compactionResponse.js +34 -0
  64. package/dist/esm/v3/compactionResponse.js.map +1 -0
  65. package/dist/esm/v3/concurrency-shared.d.ts +13 -0
  66. package/dist/esm/v3/concurrency-shared.js +31 -0
  67. package/dist/esm/v3/concurrency-shared.js.map +1 -0
  68. package/dist/esm/v3/concurrencyLimits.d.ts +73 -0
  69. package/dist/esm/v3/concurrencyLimits.js +158 -0
  70. package/dist/esm/v3/concurrencyLimits.js.map +1 -0
  71. package/dist/esm/v3/index.d.ts +2 -1
  72. package/dist/esm/v3/index.js +2 -1
  73. package/dist/esm/v3/index.js.map +1 -1
  74. package/dist/esm/v3/managedChatResponse.d.ts +44 -0
  75. package/dist/esm/v3/managedChatResponse.js +228 -0
  76. package/dist/esm/v3/managedChatResponse.js.map +1 -0
  77. package/dist/esm/v3/queues.d.ts +31 -0
  78. package/dist/esm/v3/queues.js +31 -0
  79. package/dist/esm/v3/queues.js.map +1 -1
  80. package/dist/esm/v3/runs.d.ts +5 -0
  81. package/dist/esm/v3/sessionTracing.d.ts +7 -0
  82. package/dist/esm/v3/sessionTracing.js +40 -0
  83. package/dist/esm/v3/sessionTracing.js.map +1 -0
  84. package/dist/esm/v3/sessions.js +31 -34
  85. package/dist/esm/v3/sessions.js.map +1 -1
  86. package/dist/esm/v3/shared.d.ts +18 -1
  87. package/dist/esm/v3/shared.js +145 -53
  88. package/dist/esm/v3/shared.js.map +1 -1
  89. package/dist/esm/v3/steeringContext.d.ts +41 -0
  90. package/dist/esm/v3/steeringContext.js +113 -0
  91. package/dist/esm/v3/steeringContext.js.map +1 -0
  92. package/dist/esm/v3/test/transcript-storage-tests.js.map +1 -1
  93. package/dist/esm/v3/transcriptStorage.d.ts +5 -2
  94. package/dist/esm/v3/transcriptStorage.js +55 -5
  95. package/dist/esm/v3/transcriptStorage.js.map +1 -1
  96. package/dist/esm/version.js +1 -1
  97. package/docs/ai-chat/anatomy.mdx +44 -24
  98. package/docs/ai-chat/background-injection.mdx +54 -1
  99. package/docs/ai-chat/client-protocol.mdx +2 -0
  100. package/docs/ai-chat/error-handling.mdx +44 -76
  101. package/docs/ai-chat/frontend.mdx +27 -21
  102. package/docs/ai-chat/lifecycle-hooks.mdx +4 -2
  103. package/docs/ai-chat/migrating-from-hydrate-messages.mdx +299 -0
  104. package/docs/ai-chat/patterns/branching-conversations.mdx +95 -230
  105. package/docs/ai-chat/patterns/database-persistence.mdx +18 -7
  106. package/docs/ai-chat/patterns/human-in-the-loop.mdx +166 -164
  107. package/docs/ai-chat/patterns/tool-result-auditing.mdx +28 -27
  108. package/docs/ai-chat/pending-messages.mdx +19 -5
  109. package/docs/ai-chat/quick-start.mdx +26 -20
  110. package/docs/ai-chat/reference.mdx +6 -3
  111. package/docs/ai-chat/side-channels.mdx +1 -1
  112. package/docs/ai-chat/testing.mdx +16 -4
  113. package/docs/ai-chat/transcript-storage.mdx +14 -6
  114. package/docs/cli-env-commands.mdx +114 -0
  115. package/docs/cli-projects-commands.mdx +62 -0
  116. package/docs/cli-runs-commands.mdx +94 -0
  117. package/docs/concurrency.mdx +384 -0
  118. package/docs/database-connections.mdx +3 -3
  119. package/docs/deploy-environment-variables.mdx +6 -0
  120. package/docs/idempotency.mdx +43 -5
  121. package/docs/introduction.mdx +58 -152
  122. package/docs/limits.mdx +16 -6
  123. package/docs/observability/query.mdx +25 -0
  124. package/docs/queues.mdx +248 -0
  125. package/docs/reports.mdx +1 -1
  126. package/docs/runs/priority.mdx +2 -25
  127. package/docs/self-hosting/env/webapp.mdx +7 -0
  128. package/docs/tasks/overview.mdx +3 -5
  129. package/docs/troubleshooting-alerts.mdx +124 -1
  130. package/docs/troubleshooting.mdx +12 -0
  131. package/docs/writing-tasks-introduction.mdx +2 -1
  132. package/package.json +2 -2
  133. package/docs/queue-concurrency.mdx +0 -358
@@ -15,8 +15,12 @@ const v3_1 = require("@trigger.dev/core/v3");
15
15
  // Runtime VALUES go through the ESM/CJS shim so the CJS build can `require`
16
16
  // ESM-only `ai@7` (see ../imports/ai-runtime.ts).
17
17
  const api_1 = require("@opentelemetry/api");
18
+ const sessionTracing_js_1 = require("./sessionTracing.js");
18
19
  const ai_runtime_js_1 = require("../imports/ai-runtime.js");
19
20
  const transcriptStorage_js_1 = require("./transcriptStorage.js");
21
+ const compactionResponse_js_1 = require("./compactionResponse.js");
22
+ const managedChatResponse_js_1 = require("./managedChatResponse.js");
23
+ const steeringContext_js_1 = require("./steeringContext.js");
20
24
  let transcriptStorageOverride;
21
25
  /**
22
26
  * Test-only override for the storage `chat.agent` persists through, so a
@@ -46,6 +50,7 @@ const externalDeploymentId_js_1 = require("./externalDeploymentId.js");
46
50
  const chatVersionSkew_js_1 = require("./chatVersionSkew.js");
47
51
  const sessions_js_1 = require("./sessions.js");
48
52
  const shared_js_1 = require("./shared.js");
53
+ const concurrency_shared_js_1 = require("./concurrency-shared.js");
49
54
  const streams_js_1 = require("./streams.js");
50
55
  const tracer_js_1 = require("./tracer.js");
51
56
  const METADATA_KEY = "tool.execute.options";
@@ -54,17 +59,17 @@ const METADATA_KEY = "tool.execute.options";
54
59
  * `ignoreIncompleteToolCalls: true` to prevent failures from
55
60
  * stopped/aborted conversations with partial tool parts.
56
61
  */
57
- function toModelMessages(messages) {
62
+ function toModelMessages(messages, context) {
58
63
  // Pass the resolved per-turn `tools` (if any) so the AI SDK can look up each
59
64
  // tool's `toModelOutput` and re-apply it to prior-turn tool results. Without
60
65
  // `tools` it falls back to JSON-stringifying the raw output (TRI-10149). The
61
66
  // conditional spread keeps the options object byte-identical to the no-tools
62
67
  // path when nothing was declared.
63
68
  const tools = locals_js_1.locals.get(chatResolvedToolsKey);
64
- return (0, ai_runtime_js_1.convertToModelMessages)(messages, {
69
+ return (0, steeringContext_js_1.convertSteeredMessages)(messages, async (batch) => (0, ai_runtime_js_1.convertToModelMessages)(batch, {
65
70
  ignoreIncompleteToolCalls: true,
66
71
  ...(tools ? { tools } : {}),
67
- });
72
+ }), locals_js_1.locals.get(chatSteeringInjectionsKey) ?? new Map(), context ?? locals_js_1.locals.get(chatCurrentUIMessagesKey) ?? messages, context !== undefined || locals_js_1.locals.get(chatCurrentUIMessagesKey) !== undefined);
68
73
  }
69
74
  const chatTurnContextKey = locals_js_1.locals.create("chat.turnContext");
70
75
  /**
@@ -683,31 +688,158 @@ exports.ai = {
683
688
  function createChatAccessToken(taskId) {
684
689
  return auth_js_1.auth.createTriggerPublicToken(taskId, { expirationTime: "24h" });
685
690
  }
686
- // ---------------------------------------------------------------------------
687
- // Chat transport helpers — backend side
688
- // ---------------------------------------------------------------------------
691
+ function createChatOutGate() {
692
+ let resolveOpened;
693
+ const opened = new Promise((resolve) => {
694
+ resolveOpened = resolve;
695
+ });
696
+ const gate = {
697
+ pending: new Set(),
698
+ open: false,
699
+ opened,
700
+ failOpen() {
701
+ if (gate.open)
702
+ return;
703
+ gate.open = true;
704
+ resolveOpened();
705
+ },
706
+ };
707
+ return gate;
708
+ }
709
+ const chatOutGateKey = locals_js_1.locals.create("chat.outGate");
710
+ /**
711
+ * How long a write waits on the gate before giving up. A storage that hangs
712
+ * degrades to an ungated write rather than stalling the conversation.
713
+ * @internal
714
+ */
715
+ const CHAT_OUT_GATE_TIMEOUT_MS = 10_000;
716
+ /**
717
+ * The ids to save non-final after a failed turn: the stream's partial answer,
718
+ * but only while the message under its id is still that partial by content.
719
+ * `onTurnComplete` may hand back a cloned history (same content, new objects),
720
+ * which keeps it partial, or replace it in place, which finishes it.
721
+ * @internal
722
+ */
723
+ function partialStillUnfinished(partial, fingerprint, messages) {
724
+ if (!partial || fingerprint === undefined)
725
+ return undefined;
726
+ const current = messages.find((message) => message.id === partial.id);
727
+ if (!current || (0, transcriptStorage_js_1.fingerprintMessage)(current) !== fingerprint)
728
+ return undefined;
729
+ return new Set([partial.id]);
730
+ }
731
+ async function awaitChatOutGate() {
732
+ const gate = locals_js_1.locals.get(chatOutGateKey);
733
+ if (!gate || gate.open)
734
+ return;
735
+ const deadline = Date.now() + CHAT_OUT_GATE_TIMEOUT_MS;
736
+ while (gate.pending.size > 0) {
737
+ const remaining = deadline - Date.now();
738
+ if (remaining <= 0) {
739
+ gate.failOpen();
740
+ return;
741
+ }
742
+ const waitingOn = [...gate.pending];
743
+ let timedOut = false;
744
+ let timer;
745
+ try {
746
+ await Promise.race([
747
+ Promise.allSettled(waitingOn),
748
+ gate.opened,
749
+ new Promise((resolve) => {
750
+ timer = setTimeout(() => {
751
+ timedOut = true;
752
+ resolve();
753
+ }, remaining);
754
+ }),
755
+ ]);
756
+ }
757
+ finally {
758
+ if (timer)
759
+ clearTimeout(timer);
760
+ }
761
+ if (gate.open)
762
+ return;
763
+ if (timedOut) {
764
+ gate.failOpen();
765
+ return;
766
+ }
767
+ for (const settled of waitingOn)
768
+ gate.pending.delete(settled);
769
+ }
770
+ }
689
771
  /**
690
- * Typed chat output stream — `.writer()`, `.pipe()`, `.append()`, and
691
- * `.read()` methods pre-bound to this run's Session `.out` channel and
692
- * typed to `UIMessageChunk`.
772
+ * Register work that must land before anything from this turn reaches the
773
+ * frontend.
774
+ *
775
+ * Like {@link chatDefer} the work starts immediately and is never awaited by
776
+ * the hook that registered it, so it runs alongside the model and costs no
777
+ * time to first token. Unlike `chat.defer`, the output stream waits for it:
778
+ * no chunk of the answer is written to the session until it settles. That
779
+ * makes it the right home for a write the next page load has to see (a
780
+ * conversation row, a message insert), because a reader that can see the
781
+ * answer can also see what the write persisted.
782
+ *
783
+ * Reach for `chat.defer` instead when the timing does not matter for a
784
+ * reload: analytics, audit logs, search-index updates.
785
+ *
786
+ * This is not a consistency barrier for the turn. The work is still in flight
787
+ * while the model runs, so a tool, a `prepareStep`, or anything else executing
788
+ * during the turn can still read the state as it was before the write. It
789
+ * orders the write against what the frontend can see, nothing more. When the
790
+ * turn's own code has to read the write back, `await` it instead and accept
791
+ * the cost.
693
792
  *
694
- * Use from within a `chat.agent` run to write custom chunks:
793
+ * A write registered here that fails, or outlasts the internal timeout, lets
794
+ * the stream through rather than stalling the conversation.
795
+ *
796
+ * @example
695
797
  * ```ts
696
- * const { waitUntilComplete } = chat.stream.writer({
697
- * execute: ({ write }) => {
698
- * write({ type: "text-start", id: "status-1" });
699
- * write({ type: "text-delta", id: "status-1", delta: "Processing..." });
700
- * write({ type: "text-end", id: "status-1" });
701
- * },
702
- * });
703
- * await waitUntilComplete();
798
+ * onTurnStart: async ({ chatId, uiMessages }) => {
799
+ * chat.deferBeforeOutput(
800
+ * db.chat.update({ where: { id: chatId }, data: { messages: uiMessages } })
801
+ * );
802
+ * },
704
803
  * ```
705
- *
706
- * Backed by the Session primitive so a chat's output outlives any single
707
- * run — subscribers (browser transport, server-side `ChatStream`) read
708
- * the session's `.out`, not a per-run stream. Run-scoped `target`
709
- * options on `.pipe()` are honoured as no-ops; the session is the target.
710
804
  */
805
+ function chatDeferBeforeOutput(promiseOrFn) {
806
+ const gate = locals_js_1.locals.get(chatOutGateKey);
807
+ const work = typeof promiseOrFn === "function" ? promiseOrFn() : promiseOrFn;
808
+ if (!gate || gate.open)
809
+ return;
810
+ gate.pending.add(work);
811
+ }
812
+ function gateWriterOptions(options) {
813
+ return {
814
+ ...options,
815
+ execute: async (api) => {
816
+ await awaitChatOutGate();
817
+ return await options.execute(api);
818
+ },
819
+ };
820
+ }
821
+ function gateOutStream(value) {
822
+ return (async function* () {
823
+ await awaitChatOutGate();
824
+ if (isReadableStream(value)) {
825
+ const reader = value.getReader();
826
+ try {
827
+ while (true) {
828
+ const { done, value: chunk } = await reader.read();
829
+ if (done)
830
+ break;
831
+ yield chunk;
832
+ }
833
+ }
834
+ finally {
835
+ reader.releaseLock();
836
+ }
837
+ }
838
+ else {
839
+ yield* value;
840
+ }
841
+ })();
842
+ }
711
843
  const chatStream = {
712
844
  // Stable opaque label for the run-scoped `RealtimeDefinedStream` shape.
713
845
  // `chatStream` is backed by the Session's `.out` channel — this id is
@@ -717,7 +849,7 @@ const chatStream = {
717
849
  id: "chat",
718
850
  pipe(value, options) {
719
851
  const { target: _target, ...sessionOptions } = (options ?? {});
720
- return getChatSession().out.pipe(value, sessionOptions);
852
+ return getChatSession().out.pipe(gateOutStream(value), sessionOptions);
721
853
  },
722
854
  async read(_runId, options) {
723
855
  // Session channels don't need a runId — the session is the address.
@@ -727,10 +859,11 @@ const chatStream = {
727
859
  },
728
860
  async append(value, options) {
729
861
  const { target: _target, ...sessionOptions } = (options ?? {});
862
+ await awaitChatOutGate();
730
863
  return getChatSession().out.append(value, sessionOptions);
731
864
  },
732
865
  writer(options) {
733
- return getChatSession().out.writer(options);
866
+ return getChatSession().out.writer(gateWriterOptions(options));
734
867
  },
735
868
  };
736
869
  // ---------------------------------------------------------------------------
@@ -741,7 +874,7 @@ const chatStream = {
741
874
  * `onTurnComplete`'s `responseMessage` and `uiMessages`.
742
875
  *
743
876
  * Non-transient data chunks (`type` starts with `data-`, no `transient: true`)
744
- * are queued for accumulation into the assistant response message.
877
+ * are accumulated in emission order into the assistant response message.
745
878
  * Transient or non-data chunks are streamed only (same as `chat.stream`).
746
879
  *
747
880
  * @example
@@ -759,15 +892,7 @@ const chatResponse = {
759
892
  * response message; everything else is stream-only.
760
893
  */
761
894
  write(part) {
762
- queueResponsePart(part);
763
- const { waitUntilComplete } = chatStream.writer({
764
- spanName: "chat.response.write",
765
- collapsed: true,
766
- execute: ({ write }) => {
767
- write(part);
768
- },
769
- });
770
- waitUntilComplete().catch(() => { });
895
+ managedResponse().writeData(part);
771
896
  },
772
897
  };
773
898
  /**
@@ -776,46 +901,13 @@ const chatResponse = {
776
901
  * @internal
777
902
  */
778
903
  function createLazyChatWriter() {
779
- let writeImpl = null;
780
- let mergeImpl = null;
781
- let waitPromise = null;
782
- let resolveExecute = null;
783
- function ensureInitialized() {
784
- if (writeImpl)
785
- return;
786
- const executePromise = new Promise((resolve) => {
787
- resolveExecute = resolve;
788
- });
789
- const { waitUntilComplete } = chatStream.writer({
904
+ return (0, managedChatResponse_js_1.createOrderedChatWriter)(() => (locals_js_1.locals.get(chatManagedResponseActiveKey) ? managedResponse() : undefined), async (stream) => {
905
+ const { waitUntilComplete } = chatStream.pipe(stream, {
790
906
  collapsed: true,
791
907
  spanName: "callback writer",
792
- execute: ({ write, merge }) => {
793
- writeImpl = write;
794
- mergeImpl = merge;
795
- return executePromise; // Keep execute alive until flush()
796
- },
797
908
  });
798
- waitPromise = waitUntilComplete;
799
- }
800
- return {
801
- writer: {
802
- write(part) {
803
- ensureInitialized();
804
- queueResponsePart(part);
805
- writeImpl(part);
806
- },
807
- merge(stream) {
808
- ensureInitialized();
809
- mergeImpl(stream);
810
- },
811
- },
812
- async flush() {
813
- if (resolveExecute) {
814
- resolveExecute(); // Signal execute to complete
815
- await waitPromise(); // Wait for stream to finish piping
816
- }
817
- },
818
- };
909
+ await waitUntilComplete();
910
+ });
819
911
  }
820
912
  /**
821
913
  * Runs a callback with a lazy ChatWriter, flushing the stream after completion.
@@ -968,7 +1060,7 @@ async function waitOnChatRoute(route, options) {
968
1060
  return tracer_js_1.tracer.startActiveSpan(options.spanName ?? `chat.${route}.wait()`, async (span) => {
969
1061
  const idleMs = (options.idleTimeoutInSeconds ?? 0) * 1000;
970
1062
  if (idleMs > 0) {
971
- const warm = await router.next(route, { timeoutMs: idleMs });
1063
+ const warm = await (0, sessionTracing_js_1.traceSessionIdle)(session.id, idleMs / 1000, () => router.next(route, { timeoutMs: idleMs }));
972
1064
  if (warm) {
973
1065
  span.setAttribute("wait.resolved", "idle");
974
1066
  return { ok: true, output: warm.data, record: warm };
@@ -1022,10 +1114,6 @@ async function waitOnChatRoute(route, options) {
1022
1114
  session: session.id,
1023
1115
  io: "in",
1024
1116
  route,
1025
- ...(0, v3_1.accessoryAttributes)({
1026
- items: [{ text: `${session.id}.in:${route}`, variant: "normal" }],
1027
- style: "codepath",
1028
- }),
1029
1117
  },
1030
1118
  });
1031
1119
  }
@@ -1701,6 +1789,14 @@ const chatHandoverPartialKey = locals_js_1.locals.create("chat.handoverPartial")
1701
1789
  * @internal
1702
1790
  */
1703
1791
  const chatHandoverMessageIdKey = locals_js_1.locals.create("chat.handoverMessageId");
1792
+ /**
1793
+ * The model messages a turn-0 head-start splice contributed to the model lane,
1794
+ * keyed by the UI message id it was synthesized under. When the agent's response
1795
+ * completes that message under the same id, this is the run to replace: the UI
1796
+ * form alone converts to something else (its pending tool calls drop out, the
1797
+ * approval round was never on it), so it cannot locate the run itself.
1798
+ */
1799
+ const chatHandoverSplicedRunKey = locals_js_1.locals.create("chat.handoverSplicedRun");
1704
1800
  /**
1705
1801
  * Run-scoped slot indicating that the customer's step-1 head-start
1706
1802
  * response is the FINAL turn response. When true, turn 0 runs through
@@ -1784,16 +1880,19 @@ function synthesizeHandoverUIMessage(partial, messageId) {
1784
1880
  */
1785
1881
  function spliceHandoverPartial(modelMessages, uiMessages, signal) {
1786
1882
  if (!signal.partialAssistantMessage || signal.partialAssistantMessage.length === 0) {
1787
- return;
1883
+ return undefined;
1788
1884
  }
1789
1885
  // Skip if the hydrated chain already persisted the partial under this id.
1790
1886
  const alreadyInChain = signal.messageId !== undefined && uiMessages.some((m) => m.id === signal.messageId);
1791
1887
  if (alreadyInChain)
1792
- return;
1793
- modelMessages.push(...signal.partialAssistantMessage);
1888
+ return undefined;
1889
+ const run = [...signal.partialAssistantMessage];
1890
+ modelMessages.push(...run);
1794
1891
  const partialUI = synthesizeHandoverUIMessage(signal.partialAssistantMessage, signal.messageId);
1795
- if (partialUI)
1796
- uiMessages.push(partialUI);
1892
+ if (!partialUI)
1893
+ return undefined;
1894
+ uiMessages.push(partialUI);
1895
+ return { id: partialUI.id, run };
1797
1896
  }
1798
1897
  /**
1799
1898
  * Per-turn background context queue. Messages added via `chat.backgroundWork.inject()`
@@ -2399,29 +2498,19 @@ const chatTurnNewUIMessagesKey = locals_js_1.locals.create("chat.turnNewUIMessag
2399
2498
  const chatPendingSteerKey = locals_js_1.locals.create("chat.pendingSteer");
2400
2499
  /** @internal — IDs of messages that were successfully injected via prepareStep */
2401
2500
  const chatInjectedMessageIdsKey = locals_js_1.locals.create("chat.injectedMessageIds");
2402
- /** @internal — non-transient data parts queued via chat.response or writer.write() for accumulation into the response message */
2403
- const chatResponsePartsKey = locals_js_1.locals.create("chat.responseParts");
2404
- /**
2405
- * Check if a chunk is a non-transient data part that should persist to the response message.
2406
- * @internal
2407
- */
2408
- function isNonTransientDataPart(part) {
2409
- if (typeof part !== "object" || part === null)
2410
- return false;
2411
- const p = part;
2412
- return typeof p.type === "string" && p.type.startsWith("data-") && p.transient !== true;
2413
- }
2414
- /**
2415
- * Queue a chunk for accumulation into the response message (if it's a non-transient data part).
2416
- * Called by `chat.response.write()` and `ChatWriter.write()`.
2417
- * @internal
2418
- */
2419
- function queueResponsePart(part) {
2420
- if (!isNonTransientDataPart(part))
2421
- return;
2422
- const parts = locals_js_1.locals.get(chatResponsePartsKey) ?? [];
2423
- parts.push(part);
2424
- locals_js_1.locals.set(chatResponsePartsKey, parts);
2501
+ const chatManagedResponseKey = locals_js_1.locals.create("chat.managedResponse");
2502
+ const chatManagedResponseActiveKey = locals_js_1.locals.create("chat.managedResponseActive");
2503
+ const chatSteeringInjectionsKey = locals_js_1.locals.create("chat.steeringInjections");
2504
+ function managedResponse() {
2505
+ let response = locals_js_1.locals.get(chatManagedResponseKey);
2506
+ if (!response || response.isClosed) {
2507
+ response = new managedChatResponse_js_1.ManagedChatResponse(async (stream) => {
2508
+ const { waitUntilComplete } = chatStream.pipe(stream, { spanName: "managed chat response" });
2509
+ await waitUntilComplete();
2510
+ });
2511
+ locals_js_1.locals.set(chatManagedResponseKey, response);
2512
+ }
2513
+ return response;
2425
2514
  }
2426
2515
  /**
2427
2516
  * Check that no tool calls are in-flight in a step's content.
@@ -2653,6 +2742,7 @@ async function chatCompact(messages, steps, options) {
2653
2742
  locals_js_1.locals.set(chatCompactionStateKey, {
2654
2743
  summary,
2655
2744
  baseResponseMessageCount: currentStep.response.messages.length,
2745
+ baseResponseStepCount: steps.length,
2656
2746
  });
2657
2747
  // Set model-only override — UI messages stay intact for persistence.
2658
2748
  // The summary becomes the model message history for the next turn,
@@ -2772,6 +2862,8 @@ function modelFormOf(m, batch, injected) {
2772
2862
  * @internal
2773
2863
  */
2774
2864
  async function drainSteeringQueue(config, messages, steps, queueOverride) {
2865
+ if (steps.length === 0)
2866
+ managedResponse().beginGeneration();
2775
2867
  const queue = queueOverride ?? locals_js_1.locals.get(chatSteeringQueueKey);
2776
2868
  if (!queue || queue.length === 0)
2777
2869
  return EMPTY_DRAIN;
@@ -2904,31 +2996,24 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
2904
2996
  }
2905
2997
  locals_js_1.locals.set(chatPendingSteerKey, pendingSteer);
2906
2998
  }
2907
- // Write injection confirmation chunk to the stream so the frontend
2908
- // knows which messages were injected and where in the response.
2909
2999
  if (injected.length > 0) {
2910
- try {
2911
- const { waitUntilComplete } = chatStream.writer({
2912
- collapsed: true,
2913
- execute: ({ write }) => {
2914
- write({
2915
- type: ai_shared_js_1.PENDING_MESSAGE_INJECTED_TYPE,
2916
- id: (0, ai_runtime_js_1.generateId)(),
2917
- data: {
2918
- messageIds: claimedUIMessages.map((m) => m.id),
2919
- messages: claimedUIMessages.map((m) => ({
2920
- id: m.id,
2921
- text: textOfUIMessage(m),
2922
- })),
2923
- },
2924
- });
2925
- },
2926
- });
2927
- await waitUntilComplete();
2928
- }
2929
- catch {
2930
- /* non-fatal — stream write failed */
2931
- }
3000
+ const id = (0, ai_runtime_js_1.generateId)();
3001
+ const messageIds = claimedUIMessages.map((m) => m.id);
3002
+ const injections = locals_js_1.locals.get(chatSteeringInjectionsKey) ?? new Map();
3003
+ injections.set(id, { id, messageIds, messages: injected });
3004
+ locals_js_1.locals.set(chatSteeringInjectionsKey, injections);
3005
+ const response = managedResponse();
3006
+ // prepareStep can run ahead of the UI consumer. Admit the marker only
3007
+ // once the actual preceding finish-step chunk has entered this stream.
3008
+ response.afterStep(steps.length);
3009
+ response.writeData({
3010
+ type: ai_shared_js_1.PENDING_MESSAGE_INJECTED_TYPE,
3011
+ id,
3012
+ data: {
3013
+ messageIds,
3014
+ messages: claimedUIMessages.map((m) => ({ id: m.id, text: textOfUIMessage(m) })),
3015
+ },
3016
+ });
2932
3017
  }
2933
3018
  // Fire onInjected callback
2934
3019
  if (config.onInjected && injected.length > 0) {
@@ -3229,7 +3314,7 @@ function buildManagedStreamTextOptions(options, config) {
3229
3314
  * regenerate needs: without it a regenerated answer can call nothing.
3230
3315
  */
3231
3316
  tools: (tools ?? agentTools),
3232
- });
3317
+ }, false);
3233
3318
  const promptSystem = locals_js_1.locals.get(chatPromptKey)?.text;
3234
3319
  /**
3235
3320
  * Two managed sources conflict too, not only a caller against a managed one.
@@ -3256,6 +3341,9 @@ function buildManagedStreamTextOptions(options, config) {
3256
3341
  return { ...(first ?? {}), ...(second ?? {}) };
3257
3342
  };
3258
3343
  }
3344
+ if (typeof managed.prepareStep === "function") {
3345
+ managed.prepareStep = (0, steeringContext_js_1.retainStepMessages)(managed.prepareStep);
3346
+ }
3259
3347
  return { ...managed, ...rest };
3260
3348
  }
3261
3349
  /** @internal Test hook for {@link buildManagedStreamTextOptions}. */
@@ -3271,7 +3359,7 @@ function createBoundStreamText(registry, agentSystem, agentCacheControl, agentSy
3271
3359
  }));
3272
3360
  return bound;
3273
3361
  }
3274
- function toStreamTextOptions(options) {
3362
+ function toStreamTextOptions(options, retain = true) {
3275
3363
  const agentDefaults = locals_js_1.locals.get(chatAgentManagedConfigKey);
3276
3364
  if (agentDefaults) {
3277
3365
  options = {
@@ -3478,6 +3566,9 @@ function toStreamTextOptions(options) {
3478
3566
  return resultMessages ? { messages: resultMessages } : undefined;
3479
3567
  };
3480
3568
  }
3569
+ if (retain && typeof result.prepareStep === "function") {
3570
+ result.prepareStep = (0, steeringContext_js_1.retainStepMessages)(result.prepareStep);
3571
+ }
3481
3572
  return result;
3482
3573
  }
3483
3574
  const actionTurnBrand = Symbol.for("trigger.dev/chat/actionTurn");
@@ -3523,9 +3614,16 @@ function isActionTurn(value) {
3523
3614
  * tail does not match the old message's conversion, nothing is changed and
3524
3615
  * `false` is returned so the caller can fall back to a full reconversion.
3525
3616
  */
3526
- async function replaceModelRun(lane, oldUi, newUi, tailAfter) {
3527
- const oldRun = await toModelMessages([stripProviderMetadata(oldUi)]);
3617
+ async function replaceModelRun(lane, oldUi, newUi, tailAfter, knownOldRun) {
3618
+ const oldRun = knownOldRun ?? (await toModelMessages([stripProviderMetadata(oldUi)]));
3528
3619
  const newRun = await toModelMessages([stripProviderMetadata(newUi)]);
3620
+ // A message that converts to nothing (a pending tool call with no output yet,
3621
+ // which `ignoreIncompleteToolCalls` drops) locates no run in the lane. Matching
3622
+ // an empty slice would splice the new run in without removing what the message
3623
+ // actually contributed, such as a spliced head-start partial, and the lane would
3624
+ // then carry the same tool call twice.
3625
+ if (oldRun.length === 0)
3626
+ return false;
3529
3627
  const end = lane.length - tailAfter;
3530
3628
  const start = end - oldRun.length;
3531
3629
  if (start < 0 || end > lane.length)
@@ -3605,7 +3703,7 @@ function isReadableStream(value) {
3605
3703
  * }
3606
3704
  * ```
3607
3705
  */
3608
- async function pipeChat(source, options) {
3706
+ async function pipeChat(source, options, capture) {
3609
3707
  locals_js_1.locals.set(chatPipeCountKey, (locals_js_1.locals.get(chatPipeCountKey) ?? 0) + 1);
3610
3708
  let stream;
3611
3709
  if (isUIMessageStreamable(source)) {
@@ -3634,8 +3732,18 @@ async function pipeChat(source, options) {
3634
3732
  // accepts opaque UIMessageStreamable / raw iterables whose element
3635
3733
  // type we don't know at compile time. Cast — runtime behaviour is
3636
3734
  // identical (bytes go to session.out either way).
3637
- const { waitUntilComplete } = chatStream.pipe(stream, pipeOptions);
3638
- await waitUntilComplete();
3735
+ if (capture) {
3736
+ const response = managedResponse();
3737
+ response.seed(capture.originalMessages);
3738
+ await response.pipe(stream, options?.signal);
3739
+ }
3740
+ else {
3741
+ // Raw/manual pipes intentionally do not feed the managed capture. Never
3742
+ // leave injection events waiting for finish-step chunks it cannot see.
3743
+ managedResponse().useRawPipe();
3744
+ const { waitUntilComplete } = chatStream.pipe(stream, pipeOptions);
3745
+ await waitUntilComplete();
3746
+ }
3639
3747
  }
3640
3748
  function chatCustomAgent(options) {
3641
3749
  const { clientDataSchema, onClientDataValidationError, clientDataReportErrorAt, run: userRun, ...restOptions } = options;
@@ -3888,11 +3996,13 @@ function chatAgent(options) {
3888
3996
  if (!pending || pending.length === 0)
3889
3997
  return [];
3890
3998
  locals_js_1.locals.set(chatPendingSteerKey, []);
3891
- for (const entry of pending) {
3999
+ const inlineIds = new Set((0, steeringContext_js_1.steeringMarkers)(options?.response ? [options.response] : []).flatMap((m) => m.messageIds));
4000
+ const standalone = pending.filter((entry) => !inlineIds.has(entry.ui.id));
4001
+ for (const entry of standalone) {
3892
4002
  accumulatedMessages.push(...entry.model);
3893
4003
  options?.turnNew?.push(...entry.model);
3894
4004
  }
3895
- return pending;
4005
+ return standalone;
3896
4006
  };
3897
4007
  // Accumulated UI messages for persistence. Mirrors the model accumulator
3898
4008
  // but in frontend-friendly UIMessage format (with parts, id, etc.).
@@ -3955,6 +4065,17 @@ function chatAgent(options) {
3955
4065
  * keeps an action's write cursor-neutral.
3956
4066
  */
3957
4067
  let lastSnapshotOutEventId;
4068
+ /**
4069
+ * The `lastInEventId` the most recent snapshot carried.
4070
+ *
4071
+ * A turn-start save happens after the incoming message has been handed to
4072
+ * the turn loop, so the router's live resume floor has already advanced
4073
+ * past it. Persisting that floor before the turn runs would let the next
4074
+ * boot resume past a message this run never answered, which is exactly
4075
+ * what a deferred or recovered message depends on. Turn-start carries
4076
+ * this instead.
4077
+ */
4078
+ let lastSnapshotInEventId;
3958
4079
  const storageTrigger = (trigger) => trigger === "regenerate-message"
3959
4080
  ? "regenerate-message"
3960
4081
  : trigger === "action" || trigger === "action-turn"
@@ -3968,13 +4089,17 @@ function chatAgent(options) {
3968
4089
  */
3969
4090
  /** The runtime's opaque state as of the last save; carried on every changeset's transcript. */
3970
4091
  let transcriptState = null;
3971
- const saveTranscript = async (opts) => {
4092
+ let transcriptSaveChain = Promise.resolve();
4093
+ const runSaveTranscript = async (opts) => {
3972
4094
  const { changes, shadow } = (0, transcriptStorage_js_1.diffTranscript)(transcriptShadow, opts.messages, {
3973
4095
  nonFinalIds: opts.nonFinalIds,
3974
4096
  });
3975
4097
  const throughId = opts.messages.at(-1)?.id ?? "";
3976
4098
  const queued = locals_js_1.locals.get(chatBackgroundQueueKey) ?? [];
3977
- const runtimeState = laneCompacted || laneInjections.length > 0 || queued.length > 0
4099
+ const transcriptIds = new Set(opts.messages.map((message) => message.id));
4100
+ const markerIds = new Set((0, steeringContext_js_1.steeringMarkers)(opts.messages).map((marker) => marker.id));
4101
+ const steering = [...(locals_js_1.locals.get(chatSteeringInjectionsKey)?.values() ?? [])].filter((entry) => markerIds.has(entry.id) || entry.messageIds.some((id) => transcriptIds.has(id)));
4102
+ const runtimeState = laneCompacted || laneInjections.length > 0 || queued.length > 0 || steering.length > 0
3978
4103
  ? {
3979
4104
  v: 1,
3980
4105
  ...(laneCompacted
@@ -3987,13 +4112,21 @@ function chatAgent(options) {
3987
4112
  : {}),
3988
4113
  ...(laneInjections.length > 0 ? { injections: laneInjections } : {}),
3989
4114
  ...(queued.length > 0 ? { queued: [...queued] } : {}),
4115
+ ...(steering.length > 0 ? { steering } : {}),
3990
4116
  }
3991
4117
  : null;
3992
4118
  if (runtimeState !== null || persistedStateSet) {
3993
4119
  changes.push({ op: "state", value: runtimeState });
3994
4120
  }
4121
+ if (opts.skipIfUnchanged && changes.length === 0)
4122
+ return;
3995
4123
  transcriptState = runtimeState;
3996
- const inCursor = chatInputRouter().resumeFloor();
4124
+ const liveInCursor = chatInputRouter().resumeFloor();
4125
+ const inCursor = opts.carryInCursor
4126
+ ? lastSnapshotInEventId
4127
+ : liveInCursor !== undefined
4128
+ ? String(liveInCursor)
4129
+ : undefined;
3997
4130
  await transcriptStorage.save({
3998
4131
  chatId: payload.chatId,
3999
4132
  clientData: opts.clientData,
@@ -4014,12 +4147,28 @@ function chatAgent(options) {
4014
4147
  },
4015
4148
  cursors: {
4016
4149
  lastOutEventId: opts.lastOutEventId,
4017
- lastInEventId: inCursor !== undefined ? String(inCursor) : undefined,
4150
+ lastInEventId: inCursor,
4018
4151
  },
4019
4152
  });
4020
4153
  transcriptShadow = shadow;
4154
+ lastSnapshotInEventId = inCursor;
4021
4155
  persistedStateSet = runtimeState !== null;
4022
4156
  };
4157
+ /**
4158
+ * Serialise every save onto one chain. `runSaveTranscript` derives its
4159
+ * changeset from `transcriptShadow` and only advances it once the write
4160
+ * lands, so two overlapping saves would diff against stale state. The
4161
+ * message list is copied on the way in because the accumulator keeps
4162
+ * mutating while a queued save waits its turn. A rejection is handed to
4163
+ * the caller but never poisons the chain.
4164
+ */
4165
+ const saveTranscript = (opts) => {
4166
+ const queued = { ...opts, messages: [...opts.messages] };
4167
+ const run = () => runSaveTranscript(queued);
4168
+ const next = transcriptSaveChain.then(run, run);
4169
+ transcriptSaveChain = next.then(() => undefined, () => undefined);
4170
+ return next;
4171
+ };
4023
4172
  /**
4024
4173
  * Persist the accumulator outside a turn.
4025
4174
  *
@@ -4126,6 +4275,7 @@ function chatAgent(options) {
4126
4275
  // turn (chain self-bootstraps from turn 2), so this is purely an
4127
4276
  // optimization to keep continuation runs bounded from the first turn.
4128
4277
  lastSnapshotOutEventId = bootSnapshot?.lastOutEventId;
4278
+ lastSnapshotInEventId = bootSnapshot?.lastInEventId;
4129
4279
  if (bootSnapshot?.lastOutEventId !== undefined) {
4130
4280
  const seeded = Number.parseInt(bootSnapshot.lastOutEventId, 10);
4131
4281
  if (Number.isFinite(seeded)) {
@@ -4432,6 +4582,8 @@ function chatAgent(options) {
4432
4582
  }
4433
4583
  try {
4434
4584
  const bootRuntimeState = (0, transcriptStorage_js_1.parseTranscriptRuntimeState)(bootTranscriptState);
4585
+ locals_js_1.locals.set(chatSteeringInjectionsKey, new Map((bootRuntimeState?.steering ?? []).map((entry) => [entry.id, entry])));
4586
+ locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
4435
4587
  const restored = await (0, transcriptStorage_js_1.restoreModelLane)(accumulatedUIMessages, bootRuntimeState, (messages) => toModelMessages(messages));
4436
4588
  accumulatedMessages = restored.messages;
4437
4589
  laneCompacted = restored.compacted;
@@ -4613,7 +4765,7 @@ function chatAgent(options) {
4613
4765
  const preloadResult = await messagesInput.waitWithIdleTimeout({
4614
4766
  idleTimeoutInSeconds: effectivePreloadIdleTimeout,
4615
4767
  timeout: effectivePreloadTimeout,
4616
- spanName: "waiting for first message",
4768
+ spanName: "first message",
4617
4769
  skipSuspend: exitAfterPreloadIdle,
4618
4770
  onSuspend: onChatSuspend
4619
4771
  ? async () => {
@@ -4765,7 +4917,7 @@ function chatAgent(options) {
4765
4917
  const continuationResult = await messagesInput.waitWithIdleTimeout({
4766
4918
  idleTimeoutInSeconds: effectiveIdleTimeout,
4767
4919
  timeout: effectiveTurnTimeout,
4768
- spanName: "waiting for first message (continuation)",
4920
+ spanName: "first message (continuation)",
4769
4921
  onSuspend: onChatSuspend
4770
4922
  ? async () => {
4771
4923
  await tracer_js_1.tracer.startActiveSpan("onChatSuspend()", async () => {
@@ -4875,10 +5027,13 @@ function chatAgent(options) {
4875
5027
  // (errors are caught by the outer try/catch which writes an error chunk)
4876
5028
  locals_js_1.locals.set(chatPipeCountKey, 0);
4877
5029
  locals_js_1.locals.set(chatDeferKey, new Set());
5030
+ locals_js_1.locals.set(chatOutGateKey, createChatOutGate());
4878
5031
  locals_js_1.locals.set(chatCompactionStateKey, undefined);
4879
5032
  locals_js_1.locals.set(chatSteeringQueueKey, []);
4880
5033
  locals_js_1.locals.set(chatPendingBackgroundKey, []);
4881
- locals_js_1.locals.set(chatResponsePartsKey, []);
5034
+ await locals_js_1.locals.get(chatManagedResponseKey)?.close();
5035
+ locals_js_1.locals.set(chatManagedResponseKey, undefined);
5036
+ locals_js_1.locals.set(chatManagedResponseActiveKey, true);
4882
5037
  // NOTE: chatBackgroundQueueKey is NOT reset here — messages injected
4883
5038
  // by deferred work from the previous turn's onTurnComplete need to
4884
5039
  // survive into the next turn. The queue is drained before run().
@@ -5042,7 +5197,7 @@ function chatAgent(options) {
5042
5197
  if (actionOverride) {
5043
5198
  locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
5044
5199
  accumulatedUIMessages = [...actionOverride];
5045
- accumulatedMessages = await toModelMessages(actionOverride);
5200
+ accumulatedMessages = await toModelMessages(actionOverride, actionOverride);
5046
5201
  laneCompacted = false;
5047
5202
  laneInjections = [];
5048
5203
  locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
@@ -5200,7 +5355,7 @@ function chatAgent(options) {
5200
5355
  accumulatedUIMessages[accumulatedUIMessages.length - 1].role !== "user") {
5201
5356
  accumulatedUIMessages.pop();
5202
5357
  }
5203
- accumulatedMessages = await toModelMessages(accumulatedUIMessages);
5358
+ accumulatedMessages = await toModelMessages(accumulatedUIMessages, accumulatedUIMessages);
5204
5359
  laneCompacted = false;
5205
5360
  laneInjections = [];
5206
5361
  }
@@ -5252,7 +5407,7 @@ function chatAgent(options) {
5252
5407
  }
5253
5408
  if (!inPlace) {
5254
5409
  v3_1.logger.warn("chat.agent: replaced message not found at the model lane tail; reconverting the lane");
5255
- accumulatedMessages = await toModelMessages(accumulatedUIMessages);
5410
+ accumulatedMessages = await toModelMessages(accumulatedUIMessages, accumulatedUIMessages);
5256
5411
  laneCompacted = false;
5257
5412
  laneInjections = [];
5258
5413
  }
@@ -5293,10 +5448,12 @@ function chatAgent(options) {
5293
5448
  // `UIMessageStreamError: No tool invocation found`.
5294
5449
  const pendingHandoverPartial = locals_js_1.locals.get(chatHandoverPartialKey);
5295
5450
  if (pendingHandoverPartial && pendingHandoverPartial.length > 0) {
5296
- spliceHandoverPartial(accumulatedMessages, accumulatedUIMessages, {
5451
+ const spliced = spliceHandoverPartial(accumulatedMessages, accumulatedUIMessages, {
5297
5452
  partialAssistantMessage: pendingHandoverPartial,
5298
5453
  messageId: locals_js_1.locals.get(chatHandoverMessageIdKey),
5299
5454
  });
5455
+ if (spliced)
5456
+ locals_js_1.locals.set(chatHandoverSplicedRunKey, spliced);
5300
5457
  locals_js_1.locals.set(chatHandoverPartialKey, []); // consume once
5301
5458
  splicedHandoverPartial = true;
5302
5459
  }
@@ -5363,6 +5520,23 @@ function chatAgent(options) {
5363
5520
  // A no-op turn skips this block, and with it `followSessionPin`:
5364
5521
  // there is nothing to answer, so nothing to hand over.
5365
5522
  if ((!isAction || actionTurn) && !isNoOpTurn) {
5523
+ if (!hydrateMessages) {
5524
+ chatDeferBeforeOutput(saveTranscript({
5525
+ reason: "turn-start",
5526
+ messages: accumulatedUIMessages,
5527
+ turn,
5528
+ trigger: storageTrigger(currentWirePayload.trigger),
5529
+ clientData,
5530
+ lastOutEventId: lastSnapshotOutEventId,
5531
+ skipIfUnchanged: true,
5532
+ carryInCursor: true,
5533
+ }).catch((error) => {
5534
+ v3_1.logger.warn("chat.agent: turn-start transcript write failed; a reload mid-answer may not show the message being answered", {
5535
+ error: error instanceof Error ? error.message : String(error),
5536
+ sessionId: sessionIdForSnapshot,
5537
+ });
5538
+ }));
5539
+ }
5366
5540
  // Mint a scoped public access token once per turn, reused for
5367
5541
  // onChatStart, onTurnStart, onTurnComplete, and the turn-complete chunk.
5368
5542
  const currentRunId = ctx.run.id;
@@ -5450,7 +5624,7 @@ function chatAgent(options) {
5450
5624
  if (turnStartOverride) {
5451
5625
  locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
5452
5626
  accumulatedUIMessages = [...turnStartOverride];
5453
- accumulatedMessages = await toModelMessages(turnStartOverride);
5627
+ accumulatedMessages = await toModelMessages(turnStartOverride, turnStartOverride);
5454
5628
  laneCompacted = false;
5455
5629
  laneInjections = [];
5456
5630
  locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
@@ -5530,6 +5704,7 @@ function chatAgent(options) {
5530
5704
  capturedResponseMessage = lastUI;
5531
5705
  capturedFinishReason = "stop";
5532
5706
  }
5707
+ managedResponse().seed(accumulatedUIMessages);
5533
5708
  // Don't call userRun. Don't pipe. Skip directly
5534
5709
  // to the post-turn flow below.
5535
5710
  }
@@ -5589,7 +5764,7 @@ function chatAgent(options) {
5589
5764
  await pipeChat(tapUIMessageChunks(uiStream, turnBufferedChunks), {
5590
5765
  signal: combinedSignal,
5591
5766
  spanName: "stream response",
5592
- });
5767
+ }, { originalMessages: isActionTurn ? undefined : accumulatedUIMessages });
5593
5768
  }
5594
5769
  }
5595
5770
  catch (error) {
@@ -5617,6 +5792,10 @@ function chatAgent(options) {
5617
5792
  new Promise((r) => setTimeout(r, 2_000)),
5618
5793
  ]);
5619
5794
  }
5795
+ capturedResponseMessage =
5796
+ (await locals_js_1.locals.get(chatManagedResponseKey)?.snapshot()) ?? capturedResponseMessage;
5797
+ if (capturedResponseMessage)
5798
+ capturedPartialResponse = capturedResponseMessage;
5620
5799
  // Capture token usage from the streamText result (if available).
5621
5800
  // totalUsage is a PromiseLike that resolves after the stream is consumed.
5622
5801
  // Race with a 2s timeout — on stop-abort the AI SDK's totalUsage
@@ -5624,6 +5803,7 @@ function chatAgent(options) {
5624
5803
  // never reports final usage), which would block the turn loop
5625
5804
  // from ever firing onTurnComplete / writeTurnComplete.
5626
5805
  let turnUsage;
5806
+ let lastStepUsage;
5627
5807
  if (runResult != null &&
5628
5808
  typeof runResult.totalUsage?.then === "function") {
5629
5809
  try {
@@ -5636,6 +5816,18 @@ function chatAgent(options) {
5636
5816
  /* non-fatal — usage capture failed */
5637
5817
  }
5638
5818
  }
5819
+ const lastStepUsagePromise = runResult != null ? runResult.usage : undefined;
5820
+ if (typeof lastStepUsagePromise?.then === "function") {
5821
+ try {
5822
+ lastStepUsage = (await Promise.race([
5823
+ lastStepUsagePromise,
5824
+ new Promise((r) => setTimeout(() => r(undefined), 2_000)),
5825
+ ]));
5826
+ }
5827
+ catch {
5828
+ /* non-fatal — usage capture failed */
5829
+ }
5830
+ }
5639
5831
  if (turnUsage) {
5640
5832
  cumulativeUsage = addUsage(cumulativeUsage, turnUsage);
5641
5833
  previousTurnUsage = turnUsage;
@@ -5686,6 +5878,13 @@ function chatAgent(options) {
5686
5878
  // Check if compaction set a model-only override (preserves UI messages).
5687
5879
  // Apply compactUIMessages/compactModelMessages callbacks if configured.
5688
5880
  const modelOnlyOverride = locals_js_1.locals.get(chatOverrideModelMessagesKey);
5881
+ const responseCompaction = modelOnlyOverride
5882
+ ? locals_js_1.locals.get(chatCompactionStateKey)
5883
+ : undefined;
5884
+ // Capture the original assistant before compactUIMessages can remove it.
5885
+ const originalResponse = responseCompaction && capturedResponseMessage
5886
+ ? accumulatedUIMessages.find((m) => m.id === capturedResponseMessage?.id)
5887
+ : undefined;
5689
5888
  if (modelOnlyOverride) {
5690
5889
  const compactionSummary = locals_js_1.locals.get(chatCompactionStateKey)?.summary ?? "";
5691
5890
  const taskCompactionConfig = locals_js_1.locals.get(chatAgentCompactionKey);
@@ -5720,6 +5919,7 @@ function chatAgent(options) {
5720
5919
  // branches below, so a turn that captured no response is covered.
5721
5920
  const steerTailThisTurn = reconcilePendingSteer({
5722
5921
  turnNew: turnNewModelMessages,
5922
+ response: capturedResponseMessage,
5723
5923
  }).reduce((n, e) => n + e.model.length, 0) + reconcilePendingBackground();
5724
5924
  // Append the assistant's response (partial or complete) to the accumulator.
5725
5925
  // The onFinish callback fires even on abort/stop, so partial responses
@@ -5742,15 +5942,6 @@ function chatAgent(options) {
5742
5942
  id: (0, ai_runtime_js_1.generateId)(),
5743
5943
  };
5744
5944
  }
5745
- // Append any non-transient data parts queued via chat.response or writer.write()
5746
- const queuedParts = locals_js_1.locals.get(chatResponsePartsKey);
5747
- if (queuedParts && queuedParts.length > 0) {
5748
- capturedResponseMessage = {
5749
- ...capturedResponseMessage,
5750
- parts: [...capturedResponseMessage.parts, ...queuedParts],
5751
- };
5752
- locals_js_1.locals.set(chatResponsePartsKey, []);
5753
- }
5754
5945
  const responseHasContent = capturedResponseMessage.parts.some((part) => part.type !== "step-start");
5755
5946
  if (responseHasContent) {
5756
5947
  // Tool-approval continuations: the AI SDK reuses the trailing
@@ -5777,15 +5968,42 @@ function chatAgent(options) {
5777
5968
  // rationale (TRI-9137).
5778
5969
  recordToolCallIdsFromMessage(capturedResponseMessage);
5779
5970
  try {
5971
+ const responseForModel = (0, compactionResponse_js_1.responseAfterCompaction)(capturedResponseMessage, responseCompaction?.baseResponseStepCount, originalResponse);
5972
+ // Preserve the complete persistence response, including same-ID
5973
+ // replacements whose old tool parts can contain new results.
5974
+ // Convert prefix and suffix separately so each tool output is
5975
+ // converted once, while only the suffix enters model context.
5976
+ const responsePrefixMessages = responseCompaction
5977
+ ? await toModelMessages([
5978
+ stripProviderMetadata({
5979
+ ...capturedResponseMessage,
5980
+ parts: capturedResponseMessage.parts.slice(0, capturedResponseMessage.parts.length -
5981
+ responseForModel.parts.length),
5982
+ }),
5983
+ ])
5984
+ : [];
5780
5985
  const responseModelMessages = await toModelMessages([
5781
- stripProviderMetadata(capturedResponseMessage),
5986
+ stripProviderMetadata(responseForModel),
5782
5987
  ]);
5783
- if (existingIdx !== -1) {
5988
+ if (responseCompaction) {
5989
+ // The summary already replaced the original response, including
5990
+ // a same-ID approval/handover prefix. Replacing its old model run
5991
+ // would miss and fall back to the full, uncompacted UI history.
5992
+ accumulatedMessages.push(...responseModelMessages);
5993
+ locals_js_1.locals.set(chatHandoverSplicedRunKey, undefined);
5994
+ }
5995
+ else if (existingIdx !== -1) {
5996
+ const spliced = locals_js_1.locals.get(chatHandoverSplicedRunKey);
5997
+ const splicedRun = spliced && previousAtIdx && spliced.id === previousAtIdx.id
5998
+ ? spliced.run
5999
+ : undefined;
5784
6000
  const ok = previousAtIdx !== undefined &&
5785
- (await replaceModelRun(accumulatedMessages, previousAtIdx, capturedResponseMessage, steerTailThisTurn));
6001
+ (await replaceModelRun(accumulatedMessages, previousAtIdx, capturedResponseMessage, steerTailThisTurn, splicedRun));
6002
+ if (splicedRun)
6003
+ locals_js_1.locals.set(chatHandoverSplicedRunKey, undefined);
5786
6004
  if (!ok) {
5787
6005
  v3_1.logger.warn("chat.agent: replaced response not found at the model lane tail; reconverting the lane");
5788
- accumulatedMessages = await toModelMessages(accumulatedUIMessages);
6006
+ accumulatedMessages = await toModelMessages(accumulatedUIMessages, accumulatedUIMessages);
5789
6007
  laneCompacted = false;
5790
6008
  laneInjections = [];
5791
6009
  }
@@ -5793,7 +6011,7 @@ function chatAgent(options) {
5793
6011
  else {
5794
6012
  accumulatedMessages.push(...responseModelMessages);
5795
6013
  }
5796
- turnNewModelMessages.push(...responseModelMessages);
6014
+ turnNewModelMessages.push(...responsePrefixMessages, ...responseModelMessages);
5797
6015
  }
5798
6016
  catch {
5799
6017
  // Conversion failed — skip accumulation for this turn
@@ -5803,22 +6021,6 @@ function chatAgent(options) {
5803
6021
  responseWasSkipped = true;
5804
6022
  }
5805
6023
  }
5806
- // If there's no captured response (manual pipe mode) but there are
5807
- // queued data parts, create a minimal response message to hold them.
5808
- if (!capturedResponseMessage) {
5809
- const remainingParts = locals_js_1.locals.get(chatResponsePartsKey);
5810
- if (remainingParts && remainingParts.length > 0) {
5811
- capturedResponseMessage = {
5812
- id: (0, ai_runtime_js_1.generateId)(),
5813
- role: "assistant",
5814
- parts: [...remainingParts],
5815
- };
5816
- locals_js_1.locals.set(chatResponsePartsKey, []);
5817
- accumulatedUIMessages.push(capturedResponseMessage);
5818
- turnNewUIMessages.push(capturedResponseMessage);
5819
- locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
5820
- }
5821
- }
5822
6024
  if (capturedResponseMessage) {
5823
6025
  responseCommitted = true;
5824
6026
  capturedPartialResponse = capturedResponseMessage;
@@ -5842,12 +6044,14 @@ function chatAgent(options) {
5842
6044
  const outerCompaction = locals_js_1.locals.get(chatAgentCompactionKey);
5843
6045
  const innerCompactionState = locals_js_1.locals.get(chatCompactionStateKey);
5844
6046
  if (outerCompaction && !innerCompactionState && turnUsage && !wasStopped) {
6047
+ const contextUsage = lastStepUsage ?? turnUsage;
5845
6048
  const shouldTrigger = await outerCompaction.shouldCompact({
5846
6049
  messages: accumulatedMessages,
5847
- totalTokens: turnUsage.totalTokens,
5848
- inputTokens: turnUsage.inputTokens,
5849
- outputTokens: turnUsage.outputTokens,
5850
- usage: turnUsage,
6050
+ totalTokens: contextUsage.totalTokens,
6051
+ inputTokens: contextUsage.inputTokens,
6052
+ outputTokens: contextUsage.outputTokens,
6053
+ usage: contextUsage,
6054
+ turnUsage,
5851
6055
  totalUsage: cumulativeUsage,
5852
6056
  chatId: currentWirePayload.chatId,
5853
6057
  turn,
@@ -5978,6 +6182,8 @@ function chatAgent(options) {
5978
6182
  totalUsage: cumulativeUsage,
5979
6183
  finishReason: capturedFinishReason,
5980
6184
  };
6185
+ const beforeHookRevision = locals_js_1.locals.get(chatManagedResponseKey)?.revision ?? 0;
6186
+ let beforeHookHistoryEdited = false;
5981
6187
  // Fire onBeforeTurnComplete — stream is still open so the hook
5982
6188
  // can write custom chunks to the frontend (e.g. compaction progress).
5983
6189
  if (onBeforeTurnComplete) {
@@ -5988,9 +6194,10 @@ function chatAgent(options) {
5988
6194
  // Check if the hook replaced messages (compaction or chat.history)
5989
6195
  const override = locals_js_1.locals.get(chatOverrideMessagesKey);
5990
6196
  if (override) {
6197
+ beforeHookHistoryEdited = true;
5991
6198
  locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
5992
6199
  accumulatedUIMessages = [...override];
5993
- accumulatedMessages = await toModelMessages(override);
6200
+ accumulatedMessages = await toModelMessages(override, override);
5994
6201
  laneCompacted = false;
5995
6202
  laneInjections = [];
5996
6203
  locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
@@ -6007,35 +6214,33 @@ function chatAgent(options) {
6007
6214
  },
6008
6215
  });
6009
6216
  }
6010
- // Drain any late response parts added during onBeforeTurnComplete
6011
- const lateParts = locals_js_1.locals.get(chatResponsePartsKey);
6012
- if (lateParts && lateParts.length > 0 && capturedResponseMessage) {
6013
- const idx = accumulatedUIMessages.findIndex((m) => m.id === capturedResponseMessage.id);
6014
- if (idx !== -1) {
6015
- const msg = accumulatedUIMessages[idx];
6016
- accumulatedUIMessages[idx] = {
6017
- ...msg,
6018
- parts: [...(msg.parts ?? []), ...lateParts],
6019
- };
6020
- capturedResponseMessage = accumulatedUIMessages[idx];
6021
- capturedPartialResponse = capturedResponseMessage;
6022
- turnCompleteEvent.responseMessage = capturedResponseMessage;
6023
- turnCompleteEvent.uiMessages = accumulatedUIMessages;
6024
- locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
6025
- }
6026
- else if (responseWasSkipped) {
6027
- capturedResponseMessage = {
6028
- ...capturedResponseMessage,
6029
- parts: [...(capturedResponseMessage.parts ?? []), ...lateParts],
6030
- };
6031
- accumulatedUIMessages.push(capturedResponseMessage);
6032
- turnNewUIMessages.push(capturedResponseMessage);
6033
- capturedPartialResponse = capturedResponseMessage;
6034
- turnCompleteEvent.responseMessage = capturedResponseMessage;
6217
+ const editedResponse = beforeHookHistoryEdited && capturedResponseMessage
6218
+ ? accumulatedUIMessages.find((message) => message.id === capturedResponseMessage?.id)
6219
+ : undefined;
6220
+ const finalManagedResponse = await locals_js_1.locals
6221
+ .get(chatManagedResponseKey)
6222
+ ?.snapshot(editedResponse
6223
+ ? { message: editedResponse, from: beforeHookRevision }
6224
+ : undefined);
6225
+ if (finalManagedResponse?.parts.some((part) => part.type !== "step-start")) {
6226
+ const idx = accumulatedUIMessages.findIndex((m) => m.id === finalManagedResponse.id);
6227
+ const finalized = (wasStopped ? cleanupAbortedParts(finalManagedResponse) : finalManagedResponse);
6228
+ if (idx !== -1 || responseWasSkipped || !capturedResponseMessage) {
6229
+ if (idx !== -1)
6230
+ accumulatedUIMessages[idx] = finalized;
6231
+ else
6232
+ accumulatedUIMessages.push(finalized);
6233
+ const deltaIdx = turnNewUIMessages.findIndex((m) => m.id === finalized.id);
6234
+ if (deltaIdx !== -1)
6235
+ turnNewUIMessages[deltaIdx] = finalized;
6236
+ else
6237
+ turnNewUIMessages.push(finalized);
6238
+ capturedResponseMessage = finalized;
6239
+ capturedPartialResponse = finalized;
6240
+ turnCompleteEvent.responseMessage = finalized;
6035
6241
  turnCompleteEvent.uiMessages = accumulatedUIMessages;
6036
6242
  locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
6037
6243
  }
6038
- locals_js_1.locals.set(chatResponsePartsKey, []);
6039
6244
  }
6040
6245
  settleRecoveredTurn(currentWirePayload);
6041
6246
  // Write turn-complete control chunk — closes the frontend stream.
@@ -6052,7 +6257,7 @@ function chatAgent(options) {
6052
6257
  if (turnCompleteOverride) {
6053
6258
  locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
6054
6259
  accumulatedUIMessages = [...turnCompleteOverride];
6055
- accumulatedMessages = await toModelMessages(turnCompleteOverride);
6260
+ accumulatedMessages = await toModelMessages(turnCompleteOverride, turnCompleteOverride);
6056
6261
  laneCompacted = false;
6057
6262
  laneInjections = [];
6058
6263
  locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
@@ -6172,7 +6377,7 @@ function chatAgent(options) {
6172
6377
  const next = await messagesInput.waitWithIdleTimeout({
6173
6378
  idleTimeoutInSeconds: effectiveIdleTimeout,
6174
6379
  timeout: effectiveTurnTimeout,
6175
- spanName: "waiting for next message",
6380
+ spanName: "next message",
6176
6381
  onSuspend: onChatSuspend
6177
6382
  ? async () => {
6178
6383
  await tracer_js_1.tracer.startActiveSpan("onChatSuspend()", async () => {
@@ -6288,7 +6493,8 @@ function chatAgent(options) {
6288
6493
  !accumulatedUIMessages.some((m) => m.id === erroredWireMessage.id)
6289
6494
  ? [...accumulatedUIMessages, erroredWireMessage]
6290
6495
  : accumulatedUIMessages;
6291
- let partialResponse = capturedPartialResponse ??
6496
+ let partialResponse = (await locals_js_1.locals.get(chatManagedResponseKey)?.snapshot()) ??
6497
+ capturedPartialResponse ??
6292
6498
  (await assemblePartialFromChunks(turnBufferedChunks));
6293
6499
  if (partialResponse) {
6294
6500
  partialResponse = cleanupAbortedParts(partialResponse);
@@ -6296,24 +6502,21 @@ function chatAgent(options) {
6296
6502
  let partialIdx = partialResponse?.id
6297
6503
  ? erroredUIMessages.findIndex((m) => m.id === partialResponse.id)
6298
6504
  : -1;
6299
- if (partialResponse && capturedPartialResponse === undefined && partialIdx !== -1) {
6505
+ if (partialResponse &&
6506
+ capturedPartialResponse === undefined &&
6507
+ !locals_js_1.locals.get(chatManagedResponseKey)?.continues(partialResponse.id) &&
6508
+ partialIdx !== -1) {
6300
6509
  partialResponse = undefined;
6301
6510
  partialIdx = -1;
6302
6511
  }
6303
6512
  if (partialResponse && !partialResponse.id) {
6304
6513
  partialResponse = { ...partialResponse, id: (0, ai_runtime_js_1.generateId)() };
6305
6514
  }
6306
- if (partialResponse && !responseCommitted) {
6307
- const queuedParts = locals_js_1.locals.get(chatResponsePartsKey);
6308
- if (queuedParts && queuedParts.length > 0) {
6309
- partialResponse = {
6310
- ...partialResponse,
6311
- parts: [...partialResponse.parts, ...queuedParts],
6312
- };
6313
- locals_js_1.locals.set(chatResponsePartsKey, []);
6314
- }
6315
- }
6316
6515
  const includePartial = partialResponse != null && !responseCommitted;
6516
+ // What the stream left behind, by content. After `onTurnComplete` the
6517
+ // partial is still unfinished only if the message under its id is
6518
+ // byte-for-byte this: a clone keeps it partial, an edit finishes it.
6519
+ const partialFingerprint = includePartial && partialResponse ? (0, transcriptStorage_js_1.fingerprintMessage)(partialResponse) : undefined;
6317
6520
  let erroredUIMessagesWithPartial = !includePartial
6318
6521
  ? erroredUIMessages
6319
6522
  : partialIdx === -1
@@ -6341,7 +6544,7 @@ function chatAgent(options) {
6341
6544
  };
6342
6545
  let erroredNewUIMessages = buildErroredNew();
6343
6546
  let erroredNewModelMessages = [];
6344
- const reconciledSteer = reconcilePendingSteer();
6547
+ const reconciledSteer = reconcilePendingSteer({ response: partialResponse });
6345
6548
  const backgroundTailThisTurn = reconcilePendingBackground();
6346
6549
  if (!responseCommitted) {
6347
6550
  try {
@@ -6352,14 +6555,7 @@ function chatAgent(options) {
6352
6555
  * the model received it (what `prepare` produced), matching the
6353
6556
  * lane. The wire message and partial are converted as before.
6354
6557
  */
6355
- const steerModelById = new Map(reconciledSteer.map((e) => [e.ui.id, e.model]));
6356
- for (const m of erroredNewUIMessages) {
6357
- const recorded = steerModelById.get(m.id);
6358
- if (recorded)
6359
- erroredNewModelMessages.push(...recorded);
6360
- else
6361
- erroredNewModelMessages.push(...(await toModelMessages([stripProviderMetadata(m)])));
6362
- }
6558
+ erroredNewModelMessages = await toModelMessages(erroredNewUIMessages.map(stripProviderMetadata));
6363
6559
  }
6364
6560
  if (erroredUIMessagesWithPartial !== accumulatedUIMessages) {
6365
6561
  if (partialIdx === -1) {
@@ -6371,7 +6567,7 @@ function chatAgent(options) {
6371
6567
  backgroundTailThisTurn);
6372
6568
  if (!ok) {
6373
6569
  v3_1.logger.warn("chat.agent: replaced partial not found at the model lane tail; reconverting the lane");
6374
- accumulatedMessages = await toModelMessages(erroredUIMessagesWithPartial);
6570
+ accumulatedMessages = await toModelMessages(erroredUIMessagesWithPartial, erroredUIMessagesWithPartial);
6375
6571
  laneCompacted = false;
6376
6572
  laneInjections = [];
6377
6573
  }
@@ -6386,6 +6582,11 @@ function chatAgent(options) {
6386
6582
  erroredNewUIMessages = buildErroredNew().filter((m) => m !== partialResponse);
6387
6583
  }
6388
6584
  }
6585
+ // An earlier hook that set the history and then threw (which is one way
6586
+ // to get here) left its abandoned edit pending. Discard it before the
6587
+ // failed turn continues, so neither the error-path `onTurnComplete`
6588
+ // below nor the next turn's history reads mistake it for a real edit.
6589
+ locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
6389
6590
  if (onTurnComplete) {
6390
6591
  try {
6391
6592
  await tracer_js_1.tracer.startActiveSpan("onTurnComplete()", async () => {
@@ -6414,6 +6615,23 @@ function chatAgent(options) {
6414
6615
  error: turnError,
6415
6616
  lastEventId: errorTurnCompleteResult?.lastEventId,
6416
6617
  });
6618
+ // The hook may edit the history here too (a failure record, a
6619
+ // card the turn left open). Honour it the way the success path
6620
+ // does, so the edit reaches the accumulator and the save below.
6621
+ const errorTurnOverride = locals_js_1.locals.get(chatOverrideMessagesKey);
6622
+ if (errorTurnOverride) {
6623
+ locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
6624
+ // Convert first: a rejected conversion (a tool's `toModelOutput`
6625
+ // can throw) must leave every lane on the history it had.
6626
+ const overrideUIMessages = [...errorTurnOverride];
6627
+ const overrideModelMessages = await toModelMessages(errorTurnOverride, errorTurnOverride);
6628
+ erroredUIMessagesWithPartial = overrideUIMessages;
6629
+ accumulatedUIMessages = overrideUIMessages;
6630
+ accumulatedMessages = overrideModelMessages;
6631
+ laneCompacted = false;
6632
+ laneInjections = [];
6633
+ locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
6634
+ }
6417
6635
  }, {
6418
6636
  attributes: {
6419
6637
  [v3_1.SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete",
@@ -6427,6 +6645,7 @@ function chatAgent(options) {
6427
6645
  catch {
6428
6646
  // A throwing onTurnComplete on the error path must not crash
6429
6647
  // the run — keep the conversation alive for the next message.
6648
+ locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
6430
6649
  }
6431
6650
  }
6432
6651
  // Persist a snapshot so the failed turn's user message isn't
@@ -6444,7 +6663,10 @@ function chatAgent(options) {
6444
6663
  trigger: storageTrigger(currentWirePayload.trigger),
6445
6664
  clientData: turnClientData,
6446
6665
  lastOutEventId: lastSnapshotOutEventId,
6447
- nonFinalIds: includePartial && partialResponse ? new Set([partialResponse.id]) : undefined,
6666
+ // The partial is non-final only while the message under its id is
6667
+ // still what the stream left behind. A hook that replaced it (a
6668
+ // closed card, a finished body) produced a final message.
6669
+ nonFinalIds: partialStillUnfinished(partialResponse, partialFingerprint, erroredUIMessagesWithPartial),
6448
6670
  });
6449
6671
  }
6450
6672
  catch (error) {
@@ -6479,7 +6701,7 @@ function chatAgent(options) {
6479
6701
  const next = await messagesInput.waitWithIdleTimeout({
6480
6702
  idleTimeoutInSeconds: effectiveIdleTimeout,
6481
6703
  timeout: effectiveTurnTimeout,
6482
- spanName: "waiting for next message (after error)",
6704
+ spanName: "next message (after error)",
6483
6705
  });
6484
6706
  if (!next.ok) {
6485
6707
  return; // Timed out — end run gracefully
@@ -6495,6 +6717,11 @@ function chatAgent(options) {
6495
6717
  }
6496
6718
  finally {
6497
6719
  turnMsgSub?.off();
6720
+ locals_js_1.locals.set(chatManagedResponseActiveKey, false);
6721
+ await locals_js_1.locals
6722
+ .get(chatManagedResponseKey)
6723
+ ?.close()
6724
+ .catch(() => { });
6498
6725
  }
6499
6726
  }
6500
6727
  }
@@ -7464,7 +7691,7 @@ async function pipeChatAndCapture(source, options) {
7464
7691
  await pipeChat(tappedStream, {
7465
7692
  signal: options?.signal,
7466
7693
  spanName: options?.spanName ?? "stream response",
7467
- });
7694
+ }, { originalMessages: options?.originalMessages });
7468
7695
  // The pipe can drain cleanly on a stop — the source stream just ends
7469
7696
  // early — so classify by the signal rather than relying on a throw.
7470
7697
  if (options?.signal?.aborted) {
@@ -7490,6 +7717,9 @@ async function pipeChatAndCapture(source, options) {
7490
7717
  if (!captured && bufferedChunks.length > 0) {
7491
7718
  captured = await assemblePartialFromChunks(bufferedChunks);
7492
7719
  }
7720
+ captured = (await locals_js_1.locals.get(chatManagedResponseKey)?.snapshot()) ?? captured;
7721
+ if (!locals_js_1.locals.get(chatTurnContextKey))
7722
+ await locals_js_1.locals.get(chatManagedResponseKey)?.close();
7493
7723
  return {
7494
7724
  message: captured,
7495
7725
  status,
@@ -7519,8 +7749,11 @@ class ChatMessageAccumulator {
7519
7749
  modelMessages = [];
7520
7750
  uiMessages = [];
7521
7751
  _compaction;
7752
+ /** The run a spliced head-start partial contributed, until its response replaces it. */
7753
+ _handoverRun;
7522
7754
  _pendingMessages;
7523
7755
  _steeringQueue = [];
7756
+ _pendingSteer = [];
7524
7757
  constructor(options) {
7525
7758
  this._compaction = options?.compaction;
7526
7759
  this._pendingMessages = options?.pendingMessages;
@@ -7562,7 +7795,7 @@ class ChatMessageAccumulator {
7562
7795
  * `consumeHandover` for the wait+seed+apply convenience.
7563
7796
  */
7564
7797
  applyHandover(signal) {
7565
- spliceHandoverPartial(this.modelMessages, this.uiMessages, signal);
7798
+ this._handoverRun = spliceHandoverPartial(this.modelMessages, this.uiMessages, signal);
7566
7799
  }
7567
7800
  /**
7568
7801
  * One-call `chat.headStart` handover for a custom-agent loop: waits for the
@@ -7593,6 +7826,19 @@ class ChatMessageAccumulator {
7593
7826
  return { isFinal: signal.isFinal, skipped: false };
7594
7827
  }
7595
7828
  async addResponse(response) {
7829
+ const inlineIds = new Set((0, steeringContext_js_1.steeringMarkers)([response]).flatMap((m) => m.messageIds));
7830
+ for (const entry of this._pendingSteer.splice(0)) {
7831
+ if (!inlineIds.has(entry.ui.id))
7832
+ continue;
7833
+ // absorbSteering is public and updates context immediately. Move just
7834
+ // those exact appended objects into the response's chronological run;
7835
+ // never rebuild a possibly compacted lane from the UI transcript.
7836
+ for (const message of entry.model) {
7837
+ const index = this.modelMessages.lastIndexOf(message);
7838
+ if (index !== -1)
7839
+ this.modelMessages.splice(index, 1);
7840
+ }
7841
+ }
7596
7842
  if (!response.id) {
7597
7843
  response = { ...response, id: (0, ai_runtime_js_1.generateId)() };
7598
7844
  }
@@ -7605,8 +7851,13 @@ class ChatMessageAccumulator {
7605
7851
  if (existingIdx !== -1) {
7606
7852
  const previous = this.uiMessages[existingIdx];
7607
7853
  this.uiMessages[existingIdx] = response;
7854
+ const handoverRun = this._handoverRun && this._handoverRun.id === previous.id
7855
+ ? this._handoverRun.run
7856
+ : undefined;
7857
+ if (handoverRun)
7858
+ this._handoverRun = undefined;
7608
7859
  try {
7609
- if (!(await replaceModelRun(this.modelMessages, previous, response, 0))) {
7860
+ if (!(await replaceModelRun(this.modelMessages, previous, response, 0, handoverRun))) {
7610
7861
  this.modelMessages = await toModelMessages(this.uiMessages.map((m) => stripProviderMetadata(m)));
7611
7862
  }
7612
7863
  }
@@ -7664,7 +7915,10 @@ class ChatMessageAccumulator {
7664
7915
  this.uiMessages.push(...fresh);
7665
7916
  // Record what the model received. Only when the whole batch is new is
7666
7917
  // `injected` known to describe exactly these messages.
7667
- this.modelMessages.push(...(injected && fresh.length === claimed.length ? injected : await toModelMessages(fresh)));
7918
+ const model = injected && fresh.length === claimed.length ? injected : await toModelMessages(fresh);
7919
+ this.modelMessages.push(...model);
7920
+ for (const ui of fresh)
7921
+ this._pendingSteer.push({ ui, model: modelFormOf(ui, fresh, model) });
7668
7922
  }
7669
7923
  /**
7670
7924
  * Get and clear unconsumed steering messages.
@@ -7684,7 +7938,7 @@ class ChatMessageAccumulator {
7684
7938
  const comp = this._compaction;
7685
7939
  const pm = this._pendingMessages;
7686
7940
  const queue = this._steeringQueue;
7687
- return async ({ messages, steps }) => {
7941
+ return (0, steeringContext_js_1.retainStepMessages)(async ({ messages, steps }) => {
7688
7942
  let resultMessages;
7689
7943
  // 1. Compaction
7690
7944
  if (comp) {
@@ -7697,7 +7951,7 @@ class ChatMessageAccumulator {
7697
7951
  }
7698
7952
  }
7699
7953
  // 2. Pending message injection
7700
- if (pm && queue.length > 0) {
7954
+ if (pm) {
7701
7955
  const { injected, claimed } = await drainSteeringQueue(pm, resultMessages ?? messages, steps, queue);
7702
7956
  await this.absorbSteering(claimed, injected);
7703
7957
  if (injected.length > 0) {
@@ -7705,12 +7959,14 @@ class ChatMessageAccumulator {
7705
7959
  }
7706
7960
  }
7707
7961
  return resultMessages ? { messages: resultMessages } : undefined;
7708
- };
7962
+ });
7709
7963
  }
7710
7964
  /**
7711
7965
  * Run outer-loop compaction if needed. Call after adding the response
7712
- * and capturing usage. Applies `compactModelMessages` and `compactUIMessages`
7713
- * callbacks if configured.
7966
+ * and capturing usage. Pass the LAST step's usage (`result.usage`), which is
7967
+ * the context the model held on its final call; `result.totalUsage` sums every
7968
+ * step of a tool-using turn and belongs in `context.turnUsage`. Applies
7969
+ * `compactModelMessages` and `compactUIMessages` callbacks if configured.
7714
7970
  *
7715
7971
  * @returns `true` if compaction was performed, `false` otherwise.
7716
7972
  */
@@ -7723,6 +7979,7 @@ class ChatMessageAccumulator {
7723
7979
  inputTokens: usage.inputTokens,
7724
7980
  outputTokens: usage.outputTokens,
7725
7981
  usage,
7982
+ turnUsage: context?.turnUsage,
7726
7983
  totalUsage: context?.totalUsage,
7727
7984
  chatId: context?.chatId,
7728
7985
  turn: context?.turn,
@@ -7955,8 +8212,8 @@ function createChatSession(payload, options) {
7955
8212
  idleTimeoutInSeconds: sessionIdleTimeoutOpt ?? currentPayload.idleTimeoutInSeconds ?? 30,
7956
8213
  timeout,
7957
8214
  spanName: currentPayload.trigger === "preload"
7958
- ? "waiting for first message"
7959
- : "waiting for first message (continuation)",
8215
+ ? "first message"
8216
+ : "first message (continuation)",
7960
8217
  });
7961
8218
  if (!result.ok || runSignal.aborted) {
7962
8219
  stop.cleanup();
@@ -7993,7 +8250,7 @@ function createChatSession(payload, options) {
7993
8250
  const next = await messagesInput.waitWithIdleTimeout({
7994
8251
  idleTimeoutInSeconds,
7995
8252
  timeout,
7996
- spanName: "waiting for next message",
8253
+ spanName: "next message",
7997
8254
  });
7998
8255
  if (!next.ok || runSignal.aborted) {
7999
8256
  stop.cleanup();
@@ -8014,7 +8271,9 @@ function createChatSession(payload, options) {
8014
8271
  // Reset stop signal for this turn
8015
8272
  stop.reset();
8016
8273
  // Reset per-turn state
8017
- locals_js_1.locals.set(chatResponsePartsKey, []);
8274
+ await locals_js_1.locals.get(chatManagedResponseKey)?.close();
8275
+ locals_js_1.locals.set(chatManagedResponseKey, undefined);
8276
+ locals_js_1.locals.set(chatManagedResponseActiveKey, true);
8018
8277
  // Set up steering queue and pending messages config in locals
8019
8278
  // so toStreamTextOptions() auto-injects prepareStep for steering
8020
8279
  const turnSteeringQueue = [];
@@ -8159,11 +8418,6 @@ function createChatSession(payload, options) {
8159
8418
  if (captured.status === "error") {
8160
8419
  if (captured.message) {
8161
8420
  const partial = cleanupAbortedParts(captured.message);
8162
- const queuedParts = locals_js_1.locals.get(chatResponsePartsKey);
8163
- if (queuedParts && queuedParts.length > 0) {
8164
- partial.parts = [...(partial.parts ?? []), ...queuedParts];
8165
- locals_js_1.locals.set(chatResponsePartsKey, []);
8166
- }
8167
8421
  await accumulator.addResponse(partial);
8168
8422
  }
8169
8423
  throw captured.error;
@@ -8179,31 +8433,14 @@ function createChatSession(payload, options) {
8179
8433
  const cleaned = stop.signal.aborted && !runSignal.aborted
8180
8434
  ? cleanupAbortedParts(response)
8181
8435
  : response;
8182
- // Append any non-transient data parts queued via chat.response or writer.write()
8183
- const queuedParts = locals_js_1.locals.get(chatResponsePartsKey);
8184
- if (queuedParts && queuedParts.length > 0) {
8185
- cleaned.parts = [...(cleaned.parts ?? []), ...queuedParts];
8186
- locals_js_1.locals.set(chatResponsePartsKey, []);
8187
- }
8188
8436
  await accumulator.addResponse(cleaned);
8189
8437
  }
8190
- else {
8191
- // No response (manual pipe mode) but there are queued data parts
8192
- const queuedParts = locals_js_1.locals.get(chatResponsePartsKey);
8193
- if (queuedParts && queuedParts.length > 0) {
8194
- await accumulator.addResponse({
8195
- id: (0, ai_runtime_js_1.generateId)(),
8196
- role: "assistant",
8197
- parts: queuedParts,
8198
- });
8199
- locals_js_1.locals.set(chatResponsePartsKey, []);
8200
- }
8201
- }
8202
8438
  // Capture token usage from the streamText result. Race with a 2s
8203
8439
  // timeout — on stop-abort the AI SDK's totalUsage promise can hang
8204
8440
  // indefinitely, which would wedge the turn loop (same guard as
8205
8441
  // chat.agent's turn loop).
8206
8442
  let turnUsage;
8443
+ let lastStepUsage;
8207
8444
  if (typeof source.totalUsage?.then === "function") {
8208
8445
  try {
8209
8446
  const usage = (await Promise.race([
@@ -8220,14 +8457,28 @@ function createChatSession(payload, options) {
8220
8457
  /* non-fatal */
8221
8458
  }
8222
8459
  }
8460
+ const lastStepUsagePromise = source.usage;
8461
+ if (typeof lastStepUsagePromise?.then === "function") {
8462
+ try {
8463
+ lastStepUsage = (await Promise.race([
8464
+ lastStepUsagePromise,
8465
+ new Promise((r) => setTimeout(() => r(undefined), 2_000)),
8466
+ ]));
8467
+ }
8468
+ catch {
8469
+ /* non-fatal */
8470
+ }
8471
+ }
8223
8472
  // Outer-loop compaction (same logic as chat.agent)
8224
8473
  if (sessionCompaction && turnUsage && !turnObj.stopped) {
8474
+ const contextUsage = lastStepUsage ?? turnUsage;
8225
8475
  const shouldTrigger = await sessionCompaction.shouldCompact({
8226
8476
  messages: accumulator.modelMessages,
8227
- totalTokens: turnUsage.totalTokens,
8228
- inputTokens: turnUsage.inputTokens,
8229
- outputTokens: turnUsage.outputTokens,
8230
- usage: turnUsage,
8477
+ totalTokens: contextUsage.totalTokens,
8478
+ inputTokens: contextUsage.inputTokens,
8479
+ outputTokens: contextUsage.outputTokens,
8480
+ usage: contextUsage,
8481
+ turnUsage,
8231
8482
  totalUsage: cumulativeUsage,
8232
8483
  chatId: currentPayload.chatId,
8233
8484
  turn,
@@ -8274,15 +8525,6 @@ function createChatSession(payload, options) {
8274
8525
  return response;
8275
8526
  },
8276
8527
  async addResponse(response) {
8277
- // Append any non-transient data parts queued via chat.response or writer.write()
8278
- const queuedParts = locals_js_1.locals.get(chatResponsePartsKey);
8279
- if (queuedParts && queuedParts.length > 0) {
8280
- response = {
8281
- ...response,
8282
- parts: [...(response.parts ?? []), ...queuedParts],
8283
- };
8284
- locals_js_1.locals.set(chatResponsePartsKey, []);
8285
- }
8286
8528
  await accumulator.addResponse(response);
8287
8529
  },
8288
8530
  async done() {
@@ -8294,7 +8536,7 @@ function createChatSession(payload, options) {
8294
8536
  const hasPending = !!sessionPendingMessages;
8295
8537
  if (!hasCompaction && !hasPending)
8296
8538
  return undefined;
8297
- return async ({ messages: stepMsgs, steps, }) => {
8539
+ return (0, steeringContext_js_1.retainStepMessages)(async ({ messages: stepMsgs, steps, }) => {
8298
8540
  let resultMessages;
8299
8541
  if (sessionCompaction) {
8300
8542
  const compactResult = await chatCompact(stepMsgs, steps, {
@@ -8313,7 +8555,7 @@ function createChatSession(payload, options) {
8313
8555
  }
8314
8556
  }
8315
8557
  return resultMessages ? { messages: resultMessages } : undefined;
8316
- };
8558
+ });
8317
8559
  },
8318
8560
  };
8319
8561
  return { done: false, value: turnObj };
@@ -8581,6 +8823,10 @@ function createChatStartSessionAction(taskId, options) {
8581
8823
  const clientDataMetadata = params.clientData !== undefined ? { metadata: params.clientData } : {};
8582
8824
  const maxAttempts = params.triggerConfig?.maxAttempts ?? options?.triggerConfig?.maxAttempts;
8583
8825
  const maxDuration = params.triggerConfig?.maxDuration ?? options?.triggerConfig?.maxDuration;
8826
+ const concurrency = params.triggerConfig?.concurrency !== undefined
8827
+ ? params.triggerConfig.concurrency
8828
+ : options?.triggerConfig?.concurrency;
8829
+ const concurrencyKey = params.triggerConfig?.concurrencyKey ?? options?.triggerConfig?.concurrencyKey;
8584
8830
  const idleTimeoutInSeconds = params.triggerConfig?.idleTimeoutInSeconds ?? options?.triggerConfig?.idleTimeoutInSeconds;
8585
8831
  // Only `undefined` means "not supplied": a per-call `null` (opt out) has to beat a pinning
8586
8832
  // action default, which neither truthiness nor `??` would allow.
@@ -8602,6 +8848,8 @@ function createChatStartSessionAction(taskId, options) {
8602
8848
  ...(options?.triggerConfig?.queue || params.triggerConfig?.queue
8603
8849
  ? { queue: params.triggerConfig?.queue ?? options?.triggerConfig?.queue }
8604
8850
  : {}),
8851
+ ...(concurrency !== undefined ? (0, concurrency_shared_js_1.triggerConcurrencyBody)(concurrency) : {}),
8852
+ ...(concurrencyKey !== undefined ? { concurrencyKey } : {}),
8605
8853
  tags,
8606
8854
  ...(maxAttempts !== undefined ? { maxAttempts } : {}),
8607
8855
  ...(maxDuration !== undefined ? { maxDuration } : {}),
@@ -8839,6 +9087,7 @@ exports.chat = {
8839
9087
  cleanupAbortedParts,
8840
9088
  /** Register background work that runs in parallel with streaming. See {@link chatDefer}. */
8841
9089
  defer: chatDefer,
9090
+ deferBeforeOutput: chatDeferBeforeOutput,
8842
9091
  /** Queue model messages for injection at the next `prepareStep` boundary. See {@link injectBackgroundContext}. */
8843
9092
  inject: injectBackgroundContext,
8844
9093
  /** Typed chat output stream for writing custom chunks or piping from subtasks. */
@@ -8948,6 +9197,10 @@ exports.chat = {
8948
9197
  * @internal
8949
9198
  */
8950
9199
  async function writeTurnCompleteChunk(_chatId, publicAccessToken) {
9200
+ locals_js_1.locals.set(chatManagedResponseActiveKey, false);
9201
+ const response = locals_js_1.locals.get(chatManagedResponseKey);
9202
+ if (response)
9203
+ await response.close();
8951
9204
  const session = getChatSession();
8952
9205
  // A handover-prepare boot claims the handover kinds so a signal arriving
8953
9206
  // before `waitForHandover` attaches is not drained. Released here rather than