@agent-native/core 0.84.49 → 0.84.52

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 (42) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +20 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/run-store.ts +33 -6
  5. package/corpus/core/src/client/AssistantChat.tsx +84 -23
  6. package/corpus/core/src/client/agent-chat-adapter.ts +59 -9
  7. package/corpus/core/src/client/blocks/library/DiffBlock.tsx +7 -5
  8. package/corpus/core/src/client/session-replay.ts +170 -47
  9. package/corpus/core/src/client/sse-event-processor.ts +106 -29
  10. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +5 -0
  11. package/corpus/templates/analytics/changelog/2026-07-02-agent-chat-can-keep-working-through-longer-data-queries-inst.md +6 -0
  12. package/corpus/templates/analytics/changelog/2026-07-02-session-replay-playback-recovers-from-failed-snapshot-uploads.md +6 -0
  13. package/corpus/templates/analytics/netlify.toml +3 -0
  14. package/corpus/templates/analytics/server/lib/session-replay.ts +9 -5
  15. package/corpus/templates/analytics/server/plugins/agent-chat.ts +3 -0
  16. package/corpus/templates/plan/changelog/2026-07-02-agent-chat-can-keep-working-through-longer-visual-plan-updat.md +6 -0
  17. package/corpus/templates/plan/netlify.toml +3 -0
  18. package/corpus/templates/plan/server/plugins/agent-chat.ts +4 -0
  19. package/dist/agent/run-store.d.ts.map +1 -1
  20. package/dist/agent/run-store.js +30 -6
  21. package/dist/agent/run-store.js.map +1 -1
  22. package/dist/client/AssistantChat.d.ts.map +1 -1
  23. package/dist/client/AssistantChat.js +64 -13
  24. package/dist/client/AssistantChat.js.map +1 -1
  25. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  26. package/dist/client/agent-chat-adapter.js +50 -9
  27. package/dist/client/agent-chat-adapter.js.map +1 -1
  28. package/dist/client/blocks/library/DiffBlock.d.ts.map +1 -1
  29. package/dist/client/blocks/library/DiffBlock.js +6 -5
  30. package/dist/client/blocks/library/DiffBlock.js.map +1 -1
  31. package/dist/client/session-replay.d.ts.map +1 -1
  32. package/dist/client/session-replay.js +134 -39
  33. package/dist/client/session-replay.js.map +1 -1
  34. package/dist/client/sse-event-processor.d.ts +29 -3
  35. package/dist/client/sse-event-processor.d.ts.map +1 -1
  36. package/dist/client/sse-event-processor.js +72 -18
  37. package/dist/client/sse-event-processor.js.map +1 -1
  38. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  39. package/dist/notifications/routes.d.ts +1 -1
  40. package/dist/observability/routes.d.ts +7 -7
  41. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  42. 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: 2044
31
- - template files: 4998
31
+ - template files: 5001
@@ -1,5 +1,25 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.84.52
4
+
5
+ ### Patch Changes
6
+
7
+ - ca38cc7: Carry action-preparation stall detection across reconnect reads for the same run so zero-byte tool input retries cannot keep background chats alive indefinitely.
8
+ - 899ebf1: Hide the diff "Show all lines" footer when annotation anchoring already renders every line.
9
+ - c1e18fb: Make session replay uploads retry failed batches without advancing sequence ids.
10
+
11
+ ## 0.84.51
12
+
13
+ ### Patch Changes
14
+
15
+ - 55e4678: Recover durable background chat runs when action input preparation stops making byte progress, and avoid duplicate tool cards when reconnects replay completed tool events.
16
+
17
+ ## 0.84.50
18
+
19
+ ### Patch Changes
20
+
21
+ - 24a6bd2: Retry agent chat startup timeouts before showing an error when no run stream starts.
22
+
3
23
  ## 0.84.49
4
24
 
