@agent-native/core 0.84.12 → 0.84.14

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 (112) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +19 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/durable-background.ts +42 -5
  5. package/corpus/core/src/agent/production-agent.ts +72 -10
  6. package/corpus/core/src/agent/run-manager.ts +84 -4
  7. package/corpus/core/src/agent/run-store.ts +55 -11
  8. package/corpus/core/src/chat-threads/store.ts +2 -0
  9. package/corpus/core/src/cli/skills.ts +10 -6
  10. package/corpus/core/src/client/AssistantChat.tsx +40 -9
  11. package/corpus/core/src/client/agent-chat-adapter.ts +44 -5
  12. package/corpus/core/src/client/blocks/library/diagram.tsx +3 -3
  13. package/corpus/core/src/client/chat/message-components.tsx +17 -0
  14. package/corpus/core/src/client/chat/run-recovery.tsx +52 -44
  15. package/corpus/core/src/client/chat/tool-call-display.tsx +7 -2
  16. package/corpus/core/src/observability/traces.ts +17 -3
  17. package/corpus/core/src/server/agent-chat-plugin.ts +12 -1
  18. package/corpus/core/src/shared/streaming-text-smoothing.ts +10 -5
  19. package/corpus/core/src/styles/blocks.css +44 -1
  20. package/corpus/templates/clips/app/components/library/recording-card.tsx +14 -4
  21. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +10 -12
  22. package/corpus/templates/clips/app/hooks/use-library.ts +7 -4
  23. package/corpus/templates/clips/app/lib/recording-status.ts +32 -0
  24. package/corpus/templates/clips/changelog/2026-07-01-chrome-extension-recordings-stream-uploads-while-recording.md +6 -0
  25. package/corpus/templates/clips/changelog/2026-07-01-dictation-keeps-listening-through-natural-pauses-and-gives-c.md +6 -0
  26. package/corpus/templates/clips/changelog/2026-07-01-redoing-a-paused-desktop-recording-no-longer-leaves-the-recording-controls-disabled.md +6 -0
  27. package/corpus/templates/clips/changelog/2026-07-01-stalled-clip-uploads-now-update-in-the-library-and-surface-as-failed.md +6 -0
  28. package/corpus/templates/clips/chrome-extension/src/background.ts +8 -0
  29. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +61 -16
  30. package/corpus/templates/clips/desktop/src/lib/recorder.ts +36 -18
  31. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +99 -27
  32. package/corpus/templates/clips/desktop/src/overlays/flow-bar.tsx +4 -43
  33. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +3 -0
  34. package/corpus/templates/clips/desktop/src/styles.css +5 -48
  35. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +4 -5
  36. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +15 -6
  37. package/corpus/templates/design/AGENTS.md +8 -4
  38. package/corpus/templates/design/actions/create-design.ts +1 -0
  39. package/corpus/templates/design/actions/duplicate-design.ts +3 -1
  40. package/corpus/templates/design/actions/edit-design.ts +43 -13
  41. package/corpus/templates/design/actions/generate-design.ts +5 -1
  42. package/corpus/templates/design/actions/present-design-variants.ts +17 -12
  43. package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
  44. package/corpus/templates/design/app/pages/DesignEditor.tsx +2 -2
  45. package/corpus/templates/design/changelog/2026-07-01-org-members-can-see-design-projects-created-in-their-workspace.md +6 -0
  46. package/corpus/templates/design/server/plugins/db.ts +9 -0
  47. package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +44 -1
  48. package/corpus/templates/plan/changelog/2026-07-01-visual-plan-and-recap-diagram-labels-now-wrap-inside-their-b.md +6 -0
  49. package/dist/agent/durable-background.d.ts +3 -0
  50. package/dist/agent/durable-background.d.ts.map +1 -1
  51. package/dist/agent/durable-background.js +33 -3
  52. package/dist/agent/durable-background.js.map +1 -1
  53. package/dist/agent/production-agent.d.ts +1 -0
  54. package/dist/agent/production-agent.d.ts.map +1 -1
  55. package/dist/agent/production-agent.js +55 -9
  56. package/dist/agent/production-agent.js.map +1 -1
  57. package/dist/agent/run-manager.d.ts +2 -0
  58. package/dist/agent/run-manager.d.ts.map +1 -1
  59. package/dist/agent/run-manager.js +68 -5
  60. package/dist/agent/run-manager.js.map +1 -1
  61. package/dist/agent/run-store.d.ts +9 -0
  62. package/dist/agent/run-store.d.ts.map +1 -1
  63. package/dist/agent/run-store.js +47 -11
  64. package/dist/agent/run-store.js.map +1 -1
  65. package/dist/chat-threads/store.d.ts +1 -0
  66. package/dist/chat-threads/store.d.ts.map +1 -1
  67. package/dist/chat-threads/store.js +2 -0
  68. package/dist/chat-threads/store.js.map +1 -1
  69. package/dist/cli/skills.d.ts.map +1 -1
  70. package/dist/cli/skills.js +10 -6
  71. package/dist/cli/skills.js.map +1 -1
  72. package/dist/client/AssistantChat.d.ts +10 -0
  73. package/dist/client/AssistantChat.d.ts.map +1 -1
  74. package/dist/client/AssistantChat.js +26 -8
  75. package/dist/client/AssistantChat.js.map +1 -1
  76. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  77. package/dist/client/agent-chat-adapter.js +40 -5
  78. package/dist/client/agent-chat-adapter.js.map +1 -1
  79. package/dist/client/blocks/library/diagram.js +3 -3
  80. package/dist/client/blocks/library/diagram.js.map +1 -1
  81. package/dist/client/chat/message-components.d.ts +3 -1
  82. package/dist/client/chat/message-components.d.ts.map +1 -1
  83. package/dist/client/chat/message-components.js +16 -2
  84. package/dist/client/chat/message-components.js.map +1 -1
  85. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  86. package/dist/client/chat/run-recovery.js +15 -10
  87. package/dist/client/chat/run-recovery.js.map +1 -1
  88. package/dist/client/chat/tool-call-display.d.ts +1 -0
  89. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  90. package/dist/client/chat/tool-call-display.js +3 -2
  91. package/dist/client/chat/tool-call-display.js.map +1 -1
  92. package/dist/collab/awareness.d.ts +2 -2
  93. package/dist/collab/awareness.d.ts.map +1 -1
  94. package/dist/collab/routes.d.ts +1 -1
  95. package/dist/notifications/routes.d.ts +3 -3
  96. package/dist/observability/routes.d.ts +6 -6
  97. package/dist/observability/traces.d.ts +5 -0
  98. package/dist/observability/traces.d.ts.map +1 -1
  99. package/dist/observability/traces.js +9 -3
  100. package/dist/observability/traces.js.map +1 -1
  101. package/dist/resources/handlers.d.ts +2 -2
  102. package/dist/server/agent-chat-plugin.d.ts +2 -1
  103. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  104. package/dist/server/agent-chat-plugin.js +8 -3
  105. package/dist/server/agent-chat-plugin.js.map +1 -1
  106. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  107. package/dist/server/transcribe-voice.d.ts +1 -1
  108. package/dist/shared/streaming-text-smoothing.d.ts.map +1 -1
  109. package/dist/shared/streaming-text-smoothing.js +8 -5
  110. package/dist/shared/streaming-text-smoothing.js.map +1 -1
  111. package/dist/styles/blocks.css +44 -1
  112. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 2043
