@agent-native/core 0.219.0 → 0.220.0-nightly-20261023212848

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 (97) hide show
  1. package/dist/action.d.ts +23 -3
  2. package/dist/action.js +58 -3
  3. package/dist/agent/abort-reasons.d.ts +6 -0
  4. package/dist/agent/abort-reasons.js +6 -0
  5. package/dist/agent/execution-fence.js +5 -1
  6. package/dist/agent/production-agent.d.ts +22 -6
  7. package/dist/agent/production-agent.js +136 -37
  8. package/dist/agent/run-manager.js +92 -44
  9. package/dist/agent/run-store.d.ts +1 -0
  10. package/dist/agent/run-store.js +260 -31
  11. package/dist/agent/run-terminal-tracking.d.ts +29 -0
  12. package/dist/agent/run-terminal-tracking.js +56 -0
  13. package/dist/agent/run-turn-id-header.d.ts +9 -0
  14. package/dist/agent/run-turn-id-header.js +13 -0
  15. package/dist/agent/types.d.ts +11 -0
  16. package/dist/chat-threads/store.d.ts +23 -6
  17. package/dist/chat-threads/store.js +119 -36
  18. package/dist/cli/amplify-stream.js +1 -1
  19. package/dist/client/agent-chat/index.d.ts +2 -1
  20. package/dist/client/agent-chat/index.js +2 -1
  21. package/dist/client/agent-chat.d.ts +2 -0
  22. package/dist/client/agent-chat.js +31 -5
  23. package/dist/client/chat/agentkit-agent-native.js +8 -1
  24. package/dist/client/chat/agentkit-protocol.js +7 -1
  25. package/dist/client/chat/run-outcome-telemetry.js +1 -0
  26. package/dist/client/chat/run-outcome.d.ts +5 -0
  27. package/dist/client/chat/runtime.d.ts +34 -1
  28. package/dist/client/chat/runtime.js +96 -12
  29. package/dist/client/chat/turn-submitted-telemetry.d.ts +4 -0
  30. package/dist/client/chat/turn-submitted-telemetry.js +39 -0
  31. package/dist/client/chat/turn-submitted.d.ts +39 -0
  32. package/dist/client/chat/turn-submitted.js +16 -0
  33. package/dist/client/route-chunk-recovery.d.ts +3 -8
  34. package/dist/client/route-chunk-recovery.js +11 -9
  35. package/dist/client/use-chat-thread-list.d.ts +2 -0
  36. package/dist/client/use-chat-thread-list.js +48 -2
  37. package/dist/client/use-chat-threads.d.ts +13 -0
  38. package/dist/client/use-chat-threads.js +59 -4
  39. package/dist/collab/routes.d.ts +2 -2
  40. package/dist/db/client.d.ts +1 -0
  41. package/dist/db/client.js +28 -18
  42. package/dist/db/schema-snapshot.generated.js +5 -0
  43. package/dist/identity/rekey.d.ts +2 -0
  44. package/dist/identity/rekey.js +19 -0
  45. package/dist/index.d.ts +1 -1
  46. package/dist/index.js +1 -1
  47. package/dist/mcp/agent-connected.d.ts +5 -0
  48. package/dist/mcp/agent-connected.js +10 -3
  49. package/dist/mcp/analytics.d.ts +14 -1
  50. package/dist/mcp/analytics.js +54 -2
  51. package/dist/mcp/embed-app.js +2 -1
  52. package/dist/mcp/oauth-route.js +56 -1
  53. package/dist/notifications/routes.d.ts +1 -1
  54. package/dist/observability/routes.d.ts +7 -7
  55. package/dist/observability/traces.d.ts +2 -0
  56. package/dist/observability/traces.js +11 -2
  57. package/dist/provider-api/actions/custom-provider-registration.d.ts +16 -16
  58. package/dist/provider-api/actions/provider-api.d.ts +13 -13
  59. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  60. package/dist/resources/handlers.d.ts +1 -1
  61. package/dist/review/index.d.ts +1 -1
  62. package/dist/review/index.js +1 -1
  63. package/dist/review/suggestions/actions.js +25 -17
  64. package/dist/review/suggestions/store.d.ts +14 -1
  65. package/dist/review/suggestions/store.js +16 -3
  66. package/dist/review/suggestions/types.d.ts +15 -1
  67. package/dist/scripts/chat/search-chats.js +14 -2
  68. package/dist/server/agent-chat/browser-team-tools.js +39 -6
  69. package/dist/server/agent-chat/script-entries.js +44 -9
  70. package/dist/server/agent-chat-plugin.d.ts +2 -0
  71. package/dist/server/agent-chat-plugin.js +66 -16
  72. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  73. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  74. package/dist/server/first-touch-route.d.ts +2 -2
  75. package/dist/shared/route-chunk-recovery-bootstrap.d.ts +9 -0
  76. package/dist/shared/route-chunk-recovery-bootstrap.js +25 -0
  77. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  78. package/dist/templates/chat/.agents/skills/actions/SKILL.md +1 -0
  79. package/dist/templates/default/.agents/skills/actions/SKILL.md +1 -0
  80. package/dist/templates/factory/.agents/skills/review-prs/SKILL.md +27 -3
  81. package/dist/templates/headless/.agents/skills/actions/SKILL.md +1 -0
  82. package/dist/templates/workspace-core/.agents/skills/actions/SKILL.md +1 -0
  83. package/dist/templates/workspace-core/.agents/skills/external-agents/references/mcp-apps-embedding.md +12 -5
  84. package/dist/tracking/providers.js +3 -0
  85. package/dist/triggers/actions/manage-automation.d.ts +2 -2
  86. package/docs/content/external-agents.mdx +1 -1
  87. package/docs/content/mcp-apps.mdx +3 -3
  88. package/docs/content/mcp-protocol.mdx +1 -1
  89. package/docs/content/sharing.mdx +1 -0
  90. package/docs/content/template-assets-integrations.mdx +5 -3
  91. package/package.json +3 -3
  92. package/src/templates/chat/.agents/skills/actions/SKILL.md +1 -0
  93. package/src/templates/default/.agents/skills/actions/SKILL.md +1 -0
  94. package/src/templates/factory/.agents/skills/review-prs/SKILL.md +27 -3
  95. package/src/templates/headless/.agents/skills/actions/SKILL.md +1 -0
  96. package/src/templates/workspace-core/.agents/skills/actions/SKILL.md +1 -0
  97. package/src/templates/workspace-core/.agents/skills/external-agents/references/mcp-apps-embedding.md +12 -5