5
25
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.84.49",
3
+ "version": "0.84.52",
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": {
@@ -763,9 +763,9 @@ export async function reconcileTerminalRunFromEvents(
763
763
  sql: `UPDATE agent_runs
764
764
  SET status = ?,
765
765
  completed_at = COALESCE(completed_at, ?, ${livenessBasisSql()}),
766
- error_code = CASE WHEN ? IS NOT NULL THEN ? ELSE error_code END,
767
- error_detail = CASE WHEN ? IS NOT NULL THEN ? ELSE error_detail END,
768
- terminal_reason = COALESCE(terminal_reason, ?)
766
+ error_code = ?,
767
+ error_detail = ?,
768
+ terminal_reason = ?
769
769
  WHERE id = ?
770
770
  AND (
771
771
  status = 'running'
@@ -775,8 +775,6 @@ export async function reconcileTerminalRunFromEvents(
775
775
  status,
776
776
  latest.eventAt,
777
777
  errorCode,
778
- errorCode,
779
- errorDetail,
780
778
  errorDetail,
781
779
  terminalReason,
782
780
  runId,
@@ -1248,7 +1246,7 @@ export async function listRunsForThread(
1248
1246
  await ensureRunTables();
1249
1247
  const limit = Math.min(Math.max(options.limit ?? 10, 1), 50);
1250
1248
  const client = getDbExec();
1251
- const { rows } = await client.execute({
1249
+ let { rows } = await client.execute({
1252
1250
  sql: `SELECT id, thread_id, turn_id, status, started_at, heartbeat_at, completed_at, last_progress_at, error_code, abort_reason, dispatch_mode, terminal_reason, diag_stage
1253
1251
  FROM agent_runs
1254
1252
  WHERE thread_id = ?
@@ -1256,6 +1254,35 @@ export async function listRunsForThread(
1256
1254
  LIMIT ?`,
1257
1255
  args: [threadId, limit],
1258
1256
  });
1257
+ let repairedTerminalRow = false;
1258
+ for (const r of rows) {
1259
+ const row = r as {
1260
+ id?: string;
1261
+ status?: string;
1262
+ error_code?: string | null;
1263
+ };
1264
+ const runId = row.id;
1265
+ if (!runId) continue;
1266
+ const canReconcileFromEvents =
1267
+ row.status === "running" ||
1268
+ (row.status === "errored" &&
1269
+ row.error_code === STALE_RUN_ERROR_EVENT.errorCode);
1270
+ if (!canReconcileFromEvents) continue;
1271
+ repairedTerminalRow =
1272
+ (await reconcileTerminalRunFromEvents(runId).catch(() => false)) ||
1273
+ repairedTerminalRow;
1274
+ }
1275
+ if (repairedTerminalRow) {
1276
+ const refreshed = await client.execute({
1277
+ sql: `SELECT id, thread_id, turn_id, status, started_at, heartbeat_at, completed_at, last_progress_at, error_code, abort_reason, dispatch_mode, terminal_reason, diag_stage
1278
+ FROM agent_runs
1279
+ WHERE thread_id = ?
1280
+ ORDER BY started_at DESC
1281
+ LIMIT ?`,
1282
+ args: [threadId, limit],
1283
+ });
1284
+ rows = refreshed.rows;
1285
+ }
1259
1286
  return rows.map((r) => {
1260
1287
  const row = r as {
1261
1288
  id: string;
@@ -153,6 +153,7 @@ import {
153
153
  import {
154
154
  AgentAutoContinueSignal,
155
155
  type ContentPart,
156
+ type PreparingActionState,
156
157
  readSSEStreamRaw,
157
158
  settleInterruptedToolCalls,
158
159
  } from "./sse-event-processor.js";
@@ -675,7 +676,7 @@ export function resolveAssistantChatRunningStatusLabel({
675
676
  }): string {
676
677
  if (runningActivityLabel) return runningActivityLabel;
677
678
  if (isAutoResuming) return "Resuming";
678
- if (isReconnecting && hasReconnectContent) return "Continuing";
679
+ if (isReconnecting && hasReconnectContent) return "Still working";
679
680
  return "Thinking";
680
681
  }
681
682
 
@@ -1516,7 +1517,7 @@ const AssistantChatInner = forwardRef<
1516
1517
  const textStreaming = showRunningInUI || externalStreaming;
1517
1518
  // A revealed activity label wins; otherwise keep recovery states calm and
1518
1519
  // product-facing. Reconnect is transport machinery, so normal replay reads as
1519
- // "Continuing" instead of exposing "Reconnecting" mid-chat.
1520
+ // ongoing work instead of exposing "Reconnecting" mid-chat.
1520
1521
  const runningStatusLabel = resolveAssistantChatRunningStatusLabel({
1521
1522
  runningActivityLabel,
1522
1523
  isAutoResuming,
@@ -1863,6 +1864,27 @@ const AssistantChatInner = forwardRef<
1863
1864
  const streamReconnect = async () => {
1864
1865
  let noProgressDuringReconnect = false;
1865
1866
  let latestContent: ContentPart[] = [];
1867
+ const preparingActionState: PreparingActionState = {};
1868
+ const sameRunStillActive = async (): Promise<
1869
+ "active" | "inactive" | "unknown"
1870
+ > => {
1871
+ try {
1872
+ const res = await fetch(
1873
+ `${apiUrl}/runs/active?threadId=${encodeURIComponent(threadId)}`,
1874
+ { signal: abortCtrl.signal },
1875
+ );
1876
+ if (!res.ok) return "unknown";
1877
+ const info = (await res.json()) as ActiveRunLookup;
1878
+ return info.active === true &&
1879
+ String(info.runId ?? "") === runId &&
1880
+ info.status === "running" &&
1881
+ !activeRunLooksStale(info)
1882
+ ? "active"
1883
+ : "inactive";
1884
+ } catch {
1885
+ return "unknown";
1886
+ }
1887
+ };
1866
1888
  const threadPollInterval =
1867
1889
  afterSeq > 0
1868
1890
  ? window.setInterval(() => {
@@ -1871,15 +1893,28 @@ const AssistantChatInner = forwardRef<
1871
1893
  }, 2000)
1872
1894
  : undefined;
1873
1895
  try {
1874
- const sseRes = await fetch(
1875
- `${apiUrl}/runs/${encodeURIComponent(runId)}/events?after=${afterSeq}`,
1876
- { signal: abortCtrl.signal },
1877
- );
1878
- if (sseRes.ok && sseRes.body) {
1879
- const content: ContentPart[] = [];
1880
- latestContent = content;
1881
- const toolCallCounter = { value: 0 };
1896
+ const content: ContentPart[] = [];
1897
+ latestContent = content;
1898
+ const toolCallCounter = { value: 0 };
1882
1899
 
1900
+ while (
1901
+ reconnectRunIdRef.current === runId &&
1902
+ !abortCtrl.signal.aborted
1903
+ ) {
1904
+ const reconnectAfterSeq = resolveReconnectAfterSeq(threadId, runId);
1905
+ reconnectTailOnlyRef.current = reconnectAfterSeq > 0;
1906
+ const sseRes = await fetch(
1907
+ `${apiUrl}/runs/${encodeURIComponent(runId)}/events?after=${reconnectAfterSeq}`,
1908
+ { signal: abortCtrl.signal },
1909
+ );
1910
+ if (!sseRes.ok || !sseRes.body) {
1911
+ const activeState = await sameRunStillActive();
1912
+ if (activeState !== "inactive") {
1913
+ await new Promise((resolve) => window.setTimeout(resolve, 250));
1914
+ continue;
1915
+ }
1916
+ break;
1917
+ }
1883
1918
  let rafPending = false;
1884
1919
  let latestSnapshot: ContentPart[] = [];
1885
1920
  const scheduleUpdate = (snapshot: ContentPart[]) => {
@@ -1892,21 +1927,47 @@ const AssistantChatInner = forwardRef<
1892
1927
  });
1893
1928
  };
1894
1929
 
1895
- await readSSEStreamRaw(
1896
- sseRes.body,
1897
- content,
1898
- toolCallCounter,
1899
- tabId,
1900
- scheduleUpdate,
1901
- (seq) => {
1902
- markReconnectProgress();
1903
- updateActiveRunSeq(seq);
1904
- },
1905
- );
1906
- if (afterSeq === 0) {
1907
- setReconnectContent([...content]);
1930
+ try {
1931
+ await readSSEStreamRaw(
1932
+ sseRes.body,
1933
+ content,
1934
+ toolCallCounter,
1935
+ tabId,
1936
+ scheduleUpdate,
1937
+ (seq) => {
1938
+ markReconnectProgress();
1939
+ updateActiveRunSeq(seq);
1940
+ },
1941
+ { preparingActionState },
1942
+ );
1943
+ if (reconnectAfterSeq === 0) {
1944
+ setReconnectContent([...content]);
1945
+ }
1946
+ break;
1947
+ } catch (err) {
1948
+ if (
1949
+ err instanceof AgentAutoContinueSignal &&
1950
+ err.reason === "stream_ended"
1951
+ ) {
1952
+ if (reconnectAfterSeq === 0) {
1953
+ setReconnectContent([...content]);
1954
+ }
1955
+ const activeState = await sameRunStillActive();
1956
+ if (activeState !== "inactive") {
1957
+ await new Promise((resolve) =>
1958
+ window.setTimeout(resolve, 250),
1959
+ );
1960
+ continue;
1961
+ }
1962
+ }
1963
+ throw err;
1908
1964
  }
1909
1965
  }
1966
+ if (reconnectTimedOut && abortCtrl.signal.aborted) {
1967
+ const timeoutError = new Error("Reconnect timed out");
1968
+ timeoutError.name = "AbortError";
1969
+ throw timeoutError;
1970
+ }
1910
1971
  } catch (err) {
1911
1972
  if (
1912
1973
  err instanceof AgentAutoContinueSignal &&
@@ -20,6 +20,7 @@ import {
20
20
  type AgentActivityTrailEntry,
21
21
  type AgentAutoContinueErrorInfo,
22
22
  type ContentPart,
23
+ type PreparingActionState,
23
24
  readSSEStream,
24
25
  settleInterruptedToolCalls,
25
26
  } from "./sse-event-processor.js";
@@ -1380,6 +1381,11 @@ export function createAgentChatAdapter(
1380
1381
  const turnId = generateTurnId();
1381
1382
  let runId: string | null = null;
1382
1383
  let lastSeq = -1;
1384
+ const seenRunSeqs = new Map<string, number>();
1385
+ const preparingActionStatesByRun = new Map<
1386
+ string,
1387
+ PreparingActionState
1388
+ >();
1383
1389
  let currentRunDispatchMode: string | null = null;
1384
1390
  let currentMessageText = normalizeMentions(
1385
1391
  recoveryMessageText.trim() || userMessageText,
@@ -1548,9 +1554,44 @@ export function createAgentChatAdapter(
1548
1554
  if (mode) currentRunDispatchMode = mode;
1549
1555
  };
1550
1556
 
1557
+ const rememberRunSeq = (seq: number) => {
1558
+ lastSeq = seq;
1559
+ if (runId) {
1560
+ seenRunSeqs.set(runId, seq);
1561
+ }
1562
+ };
1563
+
1564
+ const reconnectCursorForRun = (
1565
+ nextRunId: string,
1566
+ previousRunId: string | null,
1567
+ ) => {
1568
+ const rememberedSeq = seenRunSeqs.get(nextRunId);
1569
+ if (rememberedSeq !== undefined) {
1570
+ lastSeq = rememberedSeq;
1571
+ return;
1572
+ }
1573
+ if (previousRunId !== nextRunId) {
1574
+ lastSeq = -1;
1575
+ }
1576
+ };
1577
+
1578
+ const preparingActionStateForRun = (
1579
+ id: string | null,
1580
+ ): PreparingActionState | undefined => {
1581
+ if (!id) return undefined;
1582
+ const existing = preparingActionStatesByRun.get(id);
1583
+ if (existing) return existing;
1584
+ const state: PreparingActionState = {};
1585
+ preparingActionStatesByRun.set(id, state);
1586
+ return state;
1587
+ };
1588
+
1551
1589
  const currentSSEOptions = () => ({
1552
1590
  durableBackgroundRun:
1553
1591
  currentRunDispatchMode?.startsWith("background") === true,
1592
+ ...(runId
1593
+ ? { preparingActionState: preparingActionStateForRun(runId) }
1594
+ : {}),
1554
1595
  });
1555
1596
 
1556
1597
  const captureChatClientError = (
@@ -1671,7 +1712,7 @@ export function createAgentChatAdapter(
1671
1712
  toolCallCounter,
1672
1713
  tabId,
1673
1714
  (seq) => {
1674
- lastSeq = seq;
1715
+ rememberRunSeq(seq);
1675
1716
  if (threadId) updateActiveRunSeq(seq);
1676
1717
  },
1677
1718
  runId,
@@ -1765,12 +1806,13 @@ export function createAgentChatAdapter(
1765
1806
  return false;
1766
1807
  }
1767
1808
  const activeRunId = String(active.runId);
1809
+ const previousRunId = runId;
1768
1810
  runId = activeRunId;
1769
1811
  if (!attemptedRunIds.includes(activeRunId)) {
1770
1812
  attemptedRunIds.push(activeRunId);
1771
1813
  }
1772
- lastSeq = -1;
1773
- setActiveRun({ threadId, runId: activeRunId, lastSeq: -1 });
1814
+ reconnectCursorForRun(activeRunId, previousRunId);
1815
+ setActiveRun({ threadId, runId: activeRunId, lastSeq });
1774
1816
  const reconnected = yield* reconnectCurrentRun();
1775
1817
  if (reconnected) return true;
1776
1818
  }
@@ -1844,12 +1886,13 @@ export function createAgentChatAdapter(
1844
1886
  if (activeStatus !== "running" && activeStatus !== "starting") {
1845
1887
  return false;
1846
1888
  }
1889
+ const previousRunId = runId;
1847
1890
  runId = activeRunId;
1848
1891
  if (!attemptedRunIds.includes(activeRunId)) {
1849
1892
  attemptedRunIds.push(activeRunId);
1850
1893
  }
1851
- lastSeq = -1;
1852
- setActiveRun({ threadId, runId: activeRunId, lastSeq: -1 });
1894
+ reconnectCursorForRun(activeRunId, previousRunId);
1895
+ setActiveRun({ threadId, runId: activeRunId, lastSeq });
1853
1896
  const reconnected = yield* reconnectCurrentRun();
1854
1897
  if (reconnected) return true;
1855
1898
  } catch (activeErr: unknown) {
@@ -2290,13 +2333,14 @@ export function createAgentChatAdapter(
2290
2333
  }
2291
2334
  if (activeRunId) {
2292
2335
  try {
2336
+ const previousRunId = runId;
2293
2337
  runId = activeRunId;
2294
2338
  if (!attemptedRunIds.includes(runId)) {
2295
2339
  attemptedRunIds.push(runId);
2296
2340
  }
2297
- lastSeq = -1;
2341
+ reconnectCursorForRun(activeRunId, previousRunId);
2298
2342
  if (threadId) {
2299
- setActiveRun({ threadId, runId, lastSeq: -1 });
2343
+ setActiveRun({ threadId, runId, lastSeq });
2300
2344
  }
2301
2345
  const reconnected = yield* reconnectCurrentRun();
2302
2346
  if (reconnected) return;
@@ -2423,7 +2467,7 @@ export function createAgentChatAdapter(
2423
2467
  toolCallCounter,
2424
2468
  tabId,
2425
2469
  (seq) => {
2426
- lastSeq = seq;
2470
+ rememberRunSeq(seq);
2427
2471
  if (runId && threadId) {
2428
2472
  updateActiveRunSeq(seq);
2429
2473
  }
@@ -2616,10 +2660,16 @@ export function createAgentChatAdapter(
2616
2660
  if (activeReconnected) return;
2617
2661
 
2618
2662
  if (err instanceof AgentStartupTimeoutError) {
2663
+ if (startupRecoveryAttempts < MAX_STARTUP_RECOVERY_ATTEMPTS) {
2664
+ await retryDelay(startupRecoveryAttempts++, abortSignal);
2665
+ if (abortSignal.aborted) return;
2666
+ continue;
2667
+ }
2619
2668
  const message =
2620
- "The agent chat endpoint accepted the request but did not start streaming in time. This usually means prompt setup, the LLM gateway, or the provider is stalled.";
2669
+ "The agent chat endpoint did not start streaming in time after several recovery attempts. This usually means prompt setup, the LLM gateway, or the provider is stalled.";
2621
2670
  captureChatClientError(err, "startup-timeout", {
2622
2671
  timeoutMs: err.timeoutMs,
2672
+ startupRecoveryAttempts,
2623
2673
  });
2624
2674
  const runError = {
2625
2675
  message,
@@ -838,8 +838,8 @@ function DiffRead({
838
838
  effectiveMode === "split" ? splitLineCount : rows.length;
839
839
  const shouldLimitRows = totalVisibleLineCount > DEFAULT_VISIBLE_DIFF_LINES;
840
840
  // Never truncate away an annotated row: extend the window past the last one.
841
- const effectiveRowLimit = useMemo(() => {
842
- if (showAllRows || !shouldLimitRows) return undefined;
841
+ const collapsedRowLimit = useMemo(() => {
842
+ if (!shouldLimitRows) return undefined;
843
843
  let limit = DEFAULT_VISIBLE_DIFF_LINES;
844
844
  if (hasAnnotations) {
845
845
  for (let idx = rows.length - 1; idx >= limit; idx -= 1) {
@@ -850,8 +850,10 @@ function DiffRead({
850
850
  }
851
851
  }
852
852
  return limit;
853
- }, [showAllRows, shouldLimitRows, hasAnnotations, rows, markersForRow]);
854
- const rowLimit = effectiveRowLimit;
853
+ }, [shouldLimitRows, hasAnnotations, rows, markersForRow]);
854
+ const hasHiddenRows =
855
+ collapsedRowLimit != null && collapsedRowLimit < totalVisibleLineCount;
856
+ const rowLimit = showAllRows ? undefined : collapsedRowLimit;
855
857
  const displayedRows =
856
858
  effectiveMode === "unified" && rowLimit ? rows.slice(0, rowLimit) : rows;
857
859
 
@@ -965,7 +967,7 @@ function DiffRead({
965
967
  ctx={ctx}
966
968
  />
967
969
  )}
968
- {!unchanged && shouldLimitRows && (
970
+ {!unchanged && hasHiddenRows && (
969
971
  <button
970
972
  type="button"
971
973
  data-plan-interactive