@trigger.dev/sdk 0.0.0-prerelease-20260908122921 → 0.0.0-prerelease-20260909071038

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 (76) hide show
  1. package/dist/commonjs/v3/ai-shared.d.ts +15 -0
  2. package/dist/commonjs/v3/ai-shared.js +35 -1
  3. package/dist/commonjs/v3/ai-shared.js.map +1 -1
  4. package/dist/commonjs/v3/ai.d.ts +76 -9
  5. package/dist/commonjs/v3/ai.js +316 -222
  6. package/dist/commonjs/v3/ai.js.map +1 -1
  7. package/dist/commonjs/v3/chat-client.d.ts +8 -0
  8. package/dist/commonjs/v3/chat-client.js +199 -109
  9. package/dist/commonjs/v3/chat-client.js.map +1 -1
  10. package/dist/commonjs/v3/chat-react.d.ts +54 -0
  11. package/dist/commonjs/v3/chat-react.js +83 -0
  12. package/dist/commonjs/v3/chat-react.js.map +1 -1
  13. package/dist/commonjs/v3/chat.d.ts +11 -0
  14. package/dist/commonjs/v3/chat.js +36 -27
  15. package/dist/commonjs/v3/chat.js.map +1 -1
  16. package/dist/commonjs/v3/chatSnapshotIo.d.ts +2 -0
  17. package/dist/commonjs/v3/chatSnapshotIo.js +174 -0
  18. package/dist/commonjs/v3/chatSnapshotIo.js.map +1 -0
  19. package/dist/commonjs/v3/test/index.d.ts +1 -0
  20. package/dist/commonjs/v3/test/index.js +3 -1
  21. package/dist/commonjs/v3/test/index.js.map +1 -1
  22. package/dist/commonjs/v3/test/mock-chat-agent.d.ts +4 -4
  23. package/dist/commonjs/v3/test/mock-chat-agent.js +25 -4
  24. package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
  25. package/dist/commonjs/v3/test/transcript-storage-tests.d.ts +40 -0
  26. package/dist/commonjs/v3/test/transcript-storage-tests.js +258 -0
  27. package/dist/commonjs/v3/test/transcript-storage-tests.js.map +1 -0
  28. package/dist/commonjs/v3/transcriptStorage.d.ts +229 -0
  29. package/dist/commonjs/v3/transcriptStorage.js +321 -0
  30. package/dist/commonjs/v3/transcriptStorage.js.map +1 -0
  31. package/dist/commonjs/version.js +1 -1
  32. package/dist/esm/v3/ai-shared.d.ts +15 -0
  33. package/dist/esm/v3/ai-shared.js +33 -0
  34. package/dist/esm/v3/ai-shared.js.map +1 -1
  35. package/dist/esm/v3/ai.d.ts +76 -9
  36. package/dist/esm/v3/ai.js +306 -217
  37. package/dist/esm/v3/ai.js.map +1 -1
  38. package/dist/esm/v3/chat-client.d.ts +8 -0
  39. package/dist/esm/v3/chat-client.js +200 -110
  40. package/dist/esm/v3/chat-client.js.map +1 -1
  41. package/dist/esm/v3/chat-react.d.ts +54 -0
  42. package/dist/esm/v3/chat-react.js +81 -0
  43. package/dist/esm/v3/chat-react.js.map +1 -1
  44. package/dist/esm/v3/chat.d.ts +11 -0
  45. package/dist/esm/v3/chat.js +36 -27
  46. package/dist/esm/v3/chat.js.map +1 -1
  47. package/dist/esm/v3/chatSnapshotIo.d.ts +2 -0
  48. package/dist/esm/v3/chatSnapshotIo.js +166 -0
  49. package/dist/esm/v3/chatSnapshotIo.js.map +1 -0
  50. package/dist/esm/v3/test/index.d.ts +1 -0
  51. package/dist/esm/v3/test/index.js +1 -0
  52. package/dist/esm/v3/test/index.js.map +1 -1
  53. package/dist/esm/v3/test/mock-chat-agent.d.ts +4 -4
  54. package/dist/esm/v3/test/mock-chat-agent.js +25 -4
  55. package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
  56. package/dist/esm/v3/test/transcript-storage-tests.d.ts +40 -0
  57. package/dist/esm/v3/test/transcript-storage-tests.js +255 -0
  58. package/dist/esm/v3/test/transcript-storage-tests.js.map +1 -0
  59. package/dist/esm/v3/transcriptStorage.d.ts +229 -0
  60. package/dist/esm/v3/transcriptStorage.js +309 -0
  61. package/dist/esm/v3/transcriptStorage.js.map +1 -0
  62. package/dist/esm/version.js +1 -1
  63. package/docs/ai-chat/actions.mdx +2 -2
  64. package/docs/ai-chat/background-injection.mdx +2 -0
  65. package/docs/ai-chat/compaction.mdx +2 -0
  66. package/docs/ai-chat/frontend.mdx +2 -0
  67. package/docs/ai-chat/lifecycle-hooks.mdx +6 -2
  68. package/docs/ai-chat/patterns/database-persistence.mdx +6 -2
  69. package/docs/ai-chat/patterns/persistence-and-replay.mdx +34 -23
  70. package/docs/ai-chat/reference.mdx +56 -5
  71. package/docs/ai-chat/transcript-storage.mdx +233 -0
  72. package/docs/config/extensions/syncEnvVars.mdx +6 -0
  73. package/docs/deploy-environment-variables.mdx +23 -2
  74. package/package.json +2 -2
  75. package/skills/trigger-authoring-chat-agent/SKILL.md +3 -2
  76. package/skills/trigger-chat-agent-advanced/SKILL.md +5 -5
@@ -1,10 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.chat = exports.__buildManagedStreamTextOptionsForTests = exports.upsertIncomingMessage = exports.PENDING_MESSAGE_INJECTED_TYPE = exports.ai = void 0;
4
- exports.__setReadChatSnapshotImplForTests = __setReadChatSnapshotImplForTests;
5
- exports.__setWriteChatSnapshotImplForTests = __setWriteChatSnapshotImplForTests;
6
- exports.__readChatSnapshotProductionPathForTests = __readChatSnapshotProductionPathForTests;
7
- exports.__writeChatSnapshotProductionPathForTests = __writeChatSnapshotProductionPathForTests;
3
+ exports.chat = exports.__buildManagedStreamTextOptionsForTests = exports.upsertIncomingMessage = exports.PENDING_MESSAGE_INJECTED_TYPE = exports.ai = exports.snapshotTranscriptStorage = exports.reduceTranscriptChanges = exports.memoryTranscriptStorage = exports.defaultStorage = exports.__writeChatSnapshotProductionPathForTests = exports.__setWriteChatSnapshotImplForTests = exports.__setReadChatSnapshotImplForTests = exports.__readChatSnapshotProductionPathForTests = void 0;
4
+ exports.__setTranscriptStorageForTests = __setTranscriptStorageForTests;
8
5
  exports.__mergeByIdReplaceWinsForTests = __mergeByIdReplaceWinsForTests;
