@agent-native/core 0.70.3 → 0.72.0

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 (221) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +51 -0
  3. package/corpus/core/docs/content/actions.md +23 -0
  4. package/corpus/core/docs/content/audit-log.md +111 -0
  5. package/corpus/core/docs/content/multi-app-workspace.md +2 -2
  6. package/corpus/core/docs/design/durable-agent-runs.md +458 -4
  7. package/corpus/core/package.json +2 -1
  8. package/corpus/core/src/action.ts +80 -1
  9. package/corpus/core/src/agent/durable-background.ts +192 -0
  10. package/corpus/core/src/agent/production-agent.ts +357 -13
  11. package/corpus/core/src/agent/run-manager.ts +66 -3
  12. package/corpus/core/src/agent/run-store.ts +143 -23
  13. package/corpus/core/src/agent/types.ts +20 -0
  14. package/corpus/core/src/application-state/store.ts +5 -0
  15. package/corpus/core/src/audit/actions/get-audit-event.ts +23 -0
  16. package/corpus/core/src/audit/actions/list-audit-events.ts +65 -0
  17. package/corpus/core/src/audit/cleanup-job.ts +100 -0
  18. package/corpus/core/src/audit/config.ts +91 -0
  19. package/corpus/core/src/audit/index.ts +43 -0
  20. package/corpus/core/src/audit/record.ts +143 -0
  21. package/corpus/core/src/audit/redact.ts +109 -0
  22. package/corpus/core/src/audit/store.ts +244 -0
  23. package/corpus/core/src/audit/types.ts +125 -0
  24. package/corpus/core/src/chat-threads/store.ts +10 -0
  25. package/corpus/core/src/client/AgentPanel.tsx +12 -9
  26. package/corpus/core/src/client/AssistantChat.tsx +6 -0
  27. package/corpus/core/src/client/blocks/library/FileTreeBlock.tsx +72 -14
  28. package/corpus/core/src/client/chat/repo-helpers.ts +38 -0
  29. package/corpus/core/src/db/client.ts +4 -0
  30. package/corpus/core/src/db/widen-columns.ts +75 -0
  31. package/corpus/core/src/deploy/build.ts +96 -0
  32. package/corpus/core/src/deploy/workspace-deploy.ts +121 -0
  33. package/corpus/core/src/mcp/build-server.ts +23 -13
  34. package/corpus/core/src/oauth-tokens/store.ts +6 -0
  35. package/corpus/core/src/provider-api/custom-registry.ts +8 -0
  36. package/corpus/core/src/resources/store.ts +10 -0
  37. package/corpus/core/src/scripts/runner.ts +6 -3
  38. package/corpus/core/src/server/action-discovery.ts +6 -0
  39. package/corpus/core/src/server/action-routes.ts +1 -0
  40. package/corpus/core/src/server/agent-chat-plugin.ts +133 -66
  41. package/corpus/core/src/server/auth.ts +5 -0
  42. package/corpus/core/src/server/core-routes-plugin.ts +14 -0
  43. package/corpus/core/src/server/security-headers.ts +9 -6
  44. package/corpus/core/src/settings/store.ts +6 -0
  45. package/corpus/core/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
  46. package/corpus/core/src/usage/store.ts +6 -0
  47. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +19 -0
  48. package/corpus/templates/analytics/AGENTS.md +8 -0
  49. package/corpus/templates/analytics/actions/compose-dashboard.ts +317 -0
  50. package/corpus/templates/analytics/changelog/2026-06-23-build-large-first-party-analytics-dashboards-in-one-fast-cal.md +6 -0
  51. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +574 -0
  52. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +60 -6
  53. package/corpus/templates/calendar/changelog/2026-06-23-added-a-heads-up-explaining-google-s-app-not-verified-screen.md +6 -0
  54. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +20 -43
  55. package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +5 -1
  56. package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +3 -51
  57. package/corpus/templates/clips/changelog/2026-06-23-dragging-the-desktop-camera-bubble-now-glides-to-a-stop-at-t.md +6 -0
  58. package/corpus/templates/clips/changelog/2026-06-23-public-clips-now-play-inline-in-slack-connect-a-workspac.md +6 -0
  59. package/corpus/templates/clips/chrome-extension/PERMISSIONS.md +72 -0
  60. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -10
  61. package/corpus/templates/clips/chrome-extension/src/background.ts +163 -32
  62. package/corpus/templates/clips/chrome-extension/src/content-script.ts +301 -24
  63. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +245 -67
  64. package/corpus/templates/clips/chrome-extension/src/overlay.css +77 -43
  65. package/corpus/templates/clips/chrome-extension/src/overlay.html +7 -1
  66. package/corpus/templates/clips/chrome-extension/src/overlay.ts +102 -38
  67. package/corpus/templates/clips/chrome-extension/src/popup.html +62 -5
  68. package/corpus/templates/clips/chrome-extension/src/popup.ts +275 -4
  69. package/corpus/templates/clips/chrome-extension/src/styles.css +34 -0
  70. package/corpus/templates/clips/chrome-extension/vite.config.ts +5 -0
  71. package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +21 -16
  72. package/corpus/templates/clips/desktop/src/lib/recorder.ts +10 -17
  73. package/corpus/templates/clips/desktop/src/overlays/bubble.tsx +73 -23
  74. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +0 -6
  75. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +56 -46
  76. package/corpus/templates/clips/desktop/src/styles.css +33 -5
  77. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +113 -0
  78. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
  79. package/corpus/templates/clips/server/lib/media-permissions.ts +5 -1
  80. package/corpus/templates/clips/shared/recording-audio.ts +62 -0
  81. package/corpus/templates/clips/shared/recording-core.ts +94 -0
  82. package/dist/action.d.ts +31 -0
  83. package/dist/action.d.ts.map +1 -1
  84. package/dist/action.js +45 -1
  85. package/dist/action.js.map +1 -1
  86. package/dist/agent/context-xray/schema.d.ts +1 -1
  87. package/dist/agent/durable-background.d.ts +60 -0
  88. package/dist/agent/durable-background.d.ts.map +1 -0
  89. package/dist/agent/durable-background.js +144 -0
  90. package/dist/agent/durable-background.js.map +1 -0
  91. package/dist/agent/observational-memory/schema.d.ts +1 -1
  92. package/dist/agent/production-agent.d.ts +20 -0
  93. package/dist/agent/production-agent.d.ts.map +1 -1
  94. package/dist/agent/production-agent.js +320 -14
  95. package/dist/agent/production-agent.js.map +1 -1
  96. package/dist/agent/run-manager.d.ts +48 -0
  97. package/dist/agent/run-manager.d.ts.map +1 -1
  98. package/dist/agent/run-manager.js +45 -3
  99. package/dist/agent/run-manager.js.map +1 -1
  100. package/dist/agent/run-store.d.ts +30 -1
  101. package/dist/agent/run-store.d.ts.map +1 -1
  102. package/dist/agent/run-store.js +138 -24
  103. package/dist/agent/run-store.js.map +1 -1
  104. package/dist/agent/types.d.ts +20 -0
  105. package/dist/agent/types.d.ts.map +1 -1
  106. package/dist/agent/types.js.map +1 -1
  107. package/dist/application-state/store.d.ts.map +1 -1
  108. package/dist/application-state/store.js +5 -0
  109. package/dist/application-state/store.js.map +1 -1
  110. package/dist/audit/actions/get-audit-event.d.ts +11 -0
  111. package/dist/audit/actions/get-audit-event.d.ts.map +1 -0
  112. package/dist/audit/actions/get-audit-event.js +22 -0
  113. package/dist/audit/actions/get-audit-event.js.map +1 -0
  114. package/dist/audit/actions/list-audit-events.d.ts +22 -0
  115. package/dist/audit/actions/list-audit-events.d.ts.map +1 -0
  116. package/dist/audit/actions/list-audit-events.js +61 -0
  117. package/dist/audit/actions/list-audit-events.js.map +1 -0
  118. package/dist/audit/cleanup-job.d.ts +12 -0
  119. package/dist/audit/cleanup-job.d.ts.map +1 -0
  120. package/dist/audit/cleanup-job.js +93 -0
  121. package/dist/audit/cleanup-job.js.map +1 -0
  122. package/dist/audit/config.d.ts +30 -0
  123. package/dist/audit/config.d.ts.map +1 -0
  124. package/dist/audit/config.js +65 -0
  125. package/dist/audit/config.js.map +1 -0
  126. package/dist/audit/index.d.ts +13 -0
  127. package/dist/audit/index.d.ts.map +1 -0
  128. package/dist/audit/index.js +6 -0
  129. package/dist/audit/index.js.map +1 -0
  130. package/dist/audit/record.d.ts +24 -0
  131. package/dist/audit/record.d.ts.map +1 -0
  132. package/dist/audit/record.js +106 -0
  133. package/dist/audit/record.js.map +1 -0
  134. package/dist/audit/redact.d.ts +28 -0
  135. package/dist/audit/redact.d.ts.map +1 -0
  136. package/dist/audit/redact.js +109 -0
  137. package/dist/audit/redact.js.map +1 -0
  138. package/dist/audit/store.d.ts +14 -0
  139. package/dist/audit/store.d.ts.map +1 -0
  140. package/dist/audit/store.js +219 -0
  141. package/dist/audit/store.js.map +1 -0
  142. package/dist/audit/types.d.ts +114 -0
  143. package/dist/audit/types.d.ts.map +1 -0
  144. package/dist/audit/types.js +15 -0
  145. package/dist/audit/types.js.map +1 -0
  146. package/dist/chat-threads/store.d.ts.map +1 -1
  147. package/dist/chat-threads/store.js +10 -0
  148. package/dist/chat-threads/store.js.map +1 -1
  149. package/dist/client/AgentPanel.d.ts.map +1 -1
  150. package/dist/client/AgentPanel.js +2 -2
  151. package/dist/client/AgentPanel.js.map +1 -1
  152. package/dist/client/AssistantChat.d.ts.map +1 -1
  153. package/dist/client/AssistantChat.js +6 -1
  154. package/dist/client/AssistantChat.js.map +1 -1
  155. package/dist/client/blocks/library/FileTreeBlock.d.ts.map +1 -1
  156. package/dist/client/blocks/library/FileTreeBlock.js +37 -4
  157. package/dist/client/blocks/library/FileTreeBlock.js.map +1 -1
  158. package/dist/client/chat/repo-helpers.d.ts +16 -0
  159. package/dist/client/chat/repo-helpers.d.ts.map +1 -1
  160. package/dist/client/chat/repo-helpers.js +40 -0
  161. package/dist/client/chat/repo-helpers.js.map +1 -1
  162. package/dist/db/client.d.ts.map +1 -1
  163. package/dist/db/client.js +3 -0
  164. package/dist/db/client.js.map +1 -1
  165. package/dist/db/widen-columns.d.ts +39 -0
  166. package/dist/db/widen-columns.d.ts.map +1 -0
  167. package/dist/db/widen-columns.js +73 -0
  168. package/dist/db/widen-columns.js.map +1 -0
  169. package/dist/deploy/build.d.ts +29 -0
  170. package/dist/deploy/build.d.ts.map +1 -1
  171. package/dist/deploy/build.js +85 -0
  172. package/dist/deploy/build.js.map +1 -1
  173. package/dist/deploy/workspace-deploy.d.ts.map +1 -1
  174. package/dist/deploy/workspace-deploy.js +108 -0
  175. package/dist/deploy/workspace-deploy.js.map +1 -1
  176. package/dist/mcp/build-server.d.ts.map +1 -1
  177. package/dist/mcp/build-server.js +24 -9
  178. package/dist/mcp/build-server.js.map +1 -1
  179. package/dist/oauth-tokens/store.d.ts.map +1 -1
  180. package/dist/oauth-tokens/store.js +6 -0
  181. package/dist/oauth-tokens/store.js.map +1 -1
  182. package/dist/provider-api/custom-registry.d.ts.map +1 -1
  183. package/dist/provider-api/custom-registry.js +8 -0
  184. package/dist/provider-api/custom-registry.js.map +1 -1
  185. package/dist/resources/store.d.ts.map +1 -1
  186. package/dist/resources/store.js +9 -0
  187. package/dist/resources/store.js.map +1 -1
  188. package/dist/scripts/runner.js +4 -3
  189. package/dist/scripts/runner.js.map +1 -1
  190. package/dist/server/action-discovery.d.ts.map +1 -1
  191. package/dist/server/action-discovery.js +6 -0
  192. package/dist/server/action-discovery.js.map +1 -1
  193. package/dist/server/action-routes.d.ts.map +1 -1
  194. package/dist/server/action-routes.js +1 -0
  195. package/dist/server/action-routes.js.map +1 -1
  196. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  197. package/dist/server/agent-chat-plugin.js +68 -13
  198. package/dist/server/agent-chat-plugin.js.map +1 -1
  199. package/dist/server/auth.d.ts.map +1 -1
  200. package/dist/server/auth.js +5 -0
  201. package/dist/server/auth.js.map +1 -1
  202. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  203. package/dist/server/core-routes-plugin.js +13 -0
  204. package/dist/server/core-routes-plugin.js.map +1 -1
  205. package/dist/server/security-headers.d.ts +8 -5
  206. package/dist/server/security-headers.d.ts.map +1 -1
  207. package/dist/server/security-headers.js +9 -6
  208. package/dist/server/security-headers.js.map +1 -1
  209. package/dist/settings/store.d.ts.map +1 -1
  210. package/dist/settings/store.js +6 -0
  211. package/dist/settings/store.js.map +1 -1
  212. package/dist/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
  213. package/dist/usage/store.d.ts.map +1 -1
  214. package/dist/usage/store.js +5 -0
  215. package/dist/usage/store.js.map +1 -1
  216. package/docs/content/actions.md +23 -0
  217. package/docs/content/audit-log.md +111 -0
  218. package/docs/content/multi-app-workspace.md +2 -2
  219. package/docs/design/durable-agent-runs.md +458 -4
  220. package/package.json +2 -1
  221. package/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