31
- - template files: 4912
31
+ - template files: 4919
@@ -1,5 +1,24 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.84.14
4
+
5
+ ### Patch Changes
6
+
7
+ - bbc0a56: Persist terminal reasons for agent runs and classify recoverable run-timeout continuations separately in traces.
8
+ - bbc0a56: Improve agent run diagnostics by recording terminal continuation reasons and adding safe run context to copied recovery debug details.
9
+ - bbc0a56: Keep visual plan and recap diagram labels wrapped inside their boxes instead of overflowing authored diagram shapes.
10
+
11
+ ## 0.84.13
12
+
13
+ ### Patch Changes
14
+
15
+ - dd5b0a4: Honor the long Netlify background-function timeout for durable agent-chat workers and record runtime diagnostics when a background run starts.
16
+ - dd5b0a4: Make agent chat recovery surface completed-tool timeouts more reliably and keep Design variant follow-ups bounded to the selected screen.
17
+ - dd5b0a4: Double the agent chat smooth-streaming reveal speed.
18
+ - dd5b0a4: Avoid noisy Sentry reports from expected chat auth states and best-effort client thread-save conflicts.
19
+ - dd5b0a4: Keep activity-only tool cards and the latest assistant message visibly running during agent chat continuations, steer interrupted Design generation retries toward smaller existing-file edits, and capture run persistence failures with run IDs so missing request IDs are diagnosable.
20
+ - dd5b0a4: Compress the Plan mode composer callout into a quieter inline status pill.
21
+
3
22
  ## 0.84.12
