@agent-native/core 0.98.4 → 0.98.5

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 (217) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +16 -0
  3. package/corpus/core/docs/content/durable-background-runs.mdx +37 -0
  4. package/corpus/core/docs/content/locales/ar-SA/tracking.mdx +2 -0
  5. package/corpus/core/docs/content/locales/de-DE/tracking.mdx +2 -0
  6. package/corpus/core/docs/content/locales/es-ES/tracking.mdx +2 -0
  7. package/corpus/core/docs/content/locales/fr-FR/tracking.mdx +4 -2
  8. package/corpus/core/docs/content/locales/hi-IN/tracking.mdx +2 -0
  9. package/corpus/core/docs/content/locales/ja-JP/tracking.mdx +2 -0
  10. package/corpus/core/docs/content/locales/ko-KR/tracking.mdx +2 -0
  11. package/corpus/core/docs/content/locales/pt-BR/tracking.mdx +2 -0
  12. package/corpus/core/docs/content/locales/zh-CN/tracking.mdx +2 -0
  13. package/corpus/core/docs/content/locales/zh-TW/durable-background-runs.mdx +14 -0
  14. package/corpus/core/docs/content/locales/zh-TW/tracking.mdx +2 -0
  15. package/corpus/core/docs/content/tracking.mdx +2 -0
  16. package/corpus/core/package.json +1 -1
  17. package/corpus/core/src/agent/durable-background.ts +16 -16
  18. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +25 -8
  19. package/corpus/core/src/agent/engine/anthropic-engine.ts +17 -3
  20. package/corpus/core/src/agent/engine/output-tokens.ts +6 -6
  21. package/corpus/core/src/agent/production-agent.ts +200 -27
  22. package/corpus/core/src/agent/run-manager.ts +81 -1
  23. package/corpus/core/src/agent/run-store.ts +301 -41
  24. package/corpus/core/src/client/AgentPanel.tsx +6 -2
  25. package/corpus/core/src/client/AssistantChat.tsx +364 -69
  26. package/corpus/core/src/client/agent-chat-adapter.ts +169 -1
  27. package/corpus/core/src/client/chat/message-components.tsx +3 -0
  28. package/corpus/core/src/client/chat/tool-call-display.tsx +20 -36
  29. package/corpus/core/src/client/conversation/use-near-bottom-autoscroll.ts +7 -0
  30. package/corpus/core/src/client/extensions/AgentNativeExtensionFrame.tsx +2 -0
  31. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +2 -0
  32. package/corpus/core/src/client/extensions/ExtensionEditor.tsx +9 -2
  33. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +2 -0
  34. package/corpus/core/src/client/extensions/InlineExtensionFrame.tsx +2 -0
  35. package/corpus/core/src/client/extensions/portable-extension.ts +2 -0
  36. package/corpus/core/src/client/session-replay.ts +361 -29
  37. package/corpus/core/src/client/sse-event-processor.ts +43 -1
  38. package/corpus/core/src/demo/actions/toggle-demo-mode.ts +1 -1
  39. package/corpus/core/src/demo/config.ts +6 -6
  40. package/corpus/core/src/demo/fetch-interceptor.ts +13 -4
  41. package/corpus/core/src/demo/redact.ts +13 -149
  42. package/corpus/core/src/extensions/html-shell.ts +3 -0
  43. package/corpus/core/src/extensions/session-replay-iframe.ts +131 -0
  44. package/corpus/core/src/scripts/agent-engines/list-agent-engines.ts +1 -20
  45. package/corpus/core/src/session-replay-iframe-protocol.ts +58 -0
  46. package/corpus/core/src/shared/reasoning-effort.ts +35 -0
  47. package/corpus/core/src/styles/agent-conversation.css +26 -0
  48. package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +0 -15
  49. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +30 -21
  50. package/corpus/templates/analytics/AGENTS.md +12 -0
  51. package/corpus/templates/analytics/actions/get-error-issue.ts +1 -0
  52. package/corpus/templates/analytics/actions/get-session-replay-events.ts +1 -0
  53. package/corpus/templates/analytics/actions/get-session-replay-summary.ts +1 -0
  54. package/corpus/templates/analytics/actions/list-error-issues.ts +1 -0
  55. package/corpus/templates/analytics/actions/list-session-recordings.ts +1 -0
  56. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +2 -1
  57. package/corpus/templates/analytics/app/global.css +2 -2
  58. package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +41 -4
  59. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +52 -203
  60. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +34 -12
  61. package/corpus/templates/analytics/changelog/2026-07-12-analytics-chat-retries-before-showing-a-generic-no-data-mess.md +6 -0
  62. package/corpus/templates/analytics/changelog/2026-07-12-analytics-sessions-search-no-longer-loses-fast-keystrokes-an.md +6 -0
  63. package/corpus/templates/analytics/changelog/2026-07-12-chart-selection-respects-chat-state.md +6 -0
  64. package/corpus/templates/analytics/changelog/2026-07-12-demo-mode-anonymizes-error-reporting-emails.md +6 -0
  65. package/corpus/templates/analytics/changelog/2026-07-12-session-replays-show-iframe-content.md +6 -0
  66. package/corpus/templates/analytics/netlify.toml +0 -3
  67. package/corpus/templates/analytics/server/lib/analytics-connector-catalog.ts +14 -0
  68. package/corpus/templates/analytics/server/lib/error-capture.ts +59 -2
  69. package/corpus/templates/analytics/server/lib/real-data-actions.ts +24 -0
  70. package/corpus/templates/analytics/server/lib/session-replay.ts +2 -9
  71. package/corpus/templates/analytics/server/plugins/agent-chat.ts +39 -6
  72. package/corpus/templates/assets/app/global.css +1 -1
  73. package/corpus/templates/brain/app/global.css +2 -2
  74. package/corpus/templates/brain/changelog/2026-07-12-fix-long-response-progress.md +6 -0
  75. package/corpus/templates/forms/app/components/CloudUpgrade.tsx +1 -1
  76. package/corpus/templates/forms/app/components/ThemeToggle.tsx +22 -6
  77. package/corpus/templates/forms/app/components/builder/FieldPropertiesPanel.tsx +5 -5
  78. package/corpus/templates/forms/app/components/builder/FieldRenderer.tsx +3 -3
  79. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +16 -16
  80. package/corpus/templates/forms/app/global.css +110 -5
  81. package/corpus/templates/forms/app/pages/AskPage.tsx +3 -3
  82. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +134 -52
  83. package/corpus/templates/forms/app/pages/FormFillPage.tsx +44 -11
  84. package/corpus/templates/forms/app/pages/FormsListPage.tsx +42 -24
  85. package/corpus/templates/forms/app/pages/ResponseInsightsPage.tsx +28 -15
  86. package/corpus/templates/forms/app/pages/ResponsesPage.tsx +38 -15
  87. package/corpus/templates/forms/app/routes/_app.settings.tsx +1 -1
  88. package/corpus/templates/forms/changelog/2026-07-12-form-builder-surfaces-are-calmer-fields-stay-stable-on-hover.md +6 -0
  89. package/corpus/templates/forms/changelog/2026-07-12-smoother-more-tactile-controls-throughout-animated-icon-and-.md +6 -0
  90. package/corpus/templates/mail/app/components/email/EmailThread.tsx +21 -13
  91. package/corpus/templates/mail/app/components/email/email-iframe-document.ts +18 -0
  92. package/dist/agent/durable-background.d.ts +10 -10
  93. package/dist/agent/durable-background.d.ts.map +1 -1
  94. package/dist/agent/durable-background.js +16 -16
  95. package/dist/agent/durable-background.js.map +1 -1
  96. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  97. package/dist/agent/engine/ai-sdk-engine.js +19 -8
  98. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  99. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  100. package/dist/agent/engine/anthropic-engine.js +11 -3
  101. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  102. package/dist/agent/engine/output-tokens.js +6 -6
  103. package/dist/agent/engine/output-tokens.js.map +1 -1
  104. package/dist/agent/production-agent.d.ts +52 -0
  105. package/dist/agent/production-agent.d.ts.map +1 -1
  106. package/dist/agent/production-agent.js +154 -26
  107. package/dist/agent/production-agent.js.map +1 -1
  108. package/dist/agent/run-manager.d.ts +9 -0
  109. package/dist/agent/run-manager.d.ts.map +1 -1
  110. package/dist/agent/run-manager.js +71 -1
  111. package/dist/agent/run-manager.js.map +1 -1
  112. package/dist/agent/run-store.d.ts +9 -0
  113. package/dist/agent/run-store.d.ts.map +1 -1
  114. package/dist/agent/run-store.js +243 -48
  115. package/dist/agent/run-store.js.map +1 -1
  116. package/dist/client/AgentPanel.d.ts.map +1 -1
  117. package/dist/client/AgentPanel.js +6 -2
  118. package/dist/client/AgentPanel.js.map +1 -1
  119. package/dist/client/AssistantChat.d.ts +21 -0
  120. package/dist/client/AssistantChat.d.ts.map +1 -1
  121. package/dist/client/AssistantChat.js +298 -67
  122. package/dist/client/AssistantChat.js.map +1 -1
  123. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  124. package/dist/client/agent-chat-adapter.js +146 -1
  125. package/dist/client/agent-chat-adapter.js.map +1 -1
  126. package/dist/client/chat/message-components.d.ts.map +1 -1
  127. package/dist/client/chat/message-components.js +2 -1
  128. package/dist/client/chat/message-components.js.map +1 -1
  129. package/dist/client/chat/tool-call-display.d.ts +3 -1
  130. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  131. package/dist/client/chat/tool-call-display.js +13 -37
  132. package/dist/client/chat/tool-call-display.js.map +1 -1
  133. package/dist/client/conversation/use-near-bottom-autoscroll.d.ts +1 -0
  134. package/dist/client/conversation/use-near-bottom-autoscroll.d.ts.map +1 -1
  135. package/dist/client/conversation/use-near-bottom-autoscroll.js +6 -0
  136. package/dist/client/conversation/use-near-bottom-autoscroll.js.map +1 -1
  137. package/dist/client/extensions/AgentNativeExtensionFrame.d.ts.map +1 -1
  138. package/dist/client/extensions/AgentNativeExtensionFrame.js +2 -1
  139. package/dist/client/extensions/AgentNativeExtensionFrame.js.map +1 -1
  140. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  141. package/dist/client/extensions/EmbeddedExtension.js +2 -1
  142. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  143. package/dist/client/extensions/ExtensionEditor.d.ts.map +1 -1
  144. package/dist/client/extensions/ExtensionEditor.js +5 -2
  145. package/dist/client/extensions/ExtensionEditor.js.map +1 -1
  146. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  147. package/dist/client/extensions/ExtensionViewer.js +2 -1
  148. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  149. package/dist/client/extensions/InlineExtensionFrame.d.ts.map +1 -1
  150. package/dist/client/extensions/InlineExtensionFrame.js +2 -1
  151. package/dist/client/extensions/InlineExtensionFrame.js.map +1 -1
  152. package/dist/client/extensions/portable-extension.d.ts.map +1 -1
  153. package/dist/client/extensions/portable-extension.js +2 -0
  154. package/dist/client/extensions/portable-extension.js.map +1 -1
  155. package/dist/client/session-replay.d.ts.map +1 -1
  156. package/dist/client/session-replay.js +308 -29
  157. package/dist/client/session-replay.js.map +1 -1
  158. package/dist/client/sse-event-processor.d.ts +5 -0
  159. package/dist/client/sse-event-processor.d.ts.map +1 -1
  160. package/dist/client/sse-event-processor.js +37 -1
  161. package/dist/client/sse-event-processor.js.map +1 -1
  162. package/dist/collab/routes.d.ts +1 -1
  163. package/dist/collab/struct-routes.d.ts +1 -1
  164. package/dist/demo/actions/toggle-demo-mode.js +1 -1
  165. package/dist/demo/actions/toggle-demo-mode.js.map +1 -1
  166. package/dist/demo/config.js +6 -6
  167. package/dist/demo/config.js.map +1 -1
  168. package/dist/demo/fetch-interceptor.d.ts.map +1 -1
  169. package/dist/demo/fetch-interceptor.js +13 -4
  170. package/dist/demo/fetch-interceptor.js.map +1 -1
  171. package/dist/demo/redact.d.ts +7 -7
  172. package/dist/demo/redact.d.ts.map +1 -1
  173. package/dist/demo/redact.js +8 -145
  174. package/dist/demo/redact.js.map +1 -1
  175. package/dist/extensions/html-shell.d.ts.map +1 -1
  176. package/dist/extensions/html-shell.js +2 -0
  177. package/dist/extensions/html-shell.js.map +1 -1
  178. package/dist/extensions/session-replay-iframe.d.ts +10 -0
  179. package/dist/extensions/session-replay-iframe.d.ts.map +1 -0
  180. package/dist/extensions/session-replay-iframe.js +121 -0
  181. package/dist/extensions/session-replay-iframe.js.map +1 -0
  182. package/dist/progress/routes.d.ts +1 -1
  183. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  184. package/dist/resources/handlers.d.ts +3 -3
  185. package/dist/scripts/agent-engines/list-agent-engines.d.ts.map +1 -1
  186. package/dist/scripts/agent-engines/list-agent-engines.js +1 -19
  187. package/dist/scripts/agent-engines/list-agent-engines.js.map +1 -1
  188. package/dist/session-replay-iframe-protocol.d.ts +36 -0
  189. package/dist/session-replay-iframe-protocol.d.ts.map +1 -0
  190. package/dist/session-replay-iframe-protocol.js +24 -0
  191. package/dist/session-replay-iframe-protocol.js.map +1 -0
  192. package/dist/shared/reasoning-effort.d.ts +11 -0
  193. package/dist/shared/reasoning-effort.d.ts.map +1 -1
  194. package/dist/shared/reasoning-effort.js +36 -0
  195. package/dist/shared/reasoning-effort.js.map +1 -1
  196. package/dist/styles/agent-conversation.css +26 -0
  197. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +0 -15
  198. package/docs/content/durable-background-runs.mdx +37 -0
  199. package/docs/content/locales/ar-SA/tracking.mdx +2 -0
  200. package/docs/content/locales/de-DE/tracking.mdx +2 -0
  201. package/docs/content/locales/es-ES/tracking.mdx +2 -0
  202. package/docs/content/locales/fr-FR/tracking.mdx +4 -2
  203. package/docs/content/locales/hi-IN/tracking.mdx +2 -0
  204. package/docs/content/locales/ja-JP/tracking.mdx +2 -0
  205. package/docs/content/locales/ko-KR/tracking.mdx +2 -0
  206. package/docs/content/locales/pt-BR/tracking.mdx +2 -0
  207. package/docs/content/locales/zh-CN/tracking.mdx +2 -0
  208. package/docs/content/locales/zh-TW/durable-background-runs.mdx +14 -0
  209. package/docs/content/locales/zh-TW/tracking.mdx +2 -0
  210. package/docs/content/tracking.mdx +2 -0
  211. package/package.json +2 -2
  212. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +0 -15
  213. package/corpus/core/src/observability/hosted-model-experiment.ts +0 -118
  214. package/dist/observability/hosted-model-experiment.d.ts +0 -39
  215. package/dist/observability/hosted-model-experiment.d.ts.map +0 -1
  216. package/dist/observability/hosted-model-experiment.js +0 -90
  217. package/dist/observability/hosted-model-experiment.js.map +0 -1