@@ -12,6 +12,8 @@ import { isDemoModeEnabled } from "../demo/config.js";
12
12
  import { redactDemoData, redactDemoString } from "../demo/redact.js";
13
13
  import { redactSensitiveFields, sanitizeToolErrorText, sanitizeToolErrorValue, } from "./tool-error-redaction.js";
14
14
  import { startRun, subscribeToRun, getActiveRunForThread, getActiveRunForThreadAsync, getRun, abortRun, tryClaimRunSlot, } from "./run-manager.js";
15
+ import { AGENT_CHAT_PROCESS_RUN_PATH, AGENT_CHAT_BACKGROUND_RUN_FIELD, isAgentChatDurableBackgroundEnabled, } from "./durable-background.js";
16
+ import { fireInternalDispatch } from "../server/self-dispatch.js";
15
17
  import { readBody } from "../server/h3-helpers.js";
16
18
  import { isReadOnlyShellCommand } from "../coding-tools/index.js";
17
19
  import { getRequestRunContext, ensureRequestRunContext, getRequestOrgId, getRequestUserEmail, } from "../server/request-context.js";
@@ -22,7 +24,7 @@ import { createToolSearchEntry, TOOL_SEARCH_ACTION_NAME, } from "./tool-search.j
22
24
  import { getDefaultMaxIterations, normalizeMaxIterations, readAgentLoopSettings, } from "./loop-settings.js";
