@agent-native/core 0.85.3 → 0.85.4

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,11 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.85.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 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.
8
+
3
9
  ## 0.85.3
4
10
 
5
11
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.85.3",
3
+ "version": "0.85.4",
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": {
@@ -1362,20 +1362,32 @@ export async function getActiveRunForThreadAsync(threadId: string): Promise<{
1362
1362
  // the full conversation from completed runs via SSE.
1363
1363
  const memRun = getActiveRunForThread(threadId);
1364
1364
  if (memRun && (memRun.status === "running" || memRun.events.length > 0)) {
1365
+ const sqlSnapshot = await fetchRunThreadSnapshot(memRun.runId, threadId);
1366
+ const status = sqlSnapshot?.status ?? memRun.status;
1367
+ const heartbeatAt =
1368
+ status === "running"
1369
+ ? Date.now()
1370
+ : (sqlSnapshot?.heartbeatAt ?? memRun.startedAt);
1365
1371
  return {
1366
1372
  runId: memRun.runId,
1367
1373
  threadId: memRun.threadId,
1368
1374
  turnId: memRun.turnId,
1369
- status: memRun.status,
1375
+ status,
1370
1376
  // 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(),
1377
+ // heartbeat is fresh as of "now" while the run is still running. Once
1378
+ // SQL has terminal truth, prefer that timestamp so a stale in-memory
1379
+ // buffer cannot keep the browser believing a finished background run is
1380
+ // still alive.
1381
+ heartbeatAt,
1373
1382
  // For an in-memory run we don't have a separate "last event emit"
1374
1383
  // timestamp tracked in JS — the SQL bump is throttled per-second.
1375
1384
  // Read it back from SQL on demand. For the common case the SQL row
1376
1385
  // is well under 1s old; if it isn't, the stuck-detector will pick
1377
1386
  // it up on the next poll cycle.
1378
- lastProgressAt: await fetchLastProgressAt(memRun.runId),
1387
+ lastProgressAt: sqlSnapshot?.lastProgressAt ?? null,
1388
+ dispatchMode: sqlSnapshot?.dispatchMode ?? null,
1389
+ terminalReason: sqlSnapshot?.terminalReason ?? null,
1390
+ diagStage: sqlSnapshot?.diagStage ?? null,
1379
1391
  };
1380
1392
  }
1381
1393
  // Fall back to SQL — also surface recently terminated runs so the client
@@ -1454,16 +1466,14 @@ export async function getActiveRunForThreadAsync(threadId: string): Promise<{
1454
1466
  return null;
1455
1467
  }
1456
1468
 
1457
- async function fetchLastProgressAt(runId: string): Promise<number | null> {
1469
+ async function fetchRunThreadSnapshot(runId: string, threadId: string) {
1458
1470
  try {
1459
- const run = await getRunById(runId);
1460
- if (!run) return null;
1461
1471
  // `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, {
1472
+ // the thread lookup which carries dispatch/terminal/progress fields.
1473
+ const byThread = await getRunByThread(threadId, {
1464
1474
  includeTerminal: true,
1465
1475
  });
1466
- if (byThread && byThread.id === runId) return byThread.lastProgressAt;
1476
+ if (byThread && byThread.id === runId) return byThread;
1467
1477
  return null;
1468
1478
  } catch {
1469
1479
  return null;
@@ -239,6 +239,7 @@ 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;
244
245
  // How long a single activity (model call, tool prep, long tool) must stay
@@ -271,15 +272,24 @@ function isReplayableTerminalRun(runInfo: ActiveRunLookup): boolean {
271
272
  );
272
273
  }
273
274
 
275
+ function activeRunStuckThresholdMs(runInfo: ActiveRunLookup): number {
276
+ const dispatchMode =
277
+ typeof runInfo.dispatchMode === "string" ? runInfo.dispatchMode : "";
278
+ return dispatchMode.startsWith("background")
279
+ ? BACKGROUND_ACTIVE_RUN_STUCK_THRESHOLD_MS
280
+ : ACTIVE_RUN_STUCK_THRESHOLD_MS;
281
+ }
282
+
274
283
  function activeRunLooksStale(runInfo: ActiveRunLookup): boolean {
275
284
  const lastProgressAt =
276
285
  typeof runInfo.lastProgressAt === "number" ? runInfo.lastProgressAt : null;
277
286
  const nowMs =
278
287
  typeof runInfo.serverNow === "number" ? runInfo.serverNow : Date.now();
288
+ const thresholdMs = activeRunStuckThresholdMs(runInfo);
279
289
  return (
280
290
  runInfo.status === "running" &&
281
291
  lastProgressAt != null &&
282
- nowMs - lastProgressAt > ACTIVE_RUN_STUCK_THRESHOLD_MS
292
+ nowMs - lastProgressAt > thresholdMs
283
293
  );
284
294
  }
285
295
 
@@ -1894,6 +1904,7 @@ const AssistantChatInner = forwardRef<
1894
1904
  reconnectAbortRef.current = abortCtrl;
1895
1905
  let reconnectTerminalReason: AgentAutoContinueSignal["reason"] | null =
1896
1906
  null;
1907
+ const reconnectStuckThresholdMs = activeRunStuckThresholdMs(runInfo);
1897
1908
 
1898
1909
  const watchdog = setInterval(async () => {
1899
1910
  try {
@@ -1937,6 +1948,7 @@ const AssistantChatInner = forwardRef<
1937
1948
  !reconnectProgressTimedOut({
1938
1949
  lastProgressAt: lastReconnectProgressAt,
1939
1950
  now: Date.now(),
1951
+ thresholdMs: reconnectStuckThresholdMs,
1940
1952
  })
1941
1953
  ) {
1942
1954
  return;
@@ -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":"AAwBA,OAAO,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEtE,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,QAAQ,EAAE,CAAC;IACnB,MAAM,EAAE,SAAS,CAAC;IAClB,WAAW,EAAE,GAAG,CAAC,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAC,CAAC;IAC5C,KAAK,EAAE,eAAe,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB;AAQD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,kCAAkC,QAAS,CAAC;AAEzD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,8BAA8B,QAAS,CAAC;AAErD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,kCAAkC,QAAc,CAAC;AAE9D;;;;;GAKG;AACH,eAAO,MAAM,sCAAsC,QACf,CAAC;AAErC;;;;;;;GAOG;AACH,eAAO,MAAM,yCAAyC,QACT,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,+BAA+B,SAAU,CAAC;AAEvD,qEAAqE;AACrE,eAAO,MAAM,kCAAkC,QAAsB,CAAC;AAEtE;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,QAA0B,CAAC;AAExE;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,QAAiB,CAAC;AAE/D,wFAAwF;AACxF,eAAO,MAAM,+BAA+B,MAAM,CAAC;AAEnD,iEAAiE;AACjE,eAAO,MAAM,6BAA6B,MAAM,CAAC;AAEjD;;;GAGG;AACH,eAAO,MAAM,gCAAgC,OAAQ,CAAC;AAEtD,8EAA8E;AAC9E,eAAO,MAAM,+BAA+B,MAAM,CAAC;AAEnD,wBAAgB,4BAA4B,CAC1C,GAAG,EAAE,MAAM,EACX,eAAe,EAAE,MAAM,GACtB,MAAM,CAIR;AAmDD,MAAM,WAAW,eAAe;IAC9B;;2CAEuC;IACvC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;4DACwD;IACxD,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC;;;+EAG2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;OAQG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,4BAA4B;IAC3C,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AA0BD,wBAAgB,uBAAuB,CACrC,UAAU,CAAC,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,4BAA4B,GACrC,MAAM,CAkCR;AAED,wBAAgB,8BAA8B,IAAI,MAAM,CAOvD;AAED,wBAAgB,4BAA4B,IAAI,MAAM,CAOrD;AAiDD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CACtB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,CACL,IAAI,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,EACrC,MAAM,EAAE,WAAW,KAChB,OAAO,CAAC,IAAI,CAAC,EAClB,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,SAAS,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EACrD,OAAO,CAAC,EAAE,eAAe,GACxB,SAAS,CAqlBX;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,GACd,cAAc,CAAC,UAAU,CAAC,GAAG,IAAI,CAOnC;AAwRD,wEAAwE;AACxE,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAOxE;AAED;;;;;;;;;GASG;AACH,wBAAsB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1E,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,uFAAuF;IACvF,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,0EAA0E;IAC1E,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,GAAG,IAAI,CAAC,CAgGR;AAkBD,sBAAsB;AACtB,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAEtD;AAED,gDAAgD;AAChD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,GAAE,MAAe,GAAG,OAAO,CAQxE;AAGD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"run-manager.d.ts","sourceRoot":"","sources":["../../src/agent/run-manager.ts"],"names":[],"mappings":"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"}
@@ -1118,20 +1118,31 @@ export async function getActiveRunForThreadAsync(threadId) {
1118
1118
  // the full conversation from completed runs via SSE.
1119
1119
  const memRun = getActiveRunForThread(threadId);
1120
1120
  if (memRun && (memRun.status === "running" || memRun.events.length > 0)) {
1121
+ const sqlSnapshot = await fetchRunThreadSnapshot(memRun.runId, threadId);
1122
+ const status = sqlSnapshot?.status ?? memRun.status;
1123
+ const heartbeatAt = status === "running"
1124
+ ? Date.now()
1125
+ : (sqlSnapshot?.heartbeatAt ?? memRun.startedAt);
1121
1126
  return {
1122
1127
  runId: memRun.runId,
1123
1128
  threadId: memRun.threadId,
1124
1129
  turnId: memRun.turnId,
1125
- status: memRun.status,
1130
+ status,
1126
1131
  // 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(),
1132
+ // heartbeat is fresh as of "now" while the run is still running. Once
1133
+ // SQL has terminal truth, prefer that timestamp so a stale in-memory
1134
+ // buffer cannot keep the browser believing a finished background run is
1135
+ // still alive.
1136
+ heartbeatAt,
1129
1137
  // For an in-memory run we don't have a separate "last event emit"
1130
1138
  // timestamp tracked in JS — the SQL bump is throttled per-second.
1131
1139
  // Read it back from SQL on demand. For the common case the SQL row
1132
1140
  // is well under 1s old; if it isn't, the stuck-detector will pick
1133
1141
  // it up on the next poll cycle.
1134
- lastProgressAt: await fetchLastProgressAt(memRun.runId),
1142
+ lastProgressAt: sqlSnapshot?.lastProgressAt ?? null,
1143
+ dispatchMode: sqlSnapshot?.dispatchMode ?? null,
1144
+ terminalReason: sqlSnapshot?.terminalReason ?? null,
1145
+ diagStage: sqlSnapshot?.diagStage ?? null,
1135
1146
  };
1136
1147
  }
1137
1148
  // Fall back to SQL — also surface recently terminated runs so the client
@@ -1211,18 +1222,15 @@ export async function getActiveRunForThreadAsync(threadId) {
1211
1222
  }
1212
1223
  return null;
1213
1224
  }
1214
- async function fetchLastProgressAt(runId) {
1225
+ async function fetchRunThreadSnapshot(runId, threadId) {
1215
1226
  try {
1216
- const run = await getRunById(runId);
1217
- if (!run)
1218
- return null;
1219
1227
  // `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, {
1228
+ // the thread lookup which carries dispatch/terminal/progress fields.
1229
+ const byThread = await getRunByThread(threadId, {
1222
1230
  includeTerminal: true,
1223
1231
  });
1224
1232
  if (byThread && byThread.id === runId)
1225
- return byThread.lastProgressAt;
1233
+ return byThread;
1226
1234
  return null;
1227
1235
  }
1228
1236
  catch {