9
6
  exports.__setReplaySessionOutTailImplForTests = __setReplaySessionOutTailImplForTests;
10
7
  exports.__replaySessionOutTailProductionPathForTests = __replaySessionOutTailProductionPathForTests;
@@ -19,6 +16,16 @@ const v3_1 = require("@trigger.dev/core/v3");
19
16
  // ESM-only `ai@7` (see ../imports/ai-runtime.ts).
20
17
  const api_1 = require("@opentelemetry/api");
21
18
  const ai_runtime_js_1 = require("../imports/ai-runtime.js");
19
+ const transcriptStorage_js_1 = require("./transcriptStorage.js");
20
+ let transcriptStorageOverride;
21
+ /**
22
+ * Test-only override for the storage `chat.agent` persists through, so a
23
+ * test can capture the exact changesets the runtime produces.
24
+ * @internal
25
+ */
26
+ function __setTranscriptStorageForTests(storage) {
27
+ transcriptStorageOverride = storage;
28
+ }
22
29
  const ai_shared_js_1 = require("./ai-shared.js");
23
30
  Object.defineProperty(exports, "PENDING_MESSAGE_INJECTED_TYPE", { enumerable: true, get: function () { return ai_shared_js_1.PENDING_MESSAGE_INJECTED_TYPE; } });
24
31
  Object.defineProperty(exports, "upsertIncomingMessage", { enumerable: true, get: function () { return ai_shared_js_1.upsertIncomingMessage; } });
@@ -123,182 +130,16 @@ async function findLatestSessionInCursor(chatId) {
123
130
  }
124
131
  return latestCursor;
125
132
  }
126
- let readChatSnapshotImpl;
127
- function __setReadChatSnapshotImplForTests(impl) {
128
- readChatSnapshotImpl = impl;
129
- }
130
- let writeChatSnapshotImpl;
131
- function __setWriteChatSnapshotImplForTests(impl) {
132
- writeChatSnapshotImpl = impl;
133
- }
134
- /**
135
- * Read the persisted snapshot for a session. Returns `undefined` on:
136
- * - missing object (404 from the presigned GET — fresh session, never
137
- * persisted)
138
- * - presign failure (network/auth issue)
139
- * - malformed JSON
140
- * - version mismatch (forward-compat — older runtimes ignore newer blobs)
141
- *
142
- * Always swallows errors via `logger.warn`. The agent boot loop must stay
143
- * available even if S3 hiccups; the worst case is replaying more of
144
- * `session.out` than strictly necessary.
145
- * @internal
146
- */
147
- async function readChatSnapshot(sessionId) {
148
- if (readChatSnapshotImpl) {
149
- return (await readChatSnapshotImpl(sessionId)) ?? undefined;
150
- }
151
- const apiClient = v3_1.apiClientManager.clientOrThrow();
152
- let presignedUrl;
153
- try {
154
- const resp = await apiClient.getChatSnapshotUrl(sessionId);
155
- presignedUrl = resp.presignedUrl;
156
- }
157
- catch (error) {
158
- v3_1.logger.warn("chat.agent: snapshot presign (read) failed; continuing without snapshot", {
159
- error: error instanceof Error ? error.message : String(error),
160
- sessionId,
161
- });
162
- return undefined;
163
- }
164
- let response;
165
- try {
166
- response = await fetch(presignedUrl, { method: "GET" });
167
- }
168
- catch (error) {
169
- v3_1.logger.warn("chat.agent: snapshot fetch failed; continuing without snapshot", {
170
- error: error instanceof Error ? error.message : String(error),
171
- sessionId,
172
- });
173
- return undefined;
174
- }
175
- if (response.status === 404) {
176
- // First-ever boot for this session — no snapshot yet. Caller falls
177
- // through to replay-only.
178
- return undefined;
179
- }
180
- if (!response.ok) {
181
- v3_1.logger.warn("chat.agent: snapshot fetch returned non-OK; continuing without snapshot", {
182
- status: response.status,
183
- sessionId,
184
- });
185
- return undefined;
186
- }
187
- let parsed;
188
- try {
189
- parsed = await response.json();
190
- }
191
- catch (error) {
192
- v3_1.logger.warn("chat.agent: snapshot JSON parse failed; continuing without snapshot", {
193
- error: error instanceof Error ? error.message : String(error),
194
- sessionId,
195
- });
196
- return undefined;
197
- }
198
- if (!parsed || typeof parsed !== "object")
199
- return undefined;
200
- const candidate = parsed;
201
- if (candidate.version !== 1 || !Array.isArray(candidate.messages)) {
202
- v3_1.logger.warn("chat.agent: snapshot version/shape mismatch; ignoring", {
203
- version: candidate.version,
204
- sessionId,
205
- });
206
- return undefined;
207
- }
208
- return candidate;
209
- }
210
- /**
211
- * Persist the snapshot for a session. Awaited by callers immediately after
212
- * `onTurnComplete` — the agent may suspend right after this point, and
213
- * fire-and-forget promises don't reliably complete on suspend.
214
- *
215
- * Errors are swallowed via `logger.warn`. A failed write means the next
216
- * boot replays slightly more of `session.out` (back to the previous
217
- * snapshot's cursor) instead of failing — the conversation stays
218
- * coherent, only the boot path does marginally more work.
219
- * @internal
220
- */
221
- async function writeChatSnapshot(sessionId, snapshot) {
222
- if (writeChatSnapshotImpl) {
223
- await writeChatSnapshotImpl(sessionId, snapshot);
224
- return;
225
- }
226
- const apiClient = v3_1.apiClientManager.clientOrThrow();
227
- let presignedUrl;
228
- try {
229
- const resp = await apiClient.createChatSnapshotUploadUrl(sessionId);
230
- presignedUrl = resp.presignedUrl;
231
- }
232
- catch (error) {
233
- v3_1.logger.warn("chat.agent: snapshot presign (write) failed; next run will replay further", {
234
- error: error instanceof Error ? error.message : String(error),
235
- sessionId,
236
- });
237
- return;
238
- }
239
- let response;
240
- try {
241
- response = await fetch(presignedUrl, {
242
- method: "PUT",
243
- headers: { "content-type": "application/json" },
244
- body: JSON.stringify(snapshot),
245
- });
246
- }
247
- catch (error) {
248
- v3_1.logger.warn("chat.agent: snapshot upload failed; next run will replay further", {
249
- error: error instanceof Error ? error.message : String(error),
250
- sessionId,
251
- });
252
- return;
253
- }
254
- if (!response.ok) {
255
- v3_1.logger.warn("chat.agent: snapshot upload returned non-OK; next run will replay further", {
256
- status: response.status,
257
- sessionId,
258
- });
259
- }
260
- }
261
- /**
262
- * Test-only entry point that bypasses `__setReadChatSnapshotImplForTests`
263
- * and reaches the real `apiClient.getPayloadUrl` + `fetch` + JSON-parse path.
264
- * Used by `chat-snapshot.test.ts` to verify 404 / 500 / malformed JSON /
265
- * version-mismatch / network-error behavior end-to-end. Tests mock global
266
- * `fetch` and the api-client config; this wrapper lets them drive the
267
- * production code without the override hook short-circuiting.
268
- *
269
- * Not part of the public API. The `__` prefix and `ForTests` suffix mirror
270
- * the override-hook setters above.
271
- * @internal
272
- */
273
- async function __readChatSnapshotProductionPathForTests(sessionId) {
274
- const saved = readChatSnapshotImpl;
275
- readChatSnapshotImpl = undefined;
276
- try {
277
- return await readChatSnapshot(sessionId);
278
- }
279
- finally {
280
- readChatSnapshotImpl = saved;
281
- }
282
- }
283
- /**
284
- * Test-only entry point that bypasses `__setWriteChatSnapshotImplForTests`
285
- * and reaches the real `apiClient.createUploadPayloadUrl` + `fetch` PUT
286
- * path. Pairs with `__readChatSnapshotProductionPathForTests` — see that
287
- * function's note for the rationale.
288
- *
289
- * Not part of the public API.
290
- * @internal
291
- */
292
- async function __writeChatSnapshotProductionPathForTests(sessionId, snapshot) {
293
- const saved = writeChatSnapshotImpl;
294
- writeChatSnapshotImpl = undefined;
295
- try {
296
- await writeChatSnapshot(sessionId, snapshot);
297
- }
298
- finally {
299
- writeChatSnapshotImpl = saved;
300
- }
301
- }
133
+ var chatSnapshotIo_js_1 = require("./chatSnapshotIo.js");
134
+ Object.defineProperty(exports, "__readChatSnapshotProductionPathForTests", { enumerable: true, get: function () { return chatSnapshotIo_js_1.__readChatSnapshotProductionPathForTests; } });
135
+ Object.defineProperty(exports, "__setReadChatSnapshotImplForTests", { enumerable: true, get: function () { return chatSnapshotIo_js_1.__setReadChatSnapshotImplForTests; } });
136
+ Object.defineProperty(exports, "__setWriteChatSnapshotImplForTests", { enumerable: true, get: function () { return chatSnapshotIo_js_1.__setWriteChatSnapshotImplForTests; } });
137
+ Object.defineProperty(exports, "__writeChatSnapshotProductionPathForTests", { enumerable: true, get: function () { return chatSnapshotIo_js_1.__writeChatSnapshotProductionPathForTests; } });
138
+ var transcriptStorage_js_2 = require("./transcriptStorage.js");
139
+ Object.defineProperty(exports, "defaultStorage", { enumerable: true, get: function () { return transcriptStorage_js_2.defaultStorage; } });
140
+ Object.defineProperty(exports, "memoryTranscriptStorage", { enumerable: true, get: function () { return transcriptStorage_js_2.memoryTranscriptStorage; } });
141
+ Object.defineProperty(exports, "reduceTranscriptChanges", { enumerable: true, get: function () { return transcriptStorage_js_2.reduceTranscriptChanges; } });
142
+ Object.defineProperty(exports, "snapshotTranscriptStorage", { enumerable: true, get: function () { return transcriptStorage_js_2.snapshotTranscriptStorage; } });
302
143
  /**
303
144
  * Merge two `UIMessage[]` lists by `id`, with the second list winning on
304
145
  * collision. Used at run boot to combine the snapshot's persisted history
@@ -1964,6 +1805,12 @@ function spliceHandoverPartial(modelMessages, uiMessages, signal) {
1964
1805
  * @internal
1965
1806
  */
