@agent-native/core 0.85.3 → 0.85.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: 2046
31
- - template files: 5038
31
+ - template files: 5039
@@ -1,5 +1,17 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.85.5
4
+
5
+ ### Patch Changes
6
+
7
+ - bd7c040: Auto-continue active chat reconnects when action preparation stalls, and count first action-prep events as durable progress.
8
+
9
+ ## 0.85.4
10
+
11
+ ### Patch Changes
12
+
13
+ - 00c6f16: Keep chat restore and active-run polling on the durable background timeout budget so healthy long-running background turns do not look stale to the browser.
14
+
3
15
  ## 0.85.3
4
16
 
5
17
  ### Patch Changes
@@ -235,7 +235,7 @@ Privacy defaults are intentionally conservative but still useful for playback:
235
235
  - URLs are scrubbed with the same `scrubUrl()` helper used by browser analytics.
236
236
  - Replay capture is web-only and opt-in; it does not record native desktop screens.
237
237
 
238
- While recording, session replay also captures browser console output (`log`, `info`, `warn`, `error`, `debug`, plus window `error` / `unhandledrejection`) and network request metadata (`fetch` and XHR) as tagged rrweb custom events, so agents and the replay viewer can debug user-reported issues. Capture is on by default when replay is enabled; tune or disable it with the `sessionReplay.console` and `sessionReplay.network` options, each accepting a boolean or an options object. Request/response bodies and headers are never captured, URLs are scrubbed, messages are truncated, the recorder's own ingest/tracking traffic is excluded, and per-session budgets (1000 console / 2000 network events) add a truncation notice when exceeded.
238
+ While recording, session replay also captures browser console output (`log`, `info`, `warn`, `error`, `debug`, plus window `error` / `unhandledrejection`) and network request metadata (`fetch` and XHR) as tagged rrweb custom events, so agents and the replay viewer can debug user-reported issues. Capture is on by default when replay is enabled; tune or disable it with the `sessionReplay.console` and `sessionReplay.network` options, each accepting a boolean or an options object (`{ maxEvents?: number }`). Request/response bodies and headers are never captured, URLs are scrubbed, messages are truncated, the recorder's own ingest/tracking traffic is excluded, and per-session budgets (1000 console / 2000 network events) add a truncation notice when exceeded.
239
239
 
240
240
  The Analytics template stores replay metadata in SQL (`session_recordings`) and stores chunks through private blob refs (`session_replay_chunks`). Browsers and agents never receive provider URLs. Playback goes through scoped server routes and the default agent tools return summaries or bounded replay events, not raw chunk table access.
241
241
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.85.3",
3
+ "version": "0.85.5",
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": {
@@ -553,6 +553,16 @@ export function startRun(
553
553
  preparingActivityBytes.get(activityKey) ?? 0,
554
554
  restartHighWater,
555
555
  );
