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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/dist/commonjs/imports/ai-runtime-cjs.cjs.map +1 -1
  2. package/dist/commonjs/imports/ai-runtime.js +0 -2
  3. package/dist/commonjs/v3/ai.d.ts +16 -199
  4. package/dist/commonjs/v3/ai.js +102 -983
  5. package/dist/commonjs/v3/ai.js.map +1 -1
  6. package/dist/commonjs/v3/chat-client.d.ts +2 -3
  7. package/dist/commonjs/v3/chat-client.js +5 -31
  8. package/dist/commonjs/v3/chat-client.js.map +1 -1
  9. package/dist/commonjs/v3/chat-react.d.ts +0 -34
  10. package/dist/commonjs/v3/chat-react.js +1 -47
  11. package/dist/commonjs/v3/chat-react.js.map +1 -1
  12. package/dist/commonjs/v3/chat-server.d.ts +6 -42
  13. package/dist/commonjs/v3/chat-server.js +7 -52
  14. package/dist/commonjs/v3/chat-server.js.map +1 -1
  15. package/dist/commonjs/v3/chat.d.ts +10 -81
  16. package/dist/commonjs/v3/chat.js +46 -292
  17. package/dist/commonjs/v3/chat.js.map +1 -1
  18. package/dist/commonjs/v3/sessions.d.ts +2 -15
  19. package/dist/commonjs/v3/sessions.js +1 -12
  20. package/dist/commonjs/v3/sessions.js.map +1 -1
  21. package/dist/commonjs/v3/shared.js +36 -30
  22. package/dist/commonjs/v3/shared.js.map +1 -1
  23. package/dist/commonjs/v3/test/mock-chat-agent.d.ts +0 -43
  24. package/dist/commonjs/v3/test/mock-chat-agent.js +0 -90
  25. package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
  26. package/dist/commonjs/v3/test/test-session-handle.js +0 -6
  27. package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
  28. package/dist/commonjs/version.js +1 -1
  29. package/dist/esm/imports/ai-runtime.d.ts +2 -2
  30. package/dist/esm/imports/ai-runtime.js +2 -2
  31. package/dist/esm/imports/ai-runtime.js.map +1 -1
  32. package/dist/esm/v3/ai.d.ts +16 -199
  33. package/dist/esm/v3/ai.js +103 -984
  34. package/dist/esm/v3/ai.js.map +1 -1
  35. package/dist/esm/v3/chat-client.d.ts +2 -3
  36. package/dist/esm/v3/chat-client.js +5 -31
  37. package/dist/esm/v3/chat-client.js.map +1 -1
  38. package/dist/esm/v3/chat-react.d.ts +0 -34
  39. package/dist/esm/v3/chat-react.js +1 -46
  40. package/dist/esm/v3/chat-react.js.map +1 -1
  41. package/dist/esm/v3/chat-server.d.ts +6 -42
  42. package/dist/esm/v3/chat-server.js +8 -53
  43. package/dist/esm/v3/chat-server.js.map +1 -1
  44. package/dist/esm/v3/chat.d.ts +10 -81
  45. package/dist/esm/v3/chat.js +47 -293
  46. package/dist/esm/v3/chat.js.map +1 -1
  47. package/dist/esm/v3/sessions.d.ts +2 -15
  48. package/dist/esm/v3/sessions.js +1 -11
  49. package/dist/esm/v3/sessions.js.map +1 -1
  50. package/dist/esm/v3/shared.js +23 -17
  51. package/dist/esm/v3/shared.js.map +1 -1
  52. package/dist/esm/v3/test/mock-chat-agent.d.ts +0 -43
  53. package/dist/esm/v3/test/mock-chat-agent.js +2 -92
  54. package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
  55. package/dist/esm/v3/test/test-session-handle.js +0 -6
  56. package/dist/esm/v3/test/test-session-handle.js.map +1 -1
  57. package/dist/esm/version.js +1 -1
  58. package/docs/ai-chat/actions.mdx +23 -55
  59. package/docs/ai-chat/anatomy.mdx +3 -3
  60. package/docs/ai-chat/backend.mdx +48 -125
  61. package/docs/ai-chat/background-injection.mdx +19 -67
  62. package/docs/ai-chat/client-protocol.mdx +4 -5
  63. package/docs/ai-chat/compaction.mdx +7 -11
  64. package/docs/ai-chat/custom-agents.mdx +0 -23
  65. package/docs/ai-chat/fast-starts.mdx +20 -27
  66. package/docs/ai-chat/frontend.mdx +14 -17
  67. package/docs/ai-chat/migrating-from-a-route-handler.mdx +14 -16
  68. package/docs/ai-chat/patterns/skills.mdx +10 -7
  69. package/docs/ai-chat/patterns/version-upgrades.mdx +6 -79
  70. package/docs/ai-chat/pending-messages.mdx +3 -3
  71. package/docs/ai-chat/prompt-caching.mdx +25 -23
  72. package/docs/ai-chat/quick-start.mdx +11 -11
  73. package/docs/ai-chat/reference.mdx +5 -12
  74. package/docs/ai-chat/sessions.mdx +1 -6
  75. package/docs/ai-chat/testing.mdx +1 -2
  76. package/docs/ai-chat/tools.mdx +13 -18
  77. package/docs/ai-chat/upgrade-guide.mdx +2 -2
  78. package/docs/apikeys.mdx +45 -27
  79. package/docs/deployment/overview.mdx +8 -4
  80. package/docs/deployment/preview-branches.mdx +4 -4
  81. package/docs/deployment/version-skew-protection.mdx +0 -62
  82. package/docs/manual-setup.mdx +7 -7
  83. package/docs/mcp-tools.mdx +0 -9
  84. package/docs/quick-start.mdx +3 -3
  85. package/docs/realtime/auth.mdx +1 -1
  86. package/docs/self-hosting/security.mdx +0 -5
  87. package/docs/tasks/scheduled.mdx +0 -24
  88. package/docs/triggering.mdx +1 -1
  89. package/package.json +4 -4
  90. package/skills/trigger-authoring-chat-agent/SKILL.md +27 -38
  91. package/skills/trigger-chat-agent-advanced/SKILL.md +12 -31
  92. package/dist/commonjs/v3/chatVersionSkew.d.ts +0 -12
  93. package/dist/commonjs/v3/chatVersionSkew.js +0 -30
  94. package/dist/commonjs/v3/chatVersionSkew.js.map +0 -1
  95. package/dist/commonjs/v3/externalDeploymentId.d.ts +0 -23
  96. package/dist/commonjs/v3/externalDeploymentId.js +0 -43
  97. package/dist/commonjs/v3/externalDeploymentId.js.map +0 -1
  98. package/dist/esm/v3/chatVersionSkew.d.ts +0 -12
  99. package/dist/esm/v3/chatVersionSkew.js +0 -27
  100. package/dist/esm/v3/chatVersionSkew.js.map +0 -1
  101. package/dist/esm/v3/externalDeploymentId.d.ts +0 -23
  102. package/dist/esm/v3/externalDeploymentId.js +0 -38
  103. package/dist/esm/v3/externalDeploymentId.js.map +0 -1
  104. package/docs/ai-chat/patterns/native-compaction.mdx +0 -310
  105. package/docs/reports.mdx +0 -157
  106. package/docs/troubleshooting-zod.mdx +0 -158
@@ -1,6 +1,6 @@
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;
3
+ exports.chat = exports.upsertIncomingMessage = exports.PENDING_MESSAGE_INJECTED_TYPE = exports.ai = void 0;
4
4
  exports.__setReadChatSnapshotImplForTests = __setReadChatSnapshotImplForTests;
5
5
  exports.__setWriteChatSnapshotImplForTests = __setWriteChatSnapshotImplForTests;
6
6
  exports.__readChatSnapshotProductionPathForTests = __readChatSnapshotProductionPathForTests;
@@ -35,8 +35,6 @@ const metadata_js_1 = require("./metadata.js");
35
35
  // pulled in transitively here never reach a client chunk.
36
36
  const agentSkillsRuntime_js_1 = require("./agentSkillsRuntime.js");
37
37
  const aiAutoTelemetry_js_1 = require("./aiAutoTelemetry.js");
38
- const externalDeploymentId_js_1 = require("./externalDeploymentId.js");
39
- const chatVersionSkew_js_1 = require("./chatVersionSkew.js");
40
38
  const sessions_js_1 = require("./sessions.js");
41
39
  const shared_js_1 = require("./shared.js");
42
40
  const streams_js_1 = require("./streams.js");
@@ -1774,13 +1772,6 @@ async function installChatInputRouter(chatId, options) {
1774
1772
  checkpoint.appliedThrough = Math.max(checkpoint.appliedThrough ?? replayWindowEnd, replayWindowEnd);
1775
1773
  }
1776
1774
  }
1777
- // A boot that replayed `.in` itself has already answered everything up to
1778
- // `recoveredThrough`, so the floor has to cover it before the tail opens.
1779
- if (options?.recoveredThrough !== undefined) {
1780
- const recovered = options.recoveredThrough;
1781
- checkpoint.resumeFrom = Math.max(checkpoint.resumeFrom ?? recovered, recovered);
1782
- checkpoint.appliedThrough = Math.max(checkpoint.appliedThrough ?? checkpoint.resumeFrom, checkpoint.resumeFrom);
1783
- }
1784
1775
  const router = entry.router;
1785
1776
  router.restore(checkpoint);
1786
1777
  const floor = router.resumeFrom();
@@ -1789,10 +1780,6 @@ async function installChatInputRouter(chatId, options) {
1789
1780
  v3_1.sessionStreams.setLastDispatchedSeqNum(chatId, "in", floor);
1790
1781
  }
1791
1782
  v3_1.sessionStreams.onRecord(chatId, "in", (record) => {
1792
- // The floor is the tail's `Last-Event-ID`, but a reconnect can still
1793
- // re-deliver below it and a replayable route would re-queue it.
1794
- if (floor !== undefined && record.seqNum <= floor)
1795
- return true;
1796
1783
  router.ingest(record);
1797
1784
  return true;
1798
1785
  });