1966
1807
  const chatBackgroundQueueKey = locals_js_1.locals.create("chat.backgroundQueue");
1808
+ /**
1809
+ * Background injections a step-boundary drain handed to the model this turn,
1810
+ * with the transcript message they followed. Reconciled into the model lane
1811
+ * and the persisted injections once the turn's response is in.
1812
+ */
1813
+ const chatPendingBackgroundKey = locals_js_1.locals.create("chat.pendingBackground");
1967
1814
  /**
1968
1815
  * System-role context injected mid-conversation, held for the instructions lane.
1969
1816
  *
@@ -3625,6 +3472,12 @@ function toStreamTextOptions(options) {
3625
3472
  if (bgQueue && bgQueue.length > 0) {
3626
3473
  const injected = bgQueue.splice(0); // drain
3627
3474
  resultMessages = [...(resultMessages ?? messages), ...injected];
3475
+ const pendingBackground = locals_js_1.locals.get(chatPendingBackgroundKey) ?? [];
3476
+ pendingBackground.push({
3477
+ afterId: locals_js_1.locals.get(chatCurrentUIMessagesKey)?.at(-1)?.id ?? "",
3478
+ messages: injected,
3479
+ });
3480
+ locals_js_1.locals.set(chatPendingBackgroundKey, pendingBackground);
3628
3481
  }
3629
3482
  return resultMessages ? { messages: resultMessages } : undefined;
3630
3483
  };
@@ -3692,6 +3545,16 @@ function isUIMessageStreamable(value) {
3692
3545
  "toUIMessageStream" in value &&
3693
3546
  typeof value.toUIMessageStream === "function");
3694
3547
  }
3548
+ const warnedHydrateMessagesDeprecated = new Set();
3549
+ function warnHydrateMessagesDeprecatedOnce(agentId) {
3550
+ if (warnedHydrateMessagesDeprecated.has(agentId))
3551
+ return;
3552
+ warnedHydrateMessagesDeprecated.add(agentId);
3553
+ console.warn(`[chat.agent] \`hydrateMessages\` on "${agentId}" is deprecated. Give the agent a transcript ` +
3554
+ "storage instead: `save` receives every change to the conversation and `loadContext` " +
3555
+ "lets the application own the model's context, with crash recovery and durable " +
3556
+ "compaction that `hydrateMessages` never had.");
3557
+ }
3695
3558
  let warnedMissingOnAction = false;
3696
3559
  function warnMissingOnActionOnce() {
3697
3560
  if (warnedMissingOnAction)
@@ -3904,7 +3767,19 @@ function chatCustomAgent(options) {
3904
3767
  return task;
3905
3768
  }
3906
3769
  function chatAgent(options) {
3907
- const { run: userRun, clientDataSchema, onBoot, onRecoveryBoot, onPreload, onChatStart, onValidateMessages, hydrateMessages, actionSchema, onAction, onTurnStart, onBeforeTurnComplete, onCompacted, compaction, pendingMessages: pendingMessagesConfig, prepareMessages, tools: toolsOption, onTurnComplete, maxTurns = 100, turnTimeout = "1h", idleTimeoutInSeconds = 30, chatAccessTokenTTL = "1h", preloadIdleTimeoutInSeconds, preloadTimeout, uiMessageStreamOptions, onChatSuspend, onChatResume, exitAfterPreloadIdle = false, oomMachine, registry: promptRegistry, system: agentSystem, cacheControl: agentCacheControl, systemProviderOptions: agentSystemProviderOptions, versionSkew, ...restOptions } = options;
3770
+ const { run: userRun, clientDataSchema, onBoot, onRecoveryBoot, onPreload, onChatStart, onValidateMessages, hydrateMessages, storage, actionSchema, onAction, onTurnStart, onBeforeTurnComplete, onCompacted, compaction, pendingMessages: pendingMessagesConfig, prepareMessages, tools: toolsOption, onTurnComplete, maxTurns = 100, turnTimeout = "1h", idleTimeoutInSeconds = 30, chatAccessTokenTTL = "1h", preloadIdleTimeoutInSeconds, preloadTimeout, uiMessageStreamOptions, onChatSuspend, onChatResume, exitAfterPreloadIdle = false, oomMachine, registry: promptRegistry, system: agentSystem, cacheControl: agentCacheControl, systemProviderOptions: agentSystemProviderOptions, versionSkew, ...restOptions } = options;
3771
+ if (hydrateMessages) {
3772
+ if (storage) {
3773
+ throw new Error(`chat.agent: "${options.id}" sets both \`hydrateMessages\` and \`storage\`. ` +
3774
+ "`hydrateMessages` is deprecated and replaced by the storage: `save` receives every " +
3775
+ "change and `loadContext` on the storage owns the model's context. Remove `hydrateMessages`.");
3776
+ }
3777
+ if (typeof (transcriptStorageOverride ?? transcriptStorage_js_1.defaultStorage).loadContext === "function") {
3778
+ throw new Error(`chat.agent: "${options.id}" sets \`hydrateMessages\` and uses a transcript storage with ` +
3779
+ "`loadContext`. Both would own the model's context; keep one.");
3780
+ }
3781
+ warnHydrateMessagesDeprecatedOnce(options.id);
3782
+ }
3908
3783
  const parseClientData = clientDataSchema ? (0, v3_1.getSchemaParseFn)(clientDataSchema) : undefined;
3909
3784
  const parseAction = actionSchema ? (0, v3_1.getSchemaParseFn)(actionSchema) : undefined;
3910
3785
  // chat.agent does not expose generic retry options (see docstring on
@@ -3987,6 +3862,24 @@ function chatAgent(options) {
3987
3862
  // durable snapshot + `session.out` replay (or `hydrateMessages` if
3988
3863
  // registered) — the wire is delta-only now, no longer a seed.
3989
3864
  let accumulatedMessages = [];
3865
+ /**
3866
+ * Give the model accumulator the background injections a step-boundary
3867
+ * drain handed to the model this turn, and record them for persistence.
3868
+ * Returns how many model messages were appended.
3869
+ */
3870
+ const reconcilePendingBackground = () => {
3871
+ const pending = locals_js_1.locals.get(chatPendingBackgroundKey);
3872
+ if (!pending || pending.length === 0)
3873
+ return 0;
3874
+ locals_js_1.locals.set(chatPendingBackgroundKey, []);
3875
+ let appended = 0;
3876
+ for (const entry of pending) {
3877
+ accumulatedMessages.push(...entry.messages);
3878
+ laneInjections.push(entry);
3879
+ appended += entry.messages.length;
3880
+ }
3881
+ return appended;
3882
+ };
3990
3883
  /**
3991
3884
  * Give the model accumulator the steering messages a drain consumed,
3992
3885
  * in the form the model actually received. Appended, never reconverted
@@ -4022,7 +3915,41 @@ function chatAgent(options) {
4022
3915
  // collectively cost ~600ms on every first-message TTFC. Both reads
4023
3916
  // swallow errors internally; the agent stays available either way.
4024
3917
  const sessionIdForSnapshot = payload.sessionId ?? payload.chatId;
3918
+ const transcriptStorage = storage ??
3919
+ transcriptStorageOverride ??
3920
+ transcriptStorage_js_1.defaultStorage;
3921
+ const storageLoadContext = transcriptStorage.loadContext?.bind(transcriptStorage);
3922
+ /**
3923
+ * Who supplies the model's context each turn: the deprecated
3924
+ * `hydrateMessages` hook, the storage's `loadContext`, or (undefined)
3925
+ * the runtime's own transcript.
3926
+ */
3927
+ const loadContextHook = hydrateMessages
3928
+ ? (event) => hydrateMessages(event)
3929
+ : storageLoadContext
3930
+ ? (event) => storageLoadContext({ chatId: event.chatId, clientData: event.clientData }, event)
3931
+ : undefined;
3932
+ let transcriptShadow = (0, transcriptStorage_js_1.createTranscriptShadow)([]);
3933
+ let bootTranscriptState = null;
3934
+ /**
3935
+ * True while the model lane holds a compaction summary, so it cannot be
3936
+ * rebuilt from the transcript and has to be persisted as state. Reset
3937
+ * wherever the lane is reconverted from the UI lane.
3938
+ */
3939
+ let laneCompacted = false;
3940
+ /** Conversational `chat.inject` messages in the lane, anchored to the transcript. */
3941
+ let laneInjections = [];
3942
+ let persistedStateSet = false;
4025
3943
  let bootSnapshot;