4
23
 
5
24
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.84.12",
3
+ "version": "0.84.14",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -97,6 +97,17 @@ function resolveWorkspaceBackgroundFunctionUrlPath(): string | null {
97
97
  return `/.netlify/functions/${candidate}-agent-background`;
98
98
  }
99
99
 
100
+ function isNetlifyHostedRuntimeForDispatch(): boolean {
101
+ if (process.env.NETLIFY_LOCAL === "true") return false;
102
+ if (process.env.NETLIFY === "false") return false;
103
+ if (process.env.NETLIFY && process.env.NETLIFY !== "false") return true;
104
+ // Netlify sets AWS Lambda runtime env on deployed Functions, but the build-time
105
+ // NETLIFY flag is not always present in the runtime isolate. Treat Lambda as
106
+ // Netlify here unless Netlify was explicitly disabled above; non-Netlify AWS
107
+ // falls back inline if the /.netlify/functions dispatch fast-fails.
108
+ return Boolean(process.env.AWS_LAMBDA_FUNCTION_NAME);
109
+ }
110
+
100
111
  /**
101
112
  * Resolve the path the foreground POST should self-dispatch the chat background
102
113
  * worker to.
@@ -129,11 +140,7 @@ function resolveWorkspaceBackgroundFunctionUrlPath(): string | null {
129
140
  * to shadow because `/.netlify/*` is already excluded from the `server` catch-all.
130
141
  */