@@ -1964,29 +1951,6 @@ function spliceHandoverPartial(modelMessages, uiMessages, signal) {
1964
1951
  * @internal
1965
1952
  */
1966
1953
  const chatBackgroundQueueKey = locals_js_1.locals.create("chat.backgroundQueue");
1967
- /**
1968
- * System-role context injected mid-conversation, held for the instructions lane.
1969
- *
1970
- * Kept apart from the message queue because ai@7 rejects a system message inside
1971
- * `messages` for every provider — `standardizePrompt` throws upstream of any
1972
- * provider call, and its own advice is to use the instructions option. Instructions
1973
- * accept `Array<SystemModelMessage>`, so a system-role injection has a correct
1974
- * home: appended as another system block rather than smuggled into the transcript.
1975
- *
1976
- * This is also the only way to inject *trusted* context. A message injected as
1977
- * `user` is untrusted by construction, and a well-aligned model treats it that
1978
- * way — it will say so, and re-derive the answer from tools instead.
1979
- */
1980
- const chatInjectedInstructionsKey = locals_js_1.locals.create("chat.injectedInstructions");
1981
- /**
1982
- * What a turn already consumed from the instructions lane, so a second
1983
- * `toStreamTextOptions()` call in the same turn sees the same blocks.
1984
- *
1985
- * Consumed blocks are moved here rather than left in the pending lane: leaving
1986
- * them there means an injection made during the consumed turn sits behind them,
1987
- * and clearing the lane on the next turn destroys both.
1988
- */
1989
- const chatInstructionsConsumedKey = locals_js_1.locals.create("chat.injectedInstructionsConsumed");
1990
1954
  /**
1991
1955
  * Run-scoped pipe counter. Stored in locals so concurrent runs in the
1992
1956
  * same worker don't share state.
@@ -2497,63 +2461,18 @@ const chatToolsOptionKey = locals_js_1.locals.create("chat.toolsOption");
2497
2461
  const chatResolvedToolsKey = locals_js_1.locals.create("chat.resolvedTools");
2498
2462
  /** @internal Flag set by `chat.requestUpgrade()` to exit the loop after the current turn. */
2499
2463
  const chatUpgradeRequestedKey = locals_js_1.locals.create("chat.upgradeRequested");
2500
- /** @internal Target for the upgrade handoff, set by `chat.requestUpgrade({ externalDeploymentId })`. */
2501
- const chatUpgradeExternalDeploymentIdKey = locals_js_1.locals.create("chat.upgradeExternalDeploymentId");
2502
2464
  /**
2503
2465
  * @internal Flag set by `chat.endRun()` to exit the loop after the current
2504
2466
  * turn completes, without any upgrade semantics. Checked at the same
2505
2467
  * post-turn / pre-wait sites as `chatUpgradeRequestedKey`.
2506
2468
  */
2507
2469
  const chatEndRunRequestedKey = locals_js_1.locals.create("chat.endRunRequested");
2508
- /**
2509
- * @internal Set by `chat.close()`. Holds the close request (and its reason)
2510
- * for the rest of the run: the loop writes the terminal `session-closed`
2511
- * record, closes the session row, and exits at the same post-turn /
2512
- * pre-wait sites as `chatEndRunRequestedKey`.
2513
- */
2514
- const chatCloseRequestedKey = locals_js_1.locals.create("chat.closeRequested");
2515
- /** @internal Matches the server's `CloseSessionRequestBody.reason` cap. */
2516
- const CHAT_CLOSE_REASON_MAX_LENGTH = 256;
2517
- /** @internal Set once the session row is closed, so the close happens once. */
2518
- const chatClosePerformedKey = locals_js_1.locals.create("chat.closePerformed");
2519
- /**
2520
- * @internal Set once the terminal `.out` record is written. Tracked apart from
2521
- * {@link chatClosePerformedKey} so a retried close does not emit a second
2522
- * client-visible event.
2523
- */
2524
- const chatCloseRecordWrittenKey = locals_js_1.locals.create("chat.closeRecordWritten");
2525
2470
  /** @internal */
2526
2471
  const chatAgentCompactionKey = locals_js_1.locals.create("chat.agentCompaction");
2527
2472
  /** @internal */
2528
2473
  const chatPendingMessagesKey = locals_js_1.locals.create("chat.pendingMessages");
2529
2474
  /** @internal */
2530
2475
  const chatSteeringQueueKey = locals_js_1.locals.create("chat.steeringQueue");
2531
- /**
2532
- * This turn's new messages, as `onTurnComplete.newUIMessages` will see them.
2533
- *
2534
- * Held in locals because `drainSteeringQueue` runs outside the turn closure and
2535
- * has to append the messages it injects. Without that, an injected message
2536
- * reaches the model and the browser but no hook, so an app persisting from
2537
- * `onTurnComplete` never learns it existed.
2538
- */
2539
- const chatTurnNewUIMessagesKey = locals_js_1.locals.create("chat.turnNewUIMessages");
2540
- /**
2541
- * Steering messages a drain consumed that the model accumulator has not been
2542
- * given yet.
2543
- *
2544
- * The two accumulators are maintained separately, and the model one is
2545
- * normally advanced by appending each turn's delta. A drained message is
2546
- * appended to the UI one but reaches the model only through the `prepareStep`
2547
- * return value, which is per-step: without this the model lane never learns
2548
- * the message exists and every later turn of the run answers without it,
2549
- * while the browser, the snapshot and `chat.history.*` all still show it.
2550
- *
2551
- * Held as the messages rather than a "rebuild me" flag because the model lane
2552
- * can only be appended to, never reconstructed. Compaction replaces it with a
2553
- * summary and deliberately leaves the UI lane whole, so reconverting the UI
2554
- * lane restores every message the summary replaced.
2555
- */
2556
- const chatPendingSteerKey = locals_js_1.locals.create("chat.pendingSteer");
2557
2476
  /** @internal — IDs of messages that were successfully injected via prepareStep */
2558
2477
  const chatInjectedMessageIdsKey = locals_js_1.locals.create("chat.injectedMessageIds");
2559
2478
  /** @internal — non-transient data parts queued via chat.response or writer.write() for accumulation into the response message */
@@ -2906,32 +2825,20 @@ function chatCompactionStep(options) {
2906
2825
  return result.type === "skipped" ? undefined : result;
2907
2826
  };
2908
2827
  }
2909
- const EMPTY_DRAIN = { injected: [], claimed: [] };
2910
- /**
2911
- * The model messages to record for one claimed message. Without `prepare`
2912
- * each entry's own conversion is used. With it, `prepare` returned one list
2913
- * for the whole batch, so the first claimed message carries all of it and the
2914
- * rest carry none, which keeps the total exactly what the model received.
2915
- */
2916
- function modelFormOf(m, batch, injected) {
2917
- return batch[0] === m ? injected : [];
2918
- }
2828
+ // ---------------------------------------------------------------------------
2829
+ // Steering queue drain — shared by toStreamTextOptions, session, accumulator
2830
+ // ---------------------------------------------------------------------------
2919
2831
  /**
2920
2832
  * Drain the steering queue as a batch. Calls `shouldInject` once with all
2921
2833
  * pending messages. If it returns true, calls `prepareMessages` once to
2922
2834
  * transform the batch, then clears the queue.
2923
- * Returns the model messages to inject and the UI messages actually claimed.
2924
- *
2925
- * `claimed` is returned rather than only published to locals because each
2926
- * surface files it somewhere different: `chat.agent` has an accumulator in
2927
- * locals, while `chat.createSession` keeps its own. Publishing to locals alone
2928
- * is silently a no-op for any surface that never set the key.
2835
+ * Returns the model messages to inject (empty if none).
2929
2836
  * @internal
2930
2837
  */
2931
2838
  async function drainSteeringQueue(config, messages, steps, queueOverride) {
2932
2839
  const queue = queueOverride ?? locals_js_1.locals.get(chatSteeringQueueKey);
2933
2840
  if (!queue || queue.length === 0)
2934
- return EMPTY_DRAIN;
2841
+ return [];
2935
2842
  const ctx = locals_js_1.locals.get(chatTurnContextKey);
2936
2843
  const stepNumber = steps.length - 1;
2937
2844
  /**
@@ -2954,7 +2861,7 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
2954
2861
  // Call shouldInject once for the whole batch
2955
2862
  const shouldInject = config.shouldInject ? await config.shouldInject(batchEvent) : false;
2956
2863
  if (!shouldInject)
2957
- return EMPTY_DRAIN;
2864
+ return [];
2958
2865
  const textOfUIMessage = (m) => (m.parts ?? [])
2959
2866
  .filter((p) => p.type === "text")
2960
2867
  .map((p) => p.text)
@@ -3000,7 +2907,7 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
3000
2907
  queue.splice(at, 1);
3001
2908
  }
3002
2909
  if (claimed.length === 0)
3003
- return EMPTY_DRAIN;
2910
+ return [];
3004
2911
  /**
3005
2912
  * Give the claim back if the transform fails. `prepare` is caller code and
3006
2913
  * can throw; the records have already left the router by this point, so
@@ -3030,37 +2937,6 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
3030
2937
  for (const m of claimedUIMessages)
3031
2938
  injectedIds.add(m.id);
3032
2939
  }
3033
- // Record them as part of the conversation.
3034
- //
3035
- // The model has them and the browser has them; without this the
3036
- // accumulator does not, so they reach neither `uiMessages` nor
3037
- // `newUIMessages` on `onTurnComplete` and an app that persists from there
3038
- // silently loses the instruction the answer was shaped by. Appending here
3039
- // rather than at turn end keeps them in the order they happened: after the
3040
- // message that started the turn, before the response that answers it.
3041
- //
3042
- // De-duplicated by id because a step boundary can drain more than once per
3043
- // turn, and because a message that failed to inject falls back to becoming
3044
- // its own turn, where it is accumulated the normal way.
3045
- const currentUIMessages = locals_js_1.locals.get(chatCurrentUIMessagesKey);
3046
- const turnNew = locals_js_1.locals.get(chatTurnNewUIMessagesKey);
3047
- for (const m of claimedUIMessages) {
3048
- if (currentUIMessages && !currentUIMessages.some((existing) => existing.id === m.id)) {
3049
- currentUIMessages.push(m);
3050
- }
3051
- if (turnNew && !turnNew.some((existing) => existing.id === m.id)) {
3052
- turnNew.push(m);
3053
- }
3054
- }
3055
- if (claimedUIMessages.length > 0 && currentUIMessages) {
3056
- const pendingSteer = locals_js_1.locals.get(chatPendingSteerKey) ?? [];
3057
- for (const m of claimedUIMessages) {
3058
- if (!pendingSteer.some((existing) => existing.ui.id === m.id)) {
3059
- pendingSteer.push({ ui: m, model: modelFormOf(m, claimedUIMessages, injected) });
3060
- }
3061
- }
3062
- locals_js_1.locals.set(chatPendingSteerKey, pendingSteer);
3063
- }
3064
2940
  // Write injection confirmation chunk to the stream so the frontend
3065
2941
  // knows which messages were injected and where in the response.
3066
2942
  if (injected.length > 0) {
@@ -3102,7 +2978,7 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
3102
2978
  /* non-fatal */
3103
2979
  }
3104
2980
  }