3944
+ let bootClientData = payload.metadata;
3945
+ if (parseClientData) {
3946
+ try {
3947
+ bootClientData = await parseClientData(payload.metadata);
3948
+ }
3949
+ catch {
3950
+ bootClientData = payload.metadata;
3951
+ }
3952
+ }
4026
3953
  /**
4027
3954
  * The `lastOutEventId` the most recent snapshot carried.
4028
3955
  *
@@ -4032,31 +3959,95 @@ function chatAgent(options) {
4032
3959
  * keeps an action's write cursor-neutral.
4033
3960
  */
4034
3961
  let lastSnapshotOutEventId;
3962
+ const storageTrigger = (trigger) => trigger === "regenerate-message"
3963
+ ? "regenerate-message"
3964
+ : trigger === "action" || trigger === "action-turn"
3965
+ ? "action"
3966
+ : "submit-message";
3967
+ /**
3968
+ * Hand the runtime's view of the transcript to the storage as a
3969
+ * changeset: the diff against what was last saved, plus the cursors the
3970
+ * next boot resumes from. The shadow only advances when the save
3971
+ * succeeds, so a failed save is folded into the next changeset.
3972
+ */
3973
+ /** The runtime's opaque state as of the last save; carried on every changeset's transcript. */
3974
+ let transcriptState = null;
3975
+ const saveTranscript = async (opts) => {
3976
+ const { changes, shadow } = (0, transcriptStorage_js_1.diffTranscript)(transcriptShadow, opts.messages, {
3977
+ nonFinalIds: opts.nonFinalIds,
3978
+ });
3979
+ const throughId = opts.messages.at(-1)?.id ?? "";
3980
+ const queued = locals_js_1.locals.get(chatBackgroundQueueKey) ?? [];
3981
+ const runtimeState = laneCompacted || laneInjections.length > 0 || queued.length > 0
3982
+ ? {
3983
+ v: 1,
3984
+ ...(laneCompacted
3985
+ ? {
3986
+ compaction: {
3987
+ modelMessages: accumulatedMessages,
3988
+ throughId,
3989
+ fingerprint: (0, transcriptStorage_js_1.prefixFingerprint)(shadow, throughId),
3990
+ },
3991
+ }
3992
+ : {}),
3993
+ ...(laneInjections.length > 0 ? { injections: laneInjections } : {}),
3994
+ ...(queued.length > 0 ? { queued: [...queued] } : {}),
3995
+ }
3996
+ : null;
3997
+ if (runtimeState !== null || persistedStateSet) {
3998
+ changes.push({ op: "state", value: runtimeState });
3999
+ }
4000
+ transcriptState = runtimeState;
4001
+ const inCursor = chatInputRouter().resumeFloor();
4002
+ await transcriptStorage.save({
4003
+ chatId: payload.chatId,
4004
+ clientData: opts.clientData,
4005
+ turn: opts.turn,
4006
+ trigger: opts.trigger,
4007
+ runId: ctx.run.id,
4008
+ ctx,
4009
+ }, {
4010
+ reason: opts.reason,
4011
+ changes,
4012
+ transcript: {
4013
+ entries: opts.messages.map((message) => ({
4014
+ id: message.id,
4015
+ final: !shadow.nonFinal.has(message.id),
4016
+ message,
4017
+ })),
4018
+ state: transcriptState,
4019
+ },
4020
+ cursors: {
4021
+ lastOutEventId: opts.lastOutEventId,
4022
+ lastInEventId: inCursor !== undefined ? String(inCursor) : undefined,
4023
+ },
4024
+ });
4025
+ transcriptShadow = shadow;
4026
+ persistedStateSet = runtimeState !== null;
4027
+ };
4035
4028
  /**
4036
4029
  * Persist the accumulator outside a turn.
4037
4030
  *
4038
4031
  * An action is not a turn, so it never reaches the turn-complete path where
4039
- * the snapshot is normally written — but it can change the conversation in
4040
- * two ways: a `chat.history` mutation, and a response streamed back from
4041
- * `onAction`. Both have to survive, and one write at the end of the action
4042
- * covers both rather than writing twice for a regenerate that does both.
4032
+ * the transcript is normally saved, but a `chat.history` mutation changes
4033
+ * the conversation and has to survive the run ending.
4043
4034
  *
4044
4035
  * Cursor-neutral: an action has no turn cursor of its own, and writing
4045
4036
  * `undefined` would drop the resume point the last turn established and make
4046
4037
  * the next boot replay from further back.
4047
4038
  */
