@agent-native/core 0.84.18 → 0.84.20

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 (55) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +13 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/run-store.ts +180 -13
  5. package/corpus/core/src/client/AssistantChat.tsx +49 -0
  6. package/corpus/core/src/client/active-run-state.ts +30 -0
  7. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +41 -3
  8. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +133 -0
  9. package/corpus/core/src/client/extensions/ExtensionsSidebarSection.tsx +2 -2
  10. package/corpus/core/src/extensions/html-shell.ts +17 -3
  11. package/corpus/core/src/extensions/theme.ts +39 -0
  12. package/corpus/templates/analytics/actions/provider-api-request.ts +8 -1
  13. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +2 -1
  14. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +91 -33
  15. package/corpus/templates/clips/changelog/2026-07-01-chrome-recording-uploads-now-serialize-chunks-to-avoid-resum.md +6 -0
  16. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  17. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +30 -23
  18. package/dist/agent/run-store.d.ts +19 -6
  19. package/dist/agent/run-store.d.ts.map +1 -1
  20. package/dist/agent/run-store.js +163 -13
  21. package/dist/agent/run-store.js.map +1 -1
  22. package/dist/client/AssistantChat.d.ts +2 -0
  23. package/dist/client/AssistantChat.d.ts.map +1 -1
  24. package/dist/client/AssistantChat.js +36 -2
  25. package/dist/client/AssistantChat.js.map +1 -1
  26. package/dist/client/active-run-state.d.ts +3 -0
  27. package/dist/client/active-run-state.d.ts.map +1 -1
  28. package/dist/client/active-run-state.js +24 -0
  29. package/dist/client/active-run-state.js.map +1 -1
  30. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  31. package/dist/client/extensions/EmbeddedExtension.js +32 -3
  32. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  33. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  34. package/dist/client/extensions/ExtensionViewer.js +59 -2
  35. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  36. package/dist/client/extensions/ExtensionsSidebarSection.js +2 -2
  37. package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
  38. package/dist/collab/awareness.d.ts +2 -2
  39. package/dist/collab/awareness.d.ts.map +1 -1
  40. package/dist/collab/routes.d.ts +2 -2
  41. package/dist/extensions/html-shell.d.ts.map +1 -1
  42. package/dist/extensions/html-shell.js +17 -3
  43. package/dist/extensions/html-shell.js.map +1 -1
  44. package/dist/extensions/theme.d.ts +9 -0
  45. package/dist/extensions/theme.d.ts.map +1 -1
  46. package/dist/extensions/theme.js +38 -0
  47. package/dist/extensions/theme.js.map +1 -1
  48. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  49. package/dist/notifications/routes.d.ts +3 -3
  50. package/dist/observability/routes.d.ts +6 -6
  51. package/dist/progress/routes.d.ts +1 -1
  52. package/dist/resources/handlers.d.ts +3 -3
  53. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  54. package/dist/server/transcribe-voice.d.ts +1 -1
  55. package/package.json +1 -1
@@ -25,6 +25,8 @@ export const RUN_STALE_MS = 15_000;
25
25
  * With the normal 15s window the reaper would falsely kill that freshly-
26
26
  * inserted-but-not-yet-heartbeaten row. 90s tolerates a slow background
27
27
  * cold-start while still reaping a genuinely dead background worker promptly.
28
+ * Claimed background workers heartbeat during long work; the stale watchdog is a
29
+ * liveness timeout, not the Netlify background-function execution budget.
28
30
  *
29
31
  * Only applied to rows explicitly marked background-dispatched; ordinary
30
32
  * foreground runs keep the tight 15s window unchanged.