3105
- return { injected, claimed: claimedUIMessages };
2981
+ return injected;
3106
2982
  }, {
3107
2983
  attributes: {
3108
2984
  [v3_1.SemanticInternalAttributes.STYLE_ICON]: "tabler-message-forward",
@@ -3328,149 +3204,32 @@ function buildSkillTools(skills) {
3328
3204
  return { loadSkill, readFile, bash };
3329
3205
  }
3330
3206
  /**
3331
- * A `streamText` with the agent's managed options already applied.
3332
- *
3333
- * Handed to `run()` so the managed state cannot be missed by omission. Spreading
3334
- * `chat.toStreamTextOptions()` is still supported and equivalent; this exists
3335
- * because forgetting the spread silently drops the managed prompt, the skill
3336
- * tools, telemetry, and the `prepareStep` that delivers steering, compaction and
3337
- * conversational injection.
3338
- *
3339
- * Caller options win for everything the caller owns (model, messages, signal,
3340
- * stopWhen). The three that would otherwise clobber managed behaviour are
3341
- * merged instead of replaced:
3342
- *
3343
- * - `tools` are passed into the helper, so skill tools survive.
3344
- * - `prepareStep` is composed after the managed one, so a caller's per-step
3345
- * overrides apply on top of steering and compaction instead of disabling them.
3346
- *
3347
- * `system` may be set at the call site, on `chat.agent({ system })`, or
3348
- * through `chat.prompt.set()`, but only in one of them. Setting it in two
3349
- * places throws: no shape merges two system values on every supported
3350
- * version, and dropping one silently is the failure this seam exists to
3351
- * prevent. Injected instructions append to whichever one is in play.
3352
- */
3353
- /**
3354
- * The agent-level managed options (`registry`, `system`, `cacheControl`,
3355
- * `systemProviderOptions`), published for the run so that
3356
- * `chat.toStreamTextOptions()` applies them too. Without this only the bound
3357
- * `streamText` saw them, and the documented spread form silently ran without
3358
- * the agent's system prompt or model.
3359
- */
3360
- const chatAgentManagedConfigKey = locals_js_1.locals.create("chat.agentManagedConfig");
3361
- /**
3362
- * The caller's `streamText` options merged with the agent's managed ones.
3207
+ * Returns an options object ready to spread into `streamText()`.
3208
+ *
3209
+ * Includes `system`, `experimental_telemetry`, and any config fields
3210
+ * (temperature, maxTokens, etc.) from the stored prompt.
3363
3211
  *
3364
- * Pure, and separate from the call so it can be asserted directly: everything
3365
- * the caller did not name has to survive the merge, and the way to be sure of
3366
- * that is to look at the merged object rather than at what the model received.
3212
+ * When a `registry` is provided and the prompt has a `model` string,
3213
+ * the resolved `LanguageModel` is included as `model`.
3214
+ *
3215
+ * If no prompt has been set, returns `{}` (no-op spread).
3367
3216
  */
3368
- function buildManagedStreamTextOptions(options, config) {
3369
- const { registry, system: agentSystem, cacheControl, systemProviderOptions, tools: agentTools, } = config;
3370
- /**
3371
- * Only the three keys that collide are intercepted. Everything else, telemetry
3372
- * included, stays in `rest` and reaches `streamText` untouched, with the
3373
- * caller's value winning because `rest` is spread after `managed`. Pulling a
3374
- * key out to "handle" it is how a caller's option gets silently dropped.
3375
- */
3376
- const { tools, system: callerSystem, prepareStep: callerPrepareStep, ...rest } = options;
3377
- const managed = toStreamTextOptions({
3378
- registry,
3379
- system: callerSystem ?? agentSystem,
3380
- cacheControl,
3381
- systemProviderOptions,
3382
- /**
3383
- * A call site that names `tools` replaces the agent's set rather than
3384
- * adding to it, so narrowing the tools for one call still works. Omitting
3385
- * `tools` falls back to the agent's, which is what an `onAction`
3386
- * regenerate needs: without it a regenerated answer can call nothing.
3387
- */
3388
- tools: (tools ?? agentTools),
3389
- });
3390
- const promptSystem = locals_js_1.locals.get(chatPromptKey)?.text;
3391
- /**
3392
- * Two managed sources conflict too, not only a caller against a managed one.
3393
- * `toStreamTextOptions` resolves `prompt?.text || agentSystem`, so the prompt
3394
- * would win and `chat.agent({ system })` would go nowhere.
3395
- */
3396
- if (promptSystem && agentSystem) {
3397
- throw new Error("chat.agent: `system` is set both on chat.agent({ system }) and by chat.prompt.set(), and only one " +
3398
- "of them can apply. The prompt would win and the agent's `system` would go nowhere. Keep it in one " +
3399
- "place, and add per-turn context with chat.inject({ role: 'system' }).");
3400
- }
3401
- const managedSystem = promptSystem || agentSystem;
3402
- if (callerSystem !== undefined && managedSystem) {
3403
- throw new Error("chat.agent: `system` is already set " +
3404
- (promptSystem ? "by chat.prompt.set()" : "on chat.agent({ system })") +
3405
- ", so it cannot also be passed to the `streamText` given to run(). Set it in one place, and add " +
3406
- "per-turn context with chat.inject({ role: 'system' }) rather than a second system value.");
3407
- }
3408
- const managedPrepareStep = managed.prepareStep;
3409
- if (typeof callerPrepareStep === "function") {
3410
- managed.prepareStep = async (arg) => {
3411
- const first = managedPrepareStep ? await managedPrepareStep(arg) : undefined;
3412
- const second = await callerPrepareStep({ ...arg, ...(first ?? {}) });
3413
- return { ...(first ?? {}), ...(second ?? {}) };
3414
- };
3415
- }
3416
- return { ...managed, ...rest };
3417
- }
3418
- /** @internal Test hook for {@link buildManagedStreamTextOptions}. */
3419
- exports.__buildManagedStreamTextOptionsForTests = buildManagedStreamTextOptions;
3420
- function createBoundStreamText(registry, agentSystem, agentCacheControl, agentSystemProviderOptions) {
3421
- const bound = (options = {}) => (0, ai_runtime_js_1.streamText)(buildManagedStreamTextOptions(options, {
3422
- registry,
3423
- system: agentSystem,
3424
- cacheControl: agentCacheControl,
3425
- systemProviderOptions: agentSystemProviderOptions,
3426
- /** Read per call, so per-turn tools resolved after binding are included. */
3427
- tools: locals_js_1.locals.get(chatResolvedToolsKey),
3428
- }));
3429
- return bound;
3430
- }
3431
3217
  function toStreamTextOptions(options) {
3432
- const agentDefaults = locals_js_1.locals.get(chatAgentManagedConfigKey);
3433
- if (agentDefaults) {
3434
- options = {
3435
- registry: agentDefaults.registry,
3436
- system: agentDefaults.system,
3437
- cacheControl: agentDefaults.cacheControl,
3438
- systemProviderOptions: agentDefaults.systemProviderOptions,
3439
- ...options,
3440
- };
3441
- }
3442
3218
  const prompt = locals_js_1.locals.get(chatPromptKey);
3443
3219
  const skills = locals_js_1.locals.get(chatSkillsKey);
3444
3220
  const result = {};
3445
3221
  // Build the combined system prompt: stored prompt + skills preamble.
3446
- const baseSystem = options?.system;
3447
- const baseSystemText = typeof baseSystem === "string"
3448
- ? baseSystem
3449
- : typeof baseSystem?.content === "string"
3450
- ? baseSystem.content
3451
- : "";
3452
- const promptText = prompt?.text || baseSystemText;
3222
+ const promptText = prompt?.text ?? "";
3453
3223
  const skillsText = skills && skills.length > 0 ? buildSkillsSystemPrompt(skills) : "";
3454
3224
  if (promptText || skillsText) {
3455
3225
  const systemText = [promptText, skillsText].filter(Boolean).join("\n\n");
3456
- /**
3457
- * Resolve system-prompt provider options for caching. Precedence, most
3458
- * specific first and no deep merge: explicit `systemProviderOptions`, the
3459
- * `cacheControl` sugar, the ones carried on a structured `system` message,
3460
- * then whatever `chat.prompt.set()` stored.
3461
- *
3462
- * A structured `system` counts only when its own text is the one being
3463
- * sent. When `chat.prompt.set()` supplied the text, its provider options
3464
- * are the ones that describe it.
3465
- */
3466
- const baseSystemProviderOptions = !prompt?.text && baseSystem && typeof baseSystem !== "string"
3467
- ? baseSystem.providerOptions
3468
- : undefined;
3226
+ // Resolve system-prompt provider options for caching. Precedence (most
3227
+ // specific wins, no deep merge): explicit `systemProviderOptions` →
3228
+ // `cacheControl` sugar → `providerOptions` stored on `chat.prompt.set()`.
3469
3229
  const systemProviderOptions = options?.systemProviderOptions ??
3470
3230
  (options?.cacheControl
3471
3231
  ? { anthropic: { cacheControl: options.cacheControl } }
3472
3232
  : undefined) ??
3473
- baseSystemProviderOptions ??
3474
3233
  locals_js_1.locals.get(chatPromptProviderOptionsKey);
3475
3234
  // A bare string stays a bare string (the unchanged default). With provider
3476
3235
  // options, emit a structured `SystemModelMessage` so the provider can cache
@@ -3479,88 +3238,6 @@ function toStreamTextOptions(options) {
3479
3238
  ? { role: "system", content: systemText, providerOptions: systemProviderOptions }
3480
3239
  : systemText;
3481
3240
  }
3482
- /**
3483
- * Append anything injected as system context, in whichever shape the installed
3484
- * AI SDK accepts.
3485
- *
3486
- * `system` widened over time: on ai@5 it is `string` only, and from ai@6 it is
3487
- * `string | SystemModelMessage | Array<SystemModelMessage>`. This package's peer
3488
- * range still spans all three, so emitting an array unconditionally would break
3489
- * v5 consumers — for whom a system-role injection used to work, since v5 accepted
3490
- * a system message inside `messages` that v7 rejects.
3491
- *
3492
- * So: concatenate into one string when the base is a plain string, which every
3493
- * version accepts and which loses nothing (separate blocks only matter for
3494
- * per-block `providerOptions`). Use the array form only when the base is already
3495
- * a structured message — that path requires v6+ regardless, because it is how
3496
- * prompt caching marks the system block, and flattening it would silently throw
3497
- * the cache away.
3498
- *
3499
- * Either way the injected text goes last: the base prompt keeps its position for
3500
- * caching, and the addition reads as a later amendment. A changed prefix does
3501
- * cost the first call its cache hit, on turns that actually injected.
3502
- */
3503
- /**
3504
- * Consumed once per turn, not once per read, and moved out of the lane rather
3505
- * than marked read in place.
3506
- *
3507
- * Per turn, because a `run()` that builds options twice (a cheap classifier
3508
- * pass and then the answer) has to see the injection in both, and draining on
3509
- * read hands it to whichever call ran first. Moved out, because blocks left in
3510
- * the lane sit in front of anything injected during the same turn, and
3511
- * clearing the lane on the next turn then destroys both. Outside a turn there
3512
- * is no turn to scope the stash to, so the lane drains on read there.
3513
- */
3514
- const injectedInstructions = locals_js_1.locals.get(chatInjectedInstructionsKey);
3515
- const currentTurn = locals_js_1.locals.get(chatTurnContextKey)?.turn;
3516
- const consumedThisTurn = currentTurn === undefined ? undefined : locals_js_1.locals.get(chatInstructionsConsumedKey);
3517
- let injectedBlocks = [];
3518
- if (consumedThisTurn && consumedThisTurn.turn === currentTurn) {
3519
- injectedBlocks = consumedThisTurn.blocks;
3520
- // Anything injected since the stash was taken joins it, so an instruction
3521
- // added after an action read the lane still reaches the real turn that
3522
- // shares the action's turn number, rather than the one after.
3523
- if (injectedInstructions && injectedInstructions.length > 0) {
3524
- injectedBlocks.push(...injectedInstructions.splice(0));
3525
- }
3526
- }
3527
- else if (injectedInstructions && injectedInstructions.length > 0) {
3528
- injectedBlocks = injectedInstructions.splice(0);
3529
- if (currentTurn !== undefined) {
3530
- locals_js_1.locals.set(chatInstructionsConsumedKey, { turn: currentTurn, blocks: injectedBlocks });
3531
- }
3532
- }
3533
- if (injectedBlocks.length > 0) {
3534
- const blocks = injectedBlocks;
3535
- const injectedText = blocks
3536
- .map((block) => (typeof block.content === "string" ? block.content : ""))
3537
- .filter(Boolean)
3538
- .join("\n\n");
3539
- const base = result.system;
3540
- if (base === undefined) {
3541
- result.system = injectedText;
3542
- }
3543
- else if (typeof base === "string") {
3544
- result.system = [base, injectedText].filter(Boolean).join("\n\n");
3545
- }
3546
- else {
3547
- // Merged into the existing block rather than added as a second one. An array
3548
- // of system blocks would keep the base block's cache entry, but ai@5 rejects
3549
- // it outright ("Invalid prompt: system must be a string") while accepting a
3550
- // single structured block, and this package's peer range still spans v5.
3551
- // Choosing per version would mean resolving the installed version at runtime,
3552
- // which is not something to build on: `import.meta.url` is illegal in this
3553
- // package's CommonJS output, and a bundled task may have no resolvable `ai`
3554
- // to read. One shape that works everywhere beats a cache hit.
3555
- const baseBlock = base;
3556
- result.system = {
3557
- ...baseBlock,
3558
- content: [typeof baseBlock.content === "string" ? baseBlock.content : "", injectedText]
3559
- .filter(Boolean)
3560
- .join("\n\n"),
3561
- };
3562
- }
3563
- }
3564
3241
  // Prompt-related options (only if chat.prompt.set() was called)
3565
3242
  if (prompt) {
3566
3243
  // Resolve model via registry if both are present
@@ -3615,7 +3292,7 @@ function toStreamTextOptions(options) {
3615
3292
  }
3616
3293
  // 2. Pending message injection (steering)
3617
3294
  if (taskPendingMessages) {
3618
- const { injected } = await drainSteeringQueue(taskPendingMessages, resultMessages ?? messages, steps);
3295
+ const injected = await drainSteeringQueue(taskPendingMessages, resultMessages ?? messages, steps);
3619
3296
  if (injected.length > 0) {
3620
3297
  resultMessages = [...(resultMessages ?? messages), ...injected];
3621
3298
  }
@@ -3631,61 +3308,6 @@ function toStreamTextOptions(options) {
3631
3308
  }
3632
3309
  return result;
3633
3310
  }
3634
- const actionTurnBrand = Symbol.for("trigger.dev/chat/actionTurn");
3635
- /**
3636
- * Turn the current action into a turn.
3637
- *
3638
- * Return it from `onAction` after editing history. The action's own work is
3639
- * finished first (the edit is applied and snapshotted), then a turn runs on the
3640
- * result exactly as a message turn does: `onTurnStart`, `run()` with the edited
3641
- * history, `onBeforeTurnComplete`, `onTurnComplete`, and the turn counter
3642
- * advances. That gives the answer everything a turn has, the system prompt,
3643
- * tools, steering, compaction, injected instructions and persistence, with no
3644
- * action-specific handling.
3645
- *
3646
- * @example
3647
- * ```ts
3648
- * onAction: async ({ action }) => {
3649
- * if (action.type === "regenerate") {
3650
- * chat.history.slice(0, -1);
3651
- * return chat.turn();
3652
- * }
3653
- * if (action.type === "undo") chat.history.slice(0, -2); // no turn
3654
- * },
3655
- * ```
3656
- */
3657
- function chatTurn() {
3658
- return { [actionTurnBrand]: true };
3659
- }
3660
- function isActionTurn(value) {
3661
- return typeof value === "object" && value !== null && value[actionTurnBrand] === true;
3662
- }
3663
- /**
3664
- * Replace, in a model lane, the run of messages one UI message contributed.
3665
- *
3666
- * Used when a UI message is replaced in place (a tool-approval continuation
3667
- * merging onto the trailing assistant, a captured response reusing an existing
3668
- * id, a partial replacing an existing message). Reconverting the whole lane
3669
- * from the UI lane would also replace a compaction summary with the full
3670
- * transcript and drop the model forms `pendingMessages.prepare` produced.
3671
- *
3672
- * The replaced message is the trailing one, so its run is the lane's tail,
3673
- * before any steer forms appended after it this turn (`tailAfter`). If the
3674
- * tail does not match the old message's conversion, nothing is changed and
3675
- * `false` is returned so the caller can fall back to a full reconversion.
3676
- */
3677
- async function replaceModelRun(lane, oldUi, newUi, tailAfter) {
3678
- const oldRun = await toModelMessages([stripProviderMetadata(oldUi)]);
3679
- const newRun = await toModelMessages([stripProviderMetadata(newUi)]);
3680
- const end = lane.length - tailAfter;
3681
- const start = end - oldRun.length;
3682
- if (start < 0 || end > lane.length)
3683
- return false;
3684
- if (JSON.stringify(lane.slice(start, end)) !== JSON.stringify(oldRun))
3685
- return false;
3686
- lane.splice(start, oldRun.length, ...newRun);
3687
- return true;
3688
- }
3689
3311
  function isUIMessageStreamable(value) {
3690
3312
  return (typeof value === "object" &&
3691
3313
  value !== null &&
@@ -3824,22 +3446,10 @@ function chatCustomAgent(options) {
3824
3446
  await installChatInputRouter(payload.chatId, {
3825
3447
  resuming: Boolean(payload.continuation),
3826
3448
  });
3827
- // A custom agent's loop is the customer's, so there is no exit site the
3828
- // SDK controls. Perform a requested close when `run()` returns, whatever
3829
- // shape the loop had. Idempotent, so the createSession iterator having
3830
- // already closed on its own exit costs nothing.
3831
- const withClose = async (result) => {
3832
- try {
3833
- return await result;
3834
- }
3835
- finally {
3836
- await performChatClose();
3837
- }
3838
- };
3839
3449
  // Keep the schema-free path identical to the original custom-agent
3840
3450
  // wrapper, including when userRun starts executing.
3841
3451
  if (!parseClientData) {
3842
- return withClose(userRun(payload, runOptions));
3452
+ return userRun(payload, runOptions);
3843
3453
  }
3844
3454
  const isHandoverBoot = payload.trigger === "handover-prepare";
3845
3455
  const isMessagelessBoot = payload.trigger === "preload" ||
@@ -3855,7 +3465,7 @@ function chatCustomAgent(options) {
3855
3465
  writeErrorToStream: !isMessagelessBoot && !isHandoverBoot,
3856
3466
  });
3857
3467
  if (validated.ok) {
3858
- return withClose(userRun(validated.payload, runOptions));
3468
+ return userRun(validated.payload, runOptions);
3859
3469
  }
3860
3470
  if (isHandoverBoot) {
3861
3471
  const signal = await waitForHandover({
@@ -3891,7 +3501,7 @@ function chatCustomAgent(options) {
3891
3501
  sessionId: next.output.sessionId ?? payload.sessionId,
3892
3502
  idleTimeoutInSeconds: next.output.idleTimeoutInSeconds ?? payload.idleTimeoutInSeconds,
3893
3503
  };
3894
- return withClose(userRun(recoveredPayload, runOptions));
3504
+ return userRun(recoveredPayload, runOptions);
3895
3505
  },
3896
3506
  });
3897
3507
  // Register clientDataSchema so the CLI converts it to JSONSchema
@@ -3904,7 +3514,7 @@ function chatCustomAgent(options) {
3904
3514
  return task;
3905
3515
  }
3906
3516
  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;
3517
+ 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, ...restOptions } = options;
3908
3518
  const parseClientData = clientDataSchema ? (0, v3_1.getSchemaParseFn)(clientDataSchema) : undefined;
3909
3519
  const parseAction = actionSchema ? (0, v3_1.getSchemaParseFn)(actionSchema) : undefined;
3910
3520
  // chat.agent does not expose generic retry options (see docstring on
@@ -3987,24 +3597,6 @@ function chatAgent(options) {
3987
3597
  // durable snapshot + `session.out` replay (or `hydrateMessages` if
3988
3598
  // registered) — the wire is delta-only now, no longer a seed.
3989
3599
  let accumulatedMessages = [];
3990
- /**
3991
- * Give the model accumulator the steering messages a drain consumed,
3992
- * in the form the model actually received. Appended, never reconverted
3993
- * from the UI lane, so a model-only compaction summary survives. Called
3994
- * on both the success and the error path, before the response or the
3995
- * partial joins the lane, so the order stays steer-then-answer.
3996
- */
3997
- const reconcilePendingSteer = (options) => {
3998
- const pending = locals_js_1.locals.get(chatPendingSteerKey);
3999
- if (!pending || pending.length === 0)
4000
- return [];
4001
- locals_js_1.locals.set(chatPendingSteerKey, []);
4002
- for (const entry of pending) {
4003
- accumulatedMessages.push(...entry.model);
4004
- options?.turnNew?.push(...entry.model);
4005
- }
4006
- return pending;
4007
- };
4008
3600
  // Accumulated UI messages for persistence. Mirrors the model accumulator
4009
3601
  // but in frontend-friendly UIMessage format (with parts, id, etc.).
4010
3602
  let accumulatedUIMessages = [];
@@ -4023,58 +3615,6 @@ function chatAgent(options) {
4023
3615
  // swallow errors internally; the agent stays available either way.
4024
3616
  const sessionIdForSnapshot = payload.sessionId ?? payload.chatId;
4025
3617
  let bootSnapshot;
4026
- /**
4027
- * The `lastOutEventId` the most recent snapshot carried.
4028
- *
4029
- * A snapshot written outside a turn — after an action mutates history — has
4030
- * no turn cursor of its own, and writing `undefined` there would drop the
4031
- * resume point and make the next boot replay from further back. Retaining it
4032
- * keeps an action's write cursor-neutral.
4033
- */
4034
- let lastSnapshotOutEventId;
4035
- /**
4036
- * Persist the accumulator outside a turn.
4037
- *
4038
- * 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.
4043
- *
4044
- * Cursor-neutral: an action has no turn cursor of its own, and writing
4045
- * `undefined` would drop the resume point the last turn established and make
4046
- * the next boot replay from further back.
4047
- */
4048
- const writeSnapshotOutsideTurn = async (reason) => {
4049
- if (hydrateMessages)
4050
- return;
4051
- try {
4052
- 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(),
4057
- messages: accumulatedUIMessages,
4058
- lastOutEventId: lastSnapshotOutEventId,
4059
- lastInEventId: snapshotInCursor !== undefined ? String(snapshotInCursor) : undefined,
4060
- });
4061
- }, {
4062
- attributes: {
4063
- [v3_1.SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart",
4064
- [v3_1.SemanticInternalAttributes.COLLAPSED]: true,
4065
- "chat.snapshot.reason": reason,
4066
- "chat.messages.count": accumulatedUIMessages.length,
4067
- },
4068
- });
4069
- }
4070
- catch (error) {
4071
- v3_1.logger.warn("chat.agent: snapshot write outside a turn failed; the change may not survive a continuation", {
4072
- error: error instanceof Error ? error.message : String(error),
4073
- sessionId: sessionIdForSnapshot,
4074
- reason,
4075
- });
4076
- }
4077
- };
4078
3618
  let replayedSettled = [];
4079
3619
  let replayedPartial;
4080
3620
  let replayedPartialRaw;
@@ -4117,7 +3657,6 @@ function chatAgent(options) {
4117
3657
  // Without seeding, the new worker would emit no trim on its first
4118
3658
  // turn (chain self-bootstraps from turn 2), so this is purely an
4119
3659
  // optimization to keep continuation runs bounded from the first turn.
4120
- lastSnapshotOutEventId = bootSnapshot?.lastOutEventId;
4121
3660
  if (bootSnapshot?.lastOutEventId !== undefined) {
4122
3661
  const seeded = Number.parseInt(bootSnapshot.lastOutEventId, 10);
4123
3662
  if (Number.isFinite(seeded)) {
@@ -4212,13 +3751,9 @@ function chatAgent(options) {
4212
3751
  // Reads the turn boundary and subscribes in one call. `bootInCursor` is
4213
3752
  // only a fallback: the boot block above may already have resolved a
4214
3753
  // cursor from the snapshot, which is used when the boundary itself
4215
- // carries none. Everything the boot replayed off `.in` is dispatched from
4216
- // `bootInjectedQueue` below, so it goes into the floor here — folded in
4217
- // after the subscription opens, the live tail re-delivers it as a turn.
4218
- const lastRecoveredInSeq = replayedInTail.length > 0 ? replayedInTail[replayedInTail.length - 1].seqNum : undefined;
3754
+ // carries none.
4219
3755
  await installChatInputRouter(payload.chatId, {
4220
3756
  fallbackResumeFrom: bootInCursorResolved ? bootInCursor : undefined,
4221
- recoveredThrough: lastRecoveredInSeq,
4222
3757
  resuming: Boolean(payload.continuation) || ctx.attempt.number > 1,
4223
3758
  });
4224
3759
  // ── Recovery boot + chain reconstruction ────────────────────────
@@ -4326,6 +3861,16 @@ function chatAgent(options) {
4326
3861
  if (hookBeforeBoot) {
4327
3862
  await hookBeforeBoot();
4328
3863
  }
3864
+ // Advance the session.in cursor past every recovered user so
3865
+ // the live subscription doesn't re-deliver them.
3866
+ if (replayedInTail.length > 0) {
3867
+ const lastRecoveredSeq = replayedInTail[replayedInTail.length - 1].seqNum;
3868
+ const currentCursor = v3_1.sessionStreams.lastSeqNum(payload.chatId, "in");
3869
+ if (currentCursor === undefined || lastRecoveredSeq > currentCursor) {
3870
+ v3_1.sessionStreams.setLastSeqNum(payload.chatId, "in", lastRecoveredSeq);
3871
+ v3_1.sessionStreams.setLastDispatchedSeqNum(payload.chatId, "in", lastRecoveredSeq);
3872
+ }
3873
+ }
4329
3874
  // Synthesize wire payloads for each recoveredTurn. The turn-loop
4330
3875
  // pops these ahead of `messagesInput.waitWithIdleTimeout` so they
4331
3876
  // dispatch as normal turns with the existing hook stack.
@@ -4418,12 +3963,6 @@ function chatAgent(options) {
4418
3963
  // before any hook (`onChatStart`, `onTurnStart`, etc.) fires.
4419
3964
  locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
4420
3965
  }
4421
- locals_js_1.locals.set(chatAgentManagedConfigKey, {
4422
- registry: promptRegistry,
4423
- system: agentSystem,
4424
- cacheControl: agentCacheControl,
4425
- systemProviderOptions: agentSystemProviderOptions,
4426
- });
4427
3966
  // Token usage tracking across turns
4428
3967
  let previousTurnUsage;
4429
3968
  let cumulativeUsage = emptyUsage();
@@ -4919,10 +4458,6 @@ function chatAgent(options) {
4919
4458
  // Track new messages for this turn (user input + assistant response).
4920
4459
  const turnNewModelMessages = [];
4921
4460
  const turnNewUIMessages = [];
4922
- locals_js_1.locals.set(chatTurnNewUIMessagesKey, turnNewUIMessages);
4923
- // A head-start handover deliberately resumes from an assistant
4924
- // message it spliced in, so it isn't a no-op turn.
4925
- let splicedHandoverPartial = false;
4926
4461
  // ── Action handling ──────────────────────────────────────
4927
4462
  // Actions arrive on the same input stream but with
4928
4463
  // trigger === "action". They are NOT turns — only
@@ -4933,16 +4468,7 @@ function chatAgent(options) {
4933
4468
  // an action, return a `StreamTextResult` (auto-piped),
4934
4469
  // string, or UIMessage from `onAction`. Turn counter
4935
4470
  // does not advance.
4936
- let actionResult = undefined;
4937
- /** Set when `onAction` returned `chat.turn()`: the turn block below runs. */
4938
- let actionTurn = false;
4939
- /**
4940
- * Whether this action changed the conversation, by rolling history
4941
- * back or by streaming a response. Drives the single snapshot write
4942
- * at the end — an action never reaches the turn-complete path that
4943
- * normally does it.
4944
- */
4945
- let actionChangedHistory = false;
4471
+ let actionStreamResult = undefined;
4946
4472
  if (isAction) {
4947
4473
  // Parse and validate the action payload
4948
4474
  const parsedAction = parseAction
@@ -4976,7 +4502,7 @@ function chatAgent(options) {
4976
4502
  // Fire onAction — handler may mutate state via
4977
4503
  // `chat.history.*` and / or return a model response.
4978
4504
  if (onAction) {
4979
- actionResult = await tracer_js_1.tracer.startActiveSpan("onAction()", async () => {
4505
+ actionStreamResult = await tracer_js_1.tracer.startActiveSpan("onAction()", async () => {
4980
4506
  return await onAction({
4981
4507
  action: parsedAction,
4982
4508
  chatId: currentWirePayload.chatId,
@@ -5002,7 +4528,6 @@ function chatAgent(options) {
5002
4528
  accumulatedUIMessages = [...actionOverride];
5003
4529
  accumulatedMessages = await toModelMessages(actionOverride);
5004
4530
  locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
5005
- actionChangedHistory = true;
5006
4531
  }
5007
4532
  }
5008
4533
  else {
@@ -5171,7 +4696,6 @@ function chatAgent(options) {
5171
4696
  // where AI SDK regenerates the id (TRI-9137) still
5172
4697
  // applies via `rewriteIncomingIdViaToolCallMap`.
5173
4698
  let replaced = false;
5174
- const replacedPairs = [];
5175
4699
  for (const raw of cleanedUIMessages) {
5176
4700
  let incoming = raw;
5177
4701
  let idx = accumulatedUIMessages.findIndex((m) => m.id === incoming.id);
@@ -5183,9 +4707,7 @@ function chatAgent(options) {
5183
4707
  }
5184
4708
  }
5185
4709
  if (idx !== -1) {
5186
- const previous = accumulatedUIMessages[idx];
5187
- accumulatedUIMessages[idx] = mergeIncomingIntoHydrated(previous, incoming);
5188
- replacedPairs.push({ previous, merged: accumulatedUIMessages[idx] });
4710
+ accumulatedUIMessages[idx] = mergeIncomingIntoHydrated(accumulatedUIMessages[idx], incoming);
5189
4711
  replaced = true;
5190
4712
  }
5191
4713
  else {
@@ -5195,17 +4717,9 @@ function chatAgent(options) {
5195
4717
  recordToolCallIdsFromMessage(incoming);
5196
4718
  }
5197
4719
  if (replaced) {
5198
- let inPlace = true;
5199
- for (const { previous, merged } of replacedPairs) {
5200
- if (!(await replaceModelRun(accumulatedMessages, previous, merged, 0))) {
5201
- inPlace = false;
5202
- break;
5203
- }
5204
- }
5205
- if (!inPlace) {
5206
- v3_1.logger.warn("chat.agent: replaced message not found at the model lane tail; reconverting the lane");
5207
- accumulatedMessages = await toModelMessages(accumulatedUIMessages);
5208
- }
4720
+ // Replacement changes structure — reconvert all model
4721
+ // messages instead of appending.
4722
+ accumulatedMessages = await toModelMessages(accumulatedUIMessages);
5209
4723
  }
5210
4724
  else {
5211
4725
  const incomingModelMessages = await toModelMessages(cleanedUIMessages);
@@ -5248,33 +4762,10 @@ function chatAgent(options) {
5248
4762
  messageId: locals_js_1.locals.get(chatHandoverMessageIdKey),
5249
4763
  });
5250
4764
  locals_js_1.locals.set(chatHandoverPartialKey, []); // consume once
5251
- splicedHandoverPartial = true;
5252
4765
  }
5253
4766
  }
5254
4767
  locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
5255
4768
  } // end if (trigger !== "action")
5256
- // ── No-op turn ──────────────────────────────────────────
5257
- //
5258
- // A submit that added no new user message and leaves the model
5259
- // chain ending on an assistant message has nothing to answer —
5260
- // calling the model would prefill its own last reply. Keyed on
5261
- // the model tail, so a `tool`-terminated chain (a merged tool
5262
- // approval) still runs.
5263
- const isNoOpTurn = !isAction &&
5264
- !splicedHandoverPartial &&
5265
- currentWirePayload.trigger === "submit-message" &&
5266
- turnNewUIMessages.length === 0 &&
5267
- accumulatedMessages[accumulatedMessages.length - 1]?.role === "assistant";
5268
- if (isNoOpTurn) {
5269
- msgSub?.off();
5270
- v3_1.logger.warn("chat.agent: turn added no new user message; skipping the model", {
5271
- chatId: currentWirePayload.chatId,
5272
- messageId: currentWirePayload.messageId,
5273
- });
5274
- await writeTurnCompleteChunk(currentWirePayload.chatId);
5275
- // Not a turn — don't consume an iteration.
5276
- turn--;
5277
- }
5278
4769
  // ── Action result handling ──────────────────────────────
5279
4770
  // For action turns, skip the turn machinery entirely.
5280
4771
  // If `onAction` returned a stream / string / UIMessage,
@@ -5284,34 +4775,34 @@ function chatAgent(options) {
5284
4775
  // The turn counter is decremented so the next iteration
5285
4776
  // sees the same `turn` value — actions don't count.
5286
4777
  if (isAction) {
5287
- if (isActionTurn(actionResult)) {
5288
- // Persist the edit before the turn starts, so a turn that is
5289
- // cancelled or runs out of memory continues from the edited
5290
- // history rather than from the snapshot the edit replaced.
5291
- // The turn then does its own hooks, completion and snapshot.
5292
- if (actionChangedHistory) {
5293
- await writeSnapshotOutsideTurn("action");
4778
+ msgSub?.off();
4779
+ if ((locals_js_1.locals.get(chatPipeCountKey) ?? 0) === 0 &&
4780
+ isUIMessageStreamable(actionStreamResult)) {
4781
+ try {
4782
+ const resolvedOptions = resolveUIMessageStreamOptions();
4783
+ const uiStream = actionStreamResult.toUIMessageStream({
4784
+ ...resolvedOptions,
4785
+ generateMessageId: resolvedOptions.generateMessageId ?? ai_runtime_js_1.generateId,
4786
+ });
4787
+ await pipeChat(uiStream, {
4788
+ signal: combinedSignal,
4789
+ spanName: "stream response",
4790
+ });
5294
4791
  }
5295
- actionTurn = true;
5296
- }
5297
- else if (actionResult !== undefined) {
5298
- throw new Error("chat.agent: onAction returned a value. An action is a state edit; to answer " +
5299
- "after the edit, return chat.turn() and a turn runs on the edited history. " +
5300
- "Returning a StreamTextResult, string or UIMessage is no longer supported.");
5301
- }
5302
- else {
5303
- msgSub?.off();
5304
- if (actionChangedHistory) {
5305
- await writeSnapshotOutsideTurn("action");
4792
+ catch (error) {
4793
+ if (error instanceof Error &&
4794
+ error.name === "AbortError" &&
4795
+ runSignal.aborted) {
4796
+ return "exit";
4797
+ }
4798
+ throw error;
5306
4799
  }
5307
- await writeTurnCompleteChunk(currentWirePayload.chatId);
5308
- // Don't consume a turn iteration — actions aren't turns.
5309
- turn--;
5310
4800
  }
4801
+ await writeTurnCompleteChunk(currentWirePayload.chatId);
4802
+ // Don't consume a turn iteration — actions aren't turns.
4803
+ turn--;
5311
4804
  }
5312
- // A no-op turn skips this block, and with it `followSessionPin`:
5313
- // there is nothing to answer, so nothing to hand over.
5314
- if ((!isAction || actionTurn) && !isNoOpTurn) {
4805
+ if (!isAction) {
5315
4806
  // Mint a scoped public access token once per turn, reused for
5316
4807
  // onChatStart, onTurnStart, onTurnComplete, and the turn-complete chunk.
5317
4808
  const currentRunId = ctx.run.id;
@@ -5416,11 +4907,9 @@ function chatAgent(options) {
5416
4907
  },
5417
4908
  });
5418
4909
  }
5419
- await followSessionPin(currentWirePayload.chatId, versionSkew);
5420
4910
  // chat.requestUpgrade() called in onTurnStart (or onValidateMessages) —
5421
- // skip run() and hand over to a fresh run on the new version. The
5422
- // successor picks the message up off session.in; the transport only
5423
- // keeps reading.
4911
+ // skip run() and signal the transport to re-trigger the same message
4912
+ // on the new version.
5424
4913
  if (locals_js_1.locals.get(chatUpgradeRequestedKey)) {
5425
4914
  await writeUpgradeRequiredChunk();
5426
4915
  return "exit";
@@ -5479,9 +4968,6 @@ function chatAgent(options) {
5479
4968
  const preparedMessages = await applyPrepareMessages(accumulatedMessages, "run");
5480
4969
  runResult = await userRun({
5481
4970
  ...restWire,
5482
- // A turn requested by chat.turn() is not the action itself:
5483
- // a run() that short-circuits on "action" must still answer.
5484
- ...(actionTurn ? { trigger: "action-turn" } : {}),
5485
4971
  messages: preparedMessages,
5486
4972
  clientData,
5487
4973
  continuation,
@@ -5494,7 +4980,6 @@ function chatAgent(options) {
5494
4980
  signal: combinedSignal,
5495
4981
  cancelSignal,
5496
4982
  stopSignal,
5497
- streamText: createBoundStreamText(promptRegistry, agentSystem, agentCacheControl, agentSystemProviderOptions),
5498
4983
  });
5499
4984
  }
5500
4985
  // Auto-pipe if the run function returned a StreamTextResult or similar,
@@ -5608,19 +5093,7 @@ function chatAgent(options) {
5608
5093
  if (runOverride) {
5609
5094
  locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
5610
5095
  accumulatedUIMessages = [...runOverride];
5611
- /**
5612
- * Steers the drain consumed are left out of the rebuild and
5613
- * appended by the reconciliation below instead, so the lane
5614
- * gets the form the model actually received rather than a
5615
- * reconversion of the UI message, and gets it once. A steer
5616
- * the edit removed is dropped from the pending list too, so
5617
- * the edit is honoured.
5618
- */
5619
- const overrideIds = new Set(runOverride.map((m) => m.id));
5620
- const pending = (locals_js_1.locals.get(chatPendingSteerKey) ?? []).filter((e) => overrideIds.has(e.ui.id));
5621
- locals_js_1.locals.set(chatPendingSteerKey, pending);
5622
- const pendingIds = new Set(pending.map((e) => e.ui.id));
5623
- accumulatedMessages = await toModelMessages(runOverride.filter((m) => !pendingIds.has(m.id)));
5096
+ accumulatedMessages = await toModelMessages(runOverride);
5624
5097
  locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
5625
5098
  }
5626
5099
  // Check if compaction set a model-only override (preserves UI messages).
@@ -5650,15 +5123,6 @@ function chatAgent(options) {
5650
5123
  }
5651
5124
  // Determine if the user stopped generation this turn (not a full run cancel).
5652
5125
  const wasStopped = stopController.signal.aborted && !runSignal.aborted;
5653
- // Give the model accumulator the steering messages the drain
5654
- // consumed. Appended, never reconverted from the UI lane, so a
5655
- // model-only compaction summary set just above survives; and done
5656
- // before the response is appended so the order stays
5657
- // steer-then-answer. Outside the `capturedResponseMessage`
5658
- // branches below, so a turn that captured no response is covered.
5659
- const steerTailThisTurn = reconcilePendingSteer({
5660
- turnNew: turnNewModelMessages,
5661
- }).reduce((n, e) => n + e.model.length, 0);
5662
5126
  // Append the assistant's response (partial or complete) to the accumulator.
5663
5127
  // The onFinish callback fires even on abort/stop, so partial responses
5664
5128
  // from stopped generation are captured correctly.
@@ -5696,7 +5160,6 @@ function chatAgent(options) {
5696
5160
  const existingIdx = capturedResponseMessage.id
5697
5161
  ? accumulatedUIMessages.findIndex((m) => m.id === capturedResponseMessage.id)
5698
5162
  : -1;
5699
- const previousAtIdx = existingIdx !== -1 ? accumulatedUIMessages[existingIdx] : undefined;
5700
5163
  if (existingIdx !== -1) {
5701
5164
  accumulatedUIMessages[existingIdx] = capturedResponseMessage;
5702
5165
  }
@@ -5716,12 +5179,8 @@ function chatAgent(options) {
5716
5179
  stripProviderMetadata(capturedResponseMessage),
5717
5180
  ]);
5718
5181
  if (existingIdx !== -1) {
5719
- const ok = previousAtIdx !== undefined &&
5720
- (await replaceModelRun(accumulatedMessages, previousAtIdx, capturedResponseMessage, steerTailThisTurn));
5721
- if (!ok) {
5722
- v3_1.logger.warn("chat.agent: replaced response not found at the model lane tail; reconverting the lane");
5723
- accumulatedMessages = await toModelMessages(accumulatedUIMessages);
5724
- }
5182
+ // Reconvert all model messages since we replaced rather than appended
5183
+ accumulatedMessages = await toModelMessages(accumulatedUIMessages);
5725
5184
  }
5726
5185
  else {
5727
5186
  accumulatedMessages.push(...responseModelMessages);
@@ -6018,13 +5477,11 @@ function chatAgent(options) {
6018
5477
  try {
6019
5478
  await tracer_js_1.tracer.startActiveSpan("snapshot.write", async () => {
6020
5479
  const snapshotInCursor = chatInputRouter().resumeFloor();
6021
- lastSnapshotOutEventId =
6022
- turnCompleteResult?.lastEventId ?? lastSnapshotOutEventId;
6023
5480
  await writeChatSnapshot(sessionIdForSnapshot, {
6024
5481
  version: 1,
6025
5482
  savedAt: Date.now(),
6026
5483
  messages: accumulatedUIMessages,
6027
- lastOutEventId: lastSnapshotOutEventId,
5484
+ lastOutEventId: turnCompleteResult?.lastEventId,
6028
5485
  lastInEventId: snapshotInCursor !== undefined ? String(snapshotInCursor) : undefined,
6029
5486
  });
6030
5487
  }, {
@@ -6058,17 +5515,10 @@ function chatAgent(options) {
6058
5515
  currentWirePayload = bootInjectedQueue.shift();
6059
5516
  return "continue";
6060
5517
  }
6061
- // chat.requestUpgrade() was called — exit the loop; the handover
6062
- // has already triggered a new run on the latest version.
5518
+ // chat.requestUpgrade() was called — exit the loop so the
5519
+ // transport triggers a new run on the latest version.
6063
5520
  // chat.endRun() — same exit, no upgrade semantics.
6064
- if (locals_js_1.locals.get(chatCloseRequestedKey)) {
6065
- await performChatClose();
6066
- return "exit";
6067
- }
6068
5521
  if (locals_js_1.locals.get(chatUpgradeRequestedKey) || locals_js_1.locals.get(chatEndRunRequestedKey)) {
6069
- if (locals_js_1.locals.get(chatUpgradeRequestedKey)) {
6070
- await persistUpgradeHandoff();
6071
- }
6072
5522
  return "exit";
6073
5523
  }
6074
5524
  // Wait for the next message — stay idle briefly, then suspend
@@ -6175,10 +5625,6 @@ function chatAgent(options) {
6175
5625
  });
6176
5626
  // Signal turn complete so the client knows this turn is done
6177
5627
  errorTurnCompleteResult = await writeTurnCompleteChunk(currentWirePayload.chatId);
6178
- // A later action's snapshot reuses this cursor, so it has to move
6179
- // here too or that snapshot resumes from before the failed turn.
6180
- lastSnapshotOutEventId =
6181
- errorTurnCompleteResult?.lastEventId ?? lastSnapshotOutEventId;
6182
5628
  }
6183
5629
  catch {
6184
5630
  // Best-effort — if stream write fails, let the run continue anyway
@@ -6223,46 +5669,15 @@ function chatAgent(options) {
6223
5669
  : partialIdx === -1
6224
5670
  ? [...erroredUIMessages, partialResponse]
6225
5671
  : erroredUIMessages.map((m, i) => i === partialIdx ? partialResponse : m);
6226
- /**
6227
- * Seeded from the per-turn list, not just the wire message and the
6228
- * partial, so a steering message the drain consumed is reported too.
6229
- * An app persisting from `newUIMessages` would otherwise lose the
6230
- * instruction whenever the turn it steered went on to fail.
6231
- */
6232
- const buildErroredNew = () => {
6233
- const out = [];
6234
- const addUnique = (m) => {
6235
- if (m && !out.some((existing) => existing.id === m.id))
6236
- out.push(m);
6237
- };
6238
- addUnique(erroredWireMessage);
6239
- for (const m of (locals_js_1.locals.get(chatTurnNewUIMessagesKey) ?? [])) {
6240
- addUnique(m);
6241
- }
6242
- if (includePartial)
6243
- addUnique(partialResponse);
6244
- return out;
6245
- };
6246
- let erroredNewUIMessages = buildErroredNew();
5672
+ let erroredNewUIMessages = erroredWireMessage ? [erroredWireMessage] : [];
5673
+ if (includePartial) {
5674
+ erroredNewUIMessages.push(partialResponse);
5675
+ }
6247
5676
  let erroredNewModelMessages = [];
6248
- const reconciledSteer = reconcilePendingSteer();
6249
5677
  if (!responseCommitted) {
6250
5678
  try {
6251
5679
  if (erroredNewUIMessages.length > 0) {
6252
- /**
6253
- * Built in order from the recorded forms rather than by
6254
- * converting the UI list, so a steer appears in the delta as
6255
- * the model received it (what `prepare` produced), matching the
6256
- * lane. The wire message and partial are converted as before.
6257
- */
6258
- const steerModelById = new Map(reconciledSteer.map((e) => [e.ui.id, e.model]));
6259
- for (const m of erroredNewUIMessages) {
6260
- const recorded = steerModelById.get(m.id);
6261
- if (recorded)
6262
- erroredNewModelMessages.push(...recorded);
6263
- else
6264
- erroredNewModelMessages.push(...(await toModelMessages([stripProviderMetadata(m)])));
6265
- }
5680
+ erroredNewModelMessages = await toModelMessages(erroredNewUIMessages.map((m) => stripProviderMetadata(m)));
6266
5681
  }
6267
5682
  if (erroredUIMessagesWithPartial !== accumulatedUIMessages) {
6268
5683
  if (partialIdx === -1) {
@@ -6270,11 +5685,7 @@ function chatAgent(options) {
6270
5685
  accumulatedMessages.push(...(await toModelMessages(appended.map((m) => stripProviderMetadata(m)))));
6271
5686
  }
6272
5687
  else {
6273
- const ok = await replaceModelRun(accumulatedMessages, erroredUIMessages[partialIdx], partialResponse, reconciledSteer.reduce((n, e) => n + e.model.length, 0));
6274
- if (!ok) {
6275
- v3_1.logger.warn("chat.agent: replaced partial not found at the model lane tail; reconverting the lane");
6276
- accumulatedMessages = await toModelMessages(erroredUIMessagesWithPartial);
6277
- }
5688
+ accumulatedMessages = await toModelMessages(erroredUIMessagesWithPartial);
6278
5689
  }
6279
5690
  accumulatedUIMessages = erroredUIMessagesWithPartial;
6280
5691
  locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
@@ -6283,7 +5694,7 @@ function chatAgent(options) {
6283
5694
  catch {
6284
5695
  erroredNewModelMessages = [];
6285
5696
  erroredUIMessagesWithPartial = erroredUIMessages;
6286
- erroredNewUIMessages = buildErroredNew().filter((m) => m !== partialResponse);
5697
+ erroredNewUIMessages = erroredWireMessage ? [erroredWireMessage] : [];
6287
5698
  }
6288
5699
  }
6289
5700
  if (onTurnComplete) {
@@ -6353,15 +5764,8 @@ function chatAgent(options) {
6353
5764
  });
6354
5765
  }
6355
5766
  }
6356
- if (locals_js_1.locals.get(chatCloseRequestedKey)) {
6357
- await performChatClose();
6358
- return;
6359
- }
6360
5767
  // chat.requestUpgrade() / chat.endRun() — exit after error turn too
6361
5768
  if (locals_js_1.locals.get(chatUpgradeRequestedKey) || locals_js_1.locals.get(chatEndRunRequestedKey)) {
6362
- if (locals_js_1.locals.get(chatUpgradeRequestedKey)) {
6363
- await persistUpgradeHandoff();
6364
- }
6365
5769
  return;
6366
5770
  }
6367
5771
  // Drain remaining recovered turns before idling — a thrown
@@ -6384,12 +5788,6 @@ function chatAgent(options) {
6384
5788
  return; // Timed out — end run gracefully
6385
5789
  }
6386
5790
  currentWirePayload = next.output;
6387
- // Same close check the success path makes. Without it a close
6388
- // record that lands after a failed turn is consumed as if it were
6389
- // a turn payload, and the loop runs on against a closed session.
6390
- if (currentWirePayload.trigger === "close") {
6391
- return;
6392
- }
6393
5791
  // Continue to next iteration of the for loop
6394
5792
  }
6395
5793
  finally {
@@ -6398,11 +5796,6 @@ function chatAgent(options) {
6398
5796
  }
6399
5797
  }
6400
5798
  finally {
6401
- // Safety net for a close requested on a path that exits without
6402
- // reaching one of the loop's close checks (a turn timeout, an OOM
6403
- // re-throw). `performChatClose` is idempotent, so the ordinary path
6404
- // having already run it costs nothing here.
6405
- await performChatClose();
6406
5799
  // `stopSub` is registered post-preload so the close-during-preload
6407
5800
  // early-return path may exit before it ever attached. Guard the
6408
5801
  // cleanup so a missing subscription doesn't throw.
@@ -6740,22 +6133,15 @@ function isStopped() {
6740
6133
  // Version upgrade
6741
6134
  // ---------------------------------------------------------------------------
6742
6135
  /**
6743
- * Hand the conversation over to another deployment.
6744
- *
6745
- * The handover happens immediately and server-side: a successor run is created
6746
- * and picks the conversation up from `session.in`. The transport keeps reading
6747
- * the same session output, so no client action is needed and nothing waits for
6748
- * the next message.
6749
- *
6750
- * Without a target the session's pin is cleared, so the successor lands on the
6751
- * latest deployed version; with `externalDeploymentId` the session is re-pinned
6752
- * to that deployment.
6136
+ * Request that the current run exits so the next message starts on the latest
6137
+ * deployed version (via the standard continuation mechanism).
6753
6138
  *
6754
6139
  * When called from `onTurnStart` or `onValidateMessages`, `run()` is skipped
6755
- * entirely and the successor answers the message that opened the turn.
6140
+ * entirely — the run exits immediately and the transport re-triggers the
6141
+ * same message on the new version.
6756
6142
  *
6757
6143
  * When called from `run()` or `chat.defer()`, the current turn completes
6758
- * normally and the handover happens afterward.
6144
+ * normally and the run exits afterward instead of waiting for the next message.
6759
6145
  *
6760
6146
  * Call from `onTurnStart`, `onValidateMessages`, `onChatResume`, `run()`,
6761
6147
  * or inside `chat.defer()`.
@@ -6775,37 +6161,8 @@ function isStopped() {
6775
6161
  * });
6776
6162
  * ```
6777
6163
  */
6778
- function requestUpgrade(options) {
6164
+ function requestUpgrade() {
6779
6165
  locals_js_1.locals.set(chatUpgradeRequestedKey, true);
6780
- // Without a target the handoff clears the session's pin; with one it re-pins to that deployment.
6781
- const target = options?.externalDeploymentId?.trim();
6782
- if (target)
6783
- locals_js_1.locals.set(chatUpgradeExternalDeploymentIdKey, target);
6784
- }
6785
- /** @internal Requests a handoff when the session's pin no longer names this deployment. */
6786
- async function followSessionPin(chatId, policy) {
6787
- if (!chatId) {
6788
- return;
6789
- }
6790
- const deployedExternalId = locals_js_1.locals.get(chatAgentRunContextKey)?.deployment?.externalId;
6791
- if (policy !== "hold" && !deployedExternalId) {
6792
- v3_1.logger.debug("chat.versionSkew: cannot follow the session pin", {
6793
- chatId,
6794
- reason: "the run context carries no deployment.externalId",
6795
- });
6796
- }
6797
- const target = await (0, chatVersionSkew_js_1.resolvePinToFollow)({
6798
- policy,
6799
- deployedExternalId,
6800
- upgradeAlreadyRequested: locals_js_1.locals.get(chatUpgradeRequestedKey) === true,
6801
- readPin: async () => (await sessions_js_1.sessions.retrieve(chatId, { retry: { maxAttempts: 2, randomize: true } }))
6802
- .triggerConfig,
6803
- });
6804
- if (!target) {
6805
- return;
6806
- }
6807
- v3_1.logger.info("chat.versionSkew: following the session pin", { chatId, target });
6808
- requestUpgrade({ externalDeploymentId: target });
6809
6166
  }
6810
6167
  /**
6811
6168
  * Hand off the current custom agent Session to a fresh run.
@@ -6844,31 +6201,20 @@ async function endAndContinue() {
6844
6201
  if ((locals_js_1.locals.get(chatActiveSessionIteratorsKey) ?? 0) > 0) {
6845
6202
  throw new Error("chat.endAndContinue() cannot be called while a chat.createSession() iterator is active. Close the iterator, then call chat.endAndContinue().");
6846
6203
  }
6847
- await performEndAndContinue({ reason: "continuation" });
6204
+ await performEndAndContinue();
6848
6205
  }
6849
6206
  /** @internal Shared server handoff used by managed and custom agent loops. */
6850
- async function performEndAndContinue(options) {
6207
+ async function performEndAndContinue() {
6851
6208
  const chatId = locals_js_1.locals.get(chatExternalIdKey);
6852
6209
  const callingRunId = locals_js_1.locals.get(chatAgentRunContextKey)?.run.id;
6853
6210
  if (!chatId || !callingRunId) {
6854
6211
  throw new Error("Cannot end and continue without an active chat agent run");
6855
6212
  }
6856
- const externalDeploymentId = options.externalDeploymentId;
6857
6213
  const apiClient = v3_1.apiClientManager.clientOrThrow();
6858
- const result = await apiClient.endAndContinueSession(chatId, {
6214
+ await apiClient.endAndContinueSession(chatId, {
6859
6215
  callingRunId,
6860
- reason: options.reason,
6861
- ...(externalDeploymentId ? { externalDeploymentId } : {}),
6216
+ reason: "upgrade",
6862
6217
  });
6863
- if (result?.pendingVersion !== true) {
6864
- return;
6865
- }
6866
- // The successor parked. Say so on `.out` while this run still can — the transport's
6867
- // subscription survives the swap, so the client learns without waiting for its next send.
6868
- const [error] = await (0, v3_1.tryCatch)(getChatSession().out.writeControl(v3_1.TRIGGER_CONTROL_SUBTYPE.PENDING_VERSION));
6869
- if (error) {
6870
- v3_1.logger.warn("could not signal a parked handoff", { chatId, error });
6871
- }
6872
6218
  }
6873
6219
  /**
6874
6220
  * Exit the run after the current turn completes, without waiting for the
@@ -6899,124 +6245,6 @@ async function performEndAndContinue(options) {
6899
6245
  function endRun() {
6900
6246
  locals_js_1.locals.set(chatEndRunRequestedKey, true);
6901
6247
  }
6902
- /**
6903
- * End the whole conversation, permanently. The session row is closed, further
6904
- * appends are refused, and the run exits without scheduling a continuation.
6905
- *
6906
- * This is the session-level stop. {@link endRun} ends the current run and lets
6907
- * the next message start a fresh one; `chat.close()` ends the session itself,
6908
- * so there is no next message. Use it for a budget cap, a completed goal,
6909
- * abuse detection, or a user signing out.
6910
- *
6911
- * In a `chat.agent`, call it from `run()`, `prepareStep`, or
6912
- * `onBeforeTurnComplete`. Called mid-step, it aborts the in-flight
6913
- * `streamText` the same way the stop signal does, so the partial response is
6914
- * still captured and streamed. The turn then completes normally, a terminal
6915
- * `session-closed` record carrying `reason` is written to the response stream,
6916
- * and the loop exits.
6917
- *
6918
- * Prefer `onBeforeTurnComplete` over `onTurnComplete`. It carries the same
6919
- * fields but still runs while the stream is open, so the closed state rides
6920
- * out on the turn's final record. `onTurnComplete` runs after that record, so
6921
- * a close decided there does not reach a reader that has already finished the
6922
- * turn, and the user only finds out when their next message is refused.
6923
- *
6924
- * In a `chat.customAgent`, call it anywhere in your own loop. The close is
6925
- * performed when `run()` returns, so it lands whether you break out of a
6926
- * `chat.createSession` loop, return early, or hand-roll the loop entirely.
6927
- *
6928
- * Closing is one-way: a closed session cannot be reopened. Its transcript
6929
- * stays readable.
6930
- *
6931
- * @example
6932
- * ```ts
6933
- * chat.agent({
6934
- * id: "budgeted-agent",
6935
- * onBeforeTurnComplete: async ({ usage }) => {
6936
- * if (await overBudget(usage)) {
6937
- * chat.close({ reason: "Monthly budget reached" });
6938
- * }
6939
- * },
6940
- * });
6941
- * ```
6942
- */
6943
- function close(options) {
6944
- if (!locals_js_1.locals.get(chatExternalIdKey)) {
6945
- throw new Error("chat.close() can only be called from inside a chat.agent() or chat.customAgent() run");
6946
- }
6947
- // Bound the reason once, here. It goes out on S2 record headers as well as
6948
- // the close API, and an oversized value would fail the turn-complete write
6949
- // that carries the turn boundary, costing the client far more than the
6950
- // reason text.
6951
- // Trailing high surrogate: the cut landed between the two halves of an
6952
- // astral character, and encoding the orphan to UTF-8 for a record header
6953
- // yields a replacement character. Drop it rather than ship mojibake.
6954
- const reason = options?.reason
6955
- ?.slice(0, CHAT_CLOSE_REASON_MAX_LENGTH)
6956
- .replace(/[\uD800-\uDBFF]$/, "");
6957
- locals_js_1.locals.set(chatCloseRequestedKey, reason ? { reason } : {});
6958
- // Mid-step call: unblock the in-flight streamText exactly like the stop
6959
- // signal, so the turn can reach its turn boundary instead of running the
6960
- // model out to completion after the decision to close has been made.
6961
- locals_js_1.locals.get(chatStopControllerKey)?.abort(reason ?? "closed");
6962
- }
6963
- /**
6964
- * @internal Terminal close sequence, run once at whichever exit site observes
6965
- * the close request. Writes the standalone `session-closed` record, then closes
6966
- * the session row.
6967
- *
6968
- * The record lands after the turn's `turn-complete`, so a client reading that
6969
- * turn's stream has already terminated on it and will not see this one. It is
6970
- * there for a reconnect and for replay. What a live client reads is the
6971
- * `session-closed` header stamped onto `turn-complete` itself by
6972
- * `writeTurnCompleteChunk`, which fires whenever the close was decided before
6973
- * the turn ended. A close decided from `onTurnComplete` is past that point, so
6974
- * the client learns from the 409 on its next send.
6975
- */
6976
- async function performChatClose() {
6977
- const request = locals_js_1.locals.get(chatCloseRequestedKey);
6978
- if (!request || locals_js_1.locals.get(chatClosePerformedKey))
6979
- return;
6980
- const reason = request.reason;
6981
- // Two flags, not one. The record is a client-visible event and must not be
6982
- // written twice, but the row close is the part that actually ends the
6983
- // conversation: flagging it as done before it succeeds would let a transient
6984
- // failure leave the session open with no later call willing to retry.
6985
- if (!locals_js_1.locals.get(chatCloseRecordWrittenKey)) {
6986
- locals_js_1.locals.set(chatCloseRecordWrittenKey, true);
6987
- try {
6988
- const session = getChatSession();
6989
- await session.out.writeControl(v3_1.TRIGGER_CONTROL_SUBTYPE.SESSION_CLOSED, reason ? [[v3_1.SESSION_CLOSED_REASON_HEADER, reason]] : undefined);
6990
- }
6991
- catch (error) {
6992
- v3_1.logger.warn("chat.close: failed to write the session-closed record", {
6993
- error: error instanceof Error ? error.message : String(error),
6994
- });
6995
- }
6996
- }
6997
- const chatId = locals_js_1.locals.get(chatExternalIdKey);
6998
- if (!chatId)
6999
- return;
7000
- try {
7001
- await sessions_js_1.sessions.close(chatId, {
7002
- ...(reason ? { reason } : {}),
7003
- ...(locals_js_1.locals.get(chatAgentRunContextKey)?.run.id
7004
- ? { callingRunId: locals_js_1.locals.get(chatAgentRunContextKey).run.id }
7005
- : {}),
7006
- });
7007
- locals_js_1.locals.set(chatClosePerformedKey, true);
7008
- }
7009
- catch (error) {
7010
- // Deliberately NOT flagged as performed: the close API is idempotent, so a
7011
- // later exit site on this run gets to retry it. Losing every retry to a
7012
- // transient failure would leave the row open and the conversation alive.
7013
- // Non-fatal either way — the run still exits.
7014
- v3_1.logger.error("chat.close: failed to close the session", {
7015
- chatId,
7016
- error: error instanceof Error ? error.message : String(error),
7017
- });
7018
- }
7019
- }
7020
6248
  // ---------------------------------------------------------------------------
7021
6249
  // Per-turn deferred work
7022
6250
  // ---------------------------------------------------------------------------
@@ -7078,18 +6306,9 @@ function chatDefer(promiseOrFn) {
7078
6306
  * ```
7079
6307
  */
7080
6308
  function injectBackgroundContext(messages) {
7081
- const systemBlocks = messages.filter((message) => message.role === "system");
7082
- const conversational = messages.filter((message) => message.role !== "system");
7083
- if (systemBlocks.length > 0) {
7084
- const instructions = locals_js_1.locals.get(chatInjectedInstructionsKey) ?? [];
7085
- instructions.push(...systemBlocks);
7086
- locals_js_1.locals.set(chatInjectedInstructionsKey, instructions);
7087
- }
7088
- if (conversational.length > 0) {
7089
- const queue = locals_js_1.locals.get(chatBackgroundQueueKey) ?? [];
7090
- queue.push(...conversational);
7091
- locals_js_1.locals.set(chatBackgroundQueueKey, queue);
7092
- }
6309
+ const queue = locals_js_1.locals.get(chatBackgroundQueueKey) ?? [];
6310
+ queue.push(...messages);
6311
+ locals_js_1.locals.set(chatBackgroundQueueKey, queue);
7093
6312
  }
7094
6313
  // ---------------------------------------------------------------------------
7095
6314
  // Aborted message cleanup
@@ -7502,12 +6721,10 @@ class ChatMessageAccumulator {
7502
6721
  // a duplicate, mirroring the chat.agent accumulator.
7503
6722
  const existingIdx = this.uiMessages.findIndex((m) => m.id === response.id);
7504
6723
  if (existingIdx !== -1) {
7505
- const previous = this.uiMessages[existingIdx];
7506
6724
  this.uiMessages[existingIdx] = response;
7507
6725
  try {
7508
- if (!(await replaceModelRun(this.modelMessages, previous, response, 0))) {
7509
- this.modelMessages = await toModelMessages(this.uiMessages.map((m) => stripProviderMetadata(m)));
7510
- }
6726
+ // Reconvert all model messages since we replaced rather than appended.
6727
+ this.modelMessages = await toModelMessages(this.uiMessages.map((m) => stripProviderMetadata(m)));
7511
6728
  }
7512
6729
  catch {
7513
6730
  // Conversion failed — leave the existing model messages in place
@@ -7543,28 +6760,6 @@ class ChatMessageAccumulator {
7543
6760
  const modelMsgs = await toModelMessages([message]);
7544
6761
  this._steeringQueue.push({ uiMessage: message, modelMessages: modelMsgs });
7545
6762
  }
7546
- /**
7547
- * Record the messages a steering drain consumed.
7548
- *
7549
- * The drain only puts them in this step's prompt, so without this they
7550
- * shape one answer and then exist in neither lane: not in `uiMessages`,
7551
- * which is what an app persists from, and not in `modelMessages`, which is
7552
- * what every later turn sends.
7553
- *
7554
- * Both lanes are appended to. The model lane is never reconverted from the
7555
- * UI lane, because `compactIfNeeded` replaces it with a summary and leaves
7556
- * the UI lane whole: a reconversion would restore everything the summary
7557
- * replaced.
7558
- */
7559
- async absorbSteering(claimed, injected) {
7560
- const fresh = claimed.filter((m) => !this.uiMessages.some((e) => e.id === m.id));
7561
- if (fresh.length === 0)
7562
- return;
7563
- this.uiMessages.push(...fresh);
7564
- // Record what the model received. Only when the whole batch is new is
7565
- // `injected` known to describe exactly these messages.
7566
- this.modelMessages.push(...(injected && fresh.length === claimed.length ? injected : await toModelMessages(fresh)));
7567
- }
7568
6763
  /**
7569
6764
  * Get and clear unconsumed steering messages.
7570
6765
  */
@@ -7597,8 +6792,7 @@ class ChatMessageAccumulator {
7597
6792
  }
7598
6793
  // 2. Pending message injection
7599
6794
  if (pm && queue.length > 0) {
7600
- const { injected, claimed } = await drainSteeringQueue(pm, resultMessages ?? messages, steps, queue);
7601
- await this.absorbSteering(claimed, injected);
6795
+ const injected = await drainSteeringQueue(pm, resultMessages ?? messages, steps, queue);
7602
6796
  if (injected.length > 0) {
7603
6797
  resultMessages = [...(resultMessages ?? messages), ...injected];
7604
6798
  }
@@ -7787,7 +6981,7 @@ function trackActiveChatSessionIterator(iterator) {
7787
6981
  * ```
7788
6982
  */
7789
6983
  function createChatSession(payload, options) {
7790
- const { signal: runSignal, idleTimeoutInSeconds: sessionIdleTimeoutOpt, timeout = "1h", maxTurns = 100, compaction: sessionCompaction, pendingMessages: sessionPendingMessages, versionSkew: sessionVersionSkew, } = options;
6984
+ const { signal: runSignal, idleTimeoutInSeconds: sessionIdleTimeoutOpt, timeout = "1h", maxTurns = 100, compaction: sessionCompaction, pendingMessages: sessionPendingMessages, } = options;
7791
6985
  const idleTimeoutInSeconds = sessionIdleTimeoutOpt ?? 30;
7792
6986
  return {
7793
6987
  [Symbol.asyncIterator]() {
@@ -7876,16 +7070,8 @@ function createChatSession(payload, options) {
7876
7070
  * without suspending.
7877
7071
  */
7878
7072
  if (turn > 0) {
7879
- if (locals_js_1.locals.get(chatCloseRequestedKey)) {
7880
- await performChatClose();
7881
- stop.cleanup();
7882
- return { done: true, value: undefined };
7883
- }
7884
7073
  // chat.requestUpgrade() / chat.endRun() — exit before waiting
7885
7074
  if (locals_js_1.locals.get(chatUpgradeRequestedKey) || locals_js_1.locals.get(chatEndRunRequestedKey)) {
7886
- if (locals_js_1.locals.get(chatUpgradeRequestedKey)) {
7887
- await persistUpgradeHandoff();
7888
- }
7889
7075
  stop.cleanup();
7890
7076
  return { done: true, value: undefined };
7891
7077
  }
@@ -7988,7 +7174,6 @@ function createChatSession(payload, options) {
7988
7174
  }
7989
7175
  accumulator.applyHandover(pendingHandoverSignal);
7990
7176
  }
7991
- await followSessionPin(currentPayload.chatId, sessionVersionSkew);
7992
7177
  // chat.requestUpgrade() called before this turn — signal transport and exit
7993
7178
  if (locals_js_1.locals.get(chatUpgradeRequestedKey)) {
7994
7179
  await writeUpgradeRequiredChunk();
@@ -8205,8 +7390,7 @@ function createChatSession(payload, options) {
8205
7390
  }
8206
7391
  }
8207
7392
  if (sessionPendingMessages) {
8208
- const { injected, claimed } = await drainSteeringQueue(sessionPendingMessages, resultMessages ?? stepMsgs, steps, turnSteeringQueue);
8209
- await accumulator.absorbSteering(claimed, injected);
7393
+ const injected = await drainSteeringQueue(sessionPendingMessages, resultMessages ?? stepMsgs, steps, turnSteeringQueue);
8210
7394
  if (injected.length > 0) {
8211
7395
  resultMessages = [...(resultMessages ?? stepMsgs), ...injected];
8212
7396
  }
@@ -8220,11 +7404,6 @@ function createChatSession(payload, options) {
8220
7404
  async return() {
8221
7405
  activeMsgSub?.off();
8222
7406
  activeMsgSub = undefined;
8223
- // Reached when the consumer leaves the `for await` early (`break`,
8224
- // `return`, a throw). A `chat.close()` from the loop body would
8225
- // otherwise be dropped: the exit that performs it lives in `next()`,
8226
- // and `next()` is never called again.
8227
- await performChatClose();
8228
7407
  // `stop` only exists once next() has booted the iterator.
8229
7408
  stop?.cleanup();
8230
7409
  return { done: true, value: undefined };
@@ -8481,11 +7660,6 @@ function createChatStartSessionAction(taskId, options) {
8481
7660
  const maxAttempts = params.triggerConfig?.maxAttempts ?? options?.triggerConfig?.maxAttempts;
8482
7661
  const maxDuration = params.triggerConfig?.maxDuration ?? options?.triggerConfig?.maxDuration;
8483
7662
  const idleTimeoutInSeconds = params.triggerConfig?.idleTimeoutInSeconds ?? options?.triggerConfig?.idleTimeoutInSeconds;
8484
- // Only `undefined` means "not supplied": a per-call `null` (opt out) has to beat a pinning
8485
- // action default, which neither truthiness nor `??` would allow.
8486
- const externalDeploymentId = params.triggerConfig?.externalDeploymentId !== undefined
8487
- ? params.triggerConfig.externalDeploymentId
8488
- : options?.triggerConfig?.externalDeploymentId;
8489
7663
  const triggerConfig = {
8490
7664
  basePayload: {
8491
7665
  messages: [],
@@ -8512,10 +7686,6 @@ function createChatStartSessionAction(taskId, options) {
8512
7686
  lockToVersion: params.triggerConfig?.lockToVersion ?? options?.triggerConfig?.lockToVersion,
8513
7687
  }
8514
7688
  : {}),
8515
- ...(params.triggerConfig?.ttl !== undefined || options?.triggerConfig?.ttl !== undefined
8516
- ? { ttl: params.triggerConfig?.ttl ?? options?.triggerConfig?.ttl }
8517
- : {}),
8518
- ...(externalDeploymentId !== undefined ? { externalDeploymentId } : {}),
8519
7689
  ...(idleTimeoutInSeconds !== undefined ? { idleTimeoutInSeconds } : {}),
8520
7690
  };
8521
7691
  const startBody = {
@@ -8560,7 +7730,6 @@ function createChatStartSessionAction(taskId, options) {
8560
7730
  publicAccessToken,
8561
7731
  runId: created.runId,
8562
7732
  sessionId: created.id,
8563
- ...(created.pendingVersion ? { pendingVersion: true } : {}),
8564
7733
  };
8565
7734
  };
8566
7735
  }
@@ -8594,8 +7763,7 @@ async function callSessionsCreateWithOverride(args) {
8594
7763
  const init = {
8595
7764
  method: "POST",
8596
7765
  headers: overrideRequestHeaders(accessToken),
8597
- // This path bypasses `sessions.start`, so it resolves the pin itself.
8598
- body: JSON.stringify((0, externalDeploymentId_js_1.withResolvedExternalDeploymentId)(args.body)),
7766
+ body: JSON.stringify(args.body),
8599
7767
  };
8600
7768
  const response = args.fetchOverride
8601
7769
  ? await args.fetchOverride(url, init, ctx)
@@ -8673,8 +7841,6 @@ exports.chat = {
8673
7841
  createStartSessionAction: createChatStartSessionAction,
8674
7842
  /** Pipe a stream to the chat transport. See {@link pipeChat}. */
8675
7843
  pipe: pipeChat,
8676
- /** Return from `onAction` to run a turn on the edited history. See {@link chatTurn}. */
8677
- turn: chatTurn,
8678
7844
  /** Create a per-run typed local. See {@link chatLocal}. */
8679
7845
  local: chatLocal,
8680
7846
  /** Create a public access token for a chat task. See {@link createChatAccessToken}. */
@@ -8695,8 +7861,6 @@ exports.chat = {
8695
7861
  endAndContinue,
8696
7862
  /** Exit the run after the current turn completes, without any upgrade signal. See {@link endRun}. */
8697
7863
  endRun,
8698
- /** End the conversation permanently: close the session and exit the run. See {@link close}. */
8699
- close,
8700
7864
  /** Clean up aborted parts from a UIMessage. See {@link cleanupAbortedParts}. */
8701
7865
  cleanupAbortedParts,
8702
7866
  /** Register background work that runs in parallel with streaming. See {@link chatDefer}. */
@@ -8844,16 +8008,6 @@ async function writeTurnCompleteChunk(_chatId, publicAccessToken) {
8844
8008
  if (consumedCursor !== undefined) {
8845
8009
  extraHeaders.push([v3_1.SESSION_IN_CONSUMED_ID_HEADER, String(consumedCursor)]);
8846
8010
  }
8847
- // A close decided before this turn ended rides out on turn-complete. Readers
8848
- // terminate their stream on turn-complete, so a standalone record written
8849
- // after it only reaches a reconnect — this header is what a live client sees.
8850
- const pendingClose = locals_js_1.locals.get(chatCloseRequestedKey);
8851
- if (pendingClose) {
8852
- extraHeaders.push([v3_1.SESSION_CLOSED_HEADER, "true"]);
8853
- if (pendingClose.reason) {
8854
- extraHeaders.push([v3_1.SESSION_CLOSED_REASON_HEADER, pendingClose.reason]);
8855
- }
8856
- }
8857
8011
  const result = await session.out.writeControl(v3_1.TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE, extraHeaders);
8858
8012
  const T_N = result.lastEventId ? Number.parseInt(result.lastEventId, 10) : undefined;
8859
8013
  // 2. Trim back to the previous turn-complete, if we have one. Skipping on
@@ -8911,47 +8065,12 @@ async function writeTurnCompleteChunk(_chatId, publicAccessToken) {
8911
8065
  *
8912
8066
  * @internal
8913
8067
  */
8914
- /**
8915
- * Persists an upgrade requested after the turn has already run.
8916
- *
8917
- * The pre-turn sites reach {@link performEndAndContinue} through
8918
- * {@link writeUpgradeRequiredChunk}, which is what clears (or re-points) the
8919
- * session's stored `externalDeploymentId`. The post-turn exits had no such path,
8920
- * so a `chat.requestUpgrade()` from `run()` or `chat.defer()` left the pin intact
8921
- * and every continuation re-pinned to the deployment the agent asked to leave.
8922
- *
8923
- * No `upgrade-required` chunk is written here: the turn already produced its
8924
- * answer, so there is nothing for a client to be told about.
8925
- */
8926
- async function persistUpgradeHandoff() {
8927
- const chatId = locals_js_1.locals.get(chatExternalIdKey);
8928
- const callingRunId = locals_js_1.locals.get(chatAgentRunContextKey)?.run.id;
8929
- if (!chatId || !callingRunId) {
8930
- return;
8931
- }
8932
- try {
8933
- await performEndAndContinue({
8934
- reason: "upgrade",
8935
- externalDeploymentId: locals_js_1.locals.get(chatUpgradeExternalDeploymentIdKey),
8936
- });
8937
- }
8938
- catch (error) {
8939
- v3_1.logger.warn("upgrade handoff failed; session keeps its current version pin", {
8940
- chatId,
8941
- callingRunId,
8942
- error,
8943
- });
8944
- }
8945
- }
8946
8068
  async function writeUpgradeRequiredChunk() {
8947
8069
  const chatId = locals_js_1.locals.get(chatExternalIdKey);
8948
8070
  const callingRunId = locals_js_1.locals.get(chatAgentRunContextKey)?.run.id;
8949
8071
  if (chatId && callingRunId) {
8950
8072
  try {
8951
- await performEndAndContinue({
8952
- reason: "upgrade",
8953
- externalDeploymentId: locals_js_1.locals.get(chatUpgradeExternalDeploymentIdKey),
8954
- });
8073
+ await performEndAndContinue();
8955
8074
  }
8956
8075
  catch (error) {
8957
8076
  // Non-fatal: the next `.in/append` re-triggers via the probe.