556
+ if (
557
+ !preparingActivityBytes.has(activityKey) &&
558
+ progressBytes === 0 &&
559
+ !preparingActivityRestartHighWater.has(toolKey)
560
+ ) {
561
+ preparingActivityTools.set(activityKey, toolKey);
562
+ preparingActivityBytes.set(activityKey, 0);
563
+ preparingActivityRestartHighWater.set(toolKey, 0);
564
+ return true;
565
+ }
556
566
  if (progressBytes <= previousBytes) {
557
567
  preparingActivityTools.set(activityKey, toolKey);
558
568
  preparingActivityBytes.set(
@@ -1362,20 +1372,32 @@ export async function getActiveRunForThreadAsync(threadId: string): Promise<{
1362
1372
  // the full conversation from completed runs via SSE.
1363
1373
  const memRun = getActiveRunForThread(threadId);
1364
1374
  if (memRun && (memRun.status === "running" || memRun.events.length > 0)) {
1375
+ const sqlSnapshot = await fetchRunThreadSnapshot(memRun.runId, threadId);
1376
+ const status = sqlSnapshot?.status ?? memRun.status;
1377
+ const heartbeatAt =
1378
+ status === "running"
1379
+ ? Date.now()
1380
+ : (sqlSnapshot?.heartbeatAt ?? memRun.startedAt);
1365
1381
  return {
1366
1382
  runId: memRun.runId,
1367
1383
  threadId: memRun.threadId,
1368
1384
  turnId: memRun.turnId,
1369
- status: memRun.status,
1385
+ status,
1370
1386
  // In-memory means this isolate is the producer. By definition, the
1371
- // heartbeat is fresh as of "now" the client can trust this.
1372
- heartbeatAt: Date.now(),
1387
+ // heartbeat is fresh as of "now" while the run is still running. Once
1388
+ // SQL has terminal truth, prefer that timestamp so a stale in-memory
1389
+ // buffer cannot keep the browser believing a finished background run is
1390
+ // still alive.
1391
+ heartbeatAt,
1373
1392
  // For an in-memory run we don't have a separate "last event emit"
1374
1393
  // timestamp tracked in JS — the SQL bump is throttled per-second.
1375
1394
  // Read it back from SQL on demand. For the common case the SQL row
1376
1395
  // is well under 1s old; if it isn't, the stuck-detector will pick
1377
1396
  // it up on the next poll cycle.
1378
- lastProgressAt: await fetchLastProgressAt(memRun.runId),
1397
+ lastProgressAt: sqlSnapshot?.lastProgressAt ?? null,
1398
+ dispatchMode: sqlSnapshot?.dispatchMode ?? null,
1399
+ terminalReason: sqlSnapshot?.terminalReason ?? null,
1400
+ diagStage: sqlSnapshot?.diagStage ?? null,
1379
1401
  };
1380
1402
  }
1381
1403
  // Fall back to SQL — also surface recently terminated runs so the client
@@ -1454,16 +1476,14 @@ export async function getActiveRunForThreadAsync(threadId: string): Promise<{
1454
1476
  return null;
1455
1477
  }
1456
1478
 
1457
- async function fetchLastProgressAt(runId: string): Promise<number | null> {
1479
+ async function fetchRunThreadSnapshot(runId: string, threadId: string) {
1458
1480
  try {
1459
- const run = await getRunById(runId);
1460
- if (!run) return null;
1461
1481
  // `getRunById` returns a narrow projection today; ask for the row via
1462
- // the thread lookup which carries last_progress_at.
1463
- const byThread = await getRunByThread(run.threadId, {
1482
+ // the thread lookup which carries dispatch/terminal/progress fields.
1483
+ const byThread = await getRunByThread(threadId, {
1464
1484
  includeTerminal: true,
1465
1485
  });
1466
- if (byThread && byThread.id === runId) return byThread.lastProgressAt;
1486
+ if (byThread && byThread.id === runId) return byThread;
1467
1487
  return null;
1468
1488
  } catch {
1469
1489
  return null;
@@ -239,8 +239,12 @@ function createUserMessageRunConfig(
239
239
  const PENDING_SELECTION_KEY = "pending-selection-context";
240
240
  const ACTIVE_RUN_CLEAR_TIMEOUT_MS = 5_000;
241
241
  const ACTIVE_RUN_STUCK_THRESHOLD_MS = 90_000;
242
+ const BACKGROUND_ACTIVE_RUN_STUCK_THRESHOLD_MS = 13 * 60_000;
242
243
  const ACTIVE_RUN_POLL_INTERVAL_MS = 150;
243
244
  const AUTO_RESUME_STATUS_TIMEOUT_MS = 30_000;
245
+ const MAX_RECONNECT_AUTO_RECOVERIES = 3;
246
+ const RECONNECT_NO_PROGRESS_CONTINUE_MESSAGE =
247
+ "Continue from where you stopped. Use the partial work above, verify what succeeded, and finish the original request. Do not rerun the exact same failed tool input unless the failure was transient or the user explicitly asked for an exact rerun. Prefer dedicated app actions over raw database edits when they exist.";
244
248
  // How long a single activity (model call, tool prep, long tool) must stay
245
249
  // in-flight before its label is surfaced in the running indicator. Below this
246
250
  // the indicator stays a steady "Thinking" so normal fast turns don't flicker
@@ -261,6 +265,11 @@ type ActiveRunLookup = {
261
265
  serverNow?: number;
262
266
  };
263
267
 
268
+ type PendingReconnectRecovery = {
269
+ id: number;
270
+ message: string;
271
+ };
272
+
264
273
  function isReplayableTerminalRun(runInfo: ActiveRunLookup): boolean {
265
274
  const dispatchMode =
266
275
  typeof runInfo.dispatchMode === "string" ? runInfo.dispatchMode : "";
@@ -271,15 +280,24 @@ function isReplayableTerminalRun(runInfo: ActiveRunLookup): boolean {
271
280
  );
272
281
  }
273
282
 
283
+ function activeRunStuckThresholdMs(runInfo: ActiveRunLookup): number {
284
+ const dispatchMode =
285
+ typeof runInfo.dispatchMode === "string" ? runInfo.dispatchMode : "";
286
+ return dispatchMode.startsWith("background")
287
+ ? BACKGROUND_ACTIVE_RUN_STUCK_THRESHOLD_MS
288
+ : ACTIVE_RUN_STUCK_THRESHOLD_MS;
289
+ }
290
+
274
291
  function activeRunLooksStale(runInfo: ActiveRunLookup): boolean {
275
292
  const lastProgressAt =
276
293
  typeof runInfo.lastProgressAt === "number" ? runInfo.lastProgressAt : null;
277
294
  const nowMs =
278
295
  typeof runInfo.serverNow === "number" ? runInfo.serverNow : Date.now();
296
+ const thresholdMs = activeRunStuckThresholdMs(runInfo);
279
297
  return (
280
298
  runInfo.status === "running" &&
281
299
  lastProgressAt != null &&
282
- nowMs - lastProgressAt > ACTIVE_RUN_STUCK_THRESHOLD_MS
300
+ nowMs - lastProgressAt > thresholdMs
283
301
  );
284
302
  }
285
303
 
@@ -1558,6 +1576,9 @@ const AssistantChatInner = forwardRef<
1558
1576
  const reconnectTailOnlyRef = useRef(false);
1559
1577
  const reconnectCanMaterializeRef = useRef(false);
1560
1578
  const reconnectAbortRef = useRef<AbortController | null>(null);
1579
+ const reconnectAutoRecoveryCountRef = useRef(0);
1580
+ const [pendingReconnectRecovery, setPendingReconnectRecovery] =
1581
+ useState<PendingReconnectRecovery | null>(null);
1561
1582
  // Nuclear stop: user clicked stop. Clears the stop button/indicator AND
1562
1583
  // lets new submissions go through immediately — prevents the "stuck
1563
1584
  // queueing forever" state where isReconnecting or isRuntimeRunning gets
@@ -1894,6 +1915,7 @@ const AssistantChatInner = forwardRef<
1894
1915
  reconnectAbortRef.current = abortCtrl;
1895
1916
  let reconnectTerminalReason: AgentAutoContinueSignal["reason"] | null =
1896
1917
  null;
1918
+ const reconnectStuckThresholdMs = activeRunStuckThresholdMs(runInfo);
1897
1919
 
1898
1920
  const watchdog = setInterval(async () => {
1899
1921
  try {
@@ -1937,6 +1959,7 @@ const AssistantChatInner = forwardRef<
1937
1959
  !reconnectProgressTimedOut({
1938
1960
  lastProgressAt: lastReconnectProgressAt,
1939
1961
  now: Date.now(),
1962
+ thresholdMs: reconnectStuckThresholdMs,
1940
1963
  })
1941
1964
  ) {
1942
1965
  return;
@@ -2122,6 +2145,38 @@ const AssistantChatInner = forwardRef<
2122
2145
  setReconnectFrozen(latestContent.length > 0);
2123
2146
  reconnectCanMaterializeRef.current = latestContent.length > 0;
2124
2147
  }
2148
+ const canAutoRecoverReconnect =
2149
+ reconnectTerminalReason !== "run_timeout" &&
2150
+ reconnectAutoRecoveryCountRef.current <
2151
+ MAX_RECONNECT_AUTO_RECOVERIES;
2152
+ if (canAutoRecoverReconnect) {
2153
+ reconnectAutoRecoveryCountRef.current += 1;
2154
+ setRunErrorInfo(null);
2155
+ setDismissedRunErrorKey(null);
2156
+ clearActiveRunIfMatches(threadId, runId);
2157
+ reconnectAbortRef.current = null;
2158
+ setIsReconnecting(false);
2159
+ reconnectRunIdRef.current = null;
2160
+ reconnectTailOnlyRef.current = false;
2161
+ if (afterSeq > 0) {
2162
+ reconnectCanMaterializeRef.current = false;
2163
+ }
2164
+ window.dispatchEvent(
2165
+ new CustomEvent("agent-chat:auto-continue", {
2166
+ detail: { tabId: tabId || threadId },
2167
+ }),
2168
+ );
2169
+ setPendingReconnectRecovery({
2170
+ id: Date.now(),
2171
+ message: RECONNECT_NO_PROGRESS_CONTINUE_MESSAGE,
2172
+ });
2173
+ window.dispatchEvent(
2174
+ new CustomEvent("agentNative.chatRunning", {
2175
+ detail: { isRunning: false, tabId: tabId || threadId },
2176
+ }),
2177
+ );
2178
+ return;
2179
+ }
2125
2180
  setRunErrorInfo({
2126
2181
  message:
2127
2182
  reconnectTerminalReason === "run_timeout"
@@ -2176,6 +2231,9 @@ const AssistantChatInner = forwardRef<
2176
2231
  if (loaded || afterSeq > 0 || latestContent.length === 0) {
2177
2232
  reconnectCanMaterializeRef.current = false;
2178
2233
  }
2234
+ if (loaded) {
2235
+ reconnectAutoRecoveryCountRef.current = 0;
2236
+ }
2179
2237
  window.dispatchEvent(
2180
2238
  new CustomEvent("agentNative.chatRunning", {
2181
2239
  detail: { isRunning: false, tabId: tabId || threadId },
@@ -3058,10 +3116,14 @@ const AssistantChatInner = forwardRef<
3058
3116
  recoveryAction?: AgentRecoveryAction,
3059
3117
  includeComposerContext = false,
3060
3118
  trackInRunsTray = false,
3119
+ preserveReconnectAutoRecoveryBudget = false,
3061
3120
  ) => {
3062
3121
  if (!(await ensureAgentEngineReadyForSubmit())) {
3063
3122
  return;
3064
3123
  }
3124
+ if (!preserveReconnectAutoRecoveryBudget) {
3125
+ reconnectAutoRecoveryCountRef.current = 0;
3126
+ }
3065
3127
  materializeFrozenReconnectContent();
3066
3128
  setShowContinue(false);
3067
3129
  setLoopLimitInfo(null);
@@ -3284,6 +3346,29 @@ const AssistantChatInner = forwardRef<
3284
3346
  ],
3285
3347
  );
3286
3348
 
3349
+ useEffect(() => {
3350
+ if (!pendingReconnectRecovery) return;
3351
+ const recovery = pendingReconnectRecovery;
3352
+ const timer = window.setTimeout(() => {
3353
+ setPendingReconnectRecovery((current) =>
3354
+ current?.id === recovery.id ? null : current,
3355
+ );
3356
+ addToQueue(
3357
+ recovery.message,
3358
+ undefined,
3359
+ undefined,
3360
+ undefined,
3361
+ undefined,
3362
+ "queued",
3363
+ "continue",
3364
+ false,
3365
+ false,
3366
+ true,
3367
+ );
3368
+ }, 0);
3369
+ return () => window.clearTimeout(timer);
3370
+ }, [addToQueue, pendingReconnectRecovery]);
3371
+
3287
3372
  // Expose imperative handle
3288
3373
  useImperativeHandle(
3289
3374
  ref,
@@ -3859,7 +3944,7 @@ const AssistantChatInner = forwardRef<
3859
3944
  onContinue={() => {
3860
3945
  setRunErrorInfo(null);
3861
3946
  addToQueue(
3862
- "Continue from where you stopped. Use the partial work above, verify what succeeded, and finish the original request. Do not rerun the exact same failed tool input unless the failure was transient or the user explicitly asked for an exact rerun. Prefer dedicated app actions over raw database edits when they exist.",
3947
+ RECONNECT_NO_PROGRESS_CONTINUE_MESSAGE,
3863
3948
  undefined,
3864
3949
  undefined,
3865
3950
  undefined,
@@ -46,7 +46,7 @@ agent answers about browser recordings in the Analytics template.
46
46
  custom events tagged `agent-native.console` and `agent-native.network`.
47
47
  - Capture is on by default whenever session replay is enabled. Tune or disable
48
48
  it with the `console` / `network` options on the session replay config; each
49
- accepts a boolean or an options object.
49
+ accepts a boolean or an options object (`{ maxEvents?: number }`).
50
50
  - Privacy bounds: request/response bodies and headers are never captured, URLs
51
51
  are scrubbed, messages are truncated, and recorder self-traffic (the replay
52
52
  ingest and tracking endpoints) is excluded.
@@ -243,10 +243,12 @@ patterns live in `.agents/skills/`.
243
243
  wait for the user to pick one in chat, delete each other generated variant
244
244
  screen with `delete-file` at most once, call `get-design-snapshot` exactly
245
245
  once with the selected screen's `fileId`, then call `edit-design` exactly once
246
- on that same `fileId` for follow-up refinement. Use `mode: "replace-file"`
247
- when expanding the representative placeholder into the full chosen direction.
248
- Do not repeat delete/snapshot cycles, and do not call `generate-design` after
249
- a variant pick.
246
+ on that same `fileId` for follow-up refinement. The kept variant screen is a
247
+ representative direction, not the final deliverable: use `mode:
248
+ "replace-file"` to replace it with the actual requested app/product UI in the
249
+ chosen visual style. Do not leave a direction board, variant brief, summary
250
+ card, or prose description as the final screen. Do not repeat delete/snapshot
251
+ cycles, and do not call `generate-design` after a variant pick.
250
252
  - If inline chat choice buttons are unavailable, the user can tell you the
251
253
  preferred screen name. Do not show a separate variant picker or ask them to
252
254
  paste a copyable handoff summary.
@@ -42,13 +42,18 @@ const FALLBACK_INSTRUCTIONS =
42
42
  "a screen by name if the inline buttons are not available; after they pick, " +
43
43
  "delete each other variant screen at most once, call get-design-snapshot with fileId for " +
44
44
  "the kept screen once, then call edit-design on that same fileId in a bounded pass. " +
45
- 'Use mode "replace-file" when expanding the representative placeholder. ' +
45
+ 'Use mode "replace-file" to replace the representative direction screen with ' +
46
+ "the actual requested product UI; do not leave a direction board, summary card, " +
47
+ "or variant brief as the final result. " +
46
48
  "Do not call generate-design after a variant pick.";
47
49
 
48
50
  const VARIANT_PICK_SUBMIT_MESSAGE =
49
51
  "Use this design direction. Keep the selected screen, clean up each other " +
50
52
  "variant screen at most once, read only the kept screen, then update that " +
51
- "same screen in one bounded pass. If a cleanup action reports a screen was " +
53
+ "same screen in one bounded pass into the full requested app/product UI. " +
54
+ "The selected screen is only a representative direction; the final saved " +
55
+ "screen must not be a direction board, variant brief, or summary card. " +
56
+ "If a cleanup action reports a screen was " +
52
57
  "already missing, continue. Use the exact file ids and tool instructions in " +
53
58
  "the selected answer below. Do not repeat cleanup/read cycles, do not create " +
54
59
  "a new index.html, and stop after the first successful screen update.";
@@ -580,7 +585,7 @@ export default defineAction({
580
585
  await writeAppStateForCurrentTab("guided-questions", {
581
586
  title: prompt ?? "Pick a direction",
582
587
  description:
583
- "All options are on the board. Choose one to keep; I will delete the others, read only the kept screen, and edit that same file in a bounded pass.",
588
+ "All options are on the board. Choose one to keep; I will delete the others, read only the kept screen, and turn that direction into the final requested screen.",
584
589
  submitLabel: "Use selected direction",
585
590
  submitMessage: VARIANT_PICK_SUBMIT_MESSAGE,
586
591
  skipLabel: "Show another set",
@@ -608,7 +613,7 @@ export default defineAction({
608
613
  value:
609
614
  `Keep "${screen.label}" (${screen.filename}, file id ${screen.id}) ` +
610
615
  `from variant set ${variantSetId}. Delete each other variant screen at most once: ${otherScreens}. If delete-file says a screen is already missing, continue. ` +
611
- `Then call get-design-snapshot exactly once with designId ${designId} and fileId ${screen.id} (filename ${screen.filename}), then call edit-design with fileId ${screen.id} on that same kept file in a bounded single-file pass. Use mode "replace-file" when replacing the representative placeholder with the full chosen direction, or search/replace for smaller refinements. Do not call generate-design after this variant pick, do not repeat delete/snapshot cycles, do not create index.html, and do not resend a huge payload. Stop after the first successful edit-design save.`,
616
+ `Then call get-design-snapshot exactly once with designId ${designId} and fileId ${screen.id} (filename ${screen.filename}), then call edit-design with fileId ${screen.id} on that same kept file in a bounded single-file pass. Use mode "replace-file" to replace the representative direction screen with the full requested app/product UI in the chosen visual style. The final saved screen must be the actual usable UI requested by the user, not a direction board, variant brief, summary card, or description of the direction. Do not call generate-design after this variant pick, do not repeat delete/snapshot cycles, do not create index.html, and do not resend a huge payload. Stop after the first successful edit-design save.`,
612
617
  };
613
618
  }),
614
619
  },
@@ -626,7 +631,7 @@ export default defineAction({
626
631
  embed: true,
627
632
  fallbackInstructions: FALLBACK_INSTRUCTIONS,
628
633
  nextRequiredAction:
629
- 'Wait for the user to pick a screen in chat. Then delete each unchosen variant screen with delete-file at most once, call get-design-snapshot exactly once with fileId for the chosen screen, and call edit-design with that same fileId in a bounded pass. Use mode "replace-file" when expanding the placeholder into the full chosen direction. Do not repeat delete/snapshot cycles. Do not call generate-design after a variant pick. Stop after the first successful edit-design save.',
634
+ 'Wait for the user to pick a screen in chat. Then delete each unchosen variant screen with delete-file at most once, call get-design-snapshot exactly once with fileId for the chosen screen, and call edit-design with that same fileId in a bounded pass. Use mode "replace-file" to replace the representative direction screen with the full requested app/product UI in the chosen visual style. Do not leave a direction board, variant brief, or summary card as the final result. Do not repeat delete/snapshot cycles. Do not call generate-design after a variant pick. Stop after the first successful edit-design save.',
630
635
  };
631
636
  },
632
637
  link: ({ result }) => {
@@ -0,0 +1,5 @@
1
+ ---
2
+ type: fixed
3
+ ---
4
+
5
+ Variant picks now more reliably expand the selected direction into the full requested screen instead of leaving a direction summary behind.
@@ -1 +1 @@
1
- {"version":3,"file":"run-manager.d.ts","sourceRoot":"","sources":["../../src/agent/run-manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"run-manager.d.ts","sourceRoot":"","sources":["../../src/agent/run-manager.ts"],"names":[],"mappings":"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"}
@@ -406,6 +406,14 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
406
406
  return progressBytes > 0;
407
407
  }
408
408
  const previousBytes = Math.max(preparingActivityBytes.get(activityKey) ?? 0, restartHighWater);
409
+ if (!preparingActivityBytes.has(activityKey) &&
410
+ progressBytes === 0 &&
411
+ !preparingActivityRestartHighWater.has(toolKey)) {
412
+ preparingActivityTools.set(activityKey, toolKey);
413
+ preparingActivityBytes.set(activityKey, 0);
414
+ preparingActivityRestartHighWater.set(toolKey, 0);
415
+ return true;
416
+ }
409
417
  if (progressBytes <= previousBytes) {
410
418
  preparingActivityTools.set(activityKey, toolKey);
411
419
  preparingActivityBytes.set(activityKey, Math.max(previousBytes, progressBytes));
@@ -1118,20 +1126,31 @@ export async function getActiveRunForThreadAsync(threadId) {
1118
1126
  // the full conversation from completed runs via SSE.
1119
1127
  const memRun = getActiveRunForThread(threadId);
1120
1128
  if (memRun && (memRun.status === "running" || memRun.events.length > 0)) {
1129
+ const sqlSnapshot = await fetchRunThreadSnapshot(memRun.runId, threadId);
1130
+ const status = sqlSnapshot?.status ?? memRun.status;
1131
+ const heartbeatAt = status === "running"
1132
+ ? Date.now()
1133
+ : (sqlSnapshot?.heartbeatAt ?? memRun.startedAt);
1121
1134
  return {
1122
1135
  runId: memRun.runId,
1123
1136
  threadId: memRun.threadId,
1124
1137
  turnId: memRun.turnId,
1125
- status: memRun.status,
1138
+ status,
1126
1139
  // In-memory means this isolate is the producer. By definition, the
1127
- // heartbeat is fresh as of "now" the client can trust this.
1128
- heartbeatAt: Date.now(),
1140
+ // heartbeat is fresh as of "now" while the run is still running. Once
1141
+ // SQL has terminal truth, prefer that timestamp so a stale in-memory
1142
+ // buffer cannot keep the browser believing a finished background run is
1143
+ // still alive.
1144
+ heartbeatAt,
1129
1145
  // For an in-memory run we don't have a separate "last event emit"
1130
1146
  // timestamp tracked in JS — the SQL bump is throttled per-second.
1131
1147
  // Read it back from SQL on demand. For the common case the SQL row
1132
1148
  // is well under 1s old; if it isn't, the stuck-detector will pick
1133
1149
  // it up on the next poll cycle.
1134
- lastProgressAt: await fetchLastProgressAt(memRun.runId),
1150
+ lastProgressAt: sqlSnapshot?.lastProgressAt ?? null,
1151
+ dispatchMode: sqlSnapshot?.dispatchMode ?? null,
1152
+ terminalReason: sqlSnapshot?.terminalReason ?? null,
1153
+ diagStage: sqlSnapshot?.diagStage ?? null,
1135
1154
  };
1136
1155
  }
1137
1156
  // Fall back to SQL — also surface recently terminated runs so the client
@@ -1211,18 +1230,15 @@ export async function getActiveRunForThreadAsync(threadId) {
1211
1230
  }
1212
1231
  return null;
1213
1232
  }
1214
- async function fetchLastProgressAt(runId) {
1233
+ async function fetchRunThreadSnapshot(runId, threadId) {
1215
1234
  try {
1216
- const run = await getRunById(runId);
1217
- if (!run)
1218
- return null;
1219
1235
  // `getRunById` returns a narrow projection today; ask for the row via
1220
- // the thread lookup which carries last_progress_at.
1221
- const byThread = await getRunByThread(run.threadId, {
1236
+ // the thread lookup which carries dispatch/terminal/progress fields.
1237
+ const byThread = await getRunByThread(threadId, {
1222
1238
  includeTerminal: true,
1223
1239
  });
1224
1240
  if (byThread && byThread.id === runId)
1225
- return byThread.lastProgressAt;
1241
+ return byThread;
1226
1242
  return null;
1227
1243
  }
1228
1244
  catch {