@@ -71,13 +73,14 @@ export const CLAIMED_BACKGROUND_WORKER_FAILED_ERROR_EVENT = {
71
73
  * Grace period before a never-claimed background run (dispatch_mode still
72
74
  * 'background', no worker claim) is treated as a dead handoff and reaped.
73
75
  *
74
- * This is intentionally MUCH tighter than `BACKGROUND_RUN_STALE_MS` (90s). The
75
- * wide 90s window exists ONLY to protect a CLAIMED worker whose heartbeat lags
76
- * during a slow cold start. A run that is still `dispatch_mode = 'background'`
77
- * has, by definition, NO worker — nothing to protect — so once a Netlify
76
+ * This is intentionally tighter than `BACKGROUND_RUN_STALE_MS`. That wider
77
+ * window protects cold-starting or temporarily delayed background dispatches,
78
+ * while claimed workers stay alive by heartbeat/progress updates. A run that is
79
+ * still `dispatch_mode = 'background'` has, by definition, NO worker — nothing
80
+ * to protect — so once a Netlify
78
81
  * background function has had a reasonable cold-start window to claim it and
79
82
  * hasn't, the handoff is dead and should surface promptly instead of leaving
80
- * the user staring at a spinner for 90s. 25s comfortably exceeds a normal
83
+ * the user staring at a spinner for the durable-worker window. 25s comfortably exceeds a normal
81
84
  * Netlify Lambda cold start while still failing fast on a silent worker death.
82
85
  */
83
86
  export const UNCLAIMED_BACKGROUND_RUN_GRACE_MS = 25_000;
@@ -109,6 +112,7 @@ async function ensureRunTables() {
109
112
  CREATE TABLE IF NOT EXISTS agent_run_events (
110
113
  run_id TEXT NOT NULL,
111
114
  seq ${intType()} NOT NULL,
115
+ event_at ${intType()},
112
116
  event_data TEXT NOT NULL,
113
117
  PRIMARY KEY (run_id, seq)
114
118
  )
@@ -179,6 +183,7 @@ async function ensureRunTables() {
179
183
  await ensureColumnExists("agent_runs", col, `ALTER TABLE agent_runs ADD COLUMN IF NOT EXISTS ${col} ${colType}`);
180
184
  }
181
185
  await ensureTableExists("agent_run_events", agentRunEventsCreateSql);
186
+ await ensureColumnExists("agent_run_events", "event_at", `ALTER TABLE agent_run_events ADD COLUMN IF NOT EXISTS event_at ${intType()}`);
182
187
  await ensureTableExists("agent_tool_ledger", agentToolLedgerCreateSql);
183
188
  // Widen millisecond-timestamp columns that older deployments created as
184
189
  // 32-bit `INTEGER`. `insertRun()` writes `Date.now()` into `started_at`
@@ -192,6 +197,7 @@ async function ensureRunTables() {
192
197
  "heartbeat_at",
193
198
  "last_progress_at",
194
199
  ]);
200
+ await widenIntColumnsToBigInt("agent_run_events", ["event_at"]);
195
201
  await widenIntColumnsToBigInt("agent_tool_ledger", ["completed_at"]);
196
202
  return;
197
203
  }
@@ -254,6 +260,12 @@ async function ensureRunTables() {
254
260
  }
255
261
  }
256
262
  await client.execute(agentRunEventsCreateSql);
263
+ try {
264
+ await client.execute(`ALTER TABLE agent_run_events ADD COLUMN event_at ${intType()}`);
265
+ }
266
+ catch {
267
+ // Column already exists — ignore
268
+ }
257
269
  await client.execute(agentToolLedgerCreateSql);
258
270
  // Widen millisecond-timestamp columns that older deployments created as
259
271
  // 32-bit `INTEGER`. `insertRun()` writes `Date.now()` into `started_at`
@@ -267,6 +279,7 @@ async function ensureRunTables() {
267
279
  "heartbeat_at",
268
280
  "last_progress_at",
269
281
  ]);
282
+ await widenIntColumnsToBigInt("agent_run_events", ["event_at"]);
270
283
  await widenIntColumnsToBigInt("agent_tool_ledger", ["completed_at"]);