@@ -7,7 +7,9 @@ import { fromMigrationEntries } from "../db/versioned-store-migrations.js";
7
7
  import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
8
8
  import { automationRecoveryMessagesForLocale } from "../localization/automation-recovery-messages.js";
9
9
  import { lockAndAssertServicePrincipalMayStartRun, prepareServicePrincipalRunStart, recordServicePrincipalDenial, } from "../org/service-principal-guard.js";
10
+ import { platformWaitUntil } from "../server/background-work.js";
10
11
  import { captureError } from "../server/capture-error.js";
12
+ import { runWithRequestContext } from "../server/request-context.js";
11
13
  import { assertNoInlineImageBytes, stripInlineBytes, stripInlineBytesFromJson, } from "../shared/inline-bytes.js";
12
14
  import { isRequestedStopAbortReason } from "./abort-reasons.js";
13
15
  import { approvalKeyForHistory } from "./approval-key.js";
@@ -17,6 +19,7 @@ import { LLM_MISSING_CREDENTIALS_ERROR_CODE, LLM_MISSING_CREDENTIALS_MESSAGE, }
17
19
  import { currentExecutionFence, currentExecutionFenceForScope, ExecutionFenceError, MissingExecutionFenceError, lockAndAssertCurrentExecutionFences, StaleExecutionFenceError, withFencedExecutionWrite, } from "./execution-fence.js";
18
20
  import { AGENT_RUN_MIGRATIONS, AGENT_RUN_MIGRATIONS_TABLE, } from "./run-migrations.js";
19
21
  import { lockThreadRunSlot } from "./run-slot-lock.js";
22
+ import { trackRunTerminal, } from "./run-terminal-tracking.js";
20
23
  import { isConsistentToolCallInputFingerprint, isUnavailableToolCallInput, toolCallInputFingerprint, } from "./tool-call-journal.js";
21
24
  import { CONTINUATION_REASONS, isContinuationTerminalReason } from "./types.js";
22
25
  import { readWriteReceipt } from "./write-receipts.js";
@@ -1537,6 +1540,14 @@ export async function setRunTerminalReason(runId, terminalReason) {
1537
1540
  // Diagnostics are best-effort; never let them break completion.
1538
1541
  }
1539
1542
  }