@@ -57,6 +57,7 @@ import {
57
57
  dispatchPathTargetsNetlifyBackgroundFunction,
58
58
  isAgentChatDurableBackgroundEnabled,
59
59
  isAgentChatForegroundSelfChainEnabled,
60
+ isInBackgroundFunctionRuntime,
60
61
  resolveAgentChatProcessRunDispatchPath,
61
62
  shouldUseBackgroundFunctionTimeoutForWorker,
62
63
  } from "./durable-background.js";
@@ -124,6 +125,8 @@ import {
124
125
  abortRun,
125
126
  abortRunDurably,
126
127
  tryClaimRunSlot,
128
+ isHostedRuntime,
129
+ resolveRunSoftTimeoutMs,
127
130
  } from "./run-manager.js";
128
131
  import type { ActiveRun } from "./run-manager.js";
129
132
  import {
@@ -1054,6 +1057,38 @@ const TOOL_INPUT_ACTIVITY_INTERVAL_MS = 1500;
1054
1057
  const ACTION_PREPARATION_NO_PROGRESS_TIMEOUT_MS = 90_000;
1055
1058
  const ACTION_PREPARATION_ZERO_BYTE_RESTART_LIMIT = 2;
1056
1059
  const MODEL_STREAM_NO_PROGRESS_TIMEOUT_MS = 90_000;
1060
+ /**
1061
+ * FIX 2 (durable-background incident): tighter no-progress deadline for ONLY
1062
+ * the FIRST engine-stream event of a model call, and ONLY on the clamped
1063
+ * HOSTED foreground runtime — `isHostedRuntime()` (run-manager.ts, the same
1064
+ * predicate that selects the 40s soft budget) AND NOT proven to be running
1065
+ * inside a Netlify background function (`isInBackgroundFunctionRuntime`). A
1066
+ * hung first model event previously rode the full
1067
+ * `MODEL_STREAM_NO_PROGRESS_TIMEOUT_MS` (90s) before the in-loop watchdog
1068
+ * could emit `auto_continue` — but the hosted foreground function is killed
1069
+ * around 40s, so that watchdog could never actually fire: the run died as a
1070
+ * silent platform kill instead of a recoverable checkpoint (observed: ~40s of
1071
+ * "Contacting model" with zero tokens, then a hard timeout with no
1072
+ * auto_continue ever emitted). Once ANY event (including a heartbeat) has
1073
+ * been observed for this model call, subsequent gaps revert to the normal 90s
1074
+ * watchdog — this only guards the "nothing has happened yet" window.
1075
+ *
1076
+ * ORDERING INVARIANT (each bound must stay strictly smaller than the next —
1077
+ * do not change one without re-checking the others). This cap exists ONLY
1078
+ * where the 40s ceiling exists: off hosted runtimes (local dev, self-hosted
1079
+ * long-lived Node) `resolveRunSoftTimeoutMs` resolves to 0 (no soft-timeout
1080
+ * regime, no platform wall), a genuinely slow first token — large local
1081
+ * contexts, slow local providers — is legitimate, and the full 90s window
1082
+ * applies unchanged:
1083
+ * FOREGROUND_FIRST_MODEL_EVENT_TIMEOUT_MS (25s, here)
1084
+ * < HOSTED_SOFT_TIMEOUT_CEILING_MS (40s, run-manager.ts)
1085
+ * < MODEL_STREAM_NO_PROGRESS_TIMEOUT_MS (90s, above)
1086
+ * < RUN_NO_PROGRESS_HARD_TIMEOUT_MS (150s, run-manager.ts)
1087
+ *
1088
+ * Background-function runs (proven 15-min budget, no ~40s wall) are likewise
1089
+ * unaffected — they keep the full 90s window for every event, first or not.
1090
+ */
1091
+ const FOREGROUND_FIRST_MODEL_EVENT_TIMEOUT_MS = 25_000;
1057
1092
  // Raised from 1 -> 2 now that each retry actually adapts (raises the token
1058
1093
  // ceiling and steps reasoning effort down a tier) instead of re-issuing the
1059
1094
  // exact same doomed request twice.
@@ -3129,6 +3164,20 @@ export async function runAgentLoop(opts: {
3129
3164
  let zeroByteToolInputRestart: ZeroByteToolInputRestart | undefined;
3130
3165
  let endedForNoProgress = false;
3131
3166
  let lastModelStreamProgressAt = Date.now();
3167
+ // FIX 2: true once ANY engine-stream event (including a heartbeat)
3168
+ // has been retrieved for THIS model call — gates
3169
+ // `FOREGROUND_FIRST_MODEL_EVENT_TIMEOUT_MS` below to only the first
3170
+ // await on a fresh `engine.stream()` call, never subsequent gaps.
3171
+ let hasReceivedFirstEngineEvent = false;
3172
+ // Resolved once per model-call attempt (not per event) so a single
3173
+ // call's deadline math can't observe the runtime predicates changing
3174
+ // mid-flight. Requires BOTH a hosted runtime (where the 40s clamp and
3175
+ // the platform wall behind it exist — local dev/self-hosted resolve
3176
+ // the soft timeout to 0 and legitimately tolerate slow first tokens)
3177
+ // AND not being a proven background-function worker. See
3178
+ // `FOREGROUND_FIRST_MODEL_EVENT_TIMEOUT_MS`.
3179
+ const isClampedForegroundRuntimeForThisCall =
3180
+ isHostedRuntime() && !isInBackgroundFunctionRuntime();
3132
3181
  const sendToolInputActivity = (
3133
3182
  toolName: string | undefined,
3134
3183
  toolInputId?: string,
@@ -3185,8 +3234,23 @@ export async function runAgentLoop(opts: {
3185
3234
  }
3186
3235
  return Number.isFinite(deadlineAt) ? deadlineAt : undefined;
3187
3236
  };
3188
- const modelStreamNoProgressDeadlineAt = () =>
3189
- lastModelStreamProgressAt + MODEL_STREAM_NO_PROGRESS_TIMEOUT_MS;
3237
+ const modelStreamNoProgressDeadlineAt = () => {
3238
+ const baseDeadlineAt =
3239
+ lastModelStreamProgressAt + MODEL_STREAM_NO_PROGRESS_TIMEOUT_MS;
3240
+ // FIX 2: cap the FIRST event's deadline tighter on the clamped
3241
+ // foreground runtime — see FOREGROUND_FIRST_MODEL_EVENT_TIMEOUT_MS
3242
+ // for the ordering invariant this protects.
3243
+ if (
3244
+ hasReceivedFirstEngineEvent ||
3245
+ !isClampedForegroundRuntimeForThisCall
3246
+ ) {
3247
+ return baseDeadlineAt;
3248
+ }
3249
+ return Math.min(
3250
+ baseDeadlineAt,
3251
+ lastModelStreamProgressAt + FOREGROUND_FIRST_MODEL_EVENT_TIMEOUT_MS,
3252
+ );
3253
+ };
3190
3254
  const noProgressDeadlineAt = () => {
3191
3255
  const actionDeadlineAt = actionPreparationDeadlineAt();
3192
3256
  const modelDeadlineAt = modelStreamNoProgressDeadlineAt();
@@ -3306,6 +3370,11 @@ export async function runAgentLoop(opts: {
3306
3370
  eventIteratorDone = true;
3307
3371
  break;
3308
3372
  }
3373
+ // FIX 2: only the deadline for THIS await could have been
3374
+ // tightened by FOREGROUND_FIRST_MODEL_EVENT_TIMEOUT_MS — every
3375
+ // subsequent call in this model call reverts to the normal 90s
3376
+ // watchdog regardless of what kind of event this turned out to be.
3377
+ hasReceivedFirstEngineEvent = true;
3309
3378
  const event = nextEvent.value;
3310
3379
  if (hasNoProgressStalled()) {
3311
3380
  checkpointNoProgress();
@@ -4325,7 +4394,7 @@ export async function runAgentLoop(opts: {
4325
4394
  isError = true;
4326
4395
  }
4327
4396
  mcpApp = mcpResult?.mcpApp;
4328
- // Demo mode: the agent must see the same fake data the UI shows, so
4397
+ // Demo mode: the agent must see the same anonymized data the UI shows, so
4329
4398
  // it can't read out a real name/email on a live screen share. Redact
4330
4399
  // the structured result (not the JSON string) so IDs/dates/URLs stay
4331
4400
  // intact and follow-up tool calls still work. Gated — the expensive
@@ -5019,6 +5088,71 @@ export function shouldChainBackgroundContinuation(opts: {
5019
5088
  );
5020
5089
  }
5021
5090
 
5091
+ /**
5092
+ * Minimum remaining budget (ms) a synchronous self-chain continuation chunk
5093
+ * must have left — after subtracting the wall-clock this invocation has
5094
+ * ALREADY spent on its own pre-`startRun` setup (auth, DB reads, marker
5095
+ * validation) — before it is safe to let it start real agent-loop work at
5096
+ * all. Below this, even a reduced soft-timeout risks the run making an LLM
5097
+ * call that then gets cut off by Netlify's ~60s synchronous-function hard
5098
+ * kill mid-stream instead of checkpointing cleanly — exactly the "run wedged
5099
+ * until stale-run recovery" failure `resolveSelfChainContinuationBudget`
5100
+ * hardens against. 8s leaves enough runway for a small model round trip (or
5101
+ * the soft-timeout timer itself) to still land cleanly before the hard wall.
5102
+ */
5103
+ export const SELF_CHAIN_MIN_CONTINUATION_BUDGET_MS = 8_000;
5104
+
5105
+ /** Result of `resolveSelfChainContinuationBudget`. */
5106
+ export interface SelfChainContinuationBudget {
5107
+ /**
5108
+ * True when there isn't enough wall-clock left in THIS invocation to
5109
+ * safely start real agent-loop work. The caller should skip straight to a
5110
+ * synthetic `run_timeout` boundary (the same one a normal soft-timeout
5111
+ * produces) instead of invoking the loop, so the existing continuation
5112
+ * machinery (`chainServerDrivenContinuation` / the client's `auto_continue`
5113
+ * re-POST fallback) hands off to a fresh invocation instead of risking a
5114
+ * mid-stream kill.
5115
+ */
5116
+ skipToBoundary: boolean;
5117
+ /**
5118
+ * The soft-timeout budget to pass into `startRun` when NOT skipping — the
5119
+ * chunk's normal ceiling minus wall-clock already spent on setup. Always
5120
+ * `<= chunkCeilingMs` and `>= minContinuationBudgetMs` when `skipToBoundary`
5121
+ * is false.
5122
+ */
5123
+ softTimeoutMs: number;
5124
+ }
5125
+
5126
+ /**
5127
+ * Budgets a synchronous (non-`-background`-function) self-chain continuation
5128
+ * chunk against the wall-clock ALREADY spent on this invocation's own setup
5129
+ * before `startRun` is ever called, instead of handing it a fresh
5130
+ * `chunkCeilingMs` window that ignores that setup cost entirely.
5131
+ *
5132
+ * Root cause this hardens against: when `AGENT_CHAT_FOREGROUND_SELF_CHAIN` is
5133
+ * enabled, a continuation is dispatched to the `_process-run` route running as
5134
+ * a REGULAR synchronous serverless function (hard ~60s wall on Netlify). The
5135
+ * handler burns setup time (DB reads, auth, marker validation) before ever
5136
+ * calling `startRun()`, which — unaware of that elapsed time — grants a fresh
5137
+ * ~40s run ceiling. Total handler time (setup + a fresh 40s) can then exceed
5138
+ * the 60s wall, so the function is killed mid-stream instead of checkpointing,
5139
+ * and the run sits "active" until stale-run recovery (~90s later) instead of
5140
+ * cleanly handing off.
5141
+ *
5142
+ * Pure function — no I/O, unit-testable in isolation.
5143
+ */
5144
+ export function resolveSelfChainContinuationBudget(
5145
+ elapsedSinceHandlerEntryMs: number,
5146
+ chunkCeilingMs: number,
5147
+ minContinuationBudgetMs: number = SELF_CHAIN_MIN_CONTINUATION_BUDGET_MS,
5148
+ ): SelfChainContinuationBudget {
5149
+ const remaining = chunkCeilingMs - Math.max(0, elapsedSinceHandlerEntryMs);
5150
+ if (remaining < minContinuationBudgetMs) {
5151
+ return { skipToBoundary: true, softTimeoutMs: 0 };
5152
+ }
5153
+ return { skipToBoundary: false, softTimeoutMs: remaining };
5154
+ }
5155
+
5022
5156
  export async function markBackgroundContinuationChunkTerminal(opts: {
5023
5157
  runId: string;
5024
5158
  continuationReason: AgentLoopContinuationReason;
@@ -6061,10 +6195,7 @@ export function createProductionAgentHandler(
6061
6195
  variantId: string;
6062
6196
  }> = [];
6063
6197
 
6064
- // Database-backed experiments remain available to app operators. A
6065
- // platform-wide hosted rollout is layered in only when no explicit or
6066
- // persisted model selection exists, and only on the cross-provider Builder
6067
- // engine. This preserves the model picker's user intent.
6198
+ // Database-backed experiments remain available to app operators.
6068
6199
  try {
6069
6200
  if (ownerEmail) {
6070
6201
  const { resolveActiveExperimentConfig } =
@@ -6080,22 +6211,6 @@ export function createProductionAgentHandler(
6080
6211
  modelSelectionSource = "experiment";
6081
6212
  }
6082
6213
  }
6083
-
6084
- if (modelSelectionSource === "default") {
6085
- const { resolveHostedDefaultModelExperiment } =
6086
- await import("../observability/hosted-model-experiment.js");
6087
- const hostedExperiment = resolveHostedDefaultModelExperiment({
6088
- userId: ownerEmail,
6089
- engineName: engine.name,
6090
- isDefaultModelSelection: true,
6091
- supportedModels: engine.supportedModels,
6092
- });
6093
- if (hostedExperiment) {
6094
- effectiveModel = hostedExperiment.model;
6095
- experimentAssignments.push(hostedExperiment.assignment);
6096
- modelSelectionSource = "experiment";
6097
- }
6098
- }
6099
6214
  }
6100
6215
  } catch {
6101
6216
  // Experiments are best-effort. Model resolution must keep working if the
@@ -6854,9 +6969,11 @@ export function createProductionAgentHandler(
6854
6969
  turnId: effectiveTurnId,
6855
6970
  }
6856
6971
  : null;
6857
- // Foreground self-chain: gated on hosted runtime + A2A_SECRET, with an
6858
- // explicit env opt-out, AND this specific run never having been routed to
6859
- // the durable background worker. A run that WAS dispatched to background
6972
+ // Foreground self-chain: opt-in only requires the app/deploy to
6973
+ // explicitly set AGENT_CHAT_FOREGROUND_SELF_CHAIN truthy (unset/false
6974
+ // means disabled), on top of the hosted runtime + A2A_SECRET gates, AND
6975
+ // this specific run never having been routed to the durable background
6976
+ // worker. A run that WAS dispatched to background
6860
6977
  // (`dispatchToBackground`) already has its recovery owned by the background
6861
6978
  // circuit-breaker / `isBackgroundWorker` chain above — this flag must never
6862
6979
  // double up with that path, only stand in for it on plain foreground turns. A persistent
@@ -7137,6 +7254,33 @@ export function createProductionAgentHandler(
7137
7254
  (backgroundRuntimeDetail ? ` ${backgroundRuntimeDetail}` : "") +
7138
7255
  firstRequestPayloadDetail;
7139
7256
 
7257
+ // Synchronous self-chain budgeting: this is a `_process-run` re-entry
7258
+ // that landed on the regular ~60s function (not a proven `-background`
7259
+ // function), AND it got here WITHOUT the durable-background path being
7260
+ // active for this app — the only other way to reach `isBackgroundWorker`
7261
+ // is `AGENT_CHAT_FOREGROUND_SELF_CHAIN` chaining onto the framework route
7262
+ // (see `chainServerDrivenContinuation`'s `chainViaDurableBackground`
7263
+ // choice). That chunk must budget its soft-timeout against the wall-clock
7264
+ // THIS invocation already spent on setup above, not a fresh ceiling — see
7265
+ // `resolveSelfChainContinuationBudget`. Deliberately excludes a durable
7266
+ // worker that merely landed on the wrong runtime (handled unchanged by
7267
+ // the existing `runsInBackgroundFunction` clamp below).
7268
+ const isSynchronousSelfChainContinuation =
7269
+ isBackgroundWorker &&
7270
+ !runsInBackgroundFunction &&
7271
+ !isAgentChatDurableBackgroundEnabled({
7272
+ appOptIn: options.durableBackgroundRuns === true,
7273
+ });
7274
+ const selfChainBudget = isSynchronousSelfChainContinuation
7275
+ ? resolveSelfChainContinuationBudget(
7276
+ Date.now() - setupT0,
7277
+ resolveRunSoftTimeoutMs(options.runSoftTimeoutMs, {
7278
+ useHostedDefault: true,
7279
+ backgroundFunction: false,
7280
+ }),
7281
+ )
7282
+ : null;
7283
+
7140
7284
  startRun(
7141
7285
  runId,
7142
7286
  effectiveThreadId,
@@ -7146,6 +7290,25 @@ export function createProductionAgentHandler(
7146
7290
  updateTrackedProgressFromEvent(event);
7147
7291
  };
7148
7292
 
7293
+ if (selfChainBudget?.skipToBoundary) {
7294
+ // Not enough wall-clock left in this synchronous function
7295
+ // invocation to safely start real agent-loop work — doing so
7296
+ // anyway risks Netlify's ~60s hard kill cutting it off mid-stream
7297
+ // (the exact incident `resolveSelfChainContinuationBudget` hardens
7298
+ // against). Skip straight to the same `run_timeout` boundary a
7299
+ // normal soft-timeout produces so the existing continuation
7300
+ // machinery (`chainServerDrivenContinuation` below, plus the
7301
+ // client's `auto_continue` re-POST fallback) hands the turn off to
7302
+ // a fresh invocation instead.
7303
+ await recordRunDiagnostic(
7304
+ runId,
7305
+ RUN_DIAG_STAGE.workerSetupStep,
7306
+ `self_chain_budget_exhausted elapsed=${Date.now() - setupT0}ms`,
7307
+ ).catch(() => {});
7308
+ send({ type: "auto_continue", reason: "run_timeout" });
7309
+ return;
7310
+ }
7311
+
7149
7312
  send({ type: "activity", label: "Starting agent" });
7150
7313
 
7151
7314
  // DIAGNOSTIC: the agent loop body actually started running. For a
@@ -7595,7 +7758,17 @@ export function createProductionAgentHandler(
7595
7758
  },
7596
7759
  handleRunComplete,
7597
7760
  {
7598
- softTimeoutMs: options.runSoftTimeoutMs,
7761
+ // A synchronous self-chain chunk with enough remaining budget gets
7762
+ // that REDUCED budget (ceiling minus setup already spent) instead of
7763
+ // a fresh `options.runSoftTimeoutMs`/hosted-default window — see
7764
+ // `resolveSelfChainContinuationBudget`. Every other caller
7765
+ // (durable-background worker, plain foreground POST) is unaffected:
7766
+ // `selfChainBudget` is `null` for them and this falls through to the
7767
+ // exact prior value.
7768
+ softTimeoutMs:
7769
+ selfChainBudget && !selfChainBudget.skipToBoundary
7770
+ ? selfChainBudget.softTimeoutMs
7771
+ : options.runSoftTimeoutMs,
7599
7772
  useHostedSoftTimeoutDefault: true,
7600
7773
  // Lift the soft-timeout clamp to ~13min ONLY when this run is actually
7601
7774
  // executing inside a real Netlify `-background` function (15-min budget,
@@ -291,7 +291,15 @@ export interface ResolveRunSoftTimeoutOptions {
291
291
  backgroundFunction?: boolean;
292
292
  }
293
293
 
294
- function isHostedRuntime(): boolean {
294
+ /**
295
+ * True on hosted/serverless runtimes where the soft-timeout regime applies
296
+ * (see `resolveRunSoftTimeoutMs`, which resolves to 0 — disabled — off these
297
+ * runtimes). Exported so production-agent.ts can gate its foreground
298
+ * first-model-event cap (`FOREGROUND_FIRST_MODEL_EVENT_TIMEOUT_MS`) on the
299
+ * SAME predicate that selects the 40s clamp: the cap only makes sense where
300
+ * that clamp (and the platform wall behind it) exists.
301
+ */
302
+ export function isHostedRuntime(): boolean {
295
303
  if (
296
304
  process.env.NETLIFY &&
297
305
  process.env.NETLIFY !== "false" &&
@@ -1516,6 +1524,44 @@ export async function getActiveRunForThreadAsync(threadId: string): Promise<{
1516
1524
  const memRun = getActiveRunForThread(threadId);
1517
1525
  if (memRun && (memRun.status === "running" || memRun.events.length > 0)) {
1518
1526
  const sqlSnapshot = await fetchRunThreadSnapshot(memRun.runId, threadId);
1527
+
1528
+ // FIX 1 (durable-background incident): a terminal in-memory run (chunk
1529
+ // completed at a soft-timeout/no-progress/loop-limit boundary, or any
1530
+ // other terminal outcome) never clears `threadToRun` — it stays this
1531
+ // thread's resident in-memory candidate for up to CLEANUP_DELAY_MS
1532
+ // (5 min), and `fetchRunThreadSnapshot` above returns null the instant
1533
+ // SQL's newest row for the thread is no longer THIS run (i.e. a
1534
+ // successor already exists). Left alone, every poll that lands on this
1535
+ // warm isolate would keep falling back to `memRun.status` below and
1536
+ // never discover that a newer, still-running successor already exists
1537
+ // in SQL — exactly the "stale terminal run masks a live successor" bug
1538
+ // that produced the mid-sentence dead turn. Only pay for the extra SQL
1539
+ // read here, in the terminal-candidate branch; the common "still
1540
+ // running" poll (the vast majority) never reaches it.
1541
+ if (!sqlSnapshot && memRun.status !== "running") {
1542
+ const successor = await fetchNewerNonTerminalRunForSameTurn(
1543
+ threadId,
1544
+ memRun,
1545
+ );
1546
+ if (successor) {
1547
+ return {
1548
+ runId: successor.id,
1549
+ threadId: successor.threadId,
1550
+ turnId: successor.turnId ?? successor.id,
1551
+ status: successor.status,
1552
+ heartbeatAt: successor.heartbeatAt ?? successor.startedAt,
1553
+ lastProgressAt: successor.lastProgressAt,
1554
+ dispatchMode: successor.dispatchMode,
1555
+ terminalReason: successor.terminalReason,
1556
+ diagStage: successor.diagStage,
1557
+ // Definitionally non-terminal and freshly read from SQL above —
1558
+ // never the stale in-memory candidate's own state.
1559
+ awaitingRedispatch: false,
1560
+ hasInFlightWork: successor.inFlightSince != null,
1561
+ };
1562
+ }
1563
+ }
1564
+
1519
1565
  const status = sqlSnapshot?.status ?? memRun.status;
1520
1566
  const heartbeatAt =
1521
1567
  status === "running"
@@ -1686,6 +1732,40 @@ async function fetchRunThreadSnapshot(runId: string, threadId: string) {
1686
1732
  }
1687
1733
  }
1688
1734
 
1735
+ /**
1736
+ * FIX 1 (durable-background incident): find a genuinely newer, still-running
1737
+ * SQL row for the SAME turn as a terminal in-memory `ActiveRun` — used only
1738
+ * when `fetchRunThreadSnapshot` found no SQL row matching the in-memory
1739
+ * run's own id (i.e. SQL's newest row for the thread is a different run).
1740
+ * `getRunByThread` always returns the thread's newest row by `started_at`,
1741
+ * so this is the same read `fetchRunThreadSnapshot` already made; we just
1742
+ * don't throw its result away when the id doesn't match.
1743
+ *
1744
+ * Scoped to the SAME `turnId` (not just the same thread) so an unrelated,
1745
+ * later user turn on the same thread is never mistaken for a continuation
1746
+ * successor of this one.
1747
+ */
1748
+ async function fetchNewerNonTerminalRunForSameTurn(
1749
+ threadId: string,
1750
+ memRun: ActiveRun,
1751
+ ): Promise<Awaited<ReturnType<typeof getRunByThread>> | null> {
1752
+ try {
1753
+ const latest = await getRunByThread(threadId, { includeTerminal: true });
1754
+ if (
1755
+ latest &&
1756
+ latest.id !== memRun.runId &&
1757
+ latest.status === "running" &&
1758
+ latest.startedAt > memRun.startedAt &&
1759
+ (latest.turnId ?? latest.id) === memRun.turnId
1760
+ ) {
1761
+ return latest;
1762
+ }
1763
+ return null;
1764
+ } catch {
1765
+ return null;
1766
+ }
1767
+ }
1768
+
1689
1769
  /** Get a run by ID */
1690
1770
  export function getRun(runId: string): ActiveRun | null {
1691
1771
  return activeRuns.get(runId) ?? null;