131
142
  export function resolveAgentChatProcessRunDispatchPath(): string {
132
- if (
133
- process.env.NETLIFY &&
134
- process.env.NETLIFY !== "false" &&
135
- process.env.NETLIFY_LOCAL !== "true"
136
- ) {
143
+ if (isNetlifyHostedRuntimeForDispatch()) {
137
144
  return (
138
145
  resolveWorkspaceBackgroundFunctionUrlPath() ??
139
146
  AGENT_BACKGROUND_FUNCTION_URL_PATH
@@ -235,6 +242,36 @@ export function isInBackgroundFunctionRuntime(): boolean {
235
242
  return false;
236
243
  }
237
244
 
245
+ export function backgroundRunMarkerExpectsBackgroundRuntime(
246
+ marker: unknown,
247
+ ): boolean {
248
+ return (
249
+ typeof marker === "object" &&
250
+ marker !== null &&
251
+ (marker as { backgroundFunctionRuntimeExpected?: unknown })
252
+ .backgroundFunctionRuntimeExpected === true
253
+ );
254
+ }
255
+
256
+ export function shouldUseBackgroundFunctionTimeoutForWorker(
257
+ marker: unknown,
258
+ ): boolean {
259
+ return (
260
+ isInBackgroundFunctionRuntime() ||
261
+ backgroundRunMarkerExpectsBackgroundRuntime(marker)
262
+ );
263
+ }
264
+
265
+ export function backgroundRuntimeDiagnosticDetail(marker: unknown): string {
266
+ return [
267
+ `markerExpected=${backgroundRunMarkerExpectsBackgroundRuntime(marker)}`,
268
+ `runtimeDetected=${isInBackgroundFunctionRuntime()}`,
269
+ `globalMarker=${(globalThis as Record<string, unknown>).__AGENT_NATIVE_BACKGROUND_RUNTIME__ === true}`,
270
+ `lambdaNameEndsBackground=${typeof process.env.AWS_LAMBDA_FUNCTION_NAME === "string" && process.env.AWS_LAMBDA_FUNCTION_NAME.toLowerCase().endsWith("-background")}`,
271
+ `forceEnv=${typeof process.env.AGENT_CHAT_FORCE_BACKGROUND_RUNTIME === "string" && process.env.AGENT_CHAT_FORCE_BACKGROUND_RUNTIME.trim().length > 0}`,
272
+ ].join(" ");
273
+ }
274
+
238
275
  function isFlagEnabled(): boolean {
239
276
  // Read the literal key (not `process.env[CONST]`) so guard:no-env-credentials
240
277
  // can statically verify it against the allowlisted `AGENT_*` prefix. Keep this
@@ -53,9 +53,10 @@ import {
53
53
  import { computeProtectedSegmentIds } from "./context-xray/segments.js";
54
54
  import {
55
55
  AGENT_CHAT_BACKGROUND_RUN_FIELD,
56
+ backgroundRuntimeDiagnosticDetail,
56
57
  isAgentChatDurableBackgroundEnabled,
57
- isInBackgroundFunctionRuntime,
58
58
  resolveAgentChatProcessRunDispatchPath,
59
+ shouldUseBackgroundFunctionTimeoutForWorker,
59
60
  } from "./durable-background.js";
60
61
  import {
61
62
  LLM_MISSING_CREDENTIALS_ERROR_CODE,
@@ -123,6 +124,7 @@ import {
123
124
  insertRun,
124
125
  updateRunHeartbeat,
125
126
  updateRunStatusIfRunning,
127
+ setRunTerminalReason,
126
128
  claimBackgroundRun,
127
129
  readBackgroundRunClaim,
128
130
  recordRunDiagnostic,
@@ -4032,6 +4034,7 @@ export async function claimBackgroundWorkerRunEarly(opts: {
4032
4034
  requestTurnId?: string | null;
4033
4035
  continuationCount: number;
4034
4036
  runsInBackgroundFunction: boolean;
4037
+ backgroundRuntimeDetail?: string;
4035
4038
  deps?: {
4036
4039
  recordRunDiagnostic?: typeof recordRunDiagnostic;
4037
4040
  insertRun?: typeof insertRun;
@@ -4057,7 +4060,13 @@ export async function claimBackgroundWorkerRunEarly(opts: {
4057
4060
  await record(
4058
4061
  opts.runId,
4059
4062
  RUN_DIAG_STAGE.workerEntered,
4060
- `runsInBackgroundFunction=${opts.runsInBackgroundFunction} continuationCount=${opts.continuationCount}`,
4063
+ [
4064
+ `runsInBackgroundFunction=${opts.runsInBackgroundFunction}`,
4065
+ `continuationCount=${opts.continuationCount}`,
4066
+ opts.backgroundRuntimeDetail,
4067
+ ]
4068
+ .filter(Boolean)
4069
+ .join(" "),
4061
4070
  ).catch(() => {});
4062
4071
 
4063
4072
  if (opts.continuationCount > 0) {
@@ -4218,7 +4227,11 @@ export function createProductionAgentHandler(
4218
4227
  // function. Only a true value unlocks the ~13-min soft-timeout budget; a
4219
4228
  // worker on the 60s function keeps the 40s clamp and checkpoints cleanly.
4220
4229
  const runsInBackgroundFunction =
4221
- isBackgroundWorker && isInBackgroundFunctionRuntime();
4230
+ isBackgroundWorker &&
4231
+ shouldUseBackgroundFunctionTimeoutForWorker(backgroundRunMarker);
4232
+ const backgroundRuntimeDetail = isBackgroundWorker
4233
+ ? backgroundRuntimeDiagnosticDetail(backgroundRunMarker)
4234
+ : "";
4222
4235
  // How many server-driven background continuations have already chained into
4223
4236
  // this logical turn (0 on the first chunk). Used to bound the chain.
4224
4237
  const backgroundContinuationCount =
@@ -4239,6 +4252,7 @@ export function createProductionAgentHandler(
4239
4252
  requestTurnId,
4240
4253
  continuationCount: backgroundContinuationCount,
4241
4254
  runsInBackgroundFunction,
4255
+ backgroundRuntimeDetail,
4242
4256
  });
4243
4257
  if (!earlyClaim.claimed) {
4244
4258
  return { ok: true, skipped: earlyClaim.skipped };
@@ -5001,6 +5015,9 @@ export function createProductionAgentHandler(
5001
5015
  }
5002
5016
 
5003
5017
  let dispatched = false;
5018
+ const backgroundDispatchPath = resolveAgentChatProcessRunDispatchPath();
5019
+ const expectsNetlifyBackgroundFunction =
5020
+ backgroundDispatchPath.startsWith("/.netlify/functions/");
5004
5021
  try {
5005
5022
  await fireInternalDispatch({
5006
5023
  event,
@@ -5014,7 +5031,7 @@ export function createProductionAgentHandler(
5014
5031
  // inline. `fireInternalDispatch` strips the app base path for
5015
5032
  // /.netlify/* targets so the request reaches the host-root function url;
5016
5033
  // the Authorization Bearer HMAC is preserved either way.
5017
- path: resolveAgentChatProcessRunDispatchPath(),
5034
+ path: backgroundDispatchPath,
5018
5035
  taskId: runId,
5019
5036
  body: {
5020
5037
  ...body,
@@ -5022,6 +5039,8 @@ export function createProductionAgentHandler(
5022
5039
  [AGENT_CHAT_BACKGROUND_RUN_FIELD]: {
5023
5040
  runId,
5024
5041
  turnId: effectiveTurnId,
5042
+ backgroundFunctionRuntimeExpected:
5043
+ expectsNetlifyBackgroundFunction,
5025
5044
  },
5026
5045
  },
5027
5046
  });
@@ -5071,7 +5090,17 @@ export function createProductionAgentHandler(
5071
5090
  }
5072
5091
  // A background worker owns this run but we cannot subscribe — surface an
5073
5092
  // error rather than risk a double-run by falling through to inline.
5074
- await updateRunStatusIfRunning(runId, "errored").catch(() => {});
5093
+ const terminalReason =
5094
+ backgroundOutcome.action === "stream"
5095
+ ? "background_subscribe_failed"
5096
+ : "background_dispatch_failed";
5097
+ const statusUpdated = await updateRunStatusIfRunning(
5098
+ runId,
5099
+ "errored",
5100
+ ).catch(() => false);
5101
+ if (statusUpdated) {
5102
+ await setRunTerminalReason(runId, terminalReason).catch(() => {});
5103
+ }
5075
5104
  setResponseStatus(event, 500);
5076
5105
  return {
5077
5106
  error:
@@ -5302,6 +5331,8 @@ export function createProductionAgentHandler(
5302
5331
  runId: nextRunId,
5303
5332
  turnId: effectiveTurnId,
5304
5333
  continuationCount: backgroundContinuationCount + 1,
5334
+ backgroundFunctionRuntimeExpected:
5335
+ runsInBackgroundFunction,
5305
5336
  },
5306
5337
  },
5307
5338
  });
@@ -5313,9 +5344,16 @@ export function createProductionAgentHandler(
5313
5344
  "[agent-chat] background continuation dispatch failed:",
5314
5345
  chainErr instanceof Error ? chainErr.message : chainErr,
5315
5346
  );
5316
- await updateRunStatusIfRunning(runId, "errored").catch(
5317
- () => {},
5318
- );
5347
+ const statusUpdated = await updateRunStatusIfRunning(
5348
+ runId,
5349
+ "errored",
5350
+ ).catch(() => false);
5351
+ if (statusUpdated) {
5352
+ await setRunTerminalReason(
5353
+ runId,
5354
+ "background_continuation_dispatch_failed",
5355
+ ).catch(() => {});
5356
+ }
5319
5357
  }
5320
5358
  }
5321
5359
  } finally {
@@ -5335,7 +5373,13 @@ export function createProductionAgentHandler(
5335
5373
  await recordRunDiagnostic(
5336
5374
  runId,
5337
5375
  RUN_DIAG_STAGE.workerEntered,
5338
- `runsInBackgroundFunction=${runsInBackgroundFunction} continuationCount=${backgroundContinuationCount}`,
5376
+ [
5377
+ `runsInBackgroundFunction=${runsInBackgroundFunction}`,
5378
+ `continuationCount=${backgroundContinuationCount}`,
5379
+ backgroundRuntimeDetail,
5380
+ ]
5381
+ .filter(Boolean)
5382
+ .join(" "),
5339
5383
  ).catch(() => {});
5340
5384
  if (isChainedBackgroundContinuation) {
5341
5385
  await insertRun(runId, effectiveThreadId, effectiveTurnId, {
@@ -5368,7 +5412,9 @@ export function createProductionAgentHandler(
5368
5412
  const setupDetail =
5369
5413
  Object.entries(setupMarks)
5370
5414
  .map(([k, v]) => `${k}=${v}`)
5371
- .join(" ") + ` total=${Date.now() - setupT0}`;
5415
+ .join(" ") +
5416
+ ` total=${Date.now() - setupT0}` +
5417
+ (backgroundRuntimeDetail ? ` ${backgroundRuntimeDetail}` : "");
5372
5418
 
5373
5419
  startRun(
5374
5420
  runId,
@@ -5738,6 +5784,22 @@ export function createProductionAgentHandler(
5738
5784
  threadId: threadId ?? null,
5739
5785
  userId: ownerEmail,
5740
5786
  config: obsConfig,
5787
+ classifyError: () => {
5788
+ if (
5789
+ agentLoopOpts.signal.aborted &&
5790
+ agentLoopOpts.signal.reason === "run_timeout"
5791
+ ) {
5792
+ return {
5793
+ status: "success",
5794
+ errorMessage: null,
5795
+ metadata: {
5796
+ terminalReason: "run_timeout",
5797
+ recoverableContinuation: true,
5798
+ },
5799
+ };
5800
+ }
5801
+ return null;
5802
+ },
5741
5803
  });
5742
5804
  }
5743
5805
  } catch (err) {
@@ -18,6 +18,7 @@ import {
18
18
  reapUnclaimedBackgroundRun,
19
19
  ensureTerminalRunEvent,
20
20
  setRunError,
21
+ setRunTerminalReason,
21
22
  STALE_RUN_ERROR_EVENT,
22
23
  } from "./run-store.js";
23
24
  import type { AgentChatEvent, RunEvent, RunStatus } from "./types.js";
@@ -293,6 +294,26 @@ function isTerminalRunEvent(event: AgentChatEvent): boolean {
293
294
  );
294
295
  }
295
296
 
297
+ function terminalReasonForRun(
298
+ finalStatus: "completed" | "errored" | "aborted",
299
+ terminalEvent: AgentChatEvent | null,
300
+ abortReason: string | undefined,
301
+ completionError: unknown,
302
+ ): string {
303
+ if (terminalEvent?.type === "auto_continue") {
304
+ return terminalEvent.reason || "auto_continue";
305
+ }
306
+ if (terminalEvent?.type === "loop_limit") return "loop_limit";
307
+ if (terminalEvent?.type === "missing_api_key") return "missing_api_key";
308
+ if (terminalEvent?.type === "error") {
309
+ return `error:${terminalEvent.errorCode || "unknown"}`;
310
+ }
311
+ if (finalStatus === "aborted") return `aborted:${abortReason ?? "user"}`;
312
+ if (completionError) return "completion_error";
313
+ if (finalStatus === "errored") return "error:unknown";
314
+ return "done";
315
+ }
316
+
296
317
  function abortInMemoryRun(run: ActiveRun, reason: string = "user") {
297
318
  run.abortReason = reason;
298
319
  run.status = "aborted";
@@ -349,11 +370,45 @@ export function startRun(
349
370
  activeRuns.set(runId, run);
350
371
  threadToRun.set(threadId, runId);
351
372
 
373
+ const captureRunPersistenceError = (
374
+ error: unknown,
375
+ phase: "insert-run" | "insert-event",
376
+ extra: Record<string, unknown> = {},
377
+ ) => {
378
+ captureError(error, {
379
+ route: "/_agent-native/agent-chat",
380
+ tags: {
381
+ source: "agent-run-manager",
382
+ phase,
383
+ runStatus: run.status,
384
+ },
385
+ extra: {
386
+ runId,
387
+ threadId,
388
+ eventCount: run.events.length,
389
+ startedAt: run.startedAt,
390
+ ...extra,
391
+ },
392
+ contexts: {
393
+ agentRun: {
394
+ runId,
395
+ threadId,
396
+ status: run.status,
397
+ phase,
398
+ eventCount: run.events.length,
399
+ startedAt: run.startedAt,
400
+ },
401
+ },
402
+ });
403
+ };
404
+
352
405
  // Persist run to SQL without blocking the response. Keep the promise so
353
406
  // final status cannot race ahead of a slow initial INSERT and then get
354
407
  // overwritten by a late row stuck at status='running'.
355
408
  const insertRunPromise = insertRun(runId, threadId, options?.turnId).catch(
356
- () => {},
409
+ (error) => {
410
+ captureRunPersistenceError(error, "insert-run");
411
+ },
357
412
  );
358
413
 
359
414
  // Per-run event persistence chain: events are chained so SQL inserts commit
@@ -368,6 +423,7 @@ export function startRun(
368
423
  // chunk. The stuck-detector threshold is on the order of tens of seconds,
369
424
  // so 1s resolution is plenty.
370
425
  let lastProgressBumpAt = 0;
426
+ let eventPersistenceErrorCaptured = false;
371
427
  const bumpProgressIfDue = () => {
372
428
  const now = Date.now();
373
429
  if (now - lastProgressBumpAt < 1000) return;
@@ -424,7 +480,7 @@ export function startRun(
424
480
  type: "auto_continue",
425
481
  reason: "run_timeout",
426
482
  });
427
- abort.abort();
483
+ abort.abort("run_timeout");
428
484
  }, softTimeoutMs)
429
485
  : null;
430
486
  let pendingTerminalEvent: RunEvent | null = null;
@@ -494,7 +550,15 @@ export function startRun(
494
550
  const thisInsert = persistenceChain.then(() =>
495
551
  insertRunEvent(runId, runEvent.seq, JSON.stringify(runEvent.event)),
496
552
  );
497
- persistenceChain = thisInsert.catch(() => {});
553
+ persistenceChain = thisInsert.catch((error) => {
554
+ if (!eventPersistenceErrorCaptured) {
555
+ eventPersistenceErrorCaptured = true;
556
+ captureRunPersistenceError(error, "insert-event", {
557
+ seq: runEvent.seq,
558
+ eventType: runEvent.event.type,
559
+ });
560
+ }
561
+ });
498
562
  const persistence = thisInsert;
499
563
  if (!options?.surfacePersistenceError) {
500
564
  persistence.catch(() => {});
@@ -591,6 +655,12 @@ export function startRun(
591
655
  : run.status === "errored" || completionError
592
656
  ? "errored"
593
657
  : "completed";
658
+ const terminalReason = terminalReasonForRun(
659
+ finalStatus,
660
+ pendingTerminalEvent?.event ?? null,
661
+ run.abortReason,
662
+ completionError,
663
+ );
594
664
 
595
665
  // 3. Emit the terminal event only after thread_data is durable. Live
596
666
  // SSE clients close on this event and usually fetch thread_data
@@ -663,7 +733,13 @@ export function startRun(
663
733
  try {
664
734
  await insertRunPromise;
665
735
  if (!terminalPersistenceError) {
666
- await updateRunStatusIfRunning(runId, finalStatus);
736
+ const statusUpdated = await updateRunStatusIfRunning(
737
+ runId,
738
+ finalStatus,
739
+ );
740
+ if (statusUpdated) {
741
+ await setRunTerminalReason(runId, terminalReason);
742
+ }
667
743
  }
668
744
  } catch {
669
745
  // Best-effort — reapIfStale will eventually clean this up via
@@ -1034,6 +1110,8 @@ export async function getActiveRunForThreadAsync(threadId: string): Promise<{
1034
1110
  lastProgressAt: number | null;
1035
1111
  /** How the run was dispatched (NULL/foreground, background, background-processing). */
1036
1112
  dispatchMode?: string | null;
1113
+ /** Compact terminal classification, e.g. done, run_timeout, stale_run. */
1114
+ terminalReason?: string | null;
1037
1115
  /**
1038
1116
  * Last reached `_process-run` worker stage as a JSON string
1039
1117
  * `{stage,detail?,at}`. Surfaced so a silent background-worker death is
@@ -1100,6 +1178,7 @@ export async function getActiveRunForThreadAsync(threadId: string): Promise<{
1100
1178
  heartbeatAt: sqlRun.heartbeatAt ?? sqlRun.startedAt,
1101
1179
  lastProgressAt: sqlRun.lastProgressAt,
1102
1180
  dispatchMode: sqlRun.dispatchMode,
1181
+ terminalReason: sqlRun.terminalReason,
1103
1182
  diagStage: sqlRun.diagStage,
1104
1183
  };
1105
1184
  }
@@ -1127,6 +1206,7 @@ export async function getActiveRunForThreadAsync(threadId: string): Promise<{
1127
1206
  heartbeatAt: sqlRun.heartbeatAt ?? sqlRun.startedAt,
1128
1207
  lastProgressAt: sqlRun.lastProgressAt,
1129
1208
  dispatchMode: sqlRun.dispatchMode,
1209
+ terminalReason: sqlRun.terminalReason,
1130
1210
  diagStage: sqlRun.diagStage,
1131
1211
  };
1132
1212
  }