1543
+ export async function setRunTerminalReasonIfStatus(runId, expectedStatus, terminalReason) {
1544
+ await ensureRunTables();
1545
+ const { rowsAffected } = await withFencedExecutionWrite((tx) => tx.execute({
1546
+ sql: `UPDATE agent_runs SET terminal_reason = ? WHERE id = ? AND status = ? AND (terminal_reason IS NULL OR terminal_reason = '')`,
1547
+ args: [terminalReason.slice(0, 200), runId, expectedStatus],
1548
+ }), { runId });
1549
+ return (rowsAffected ?? 0) > 0;
1550
+ }
1540
1551
  /**
1541
1552
  * INVARIANT: a terminal event yields `completed` if and only if its reason is
1542
1553
  * `done`. Everything else either failed (`errored`) or stopped short
@@ -2018,29 +2029,37 @@ function priorDiagStageLabel(raw) {
2018
2029
  async function reconcileTerminalRunInRecoveryTransaction(tx, runId) {
2019
2030
  const latest = await getRunEventForReconciliation(runId, tx);
2020
2031
  if (!latest)
2021
- return false;
2032
+ return null;
2022
2033
  const status = terminalStatusForEvent(latest.event);
2023
2034
  const terminalReason = terminalReasonForEvent(latest.event);
2024
2035
  if (!status || !terminalReason)
2025
- return false;
2026
- const { rowsAffected } = await tx.execute({
2036
+ return null;
2037
+ const errorCode = errorCodeForTerminalEvent(latest.event);
2038
+ const { rows, rowsAffected } = await tx.execute({
2027
2039
  sql: `UPDATE agent_runs
2028
2040
  SET status = ?,
2029
2041
  completed_at = COALESCE(completed_at, ?, ${livenessBasisSql()}),
2030
2042
  error_code = ?,
2031
2043
  error_detail = ?,
2032
2044
  terminal_reason = ?
2033
- WHERE id = ? AND status = 'running'`,
2045
+ WHERE id = ? AND status = 'running'
2046
+ ${ENDED_RUN_RETURNING}`,
2034
2047
  args: [
2035
2048
  status,
2036
2049
  latest.eventAt,
2037
- errorCodeForTerminalEvent(latest.event),
2050
+ errorCode,
2038
2051
  errorDetailForTerminalEvent(latest.event),
2039
2052
  terminalReason,
2040
2053
  runId,
2041
2054
  ],
2042
2055
  });
2043
- return (rowsAffected ?? 0) === 1;
2056
+ if ((rowsAffected ?? 0) !== 1)
2057
+ return null;
2058
+ return {
2059
+ runId,
2060
+ ending: { status, terminalReason, errorCode: errorCode ?? undefined },
2061
+ row: rows?.[0],
2062
+ };
2044
2063
  }
2045
2064
  async function reapSingleStaleRun(runId, source, maxStaleMs) {
2046
2065
  const completedAt = Date.now();
@@ -2057,7 +2076,8 @@ async function reapSingleStaleRun(runId, source, maxStaleMs) {
2057
2076
  WHERE id = ?
2058
2077
  AND status = 'running'
2059
2078
  AND ${terminalRunEventExclusionSql()}
2060
- AND ${staleClause}`;
2079
+ AND ${staleClause}
2080
+ ${ENDED_RUN_RETURNING}`;
2061
2081
  const updateArgs = [
2062
2082
  STALE_RUN_ERROR_EVENT.errorCode,
2063
2083
  STALE_RUN_ERROR_EVENT.details,
@@ -2096,7 +2116,8 @@ async function reapSingleStaleRun(runId, source, maxStaleMs) {
2096
2116
  if (lease?.isLive && generationMatches) {
2097
2117
  return { reaped: false, outcome: null };
2098
2118
  }
2099
- if (await reconcileTerminalRunInRecoveryTransaction(tx, runId)) {
2119
+ const repaired = await reconcileTerminalRunInRecoveryTransaction(tx, runId);
2120
+ if (repaired) {
2100
2121
  if (generationMatches && lease) {
2101
2122
  await tx.execute({
2102
2123
  sql: `UPDATE agent_execution_leases
@@ -2114,9 +2135,9 @@ async function reapSingleStaleRun(runId, source, maxStaleMs) {
2114
2135
  args: [runId],
2115
2136
  });
2116
2137
  }
2117
- return { reaped: false, outcome: null };
2138
+ return { reaped: false, outcome: null, repaired };
2118
2139
  }
2119
- const { rowsAffected } = await tx.execute({
2140
+ const { rows, rowsAffected } = await tx.execute({
2120
2141
  sql: updateSql,
2121
2142
  args: updateArgs,
2122
2143
  });
@@ -2150,9 +2171,32 @@ async function reapSingleStaleRun(runId, source, maxStaleMs) {
2150
2171
  }
2151
2172
  }
2152
2173
  }
2153
- return { reaped, outcome };
2174
+ return {
2175
+ reaped,
2176
+ outcome,
2177
+ reapedRow: rows?.[0],
2178
+ };
2154
2179
  });
2155
2180
  const { reaped, outcome } = reapResult;
2181
+ // Taken from the committed attempt: a rolled-back one ended nothing.
2182
+ const ended = "repaired" in reapResult && reapResult.repaired
2183
+ ? reapResult.repaired
2184
+ : reaped
2185
+ ? {
2186
+ runId,
2187
+ ending: reapedRunEnding({
2188
+ source,
2189
+ errorCode: STALE_RUN_ERROR_EVENT.errorCode,
2190
+ terminalReason: STALE_RUN_TERMINAL_REASON,
2191
+ recoveryOutcome: staleRunRecoveryLabel(outcome),
2192
+ }),
2193
+ row: "reapedRow" in reapResult ? reapResult.reapedRow : undefined,
2194
+ }
2195
+ : null;
2196
+ // Handed off as soon as it commits: a sweep cut short must not lose the
2197
+ // runs it already ended, which no later sweep can see.
2198
+ if (ended)
2199
+ trackRunsEndedInSql([ended]);
2156
2200
  let forensics = "";
2157
2201
  let priorStageInfo = "";
2158
2202
  if (reaped) {
@@ -2242,8 +2286,8 @@ export async function reapUnclaimedBackgroundRun(runId) {
2242
2286
  }
2243
2287
  const completedAt = Date.now();
2244
2288
  const cutoff = completedAt - UNCLAIMED_BACKGROUND_RUN_GRACE_MS;
2245
- const reaped = await client.transaction(async (tx) => {
2246
- const { rowsAffected } = await tx.execute({
2289
+ const { reaped, row } = await client.transaction(async (tx) => {
2290
+ const { rows, rowsAffected } = await tx.execute({
2247
2291
  sql: `UPDATE agent_runs
2248
2292
  SET status = 'errored',
2249
2293
  completed_at = ?,
@@ -2253,7 +2297,8 @@ export async function reapUnclaimedBackgroundRun(runId) {
2253
2297
  WHERE id = ?
2254
2298
  AND status = 'running'
2255
2299
  AND dispatch_mode = 'background'
2256
- AND COALESCE(heartbeat_at, started_at) < ?`,
2300
+ AND COALESCE(heartbeat_at, started_at) < ?
2301
+ ${ENDED_RUN_RETURNING}`,
2257
2302
  args: [
2258
2303
  completedAt,
2259
2304
  UNCLAIMED_BACKGROUND_RUN_ERROR_EVENT.errorCode,
@@ -2267,7 +2312,7 @@ export async function reapUnclaimedBackgroundRun(runId) {
2267
2312
  if (updated) {
2268
2313
  await appendTerminalRunEventInTransaction(tx, runId, UNCLAIMED_BACKGROUND_RUN_ERROR_EVENT);
2269
2314
  }
2270
- return updated;
2315
+ return { reaped: updated, row: rows?.[0] };
2271
2316
  });
2272
2317
  if (reaped) {
2273
2318
  try {
@@ -2282,6 +2327,11 @@ export async function reapUnclaimedBackgroundRun(runId) {
2282
2327
  extra: { runId },
2283
2328
  });
2284
2329
  }
2330
+ trackReapedRun(runId, {
2331
+ source: "reap-unclaimed-background",
2332
+ errorCode: UNCLAIMED_BACKGROUND_RUN_ERROR_EVENT.errorCode,
2333
+ terminalReason: UNCLAIMED_BACKGROUND_RUN_ERROR_EVENT.errorCode,
2334
+ }, row);
2285
2335
  }
2286
2336
  return reaped;
2287
2337
  }
@@ -2351,7 +2401,7 @@ export async function finalizeClaimedBackgroundRunFailure(runId, failure) {
2351
2401
  return false;
2352
2402
  }
2353
2403
  const expectedGeneration = scope.worker_generation == null ? null : String(scope.worker_generation);
2354
- return client.transaction(async (tx) => {
2404
+ const ended = await client.transaction(async (tx) => {
2355
2405
  await lockThreadRunSlot(tx, scope.thread_id);
2356
2406
  const lease = await lockTurnExecutionLease(tx, scope.thread_id, scope.turn_id);
2357
2407
  const selected = await tx.execute({
@@ -2364,7 +2414,7 @@ export async function finalizeClaimedBackgroundRunFailure(runId, failure) {
2364
2414
  run.dispatch_mode !== "background-processing" ||
2365
2415
  (run.worker_generation == null ? null : String(run.worker_generation)) !==
2366
2416
  expectedGeneration) {
2367
- return false;
2417
+ return null;
2368
2418
  }
2369
2419
  const generationMatches = run.worker_generation == null
2370
2420
  ? lease === null || lease.ownerRunId === runId
@@ -2372,14 +2422,15 @@ export async function finalizeClaimedBackgroundRunFailure(runId, failure) {
2372
2422
  lease.ownerRunId === runId &&
2373
2423
  String(run.worker_generation) === lease.generation;
2374
2424
  if (!generationMatches)
2375
- return false;
2425
+ return null;
2376
2426
  const updated = await tx.execute({
2377
2427
  sql: `UPDATE agent_runs
2378
2428
  SET status = 'errored', completed_at = ?, error_code = ?,
2379
2429
  error_detail = ?, terminal_reason = ?, dispatch_payload = NULL
2380
2430
  WHERE id = ? AND status = 'running'
2381
2431
  AND dispatch_mode = 'background-processing'
2382
- AND ${terminalRunEventExclusionSql()}`,
2432
+ AND ${terminalRunEventExclusionSql()}
2433
+ ${ENDED_RUN_RETURNING}`,
2383
2434
  args: [
2384
2435
  Date.now(),
2385
2436
  failure.errorCode,
@@ -2389,7 +2440,7 @@ export async function finalizeClaimedBackgroundRunFailure(runId, failure) {
2389
2440
  ],
2390
2441
  });
2391
2442
  if ((updated.rowsAffected ?? 0) !== 1)
2392
- return false;
2443
+ return null;
2393
2444
  const lastEvent = await tx.execute({
2394
2445
  sql: `SELECT seq FROM agent_run_events WHERE run_id = ? ORDER BY seq DESC LIMIT 1`,
2395
2446
  args: [runId],
@@ -2409,8 +2460,16 @@ export async function finalizeClaimedBackgroundRunFailure(runId, failure) {
2409
2460
  if ((inserted.rowsAffected ?? 0) !== 1) {
2410
2461
  throw new Error(`Failed to persist failure event for run ${runId}`);
2411
2462
  }
2412
- return true;
2463
+ return { row: updated.rows?.[0] };
2413
2464
  });
2465
+ if (!ended)
2466
+ return false;
2467
+ trackRunEndedInSql(runId, {
2468
+ status: "errored",
2469
+ terminalReason: `error:${failure.errorCode}`.slice(0, 200),
2470
+ errorCode: failure.errorCode,
2471
+ }, ended.row);
2472
+ return true;
2414
2473
  }
2415
2474
  export async function failRunBeforeDispatch(runId, errorCode, errorDetail, terminalReason) {
2416
2475
  await ensureRunTables();
@@ -2428,7 +2487,7 @@ export async function failRunBeforeDispatch(runId, errorCode, errorDetail, termi
2428
2487
  typeof scope.turn_id !== "string") {
2429
2488
  return false;
2430
2489
  }
2431
- return client.transaction(async (tx) => {
2490
+ const ended = await client.transaction(async (tx) => {
2432
2491
  const lease = await lockTurnExecutionLease(tx, scope.thread_id, scope.turn_id);
2433
2492
  const run = await tx.execute({
2434
2493
  sql: `SELECT status, worker_generation FROM agent_runs WHERE id = ? FOR UPDATE`,
@@ -2436,19 +2495,20 @@ export async function failRunBeforeDispatch(runId, errorCode, errorDetail, termi
2436
2495
  });
2437
2496
  const currentRun = run.rows[0];
2438
2497
  if (currentRun?.status !== "running") {
2439
- return false;
2498
+ return null;
2440
2499
  }
2441
2500
  if (lease?.isLive &&
2442
2501
  lease.ownerRunId === runId &&
2443
2502
  (currentRun.worker_generation == null ||
2444
2503
  String(currentRun.worker_generation) === lease.generation)) {
2445
- return false;
2504
+ return null;
2446
2505
  }
2447
2506
  const updated = await tx.execute({
2448
2507
  sql: `UPDATE agent_runs
2449
2508
  SET status = 'errored', completed_at = ?, error_code = ?,
2450
2509
  error_detail = ?, terminal_reason = ?, dispatch_payload = NULL
2451
- WHERE id = ? AND status = 'running'`,
2510
+ WHERE id = ? AND status = 'running'
2511
+ ${ENDED_RUN_RETURNING}`,
2452
2512
  args: [
2453
2513
  Date.now(),
2454
2514
  errorCode,
@@ -2457,20 +2517,34 @@ export async function failRunBeforeDispatch(runId, errorCode, errorDetail, termi
2457
2517
  runId,
2458
2518
  ],
2459
2519
  });
2460
- return (updated.rowsAffected ?? 0) === 1;
2520
+ return (updated.rowsAffected ?? 0) === 1
2521
+ ? { row: updated.rows?.[0] }
2522
+ : null;
2461
2523
  });
2524
+ if (!ended)
2525
+ return false;
2526
+ // No run loop ever started for this row, so nothing else will report it.
2527
+ trackRunEndedInSql(runId, {
2528
+ status: "errored",
2529
+ terminalReason: terminalReason.slice(0, 200),
2530
+ errorCode,
2531
+ }, ended.row);
2532
+ return true;
2462
2533
  }
2463
2534
  /** The caller owns this claim and has not called startRun yet. */