271
284
  })().catch((err) => {
272
285
  // Retry init on the next call after a failed startup.
@@ -390,6 +403,19 @@ function backgroundAwareStaleCutoffSql() {
390
403
  // overflows int4. The cast keeps the subtraction 64-bit; a no-op on SQLite.
391
404
  return `(CAST(? AS BIGINT) - CASE WHEN dispatch_mode LIKE 'background%' THEN ${BACKGROUND_RUN_STALE_MS} ELSE ${RUN_STALE_MS} END)`;
392
405
  }
406
+ function terminalRunEventExclusionSql(runIdColumn = "id") {
407
+ return `NOT EXISTS (
408
+ SELECT 1 FROM agent_run_events terminal_events
409
+ WHERE terminal_events.run_id = agent_runs.${runIdColumn}
410
+ AND (
411
+ terminal_events.event_data LIKE '{"type":"done"%'
412
+ OR terminal_events.event_data LIKE '{"type":"error"%'
413
+ OR terminal_events.event_data LIKE '{"type":"missing_api_key"%'
414
+ OR terminal_events.event_data LIKE '{"type":"loop_limit"%'
415
+ OR terminal_events.event_data LIKE '{"type":"auto_continue"%'
416
+ )
417
+ )`;
418
+ }
393
419
  /**
394
420
  * Liveness basis for the stale reapers: the MOST RECENT of `heartbeat_at`
395
421
  * ("process is up", bumped on a 1.5s timer) and `last_progress_at` ("real work
@@ -511,6 +537,7 @@ export async function tryClaimRunSlot(threadId, maxStaleMs) {
511
537
  sql: `SELECT id FROM agent_runs
512
538
  WHERE thread_id = ?
513
539
  AND status = 'running'
540
+ AND ${terminalRunEventExclusionSql()}
514
541
  AND COALESCE(heartbeat_at, started_at) >= ?
515
542
  ORDER BY started_at DESC LIMIT 1`,
516
543
  args: [threadId, heartbeatCutoff],
@@ -524,6 +551,7 @@ export async function tryClaimRunSlot(threadId, maxStaleMs) {
524
551
  sql: `SELECT id FROM agent_runs
525
552
  WHERE thread_id = ?
526
553
  AND status = 'running'
554
+ AND ${terminalRunEventExclusionSql()}
527
555
  AND COALESCE(heartbeat_at, started_at) >= ${backgroundAwareStaleCutoffSql()}
528
556
  ORDER BY started_at DESC LIMIT 1`,
529
557
  args: [threadId, now],
@@ -578,6 +606,101 @@ export async function setRunTerminalReason(runId, terminalReason) {
578
606
  // Diagnostics are best-effort; never let them break completion.
579
607
  }
580
608
  }
609
+ function terminalStatusForEvent(event) {
610
+ if (event.type === "error")
611
+ return "errored";
612
+ if (event.type === "done" ||
613
+ event.type === "missing_api_key" ||
614
+ event.type === "loop_limit" ||
615
+ event.type === "auto_continue") {
616
+ return "completed";
617
+ }
618
+ return null;
619
+ }
620
+ function terminalReasonForEvent(event) {
621
+ if (event.type === "auto_continue")
622
+ return event.reason || "auto_continue";
623
+ if (event.type === "loop_limit")
624
+ return "loop_limit";
625
+ if (event.type === "missing_api_key")
626
+ return "missing_api_key";
627
+ if (event.type === "error")
628
+ return `error:${event.errorCode || "unknown"}`;
629
+ if (event.type === "done")
630
+ return "done";
631
+ return null;
632
+ }
633
+ async function getLatestRunEvent(runId) {
634
+ const client = getDbExec();
635
+ const { rows } = await client.execute({
636
+ sql: `SELECT seq, event_data, event_at FROM agent_run_events WHERE run_id = ? ORDER BY seq DESC LIMIT 1`,
637
+ args: [runId],
638
+ });
639
+ const row = rows[0];
640
+ const raw = row?.event_data;
641
+ if (!raw)
642
+ return null;
643
+ try {
644
+ const eventAt = row.event_at == null ? NaN : Number(row.event_at);
645
+ return {
646
+ event: JSON.parse(raw),
647
+ eventAt: Number.isFinite(eventAt) && eventAt > 0 ? eventAt : null,
648
+ };
649
+ }
650
+ catch {
651
+ return null;
652
+ }
653
+ }
654
+ /**
655
+ * Repair a run whose terminal event was durably appended but whose final
656
+ * `agent_runs.status` write lost a race with reconnect/reaper code.
657
+ *
658
+ * The event ledger is the durable transcript users see. If its latest event is
659
+ * terminal, the run is no longer alive and must not be converted into a stale
660
+ * error later. This keeps `agent_runs` and `agent_run_events` from telling two
661
+ * different stories after delayed DB writes or background function teardown.
662
+ */
663
+ export async function reconcileTerminalRunFromEvents(runId) {
664
+ await ensureRunTables();
665
+ const latest = await getLatestRunEvent(runId);
666
+ if (!latest)
667
+ return false;
668
+ const status = terminalStatusForEvent(latest.event);
669
+ const terminalReason = terminalReasonForEvent(latest.event);
670
+ if (!status || !terminalReason)
671
+ return false;
672
+ const client = getDbExec();
673
+ const errorCode = latest.event.type === "error" ? (latest.event.errorCode ?? null) : null;
674
+ const errorDetail = latest.event.type === "error"
675
+ ? (latest.event.details || latest.event.error || "").slice(0, 2000) ||
676
+ null
677
+ : null;
678
+ const { rowsAffected } = await client.execute({
679
+ sql: `UPDATE agent_runs
680
+ SET status = ?,
681
+ completed_at = COALESCE(completed_at, ?, ${livenessBasisSql()}),
682
+ error_code = CASE WHEN ? IS NOT NULL THEN ? ELSE error_code END,
683
+ error_detail = CASE WHEN ? IS NOT NULL THEN ? ELSE error_detail END,
684
+ terminal_reason = COALESCE(terminal_reason, ?)
685
+ WHERE id = ?
686
+ AND (
687
+ status = 'running'
688
+ OR (status = 'errored' AND error_code = ?)
689
+ )`,
690
+ args: [
691
+ status,
692
+ latest.eventAt,
693
+ errorCode,
694
+ errorCode,
695
+ errorDetail,
696
+ errorDetail,
697
+ terminalReason,
698
+ runId,
699
+ STALE_RUN_ERROR_EVENT.errorCode,
700
+ ],
701
+ });
702
+ return (rowsAffected ?? 0) > 0;
703
+ }
581
704
  /**
582
705
  * Diagnostic stage names recorded onto a background run as it moves through the
583
706
  * `_process-run` worker pipeline. Each value is the LAST stage successfully
@@ -695,6 +818,8 @@ export async function bumpRunProgress(runId) {
695
818
  */
696
819
  export async function reapIfStale(runId, maxStaleMs) {
697
820
  await ensureRunTables();
821
+ if (await reconcileTerminalRunFromEvents(runId))
822
+ return false;
698
823
  const client = getDbExec();
699
824
  const completedAt = Date.now();
700
825
  // Background-dispatched runs get the wider stale window so a slow cold-start
@@ -713,6 +838,7 @@ export async function reapIfStale(runId, maxStaleMs) {
713
838
  terminal_reason = ?
714
839
  WHERE id = ?
715
840
  AND status = 'running'
841
+ AND ${terminalRunEventExclusionSql()}
716
842
  AND ${staleClause}`,
717
843
  args: [
718
844
  completedAt,
@@ -724,6 +850,8 @@ export async function reapIfStale(runId, maxStaleMs) {
724
850
  ],
725
851
  });
726
852
  const reaped = (rowsAffected ?? 0) > 0;
853
+ if (!reaped && (await reconcileTerminalRunFromEvents(runId)))
854
+ return false;
727
855
  if (reaped) {
728
856
  await safeAppendTerminalRunEvent(runId, STALE_RUN_ERROR_EVENT, "reap-if-stale");
729
857
  }
@@ -737,7 +865,7 @@ export async function reapIfStale(runId, maxStaleMs) {
737
865
  * before it can claim), the row stays `background`, never heartbeats again, and
738
866
  * — because dispatch returned 202 — the foreground already returned the SSE
739
867
  * stream, so the existing fast-fail inline fallback never engaged. The run would
740
- * otherwise hang for the full 90s background window and then error opaquely.
868
+ * otherwise hang for the full durable background window and then error opaquely.
741
869
  *
742
870
  * This reaps such a run EARLY and DISTINCTLY: a row that is still unclaimed
743
871
  * (`dispatch_mode = 'background'`) past the tight `UNCLAIMED_BACKGROUND_RUN_GRACE_MS`
@@ -858,8 +986,8 @@ export async function insertRunEvent(runId, seq, eventData) {
858
986
  // run aborts at the same time the producer emits its final event.
859
987
  // Treat the second write as a no-op so the run completes cleanly.
860
988
  await client.execute({
861
- sql: `INSERT INTO agent_run_events (run_id, seq, event_data) VALUES (?, ?, ?) ON CONFLICT (run_id, seq) DO NOTHING`,
862
- args: [runId, seq, eventData],
989
+ sql: `INSERT INTO agent_run_events (run_id, seq, event_at, event_data) VALUES (?, ?, ?, ?) ON CONFLICT (run_id, seq) DO NOTHING`,
990
+ args: [runId, seq, Date.now(), eventData],
863
991
  });
864
992
  }
865
993
  export async function getRunEventsSince(runId, fromSeq) {
@@ -895,12 +1023,18 @@ export async function getRunByThread(threadId, options) {
895
1023
  await ensureRunTables();
896
1024
  const client = getDbExec();
897
1025
  const sql = options?.includeTerminal
898
- ? `SELECT id, thread_id, turn_id, status, started_at, heartbeat_at, completed_at, last_progress_at, dispatch_mode, terminal_reason, diag_stage FROM agent_runs WHERE thread_id = ? ORDER BY started_at DESC LIMIT 1`
899
- : `SELECT id, thread_id, turn_id, status, started_at, heartbeat_at, completed_at, last_progress_at, dispatch_mode, terminal_reason, diag_stage FROM agent_runs WHERE thread_id = ? AND status = 'running' ORDER BY started_at DESC LIMIT 1`;
1026
+ ? `SELECT id, thread_id, turn_id, status, started_at, heartbeat_at, completed_at, last_progress_at, dispatch_mode, terminal_reason, diag_stage, error_code FROM agent_runs WHERE thread_id = ? ORDER BY started_at DESC LIMIT 1`
1027
+ : `SELECT id, thread_id, turn_id, status, started_at, heartbeat_at, completed_at, last_progress_at, dispatch_mode, terminal_reason, diag_stage, error_code FROM agent_runs WHERE thread_id = ? AND status = 'running' ORDER BY started_at DESC LIMIT 1`;
900
1028
  const { rows } = await client.execute({ sql, args: [threadId] });
901
1029
  if (rows.length === 0)
902
1030
  return null;
903
1031
  const r = rows[0];
1032
+ const canReconcileFromEvents = r.status === "running" ||
1033
+ (r.status === "errored" &&
1034
+ r.error_code === STALE_RUN_ERROR_EVENT.errorCode);
1035
+ if (canReconcileFromEvents && (await reconcileTerminalRunFromEvents(r.id))) {
1036
+ return getRunByThread(threadId, options);
1037
+ }
904
1038
  return {
905
1039
  id: r.id,
906
1040
  threadId: r.thread_id,
@@ -1018,6 +1152,12 @@ export async function reapAllStaleRuns() {
1018
1152
  AND ${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}`,
1019
1153
  args: [now],
1020
1154
  });
1155
+ for (const row of stale.rows) {
1156
+ const id = row.id;
1157
+ if (typeof id === "string") {
1158
+ await reconcileTerminalRunFromEvents(id);
1159
+ }
1160
+ }
1021
1161
  const completedAt = Date.now();
1022
1162
  const { rowsAffected } = await client.execute({
1023
1163
  sql: `UPDATE agent_runs
@@ -1027,6 +1167,7 @@ export async function reapAllStaleRuns() {
1027
1167
  error_detail = ?,
1028
1168
  terminal_reason = ?
1029
1169
  WHERE status = 'running'
1170
+ AND ${terminalRunEventExclusionSql()}
1030
1171
  AND ${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}`,
1031
1172
  args: [
1032
1173
  completedAt,
@@ -1069,6 +1210,12 @@ export async function cleanupOldRuns(olderThanMs, erroredOlderThanMs) {
1069
1210
  )`,
1070
1211
  args: [now, cutoff],
1071
1212
  });
1213
+ for (const row of stale.rows) {
1214
+ const id = row.id;
1215
+ if (typeof id === "string") {
1216
+ await reconcileTerminalRunFromEvents(id);
1217
+ }
1218
+ }
1072
1219
  const completedAt = Date.now();
1073
1220
  await client.execute({
1074
1221
  sql: `UPDATE agent_runs
@@ -1077,7 +1224,9 @@ export async function cleanupOldRuns(olderThanMs, erroredOlderThanMs) {
1077
1224
  error_code = ?,
1078
1225
  error_detail = ?,
1079
1226
  terminal_reason = ?
1080
- WHERE status = 'running' AND started_at < ?`,
1227
+ WHERE status = 'running'
1228
+ AND ${terminalRunEventExclusionSql()}
1229
+ AND started_at < ?`,
1081
1230
  args: [
1082
1231
  completedAt,
1083
1232
  STALE_RUN_ERROR_EVENT.errorCode,
@@ -1096,6 +1245,7 @@ export async function cleanupOldRuns(olderThanMs, erroredOlderThanMs) {
1096
1245
  error_detail = ?,
1097
1246
  terminal_reason = ?
1098
1247
  WHERE status = 'running'
1248
+ AND ${terminalRunEventExclusionSql()}
1099
1249
  AND ${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}`,
1100
1250
  args: [
1101
1251
  completedAt,
@@ -1242,8 +1392,8 @@ async function appendTerminalRunEvent(runId, event) {
1242
1392
  }
1243
1393
  const nextSeq = last ? Number(last.seq ?? -1) + 1 : 0;
1244
1394
  await client.execute({
1245
- sql: `INSERT INTO agent_run_events (run_id, seq, event_data) VALUES (?, ?, ?) ON CONFLICT (run_id, seq) DO NOTHING`,
1246
- args: [runId, nextSeq, JSON.stringify(event)],
1395
+ sql: `INSERT INTO agent_run_events (run_id, seq, event_at, event_data) VALUES (?, ?, ?, ?) ON CONFLICT (run_id, seq) DO NOTHING`,
1396
+ args: [runId, nextSeq, Date.now(), JSON.stringify(event)],
1247
1397
  });
1248
1398
  }
1249
1399
  //# sourceMappingURL=run-store.js.map