4048
- const writeSnapshotOutsideTurn = async (reason) => {
4039
+ const writeSnapshotOutsideTurn = async (reason, turnContext) => {
4049
4040
  if (hydrateMessages)
4050
4041
  return;
4051
4042
  try {
4052
4043
  await tracer_js_1.tracer.startActiveSpan("snapshot.write", async () => {
4053
- const snapshotInCursor = chatInputRouter().resumeFloor();
4054
- await writeChatSnapshot(sessionIdForSnapshot, {
4055
- version: 1,
4056
- savedAt: Date.now(),
4044
+ await saveTranscript({
4045
+ reason: "action",
4057
4046
  messages: accumulatedUIMessages,
4047
+ turn: turnContext.turn,
4048
+ trigger: "action",
4049
+ clientData: turnContext.clientData,
4058
4050
  lastOutEventId: lastSnapshotOutEventId,
4059
- lastInEventId: snapshotInCursor !== undefined ? String(snapshotInCursor) : undefined,
4060
4051
  });
4061
4052
  }, {
4062
4053
  attributes: {
@@ -4090,7 +4081,7 @@ function chatAgent(options) {
4090
4081
  // resume-cursor block reuses it instead of re-scanning.
4091
4082
  let bootInCursor;
4092
4083
  let bootInCursorResolved = false;
4093
- if (!hydrateMessages && couldHavePriorState) {
4084
+ if (couldHavePriorState) {
4094
4085
  // Single parent span for the whole boot read phase — snapshot
4095
4086
  // read, session.out replay, session.in replay. Per-phase timing
4096
4087
  // + result counts are attributes on the span.
@@ -4098,18 +4089,33 @@ function chatAgent(options) {
4098
4089
  // snapshot read
4099
4090
  const snapStart = Date.now();
4100
4091
  try {
4101
- bootSnapshot = await readChatSnapshot(sessionIdForSnapshot);
4092
+ const loaded = hydrateMessages
4093
+ ? undefined
4094
+ : await transcriptStorage.load({
4095
+ chatId: payload.chatId,
4096
+ clientData: bootClientData,
4097
+ });
4098
+ if (loaded) {
4099
+ transcriptShadow = (0, transcriptStorage_js_1.createTranscriptShadow)(loaded.messages, new Set(loaded.nonFinalIds ?? []));
4100
+ bootTranscriptState = loaded.state;
4101
+ transcriptState = loaded.state ?? null;
4102
+ persistedStateSet = loaded.state !== null && loaded.state !== undefined;
4103
+ bootSnapshot = {
4104
+ messages: loaded.messages,
4105
+ lastOutEventId: loaded.cursors?.lastOutEventId,
4106
+ lastInEventId: loaded.cursors?.lastInEventId,
4107
+ };
4108
+ }
4102
4109
  }
4103
4110
  catch (error) {
4104
- // `readChatSnapshot` already swallows + warns internally; this catch
4105
- // is just belt-and-suspenders against tracer/span errors.
4106
- v3_1.logger.warn("chat.agent: snapshot read failed; continuing without snapshot", {
4111
+ v3_1.logger.warn("chat.agent: transcript load failed; continuing from the stream tail", {
4107
4112
  error: error instanceof Error ? error.message : String(error),
4108
4113
  sessionId: sessionIdForSnapshot,
4109
4114
  });
4110
4115
  }
4111
4116
  bootSpan.setAttribute("chat.boot.snapshot.durationMs", Date.now() - snapStart);
4112
- bootSpan.setAttribute("chat.boot.snapshot.present", !!bootSnapshot);
4117
+ bootSpan.setAttribute("chat.boot.snapshot.present", bootSnapshot !== undefined &&
4118
+ (bootSnapshot.messages.length > 0 || bootSnapshot.lastOutEventId !== undefined));
4113
4119
  bootSpan.setAttribute("chat.boot.snapshot.messageCount", bootSnapshot?.messages?.length ?? 0);
4114
4120
  // Seed the trim chain from the snapshot's `lastOutEventId` (the SSE
4115
4121
  // id of the previous turn's `turn-complete` control record). The
@@ -4222,7 +4228,7 @@ function chatAgent(options) {
4222
4228
  resuming: Boolean(payload.continuation) || ctx.attempt.number > 1,
4223
4229
  });
4224
4230
  // ── Recovery boot + chain reconstruction ────────────────────────
4225
- if (!hydrateMessages) {
4231
+ {
4226
4232
  const settledMessages = mergeByIdReplaceWins(bootSnapshot?.messages ?? [], replayedSettled);
4227
4233
  const inFlightUsers = replayedInTail.map((r) => r.message);
4228
4234
  const partialAssistant = replayedPartial;
@@ -4375,7 +4381,8 @@ function chatAgent(options) {
4375
4381
  // path where wire-borne UIMessage[] still seeds the accumulator,
4376
4382
  // and it's safe because the route handler isn't subject to the
4377
4383
  // `/in/append` 512 KiB cap.
4378
- if (accumulatedUIMessages.length === 0 &&
4384
+ if (!loadContextHook &&
4385
+ accumulatedUIMessages.length === 0 &&
4379
4386
  payload.trigger === "handover-prepare" &&
4380
4387
  Array.isArray(payload.headStartMessages) &&
4381
4388
  payload.headStartMessages.length > 0) {
@@ -4404,7 +4411,17 @@ function chatAgent(options) {
4404
4411
  }
4405
4412
  }
4406
4413
  try {
4407
- accumulatedMessages = await toModelMessages(accumulatedUIMessages);
4414
+ const bootRuntimeState = (0, transcriptStorage_js_1.parseTranscriptRuntimeState)(bootTranscriptState);
4415
+ const restored = await (0, transcriptStorage_js_1.restoreModelLane)(accumulatedUIMessages, bootRuntimeState, (messages) => toModelMessages(messages));
4416
+ accumulatedMessages = restored.messages;
4417
+ laneCompacted = restored.compacted;
4418
+ laneInjections = restored.injections;
4419
+ if (bootRuntimeState?.queued && bootRuntimeState.queued.length > 0) {
4420
+ locals_js_1.locals.set(chatBackgroundQueueKey, [
4421
+ ...(locals_js_1.locals.get(chatBackgroundQueueKey) ?? []),
4422
+ ...bootRuntimeState.queued,
4423
+ ]);
4424
+ }
4408
4425
  }
4409
4426
  catch (error) {
4410
4427
  v3_1.logger.warn("chat.agent: toModelMessages failed at boot; starting empty", {
@@ -4781,6 +4798,7 @@ function chatAgent(options) {
4781
4798
  } // end else (no boot-injected first turn)
4782
4799
  }
4783
4800
  for (let turn = 0; turn < maxTurns; turn++) {
4801
+ let turnClientData = payload.metadata;
4784
4802
  // Declared here so the finally can detach it — a handler leaked past
4785
4803
  // its turn duplicates every mid-stream message into the shared buffer.
4786
4804
  let turnMsgSub;
@@ -4802,6 +4820,7 @@ function chatAgent(options) {
4802
4820
  // `cleanupAbortedParts` no longer has to be re-applied below.
4803
4821
  const cleanedIncomingMessages = incomingMessages.map((msg) => msg.role === "assistant" ? cleanupAbortedParts(msg) : msg);
4804
4822
  const clientData = (parseClientData ? await parseClientData(wireMetadata) : wireMetadata);
4823
+ turnClientData = clientData;
4805
4824
  const lastUserMessage = extractLastUserMessageText(cleanedIncomingMessages);
4806
4825
  // Actions are not turns. They use a different span name
4807
4826
  // and don't carry a turn.number. Branched on at `isAction`.
@@ -4838,6 +4857,7 @@ function chatAgent(options) {
4838
4857
  locals_js_1.locals.set(chatDeferKey, new Set());
4839
4858
  locals_js_1.locals.set(chatCompactionStateKey, undefined);
4840
4859
  locals_js_1.locals.set(chatSteeringQueueKey, []);
4860
+ locals_js_1.locals.set(chatPendingBackgroundKey, []);
4841
4861
  locals_js_1.locals.set(chatResponsePartsKey, []);
4842
4862
  // NOTE: chatBackgroundQueueKey is NOT reset here — messages injected
4843
4863
  // by deferred work from the previous turn's onTurnComplete need to
@@ -4949,9 +4969,9 @@ function chatAgent(options) {
4949
4969
  ? await parseAction(currentWirePayload.action)
4950
4970
  : currentWirePayload.action;
4951
4971
  // Hydrate messages from backend if configured
4952
- if (hydrateMessages) {
4972
+ if (loadContextHook) {
4953
4973
  const hydrated = await tracer_js_1.tracer.startActiveSpan("hydrateMessages()", async () => {
4954
- return hydrateMessages({
4974
+ return loadContextHook({
4955
4975
  chatId: currentWirePayload.chatId,
4956
4976
  turn,
4957
4977
  trigger: "action",
@@ -4971,6 +4991,8 @@ function chatAgent(options) {
4971
4991
  });
4972
4992
  accumulatedUIMessages = [...hydrated];
4973
4993
  accumulatedMessages = await toModelMessages(hydrated);
4994
+ laneCompacted = false;
4995
+ laneInjections = [];
4974
4996
  locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
4975
4997
  }
4976
4998
  // Fire onAction — handler may mutate state via
@@ -5001,6 +5023,8 @@ function chatAgent(options) {
5001
5023
  locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
5002
5024
  accumulatedUIMessages = [...actionOverride];
5003
5025
  accumulatedMessages = await toModelMessages(actionOverride);
5026
+ laneCompacted = false;
5027
+ laneInjections = [];
5004
5028
  locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
5005
5029
  actionChangedHistory = true;
5006
5030
  }
@@ -5023,7 +5047,7 @@ function chatAgent(options) {
5023
5047
  // route handler's first-turn history to the hydrate hook as
5024
5048
  // incoming messages instead (gated on the pending handover).
5025
5049
  if (turn === 0 &&
5026
- hydrateMessages &&
5050
+ loadContextHook &&
5027
5051
  cleanedUIMessages.length === 0 &&
5028
5052
  (locals_js_1.locals.get(chatHandoverPartialKey)?.length ?? 0) > 0 &&
5029
5053
  Array.isArray(payload.headStartMessages) &&
@@ -5053,7 +5077,7 @@ function chatAgent(options) {
5053
5077
  },
5054
5078
  }));
5055
5079
  }
5056
- if (hydrateMessages) {
5080
+ if (loadContextHook) {
5057
5081
  // Snapshot the ids the accumulator knew BEFORE this
5058
5082
  // turn ran — used below to decide whether an
5059
5083
  // incoming wire message is genuinely new or just a
@@ -5071,7 +5095,7 @@ function chatAgent(options) {
5071
5095
  // was always true for `submit-message` is now true for every
5072
5096
  // trigger.
5073
5097
  const hydrated = await tracer_js_1.tracer.startActiveSpan("hydrateMessages()", async () => {
5074
- return hydrateMessages({
5098
+ return loadContextHook({
5075
5099
  chatId: currentWirePayload.chatId,
5076
5100
  turn,
5077
5101
  trigger: currentWirePayload.trigger,
@@ -5110,6 +5134,8 @@ function chatAgent(options) {
5110
5134
  }
5111
5135
  accumulatedUIMessages = merged;
5112
5136
  accumulatedMessages = await toModelMessages(merged);
5137
+ laneCompacted = false;
5138
+ laneInjections = [];
5113
5139
  locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
5114
5140
  // Track new messages for onTurnComplete.newUIMessages.
5115
5141
  // Only push for genuinely new ids — HITL continuations
@@ -5155,6 +5181,8 @@ function chatAgent(options) {
5155
5181
  accumulatedUIMessages.pop();
5156
5182
  }
5157
5183
  accumulatedMessages = await toModelMessages(accumulatedUIMessages);
5184
+ laneCompacted = false;
5185
+ laneInjections = [];
5158
5186
  }
5159
5187
  else if (cleanedUIMessages.length > 0) {
5160
5188
  // Submit-message (and the special-cased
@@ -5205,6 +5233,8 @@ function chatAgent(options) {
5205
5233
  if (!inPlace) {
5206
5234
  v3_1.logger.warn("chat.agent: replaced message not found at the model lane tail; reconverting the lane");
5207
5235
  accumulatedMessages = await toModelMessages(accumulatedUIMessages);
5236
+ laneCompacted = false;
5237
+ laneInjections = [];
5208
5238
  }
5209
5239
  }
5210
5240
  else {
@@ -5290,7 +5320,7 @@ function chatAgent(options) {
5290
5320
  // history rather than from the snapshot the edit replaced.
5291
5321
  // The turn then does its own hooks, completion and snapshot.
5292
5322
  if (actionChangedHistory) {
5293
- await writeSnapshotOutsideTurn("action");
5323
+ await writeSnapshotOutsideTurn("action", { turn, clientData });
5294
5324
  }
5295
5325
  actionTurn = true;
5296
5326
  }
@@ -5302,7 +5332,7 @@ function chatAgent(options) {
5302
5332
  else {
5303
5333
  msgSub?.off();
5304
5334
  if (actionChangedHistory) {
5305
- await writeSnapshotOutsideTurn("action");
5335
+ await writeSnapshotOutsideTurn("action", { turn, clientData });
5306
5336
  }
5307
5337
  await writeTurnCompleteChunk(currentWirePayload.chatId);
5308
5338
  // Don't consume a turn iteration — actions aren't turns.
@@ -5400,6 +5430,8 @@ function chatAgent(options) {
5400
5430
  locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
5401
5431
  accumulatedUIMessages = [...turnStartOverride];
5402
5432
  accumulatedMessages = await toModelMessages(turnStartOverride);
5433
+ laneCompacted = false;
5434
+ laneInjections = [];
5403
5435
  locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
5404
5436
  }
5405
5437
  }, {
@@ -5460,7 +5492,12 @@ function chatAgent(options) {
5460
5492
  const lastAccumulated = accumulatedMessages[accumulatedMessages.length - 1];
5461
5493
  const bgQueue = locals_js_1.locals.get(chatBackgroundQueueKey);
5462
5494
  if (bgQueue && bgQueue.length > 0 && lastAccumulated?.role !== "tool") {
5463
- accumulatedMessages.push(...bgQueue.splice(0));
5495
+ const injected = bgQueue.splice(0);
5496
+ accumulatedMessages.push(...injected);
5497
+ laneInjections.push({
5498
+ afterId: accumulatedUIMessages.at(-1)?.id ?? "",
5499
+ messages: injected,
5500
+ });
5464
5501
  }
5465
5502
  if (isHeadStartFinalTurn) {
5466
5503
  // The synthesized partial UIMessage IS the response.
@@ -5621,6 +5658,8 @@ function chatAgent(options) {
5621
5658
  locals_js_1.locals.set(chatPendingSteerKey, pending);
5622
5659
  const pendingIds = new Set(pending.map((e) => e.ui.id));
5623
5660
  accumulatedMessages = await toModelMessages(runOverride.filter((m) => !pendingIds.has(m.id)));
5661
+ laneCompacted = false;
5662
+ laneInjections = [];
5624
5663
  locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
5625
5664
  }
5626
5665
  // Check if compaction set a model-only override (preserves UI messages).
@@ -5643,6 +5682,8 @@ function chatAgent(options) {
5643
5682
  accumulatedMessages = taskCompactionConfig?.compactModelMessages
5644
5683
  ? await taskCompactionConfig.compactModelMessages(compactEvent)
5645
5684
  : modelOnlyOverride;
5685
+ laneCompacted = true;
5686
+ laneInjections = [];
5646
5687
  // Apply UI messages: callback or default (preserve all)
5647
5688
  if (taskCompactionConfig?.compactUIMessages) {
5648
5689
  accumulatedUIMessages = (await taskCompactionConfig.compactUIMessages(compactEvent));
@@ -5658,7 +5699,7 @@ function chatAgent(options) {
5658
5699
  // branches below, so a turn that captured no response is covered.
5659
5700
  const steerTailThisTurn = reconcilePendingSteer({
5660
5701
  turnNew: turnNewModelMessages,
5661
- }).reduce((n, e) => n + e.model.length, 0);
5702
+ }).reduce((n, e) => n + e.model.length, 0) + reconcilePendingBackground();
5662
5703
  // Append the assistant's response (partial or complete) to the accumulator.
5663
5704
  // The onFinish callback fires even on abort/stop, so partial responses
5664
5705
  // from stopped generation are captured correctly.
@@ -5721,6 +5762,8 @@ function chatAgent(options) {
5721
5762
  if (!ok) {
5722
5763
  v3_1.logger.warn("chat.agent: replaced response not found at the model lane tail; reconverting the lane");
5723
5764
  accumulatedMessages = await toModelMessages(accumulatedUIMessages);
5765
+ laneCompacted = false;
5766
+ laneInjections = [];
5724
5767
  }
5725
5768
  }
5726
5769
  else {
@@ -5829,6 +5872,8 @@ function chatAgent(options) {
5829
5872
  ],
5830
5873
  },
5831
5874
  ];
5875
+ laneCompacted = true;
5876
+ laneInjections = [];
5832
5877
  // UI messages: callback or default (preserve all)
5833
5878
  if (outerCompaction.compactUIMessages) {
5834
5879
  accumulatedUIMessages = (await outerCompaction.compactUIMessages(outerCompactEvent));
@@ -5918,6 +5963,8 @@ function chatAgent(options) {
5918
5963
  locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
5919
5964
  accumulatedUIMessages = [...override];
5920
5965
  accumulatedMessages = await toModelMessages(override);
5966
+ laneCompacted = false;
5967
+ laneInjections = [];
5921
5968
  locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
5922
5969
  // Update event so onTurnComplete sees compacted messages
5923
5970
  turnCompleteEvent.messages = accumulatedMessages;
@@ -5964,6 +6011,8 @@ function chatAgent(options) {
5964
6011
  locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
5965
6012
  accumulatedUIMessages = [...turnCompleteOverride];
5966
6013
  accumulatedMessages = await toModelMessages(turnCompleteOverride);
6014
+ laneCompacted = false;
6015
+ laneInjections = [];
5967
6016
  locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
5968
6017
  }
5969
6018
  }, {
@@ -6017,15 +6066,18 @@ function chatAgent(options) {
6017
6066
  if (!hydrateMessages) {
6018
6067
  try {
6019
6068
  await tracer_js_1.tracer.startActiveSpan("snapshot.write", async () => {
6020
- const snapshotInCursor = chatInputRouter().resumeFloor();
6021
6069
  lastSnapshotOutEventId =
6022
6070
  turnCompleteResult?.lastEventId ?? lastSnapshotOutEventId;
6023
- await writeChatSnapshot(sessionIdForSnapshot, {
6024
- version: 1,
6025
- savedAt: Date.now(),
6071
+ await saveTranscript({
6072
+ reason: "turn-complete",
6026
6073
  messages: accumulatedUIMessages,
6074
+ turn,
6075
+ trigger: storageTrigger(currentWirePayload.trigger),
6076
+ clientData,
6027
6077
  lastOutEventId: lastSnapshotOutEventId,
6028
- lastInEventId: snapshotInCursor !== undefined ? String(snapshotInCursor) : undefined,
6078
+ nonFinalIds: wasStopped && capturedResponseMessage?.id
6079
+ ? new Set([capturedResponseMessage.id])
6080
+ : undefined,
6029
6081
  });
6030
6082
  }, {
6031
6083
  attributes: {
@@ -6246,6 +6298,7 @@ function chatAgent(options) {
6246
6298
  let erroredNewUIMessages = buildErroredNew();
6247
6299
  let erroredNewModelMessages = [];
6248
6300
  const reconciledSteer = reconcilePendingSteer();
6301
+ const backgroundTailThisTurn = reconcilePendingBackground();
6249
6302
  if (!responseCommitted) {
6250
6303
  try {
6251
6304
  if (erroredNewUIMessages.length > 0) {
@@ -6270,10 +6323,13 @@ function chatAgent(options) {
6270
6323
  accumulatedMessages.push(...(await toModelMessages(appended.map((m) => stripProviderMetadata(m)))));
6271
6324
  }
6272
6325
  else {
6273
- const ok = await replaceModelRun(accumulatedMessages, erroredUIMessages[partialIdx], partialResponse, reconciledSteer.reduce((n, e) => n + e.model.length, 0));
6326
+ const ok = await replaceModelRun(accumulatedMessages, erroredUIMessages[partialIdx], partialResponse, reconciledSteer.reduce((n, e) => n + e.model.length, 0) +
6327
+ backgroundTailThisTurn);
6274
6328
  if (!ok) {
6275
6329
  v3_1.logger.warn("chat.agent: replaced partial not found at the model lane tail; reconverting the lane");
6276
6330
  accumulatedMessages = await toModelMessages(erroredUIMessagesWithPartial);
6331
+ laneCompacted = false;
6332
+ laneInjections = [];
6277
6333
  }
6278
6334
  }
6279
6335
  accumulatedUIMessages = erroredUIMessagesWithPartial;
@@ -6337,13 +6393,14 @@ function chatAgent(options) {
6337
6393
  // neither the snapshot nor the replayable `.in` tail.
6338
6394
  if (!hydrateMessages) {
6339
6395
  try {
6340
- const errorSnapshotInCursor = chatInputRouter().resumeFloor();
6341
- await writeChatSnapshot(sessionIdForSnapshot, {
6342
- version: 1,
6343
- savedAt: Date.now(),
6396
+ await saveTranscript({
6397
+ reason: "turn-error",
6344
6398
  messages: erroredUIMessagesWithPartial,
6345
- lastOutEventId: errorTurnCompleteResult?.lastEventId,
6346
- lastInEventId: errorSnapshotInCursor !== undefined ? String(errorSnapshotInCursor) : undefined,
6399
+ turn,
6400
+ trigger: storageTrigger(currentWirePayload.trigger),
6401
+ clientData: turnClientData,
6402
+ lastOutEventId: lastSnapshotOutEventId,
6403
+ nonFinalIds: includePartial && partialResponse ? new Set([partialResponse.id]) : undefined,
6347
6404
  });
6348
6405
  }
6349
6406
  catch (error) {
@@ -8660,6 +8717,41 @@ async function mintPublicTokenWithOverride(args) {
8660
8717
  expirationTime: args.expirationTime,
8661
8718
  });
8662
8719
  }
8720
+ /**
8721
+ * Creates a server-side helper that reads a conversation from a transcript
8722
+ * storage, for rendering history before the chat connects. Works the same
8723
+ * for every storage, the platform default included, so the browser never
8724
+ * reads a store directly and the secret key stays on the server.
8725
+ *
8726
+ * Wrap it in a Next.js server action (or any server-side handler), scope it
8727
+ * to the authenticated user through `clientData`, and pass the result to
8728
+ * `useLoadTranscript` in the browser.
8729
+ *
8730
+ * @example
8731
+ * ```ts
8732
+ * // actions.ts
8733
+ * "use server";
8734
+ * import { chat, defaultStorage } from "@trigger.dev/sdk/ai";
8735
+ *
8736
+ * export const loadTranscript = chat.createLoadTranscriptAction(defaultStorage, { limit: 50 });
8737
+ * ```
8738
+ */
8739
+ function createChatLoadTranscriptAction(storage, options) {
8740
+ return async (params) => {
8741
+ if (!params.chatId) {
8742
+ throw new Error("chat.createLoadTranscriptAction: params.chatId is required.");
8743
+ }
8744
+ if (options?.apiClient) {
8745
+ const { apiClient, ...rest } = options;
8746
+ return v3_1.apiClientManager.runWithConfig(apiClient, () => createChatLoadTranscriptAction(storage, rest)(params));
8747
+ }
8748
+ const limit = params.limit ?? options?.limit;
8749
+ return storage.load({ chatId: params.chatId, clientData: params.clientData }, {
8750
+ ...(limit !== undefined ? { limit } : {}),
8751
+ ...(params.before !== undefined ? { before: params.before } : {}),
8752
+ });
8753
+ };
8754
+ }
8663
8755
  exports.chat = {
8664
8756
  /** Create a chat agent. See {@link chatAgent}. */
8665
8757
  agent: chatAgent,
@@ -8671,6 +8763,8 @@ exports.chat = {
8671
8763
  withClientData,
8672
8764
  /** Create a server-side helper for starting (or resuming) a Session for a chatId. See {@link createChatStartSessionAction}. */
8673
8765
  createStartSessionAction: createChatStartSessionAction,
8766
+ /** Returns a server-side helper that reads a conversation from a transcript storage. */
8767
+ createLoadTranscriptAction: createChatLoadTranscriptAction,
8674
8768
  /** Pipe a stream to the chat transport. See {@link pipeChat}. */
8675
8769
  pipe: pipeChat,
8676
8770
  /** Return from `onAction` to run a turn on the edited history. See {@link chatTurn}. */