2464
2535
  export async function releaseBackgroundRunBeforeStart(runId, errorCode, errorDetail) {
2465
2536
  await ensureRunTables();
2466
- const { rowsAffected } = await getDbExec().execute({
2537
+ const { rowsAffected, rows } = await getDbExec().execute({
2467
2538
  sql: `UPDATE agent_runs
2468
2539
  SET status = 'errored', completed_at = ?, error_code = ?, error_detail = ?, dispatch_payload = NULL
2469
- WHERE id = ? AND status = 'running' AND dispatch_mode = 'background-processing'`,
2540
+ WHERE id = ? AND status = 'running' AND dispatch_mode = 'background-processing'
2541
+ ${ENDED_RUN_RETURNING}`,
2470
2542
  args: [Date.now(), errorCode, errorDetail, runId],
2471
2543
  });
2472
2544
  if (rowsAffected !== 1)
2473
2545
  throw new Error(`Unstarted background worker ${runId} could not be released`);
2546
+ // No run loop ever started for this row, so nothing else will report it.
2547
+ trackRunEndedInSql(runId, { status: "errored", terminalReason: `error:${errorCode}`, errorCode }, rows?.[0]);
2474
2548
  }
2475
2549
  export async function getRunStatus(runId) {
2476
2550
  await ensureRunTables();
@@ -2556,7 +2630,7 @@ async function updateRunAborted(runId, reason, requireSafeBoundary) {
2556
2630
  }
2557
2631
  }
2558
2632
  const result = await tx.execute({
2559
- sql: `UPDATE agent_runs SET status = 'aborted', abort_reason = ?, completed_at = ?, terminal_reason = ? WHERE id = ? AND status = 'running'${requireSafeBoundary ? " AND in_flight_since IS NULL" : ""}`,
2633
+ sql: `UPDATE agent_runs SET status = 'aborted', abort_reason = ?, completed_at = ?, terminal_reason = ? WHERE id = ? AND status = 'running'${requireSafeBoundary ? " AND in_flight_since IS NULL" : ""} ${ENDED_RUN_RETURNING}`,
2560
2634
  args: [
2561
2635
  reason ?? "user",
2562
2636
  Date.now(),
@@ -2568,7 +2642,11 @@ async function updateRunAborted(runId, reason, requireSafeBoundary) {
2568
2642
  if (rowsAffected > 0) {
2569
2643
  await revokeExecutionLeasesForRuns(tx, [runId]);
2570
2644
  await appendAbortTerminalRunEventInTransaction(tx, runId, abortEvent);
2571
- return { rowsAffected, appendTerminalEvent: true };
2645
+ return {
2646
+ rowsAffected,
2647
+ appendTerminalEvent: true,
2648
+ endedRow: result.rows?.[0],
2649
+ };
2572
2650
  }
2573
2651
  const terminalState = await tx.execute({
2574
2652
  sql: `SELECT status, terminal_reason, in_flight_since FROM agent_runs WHERE id = ?`,
@@ -2585,6 +2663,9 @@ async function updateRunAborted(runId, reason, requireSafeBoundary) {
2585
2663
  return { rowsAffected: 0, appendTerminalEvent: false };
2586
2664
  };
2587
2665
  const abortResult = await retryAbortTerminalEventTransaction("mark-aborted", () => client.transaction(commitAbort));
2666
+ if (abortResult.rowsAffected > 0) {
2667
+ trackAbortedRun(runId, reason ?? "user", "endedRow" in abortResult ? abortResult.endedRow : undefined);
2668
+ }
2588
2669
  return abortResult.rowsAffected > 0 || abortResult.appendTerminalEvent;
2589
2670
  }
2590
2671
  async function lockExecutionLeasesForRuns(db, runIds) {
@@ -2659,7 +2740,10 @@ export async function markTurnAborted(threadId, turnId, reason = "user") {
2659
2740
  if (!client.transaction) {
2660
2741
  throw new Error("Atomic turn cancellation requires transaction support");
2661
2742
  }
2743
+ let abortedRows = [];
2662
2744
  const commitAbort = async (tx) => {
2745
+ // A retried transaction runs this again; only the committed attempt counts.
2746
+ abortedRows = [];
2663
2747
  const runIds = [];
2664
2748
  await lockThreadRunSlot(tx, threadId);
2665
2749
  await lockAndAssertCurrentExecutionFences(tx);
@@ -2682,9 +2766,10 @@ export async function markTurnAborted(threadId, turnId, reason = "user") {
2682
2766
  await revokeExecutionLeasesForRuns(tx, candidateRunIds);
2683
2767
  if (candidateRunIds.length > 0) {
2684
2768
  const aborted = await tx.execute({
2685
- sql: `UPDATE agent_runs SET status = 'aborted', abort_reason = ?, completed_at = ?, terminal_reason = ? WHERE thread_id = ? AND COALESCE(turn_id, id) = ? AND status = 'running' RETURNING id`,
2769
+ sql: `UPDATE agent_runs SET status = 'aborted', abort_reason = ?, completed_at = ?, terminal_reason = ? WHERE thread_id = ? AND COALESCE(turn_id, id) = ? AND status = 'running' ${ENDED_RUN_RETURNING}`,
2686
2770
  args: [reason, now, `aborted:${reason}`, threadId, turnId],
2687
2771
  });
2772
+ abortedRows = aborted.rows;
2688
2773
  runIds.push(...aborted.rows
2689
2774
  .map((row) => String(row.id ?? ""))
2690
2775
  .filter(Boolean));
@@ -2789,6 +2874,13 @@ export async function markTurnAborted(threadId, turnId, reason = "user") {
2789
2874
  return "aborted";
2790
2875
  };
2791
2876
  const outcome = await retryAbortTerminalEventTransaction("mark-turn-aborted", () => client.transaction(commitAbort));
2877
+ trackRunsEndedInSql(abortedRows
2878
+ .filter((row) => typeof row.id === "string")
2879
+ .map((row) => ({
2880
+ runId: row.id,
2881
+ ending: abortedRunEnding(reason),
2882
+ row,
2883
+ })));
2792
2884
  return outcome;
2793
2885
  }
2794
2886
  export async function getRunTurnRef(runId) {
@@ -3904,6 +3996,143 @@ export async function listErroredRuns(options) {
3904
3996
  export async function ensureTerminalRunEvent(runId, event) {
3905
3997
  return appendTerminalRunEvent(runId, event);
3906
3998
  }
3999
+ /**
4000
+ * A duplicate request, or a worker that lost its turn's execution lease, is
4001
+ * aborted before it does any work; the run it stood in for reports the turn.
4002
+ */
4003
+ const NOT_A_RUN_ABORT_REASONS = new Set([
4004
+ "duplicate_submission",
4005
+ "execution-lease-busy",
4006
+ ]);
4007
+ function trackAbortedRun(runId, abortReason, returnedRow) {
4008
+ trackRunEndedInSql(runId, abortedRunEnding(abortReason), returnedRow);
4009
+ }
4010
+ function abortedRunEnding(abortReason) {
4011
+ return {
4012
+ status: "aborted",
4013
+ terminalReason: `aborted:${abortReason}`,
4014
+ abortReason,
4015
+ };
4016
+ }
4017
+ /**
4018
+ * The ended run and its turn's initiator, read in the write that ended it: a
4019
+ * later read can find the initiator row already pruned.
4020
+ */
4021
+ const ENDED_RUN_COLUMNS = `id, thread_id, turn_id, dispatch_mode, started_at, completed_at,
4022
+ (SELECT principal_email FROM agent_turn_initiators i
4023
+ WHERE i.thread_id = agent_runs.thread_id
4024
+ AND i.turn_id = COALESCE(agent_runs.turn_id, agent_runs.id)) AS principal_email,
4025
+ (SELECT auth_user_id FROM agent_turn_initiators i
4026
+ WHERE i.thread_id = agent_runs.thread_id
4027
+ AND i.turn_id = COALESCE(agent_runs.turn_id, agent_runs.id)) AS auth_user_id,
4028
+ (SELECT is_anonymous FROM agent_turn_initiators i
4029
+ WHERE i.thread_id = agent_runs.thread_id
4030
+ AND i.turn_id = COALESCE(agent_runs.turn_id, agent_runs.id)) AS is_anonymous`;
4031
+ const ENDED_RUN_RETURNING = `RETURNING ${ENDED_RUN_COLUMNS}`;
4032
+ function finiteMs(value) {
4033
+ if (value == null)
4034
+ return undefined;
4035
+ const ms = Number(value);
4036
+ return Number.isFinite(ms) ? ms : undefined;
4037
+ }
4038
+ /**
4039
+ * Sends `agent_run_terminal` for a run this process ended on another
4040
+ * process's behalf. Call it only once the reap's UPDATE has affected the run:
4041
+ * the conditional `status = 'running'` write is what makes each run's terminal
4042
+ * event fire once across the reaper and the run's own process.
4043
+ *
4044
+ * Returns synchronously; the reap never waits on tracking or sees it fail.
4045
+ */
4046
+ function trackReapedRun(runId, reap, returnedRow) {
4047
+ trackRunEndedInSql(runId, reapedRunEnding(reap), returnedRow);
4048
+ }
4049
+ /** `recovered` and `not_background` stand alone; any other outcome declined. */
4050
+ function staleRunRecoveryLabel(recovery) {
4051
+ if (!recovery)
4052
+ return undefined;
4053
+ return recovery.outcome === "recovered" ||
4054
+ recovery.outcome === "not_background"
4055
+ ? recovery.outcome
4056
+ : `declined:${recovery.outcome}`;
4057
+ }
4058
+ function reapedRunEnding(reap) {
4059
+ return {
4060
+ status: "errored",
4061
+ terminalReason: reap.terminalReason,
4062
+ errorCode: reap.errorCode,
4063
+ reap: { source: reap.source, recoveryOutcome: reap.recoveryOutcome },
4064
+ };
4065
+ }
4066
+ /**
4067
+ * Sends `agent_run_terminal` for a run a SQL write ended (a reap, stop,
4068
+ * repair, or pre-start release),
4069
+ * which the run's own process then cannot report: its own conditional
4070
+ * terminal write finds no running row. Call it only for runs the ending
4071
+ * UPDATE affected, so whichever write wins is the one that reports.
4072
+ */
4073
+ function trackRunEndedInSql(runId, ending, returnedRow) {
4074
+ trackRunsEndedInSql([{ runId, ending, row: returnedRow }]);
4075
+ }
4076
+ /** One background send for the runs one write ended. */
4077
+ function trackRunsEndedInSql(endedRuns) {
4078
+ const runs = endedRuns.filter((run) => !run.ending.abortReason ||
4079
+ !NOT_A_RUN_ABORT_REASONS.has(run.ending.abortReason));
4080
+ if (runs.length === 0)
4081
+ return;
4082
+ const label = () => runs.map((run) => run.runId).join(", ");
4083
+ try {
4084
+ // The ending request is whoever happened to sweep or stop the run: its
4085
+ // synthetic flag, email and client platform must not decide or describe
4086
+ // another user's run, so the send runs without them.
4087
+ const work = Promise.resolve(runWithRequestContext({ isSyntheticTraffic: false }, () => sendSqlEndedRunTerminals(runs))).catch((error) => {
4088
+ console.error("[run-store] agent_run_terminal was not sent for a run ended in SQL:", label(), error instanceof Error ? error.message : error);
4089
+ });
4090
+ platformWaitUntil()?.(work);
4091
+ }
4092
+ catch (error) {
4093
+ console.error("[run-store] agent_run_terminal was not sent for a run ended in SQL:", label(), error instanceof Error ? error.message : error);
4094
+ }
4095
+ }
4096
+ async function sendSqlEndedRunTerminals(runs) {
4097
+ await Promise.all(runs.map(async (run) => {
4098
+ const row = run.row;
4099
+ if (typeof row?.thread_id !== "string") {
4100
+ console.error("[run-store] agent_run_terminal was not sent: ended run row is unreadable:", run.runId);
4101
+ return;
4102
+ }
4103
+ const startedAt = finiteMs(row.started_at);
4104
+ const completedAt = finiteMs(row.completed_at);
4105
+ await trackRunTerminal({
4106
+ ...run.ending,
4107
+ runId: run.runId,
4108
+ threadId: row.thread_id,
4109
+ // Same turn the initiator row and the run's own process key on.
4110
+ turnId: typeof row.turn_id === "string" && row.turn_id
4111
+ ? row.turn_id
4112
+ : run.runId,
4113
+ dispatchMode: typeof row.dispatch_mode === "string"
4114
+ ? row.dispatch_mode
4115
+ : undefined,
4116
+ durationMs: startedAt !== undefined && completedAt !== undefined
4117
+ ? completedAt - startedAt
4118
+ : undefined,
4119
+ },
4120
+ // The turn's initiator, not the request that ended the run; `track()`
4121
+ // drops it when it is a test identity. An anonymous one is not a user.
4122
+ row.is_anonymous === true || row.is_anonymous === "t"
4123
+ ? undefined
4124
+ : {
4125
+ userId: typeof row.principal_email === "string"
4126
+ ? row.principal_email
4127
+ : undefined,
4128
+ authUserId: typeof row.auth_user_id === "string"
4129
+ ? row.auth_user_id
4130
+ : undefined,
4131
+ });
4132
+ }));
4133
+ const { flushTracking } = await import("../tracking/registry.js");
4134
+ await flushTracking();
4135
+ }
3907
4136
  /** Projects a run after its stale status and terminal event committed. */
3908
4137
  async function finalizeStaleRun(runId, source, projectThread = true) {
3909
4138
  if (!projectThread)
@@ -0,0 +1,29 @@
1
+ import type { TrackingMeta } from "../tracking/registry.js";
2
+ export interface RunTerminalTrackingFields {
3
+ runId: string;
4
+ threadId: string;
5
+ turnId: string;
6
+ status: "completed" | "errored" | "aborted" | "truncated";
7
+ terminalReason: string;
8
+ errorCode?: string;
9
+ dispatchMode?: string;
10
+ abortReason?: string;
11
+ durationMs?: number;
12
+ model?: string;
13
+ engineName?: string;
14
+ attemptCount?: number;
15
+ /** For `write_tool_outcome_unknown`: the call held back, and why. */
16
+ blockedTool?: string;
17
+ blockedBy?: "rejection" | "interruption";
18
+ /** Set only by a reap, which ends a run outside the process running it. */
19
+ reap?: {
20
+ source: string;
21
+ recoveryOutcome?: string;
22
+ };
23
+ }
24
+ /**
25
+ * The one `agent_run_terminal` sender, shared by the run's own process and the
26
+ * SQL writes that end a run for it, so the two shapes cannot drift apart. The returned promise never
27
+ * rejects, so a caller can hand it to `waitUntil` without guarding it.
28
+ */
29
+ export declare function trackRunTerminal(fields: RunTerminalTrackingFields, source: TrackingMeta | undefined): Promise<void>;
@@ -0,0 +1,56 @@
1
+ import { runErrorTelemetryProperties } from "./engine/error-telemetry.js";
2
+ /**
3
+ * The one `agent_run_terminal` sender, shared by the run's own process and the
4
+ * SQL writes that end a run for it, so the two shapes cannot drift apart. The returned promise never
5
+ * rejects, so a caller can hand it to `waitUntil` without guarding it.
6
+ */
7
+ export function trackRunTerminal(fields, source) {
8
+ try {
9
+ const properties = {
10
+ source: "agent_run_manager",
11
+ run_id: fields.runId,
12
+ thread_id: fields.threadId,
13
+ turn_id: fields.turnId,
14
+ status: fields.status,
15
+ terminal_reason: fields.terminalReason,
16
+ error_code: fields.errorCode,
17
+ ...(fields.errorCode
18
+ ? runErrorTelemetryProperties(fields.errorCode)
19
+ : {}),
20
+ dispatch_mode: fields.dispatchMode,
21
+ abort_reason: fields.abortReason,
22
+ duration_ms: fields.durationMs,
23
+ model: fields.model,
24
+ engine: fields.engineName,
25
+ attempt_count: fields.attemptCount,
26
+ blocked_tool: fields.blockedTool,
27
+ blocked_by: fields.blockedBy,
28
+ ...(fields.reap
29
+ ? {
30
+ reaped: true,
31
+ reap_source: fields.reap.source,
32
+ recovery_outcome: fields.reap.recoveryOutcome,
33
+ }
34
+ : {}),
35
+ };
36
+ for (const key of Object.keys(properties)) {
37
+ if (properties[key] === undefined)
38
+ delete properties[key];
39
+ }
40
+ return Promise.all([
41
+ import("../tracking/registry.js"),
42
+ import("../observability/tracking-identity.js"),
43
+ ])
44
+ .then(([{ track }, { trackingIdentityProperties }]) => {
45
+ track("agent_run_terminal", { ...properties, ...trackingIdentityProperties() }, source);
46
+ })
47
+ .catch((error) => {
48
+ console.error("[run-terminal-tracking] agent_run_terminal was not sent:", fields.runId, error instanceof Error ? error.message : error);
49
+ });
50
+ }
51
+ catch (error) {
52
+ // Tracking must never affect the agent run or its persisted status.
53
+ console.error("[run-terminal-tracking] agent_run_terminal was not sent:", fields.runId, error instanceof Error ? error.message : error);
54
+ return Promise.resolve();
55
+ }
56
+ }
@@ -0,0 +1,9 @@
1
+ import { setResponseHeader } from "h3";
2
+ /**
3
+ * The turn this request's run is stored under, for the client to report. The
4
+ * id can come from the request body, and a header value outside visible ASCII
5
+ * throws once the run has started, so such an id is left to the client's own.
6
+ * With no id (a replayed run held by another process) no header is set, and
7
+ * the client reports the replay's turn as unknown rather than guessing it.
8
+ */
9
+ export declare function setRunTurnIdHeader(event: Parameters<typeof setResponseHeader>[0], turnId: string | undefined): void;
@@ -0,0 +1,13 @@
1
+ import { setResponseHeader } from "h3";
2
+ /**
3
+ * The turn this request's run is stored under, for the client to report. The
4
+ * id can come from the request body, and a header value outside visible ASCII
5
+ * throws once the run has started, so such an id is left to the client's own.
6
+ * With no id (a replayed run held by another process) no header is set, and
7
+ * the client reports the replay's turn as unknown rather than guessing it.
8
+ */
9
+ export function setRunTurnIdHeader(event, turnId) {
10
+ if (turnId && /^[\x20-\x7e]+$/.test(turnId)) {
11
+ setResponseHeader(event, "X-Turn-Id", turnId);
12
+ }
13
+ }
@@ -240,6 +240,11 @@ export interface AgentChatRichEventEnvelope {
240
240
  references?: AgentChatRichEventReference[];
241
241
  metadata?: Record<string, unknown>;
242
242
  }
243
+ /**
244
+ * Why a failed tool call failed, where the loop could tell: its arguments
245
+ * failed validation before it ran, or it failed having written nothing.
246
+ */
247
+ export type ToolErrorClass = "input_invalid" | "rejected";
243
248
  export type AgentChatEvent = {
244
249
  type: "text";
245
250
  text: string;
@@ -308,6 +313,8 @@ export type AgentChatEvent = {
308
313
  /** False marks current completed events; absence is reserved for older journals. */
309
314
  approvalPause?: boolean;
310
315
  outcomeUnknown?: true;
316
+ /** Why a failed call failed, where the loop could tell. */
317
+ errorClass?: ToolErrorClass;
311
318
  replayed?: true;
312
319
  fileMutation?: AgentFileMutationProof;
313
320
  artifacts?: ArtifactReceipt[];
@@ -392,6 +399,10 @@ export type AgentChatEvent = {
392
399
  details?: string;
393
400
  recoverable?: boolean;
394
401
  providerRetryable?: boolean;
402
+ /** For `write_tool_outcome_unknown`: the call held back. */
403
+ blockedTool?: string;
404
+ /** What left the earlier call of `blockedTool` unknown. */
405
+ blockedBy?: "rejection" | "interruption";
395
406
  }
396
407
  /**
397
408
  * Legacy SSE terminal event. New streams emit