23
25
  import { isReasoningEffort, normalizeReasoningEffortForModel, } from "../shared/reasoning-effort.js";
24
26
  import { isAgentActionStopError } from "../action.js";
25
- import { writeLedgerEntry, readLedgerEntry, clearLedgerForThread, getCurrentTurnEventsForThread, } from "./run-store.js";
27
+ import { writeLedgerEntry, readLedgerEntry, clearLedgerForThread, getCurrentTurnEventsForThread, insertRun, updateRunHeartbeat, updateRunStatusIfRunning, claimBackgroundRun, } from "./run-store.js";
26
28
  import { classifyToolCallJournal, findCompletedJournalEntry, } from "./tool-call-journal.js";
27
29
  import { preUploadAttachments } from "../file-upload/pre-upload-attachments.js";
28
30
  import { extensionIdFromPathname } from "../extensions/path.js";
@@ -2158,6 +2160,29 @@ export async function runAgentLoop(opts) {
2158
2160
  approvalKey,
2159
2161
  ...(toolCall.id ? { toolCallId: toolCall.id } : {}),
2160
2162
  });
2163
+ // Audit the blocked attempt: the action did NOT run, but "the agent
2164
+ // tried to do X and was gated" is itself worth recording. Best-effort,
2165
+ // but AWAITED (not fire-and-forget) so the row isn't lost to a
2166
+ // serverless freeze / request teardown when the turn pauses.
2167
+ try {
2168
+ const { recordActionAudit } = await import("../audit/record.js");
2169
+ await recordActionAudit({
2170
+ config: undefined,
2171
+ args: toolCall.input,
2172
+ ctx: {
2173
+ actionName: toolCall.name,
2174
+ caller: "tool",
2175
+ userEmail: getRequestUserEmail(),
2176
+ orgId: getRequestOrgId() ?? null,
2177
+ ...(opts.threadId ? { threadId: opts.threadId } : {}),
2178
+ ...(opts.turnId ? { turnId: opts.turnId } : {}),
2179
+ },
2180
+ status: "denied",
2181
+ });
2182
+ }
2183
+ catch {
2184
+ // Best-effort — auditing must never break the approval pause.
2185
+ }
2161
2186
  const result = `Awaiting human approval to run "${toolCall.name}". This action did ` +
2162
2187
  `NOT execute — a human must approve this specific call before it ` +
2163
2188
  `can run. The turn is paused; do not retry.`;
@@ -2378,6 +2403,11 @@ export async function runAgentLoop(opts) {
2378
2403
  caller: "tool",
2379
2404
  attachments: opts.attachments,
2380
2405
  signal,
2406
+ // Audit attribution: the action name + the agent thread/turn that
2407
+ // triggered this call, so a mutation can be traced to its run.
2408
+ actionName: toolCall.name,
2409
+ ...(opts.threadId ? { threadId: opts.threadId } : {}),
2410
+ ...(opts.turnId ? { turnId: opts.turnId } : {}),
2381
2411
  }));
2382
2412
  // When the run is aborted (soft-timeout / user cancel) while this tool
2383
2413
  // call is in flight, Promise.race below will throw "Run aborted" and the
@@ -2675,6 +2705,27 @@ function endsAtInternalContinuationBoundary(run) {
2675
2705
  }
2676
2706
  return last.type === "error" && isRecoverableContinuationError(last);
2677
2707
  }
2708
+ /**
2709
+ * Hard cap on server-driven background→background continuation chunks for a
2710
+ * single logical turn. A `backgroundFunction` run gets a ~13-min soft timeout,
2711
+ * so reaching this boundary at all is the rare exception (most turns finish in
2712
+ * one chunk). The cap bounds a pathological turn that would otherwise chain
2713
+ * background invocations forever, mirroring `MAX_AGENT_TEAM_CONTINUATIONS`.
2714
+ */
2715
+ export const MAX_BACKGROUND_RUN_CONTINUATIONS = 20;
2716
+ /**
2717
+ * Whether the background worker should self-fire the next server-driven
2718
+ * continuation chunk. True only when this is a background worker run that ended
2719
+ * at a recoverable soft-timeout boundary (not aborted/stopped) and the chain is
2720
+ * still under its budget. Extracted so the decision is unit testable without
2721
+ * booting the whole handler.
2722
+ */
2723
+ export function shouldChainBackgroundContinuation(opts) {
2724
+ return (opts.isBackgroundWorker &&
2725
+ opts.run.status !== "aborted" &&
2726
+ endsAtInternalContinuationBoundary(opts.run) &&
2727
+ opts.continuationCount < MAX_BACKGROUND_RUN_CONTINUATIONS);
2728
+ }
2678
2729
  function progressStepFromAgentChatEvent(event) {
2679
2730
  switch (event.type) {
2680
2731
  case "activity":
@@ -2729,14 +2780,46 @@ export function createProductionAgentHandler(options) {
2729
2780
  return { error: "Method not allowed" };
2730
2781
  }
2731
2782
  let body;
2732
- try {
2733
- body = await readBody(event);
2783
+ // The durable-background `_process-run` route already consumed and verified
2784
+ // the request body (h3 v2's web Request body stream is single-use, so a
2785
+ // second readBody would fail). It stashes the verified+augmented body here
2786
+ // so this re-entered handler reads it instead of the spent stream.
2787
+ const preInjectedBody = event?.context
2788
+ ?.__agentChatBackgroundBody;
2789
+ if (preInjectedBody && typeof preInjectedBody === "object") {
2790
+ body = preInjectedBody;
2734
2791
  }
2735
- catch {
2736
- setResponseStatus(event, 400);
2737
- return { error: "Invalid request body" };
2792
+ else {
2793
+ try {
2794
+ body = await readBody(event);
2795
+ }
2796
+ catch {
2797
+ setResponseStatus(event, 400);
2798
+ return { error: "Invalid request body" };
2799
+ }
2738
2800
  }
2739
2801
  const { message, history = [], structuredHistory, references = [], threadId, attachments, displayMessage, internalContinuation, turnId: requestTurnId, model: requestModel, engine: requestEngine, effort: requestEffort, browserTabId, scope, trackInRunsTray, } = body;
2802
+ // Durable-background marker. Present ONLY when this handler was re-entered
2803
+ // as the Netlify background worker via the `_process-run` self-dispatch
2804
+ // (the route HMAC-verifies the dispatch before invoking us). When set, we
2805
+ // run the loop inline with the background soft-timeout, reusing the
2806
+ // pre-claimed runId/turnId — we must NOT re-claim the slot or re-dispatch.
2807
+ const backgroundRunMarker = body[AGENT_CHAT_BACKGROUND_RUN_FIELD] &&
2808
+ typeof body[AGENT_CHAT_BACKGROUND_RUN_FIELD] === "object" &&
2809
+ typeof body[AGENT_CHAT_BACKGROUND_RUN_FIELD].runId === "string"
2810
+ ? body[AGENT_CHAT_BACKGROUND_RUN_FIELD]
2811
+ : null;
2812
+ const isBackgroundWorker = backgroundRunMarker !== null;
2813
+ // How many server-driven background continuations have already chained into
2814
+ // this logical turn (0 on the first chunk). Used to bound the chain.
2815
+ const backgroundContinuationCount = isBackgroundWorker &&
2816
+ typeof backgroundRunMarker?.continuationCount === "number" &&
2817
+ Number.isFinite(backgroundRunMarker.continuationCount)
2818
+ ? Math.max(0, Math.floor(backgroundRunMarker.continuationCount))
2819
+ : 0;
2820
+ // The foreground POST decides whether to dispatch into a background
2821
+ // function. The background worker itself never re-dispatches.
2822
+ const dispatchToBackground = !isBackgroundWorker && isAgentChatDurableBackgroundEnabled();
2740
2823
  const requestBrowserTabId = normalizeBrowserTabId(browserTabId);
2741
2824
  const requestChatScope = normalizeChatScope(scope);
2742
2825
  const requestRunCtx = ensureRequestRunContext();
@@ -3154,7 +3237,11 @@ export function createProductionAgentHandler(options) {
3154
3237
  // different serverless isolates both see the correct state — a plain
3155
3238
  // read-then-act check races on multi-isolate deployments because both
3156
3239
  // reads see no running row before either insert commits.
3157
- if (threadId) {
3240
+ //
3241
+ // The background worker SKIPS this: the foreground POST already claimed the
3242
+ // slot and inserted the run row before dispatching, so re-claiming here
3243
+ // would falsely 409 against the row the foreground holds.
3244
+ if (threadId && !isBackgroundWorker) {
3158
3245
  const slot = await tryClaimRunSlot(threadId);
3159
3246
  if (!slot.claimed) {
3160
3247
  setResponseStatus(event, 409);
@@ -3164,17 +3251,62 @@ export function createProductionAgentHandler(options) {
3164
3251
  };
3165
3252
  }
3166
3253
  }
3167
- // Start agent loop in background via run-manager
3168
- const runId = generateRunId();
3254
+ // Start agent loop in background via run-manager. The background worker
3255
+ // reuses the runId carried in the marker (signed into the dispatch token):
3256
+ // - First background chunk (count 0): the foreground generated + INSERTED
3257
+ // this runId, so the event stream the client is already subscribed to is
3258
+ // the one we write to.
3259
+ // - Chained continuation chunk (count > 0): the prior chunk minted a FRESH
3260
+ // runId for this one (a reused runId would restart `startRun`'s in-memory
3261
+ // seq log at 0 and collide with the prior chunk's persisted seqs, which
3262
+ // insertRunEvent's ON CONFLICT would drop — making the continuation
3263
+ // invisible). A fresh runId on the SAME thread + SAME turnId folds onto
3264
+ // one assistant message and is surfaced by the existing
3265
+ // `/runs/active?threadId` reconnect path. The continuation worker inserts
3266
+ // its own background row below (the foreground only inserted chunk-0's).
3267
+ const isChainedBackgroundContinuation = isBackgroundWorker && backgroundContinuationCount > 0;
3268
+ const runId = backgroundRunMarker?.runId ?? generateRunId();
3169
3269
  const effectiveThreadId = threadId ?? runId;
3170
- const effectiveTurnId = typeof requestTurnId === "string" && requestTurnId.trim()
3171
- ? requestTurnId.trim()
3172
- : runId;
3270
+ const effectiveTurnId = typeof backgroundRunMarker?.turnId === "string" &&
3271
+ backgroundRunMarker.turnId.trim()
3272
+ ? backgroundRunMarker.turnId.trim()
3273
+ : typeof requestTurnId === "string" && requestTurnId.trim()
3274
+ ? requestTurnId.trim()
3275
+ : runId;
3173
3276
  const messageToPersist = typeof requestDisplayMessage === "string" &&
3174
3277
  requestDisplayMessage.trim().length > 0
3175
3278
  ? requestDisplayMessage
3176
3279
  : requestMessage;
3177
- if (options.onRunPrepared && !internalContinuation) {
3280
+ // Server-driven background continuation: when the background worker re-fired
3281
+ // itself at a soft-timeout boundary (a chained continuation chunk), rebuild
3282
+ // the conversation from the persisted thread_data so the next chunk resumes
3283
+ // from committed progress instead of restarting from the original user
3284
+ // message (which would re-do work — the re-hydration thrash the design doc
3285
+ // calls out). Mirrors the agent-teams continuation, which seeds from
3286
+ // thread_data + appends a continuation nudge. Falls back to the body-derived
3287
+ // `messages` if thread_data is empty/unreadable — a continuation that
3288
+ // restarts is worse than one that resumes, but both are correct.
3289
+ if (isChainedBackgroundContinuation && effectiveThreadId) {
3290
+ try {
3291
+ const { getThread } = await import("../chat-threads/store.js");
3292
+ const { threadDataToEngineMessages } = await import("./thread-data-builder.js");
3293
+ const priorThreadData = (await getThread(effectiveThreadId))
3294
+ ?.threadData;
3295
+ const resumed = threadDataToEngineMessages(priorThreadData);
3296
+ if (resumed.length > 0) {
3297
+ appendAgentLoopContinuation(resumed, "run_timeout");
3298
+ messages.length = 0;
3299
+ messages.push(...resumed);
3300
+ }
3301
+ }
3302
+ catch {
3303
+ // Keep the body-derived messages — never drop the run.
3304
+ }
3305
+ }
3306
+ // Persist the user's turn exactly once. The foreground POST does this
3307
+ // before dispatching; the background worker must NOT repeat it (it re-enters
3308
+ // with the same body, which would double-persist the user message).
3309
+ if (options.onRunPrepared && !internalContinuation && !isBackgroundWorker) {
3178
3310
  await options.onRunPrepared({
3179
3311
  runId,
3180
3312
  threadId,
@@ -3182,6 +3314,73 @@ export function createProductionAgentHandler(options) {
3182
3314
  attachments: requestAttachments,
3183
3315
  });
3184
3316
  }
3317
+ // ─── Durable-background dispatch decision ──────────────────────────────
3318
+ // Flag active (hosted + A2A_SECRET + AGENT_CHAT_DURABLE_BACKGROUND) and we
3319
+ // are the FOREGROUND POST: insert the run row (marked background), fire an
3320
+ // HMAC-signed self-dispatch into the Netlify background function (15-min
3321
+ // budget), and return the SSE subscription immediately. The client streams
3322
+ // the same events via the cross-isolate SQL-poll path with no client
3323
+ // change. With the flag OFF this whole branch is skipped and the inline
3324
+ // `startRun` path below runs exactly as before (byte-for-byte).
3325
+ if (dispatchToBackground) {
3326
+ try {
3327
+ // Insert the run row up front so /runs/active sees it immediately and
3328
+ // the slot stays held while the background function cold-starts. Mark
3329
+ // it background-dispatched so the stale reaper uses the wider window.
3330
+ await insertRun(runId, effectiveThreadId, effectiveTurnId, {
3331
+ dispatchMode: "background",
3332
+ });
3333
+ }
3334
+ catch (err) {
3335
+ // A duplicate-PK collision means the row already exists (ret­ried POST);
3336
+ // any other failure means we can't safely hand off — fall back to the
3337
+ // inline path rather than dropping the turn.
3338
+ console.error("[agent-chat] background insertRun failed; falling back to inline:", err instanceof Error ? err.message : err);
3339
+ }
3340
+ let dispatched = false;
3341
+ try {
3342
+ await fireInternalDispatch({
3343
+ event,
3344
+ path: AGENT_CHAT_PROCESS_RUN_PATH,
3345
+ taskId: runId,
3346
+ body: {
3347
+ ...body,
3348
+ // Carry the pre-claimed identity so the worker reuses this run.
3349
+ [AGENT_CHAT_BACKGROUND_RUN_FIELD]: {
3350
+ runId,
3351
+ turnId: effectiveTurnId,
3352
+ },
3353
+ },
3354
+ });
3355
+ dispatched = true;
3356
+ }
3357
+ catch (err) {
3358
+ console.error("[agent-chat] background dispatch failed; falling back to inline:", err instanceof Error ? err.message : err);
3359
+ }
3360
+ if (dispatched) {
3361
+ const stream = subscribeToRun(runId, 0);
3362
+ if (stream) {
3363
+ setResponseHeader(event, "Content-Type", "text/event-stream");
3364
+ setResponseHeader(event, "Cache-Control", "no-cache");
3365
+ setResponseHeader(event, "Connection", "keep-alive");
3366
+ setResponseHeader(event, "X-Run-Id", runId);
3367
+ return stream;
3368
+ }
3369
+ // Subscription failed even though dispatch landed — surface an error
3370
+ // rather than silently running inline (the background worker is already
3371
+ // processing this runId, so an inline second run would double-execute).
3372
+ setResponseStatus(event, 500);
3373
+ return { error: "Failed to subscribe to background run" };
3374
+ }
3375
+ // Dispatch failed before any worker could claim the run. Fail loud: flip
3376
+ // the row terminal so the held slot is released (and any reconnect sees a
3377
+ // terminal status instead of spinning), then 500 so the client can retry.
3378
+ // We do NOT silently fall through to inline here — that risks a later,
3379
+ // delayed background delivery double-executing the same runId.
3380
+ await updateRunStatusIfRunning(runId, "errored").catch(() => { });
3381
+ setResponseStatus(event, 500);
3382
+ return { error: "Failed to dispatch background run" };
3383
+ }
3185
3384
  const trackedProgressOwner = trackInRunsTray === true && ownerEmail ? ownerEmail : null;
3186
3385
  const trackedProgressRunId = trackedProgressOwner
3187
3386
  ? backgroundChatProgressRunId(effectiveTurnId)
@@ -3246,7 +3445,18 @@ export function createProductionAgentHandler(options) {
3246
3445
  metadata: trackedProgressMetadata ?? undefined,
3247
3446
  }).catch(() => { });
3248
3447
  }
3249
- const handleRunComplete = options.onRunComplete || trackedProgressRunId
3448
+ // The background worker must AWAIT the run to completion before returning,
3449
+ // or Netlify freezes/kills the function the instant the handler returns and
3450
+ // the detached run dies mid-turn (mirrors the agent-teams processor, which
3451
+ // wraps startRun in `await new Promise(resolve => startRun(..., onComplete:
3452
+ // () => resolve()))`). We resolve this when the run's onComplete fires.
3453
+ let resolveBackgroundRunDone = null;
3454
+ const backgroundRunDone = isBackgroundWorker
3455
+ ? new Promise((resolve) => {
3456
+ resolveBackgroundRunDone = resolve;
3457
+ })
3458
+ : null;
3459
+ const baseHandleRunComplete = options.onRunComplete || trackedProgressRunId
3250
3460
  ? async (run) => {
3251
3461
  try {
3252
3462
  await options.onRunComplete?.(run, threadId);
@@ -3258,6 +3468,89 @@ export function createProductionAgentHandler(options) {
3258
3468
  await completeTrackedProgressRun(run);
3259
3469
  }
3260
3470
  : undefined;
3471
+ // Wrap so the background worker is unblocked even when there is no app
3472
+ // onRunComplete / tracked-progress callback configured.
3473
+ const handleRunComplete = isBackgroundWorker || baseHandleRunComplete
3474
+ ? async (run) => {
3475
+ try {
3476
+ // Persist the (partial) assistant turn to thread_data FIRST — the
3477
+ // server-driven continuation below rebuilds from it, so it must be
3478
+ // committed before we re-fire.
3479
+ await baseHandleRunComplete?.(run);
3480
+ // Server-driven background→background continuation. If this chunk
3481
+ // hit its soft-timeout still unfinished (ended at an auto_continue
3482
+ // / loop_limit / recoverable boundary), chain the next chunk by
3483
+ // re-firing the `_process-run` self-dispatch with mode "continue"
3484
+ // (carried as internalContinuation + an incremented count),
3485
+ // instead of relying on the client to re-POST. Mirrors the
3486
+ // agent-teams `fireInternalDispatch({ body: { mode: "continue" }})`
3487
+ // chain. Bounded by MAX_BACKGROUND_RUN_CONTINUATIONS. Aborted /
3488
+ // user-stopped runs do NOT chain.
3489
+ if (shouldChainBackgroundContinuation({
3490
+ isBackgroundWorker,
3491
+ run,
3492
+ continuationCount: backgroundContinuationCount,
3493
+ })) {
3494
+ // Mint the next chunk's runId here and sign the dispatch token
3495
+ // over it, so the `_process-run` route's HMAC check and the
3496
+ // worker's run identity agree. Fresh runId (not this chunk's) so
3497
+ // its seq log starts clean; same turnId folds the assistant
3498
+ // message across chunks.
3499
+ const nextRunId = generateRunId();
3500
+ try {
3501
+ await fireInternalDispatch({
3502
+ event,
3503
+ path: AGENT_CHAT_PROCESS_RUN_PATH,
3504
+ taskId: nextRunId,
3505
+ body: {
3506
+ ...body,
3507
+ internalContinuation: true,
3508
+ [AGENT_CHAT_BACKGROUND_RUN_FIELD]: {
3509
+ runId: nextRunId,
3510
+ turnId: effectiveTurnId,
3511
+ continuationCount: backgroundContinuationCount + 1,
3512
+ },
3513
+ },
3514
+ });
3515
+ }
3516
+ catch (chainErr) {
3517
+ // Chain dispatch failed — fail loud so the held row goes
3518
+ // terminal instead of spinning. The reaper would also catch
3519
+ // it, but this is immediate and truthful.
3520
+ console.error("[agent-chat] background continuation dispatch failed:", chainErr instanceof Error ? chainErr.message : chainErr);
3521
+ await updateRunStatusIfRunning(runId, "errored").catch(() => { });
3522
+ }
3523
+ }
3524
+ }
3525
+ finally {
3526
+ resolveBackgroundRunDone?.();
3527
+ }
3528
+ }
3529
+ : undefined;
3530
+ // Background worker: claim the pre-inserted run idempotently before
3531
+ // executing. A duplicate Netlify delivery loses the claim and no-ops here,
3532
+ // so the run can never be double-executed. Bump the heartbeat immediately
3533
+ // on entry so a slow cold-start doesn't leave the row looking stale to the
3534
+ // reaper before startRun's 1.5s heartbeat timer takes over.
3535
+ if (isBackgroundWorker) {
3536
+ // A chained continuation chunk's runId was minted by the prior chunk and
3537
+ // never inserted, so insert its background row now (idempotently — a
3538
+ // duplicate Netlify delivery that already inserted it just PK-collides and
3539
+ // the claim below dedups). The first chunk's row was inserted by the
3540
+ // foreground, so skip the insert there.
3541
+ if (isChainedBackgroundContinuation) {
3542
+ await insertRun(runId, effectiveThreadId, effectiveTurnId, {
3543
+ dispatchMode: "background",
3544
+ }).catch(() => { });
3545
+ }
3546
+ const won = await claimBackgroundRun(runId);
3547
+ if (!won) {
3548
+ // Already claimed by an earlier delivery — return a benign ack so
3549
+ // Netlify doesn't retry a successful handoff.
3550
+ return { ok: true, skipped: "already-claimed" };
3551
+ }
3552
+ await updateRunHeartbeat(runId).catch(() => { });
3553
+ }
3261
3554
  startRun(runId, effectiveThreadId, async (rawSend, signal) => {
3262
3555
  const send = (event) => {
3263
3556
  rawSend(event);
@@ -3591,11 +3884,24 @@ export function createProductionAgentHandler(options) {
3591
3884
  }, handleRunComplete, {
3592
3885
  softTimeoutMs: options.runSoftTimeoutMs,
3593
3886
  useHostedSoftTimeoutDefault: true,
3887
+ // Inside the Netlify background function there is no ~60s wall, so lift
3888
+ // the soft-timeout clamp to ~13min for THIS run only. Foreground runs
3889
+ // never set this, so their 40s clamp is unchanged.
3890
+ backgroundFunction: isBackgroundWorker,
3594
3891
  // Fold continuation runs of one logical turn onto a single durable
3595
3892
  // assistant message. Falls back to the runId (turn == run) when the
3596
3893
  // client doesn't supply a turnId.
3597
3894
  turnId: effectiveTurnId,
3598
3895
  });
3896
+ // Background worker: await the run to completion so Netlify keeps the
3897
+ // background function alive for the whole turn (the client is streaming the
3898
+ // same events via the foreground POST's cross-isolate SQL subscription).
3899
+ // The onComplete wrapper above resolves `backgroundRunDone`.
3900
+ if (isBackgroundWorker) {
3901
+ if (backgroundRunDone)
3902
+ await backgroundRunDone;
3903
+ return { ok: true, runId };
3904
+ }
3599
3905
  // Subscribe to the run and stream events to the client
3600
3906
  const stream = subscribeToRun(runId, 0);
3601
3907
  if (!stream) {