@agent-native/core 0.84.10 → 0.84.13

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 (140) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +48 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/durable-background.ts +30 -0
  5. package/corpus/core/src/agent/production-agent.ts +34 -6
  6. package/corpus/core/src/agent/run-manager.ts +45 -2
  7. package/corpus/core/src/agent/run-store.ts +29 -6
  8. package/corpus/core/src/chat-threads/store.ts +2 -0
  9. package/corpus/core/src/cli/skills.ts +10 -6
  10. package/corpus/core/src/client/AssistantChat.tsx +40 -9
  11. package/corpus/core/src/client/agent-chat-adapter.ts +23 -2
  12. package/corpus/core/src/client/chat/run-recovery.tsx +52 -44
  13. package/corpus/core/src/client/chat/tool-call-display.tsx +7 -2
  14. package/corpus/core/src/client/require-session.tsx +20 -1
  15. package/corpus/core/src/client/sharing/ShareButton.tsx +9 -3
  16. package/corpus/core/src/collab/storage.ts +40 -5
  17. package/corpus/core/src/server/agent-chat-plugin.ts +1 -0
  18. package/corpus/core/src/server/auth.ts +6 -0
  19. package/corpus/core/src/shared/streaming-text-smoothing.ts +10 -5
  20. package/corpus/templates/assets/.agents/skills/logo-composite/SKILL.md +7 -6
  21. package/corpus/templates/assets/AGENTS.md +15 -0
  22. package/corpus/templates/assets/actions/_helpers.ts +3 -1
  23. package/corpus/templates/assets/actions/create-generation-preset.ts +12 -1
  24. package/corpus/templates/assets/actions/generate-asset.ts +5 -1
  25. package/corpus/templates/assets/actions/generate-image-batch.ts +6 -1
  26. package/corpus/templates/assets/actions/generate-image.ts +18 -11
  27. package/corpus/templates/assets/actions/open-asset-picker.ts +4 -2
  28. package/corpus/templates/assets/actions/update-generation-preset.ts +16 -3
  29. package/corpus/templates/assets/app/i18n/zh-TW.ts +3 -0
  30. package/corpus/templates/assets/app/i18n-data.ts +30 -0
  31. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +42 -7
  32. package/corpus/templates/assets/app/routes/library.tsx +4 -2
  33. package/corpus/templates/assets/changelog/2026-07-01-generation-presets-can-now-composite-your-brand-s-canonical-.md +6 -0
  34. package/corpus/templates/assets/changelog/2026-07-01-tagging-a-preset-now-briefs-the-agent-on-that-preset-s-aesth.md +6 -0
  35. package/corpus/templates/assets/server/lib/preset-chat-context.ts +118 -0
  36. package/corpus/templates/assets/server/plugins/agent-chat.ts +5 -0
  37. package/corpus/templates/assets/shared/api.ts +1 -0
  38. package/corpus/templates/clips/app/components/library/recording-card.tsx +14 -4
  39. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +10 -12
  40. package/corpus/templates/clips/app/hooks/use-library.ts +7 -4
  41. package/corpus/templates/clips/app/lib/recording-status.ts +32 -0
  42. package/corpus/templates/clips/changelog/2026-07-01-dictation-keeps-listening-through-natural-pauses-and-gives-c.md +6 -0
  43. package/corpus/templates/clips/changelog/2026-07-01-redoing-a-paused-desktop-recording-no-longer-leaves-the-recording-controls-disabled.md +6 -0
  44. package/corpus/templates/clips/changelog/2026-07-01-stalled-clip-uploads-now-update-in-the-library-and-surface-as-failed.md +6 -0
  45. package/corpus/templates/clips/desktop/src/lib/recorder.ts +36 -18
  46. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +99 -27
  47. package/corpus/templates/clips/desktop/src/overlays/flow-bar.tsx +4 -43
  48. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +3 -0
  49. package/corpus/templates/clips/desktop/src/styles.css +5 -48
  50. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +4 -5
  51. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +15 -6
  52. package/corpus/templates/design/AGENTS.md +8 -4
  53. package/corpus/templates/design/actions/create-design.ts +1 -0
  54. package/corpus/templates/design/actions/duplicate-design.ts +3 -1
  55. package/corpus/templates/design/actions/edit-design.ts +43 -13
  56. package/corpus/templates/design/actions/export-html.ts +4 -2
  57. package/corpus/templates/design/actions/export-pdf.ts +3 -1
  58. package/corpus/templates/design/actions/export-svg.ts +4 -2
  59. package/corpus/templates/design/actions/export-zip.ts +10 -3
  60. package/corpus/templates/design/actions/generate-design.ts +5 -1
  61. package/corpus/templates/design/actions/present-design-variants.ts +17 -12
  62. package/corpus/templates/design/actions/update-file.ts +68 -17
  63. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +3 -2
  64. package/corpus/templates/design/app/components/design/EditPanel.tsx +35 -3
  65. package/corpus/templates/design/app/components/design/MotionDock.tsx +46 -11
  66. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +80 -30
  67. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +60 -10
  68. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +2 -0
  69. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +4 -0
  70. package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
  71. package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +2 -2
  72. package/corpus/templates/design/app/i18n-data.ts +1 -1
  73. package/corpus/templates/design/app/pages/DesignEditor.tsx +493 -109
  74. package/corpus/templates/design/app/pages/VisualEdit.tsx +4 -16
  75. package/corpus/templates/design/changelog/2026-07-01-org-members-can-see-design-projects-created-in-their-workspace.md +6 -0
  76. package/corpus/templates/design/e2e/helpers.ts +2 -2
  77. package/corpus/templates/design/server/plugins/db.ts +9 -0
  78. package/dist/agent/durable-background.d.ts +3 -0
  79. package/dist/agent/durable-background.d.ts.map +1 -1
  80. package/dist/agent/durable-background.js +19 -0
  81. package/dist/agent/durable-background.js.map +1 -1
  82. package/dist/agent/production-agent.d.ts +1 -0
  83. package/dist/agent/production-agent.d.ts.map +1 -1
  84. package/dist/agent/production-agent.js +29 -6
  85. package/dist/agent/production-agent.js.map +1 -1
  86. package/dist/agent/run-manager.d.ts.map +1 -1
  87. package/dist/agent/run-manager.js +40 -2
  88. package/dist/agent/run-manager.js.map +1 -1
  89. package/dist/agent/run-store.d.ts.map +1 -1
  90. package/dist/agent/run-store.js +28 -6
  91. package/dist/agent/run-store.js.map +1 -1
  92. package/dist/chat-threads/store.d.ts +1 -0
  93. package/dist/chat-threads/store.d.ts.map +1 -1
  94. package/dist/chat-threads/store.js +2 -0
  95. package/dist/chat-threads/store.js.map +1 -1
  96. package/dist/cli/skills.d.ts.map +1 -1
  97. package/dist/cli/skills.js +10 -6
  98. package/dist/cli/skills.js.map +1 -1
  99. package/dist/client/AssistantChat.d.ts +10 -0
  100. package/dist/client/AssistantChat.d.ts.map +1 -1
  101. package/dist/client/AssistantChat.js +26 -8
  102. package/dist/client/AssistantChat.js.map +1 -1
  103. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  104. package/dist/client/agent-chat-adapter.js +21 -2
  105. package/dist/client/agent-chat-adapter.js.map +1 -1
  106. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  107. package/dist/client/chat/run-recovery.js +15 -10
  108. package/dist/client/chat/run-recovery.js.map +1 -1
  109. package/dist/client/chat/tool-call-display.d.ts +1 -0
  110. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  111. package/dist/client/chat/tool-call-display.js +3 -2
  112. package/dist/client/chat/tool-call-display.js.map +1 -1
  113. package/dist/client/require-session.d.ts +14 -0
  114. package/dist/client/require-session.d.ts.map +1 -1
  115. package/dist/client/require-session.js +19 -1
  116. package/dist/client/require-session.js.map +1 -1
  117. package/dist/client/sharing/ShareButton.d.ts +4 -2
  118. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  119. package/dist/client/sharing/ShareButton.js +3 -1
  120. package/dist/client/sharing/ShareButton.js.map +1 -1
  121. package/dist/collab/routes.d.ts +2 -2
  122. package/dist/collab/storage.d.ts.map +1 -1
  123. package/dist/collab/storage.js +31 -5
  124. package/dist/collab/storage.js.map +1 -1
  125. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  126. package/dist/notifications/routes.d.ts +2 -2
  127. package/dist/observability/routes.d.ts +5 -5
  128. package/dist/progress/routes.d.ts +1 -1
  129. package/dist/resources/handlers.d.ts +3 -3
  130. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  131. package/dist/server/agent-chat-plugin.js +1 -1
  132. package/dist/server/agent-chat-plugin.js.map +1 -1
  133. package/dist/server/auth.d.ts.map +1 -1
  134. package/dist/server/auth.js +7 -0
  135. package/dist/server/auth.js.map +1 -1
  136. package/dist/server/transcribe-voice.d.ts +1 -1
  137. package/dist/shared/streaming-text-smoothing.d.ts.map +1 -1
  138. package/dist/shared/streaming-text-smoothing.js +8 -5
  139. package/dist/shared/streaming-text-smoothing.js.map +1 -1
  140. 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: 2043
31
- - template files: 4909
31
+ - template files: 4917
@@ -1,5 +1,53 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.84.13
4
+
5
+ ### Patch Changes
6
+
7
+ - dd5b0a4: Honor the long Netlify background-function timeout for durable agent-chat workers and record runtime diagnostics when a background run starts.
8
+ - dd5b0a4: Make agent chat recovery surface completed-tool timeouts more reliably and keep Design variant follow-ups bounded to the selected screen.
9
+ - dd5b0a4: Double the agent chat smooth-streaming reveal speed.
10
+ - dd5b0a4: Avoid noisy Sentry reports from expected chat auth states and best-effort client thread-save conflicts.
11
+ - dd5b0a4: Keep activity-only tool cards and the latest assistant message visibly running during agent chat continuations, steer interrupted Design generation retries toward smaller existing-file edits, and capture run persistence failures with run IDs so missing request IDs are diagnosable.
12
+ - dd5b0a4: Compress the Plan mode composer callout into a quieter inline status pill.
13
+
14
+ ## 0.84.12
15
+
16
+ ### Patch Changes
17
+
18
+ - 9032acb: Don't reap a run that is actively making progress. The stale reapers
19
+ (`reapIfStale`, `reapAllStaleRuns`, and the heartbeat-stale path of
20
+ `cleanupOldRuns`) keyed liveness solely on `heartbeat_at` (the 1.5s
21
+ process-liveness timer) and ignored `last_progress_at` (bumped whenever the
22
+ agent emits an event, including a long-running tool's periodic activity
23
+ heartbeats — e.g. image generation streaming activity every 8s). When the
24
+ heartbeat write lagged while a multi-minute tool was in flight, the run was
25
+ flipped to `errored`, and the producing isolate's SQL-abort check then
26
+ self-aborted the in-flight tool with "Run aborted" — looping on the
27
+ durable-background self-chaining path and interrupting once inline. The reapers
28
+ now use the most recent of `heartbeat_at` and `last_progress_at` (falling back to
29
+ `started_at`) as their liveness basis, so a demonstrably-progressing run is never
30
+ reaped mid-tool. Portable across SQLite and Postgres; can only make reaping more
31
+ conservative (a dead producer emits neither signal).
32
+ - 9032acb: Add a `trigger="label-icon"` option to `ShareButton` that renders a leading
33
+ share glyph alongside the "Share" label, so the trigger matches adjacent
34
+ icon+label buttons (e.g. an Upload button). The default `"label"` trigger stays
35
+ text-only and `"icon"` stays icon-only.
36
+ - 351ee93: Fix an infinite sign-in redirect loop under base-path deploys. When the
37
+ authenticated app shell (wrapped in `RequireSession`) was served at the sign-in
38
+ path — e.g. `/<app>/_agent-native/sign-in` on a path-prefixed deploy — the gate
39
+ redirected to the sign-in page from the sign-in page, nesting and re-encoding the
40
+ current URL as a fresh `?return=` on every hop (`…sign-in?return=%252F…sign-in%253Freturn…`).
41
+ `RequireSession` now refuses to redirect when already on the sign-in entry point
42
+ (new exported `isOnSignInPage` helper), and `safeReturnPath` collapses any
43
+ `return` that resolves back to `…/_agent-native/sign-in` to `/`.
44
+
45
+ ## 0.84.11
46
+
47
+ ### Patch Changes
48
+
49
+ - a2ce30e: Improve local SQLite collab write robustness for Design editor workflows.
50
+
3
51
  ## 0.84.10
4
52
 
5
53
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.84.10",
3
+ "version": "0.84.13",
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": {
@@ -235,6 +235,36 @@ export function isInBackgroundFunctionRuntime(): boolean {
235
235
  return false;
236
236
  }
237
237
 
238
+ export function backgroundRunMarkerExpectsBackgroundRuntime(
239
+ marker: unknown,
240
+ ): boolean {
241
+ return (
242
+ typeof marker === "object" &&
243
+ marker !== null &&
244
+ (marker as { backgroundFunctionRuntimeExpected?: unknown })
245
+ .backgroundFunctionRuntimeExpected === true
246
+ );
247
+ }
248
+
249
+ export function shouldUseBackgroundFunctionTimeoutForWorker(
250
+ marker: unknown,
251
+ ): boolean {
252
+ return (
253
+ isInBackgroundFunctionRuntime() ||
254
+ backgroundRunMarkerExpectsBackgroundRuntime(marker)
255
+ );
256
+ }
257
+
258
+ export function backgroundRuntimeDiagnosticDetail(marker: unknown): string {
259
+ return [
260
+ `markerExpected=${backgroundRunMarkerExpectsBackgroundRuntime(marker)}`,
261
+ `runtimeDetected=${isInBackgroundFunctionRuntime()}`,
262
+ `globalMarker=${(globalThis as Record<string, unknown>).__AGENT_NATIVE_BACKGROUND_RUNTIME__ === true}`,
263
+ `lambdaNameEndsBackground=${typeof process.env.AWS_LAMBDA_FUNCTION_NAME === "string" && process.env.AWS_LAMBDA_FUNCTION_NAME.toLowerCase().endsWith("-background")}`,
264
+ `forceEnv=${typeof process.env.AGENT_CHAT_FORCE_BACKGROUND_RUNTIME === "string" && process.env.AGENT_CHAT_FORCE_BACKGROUND_RUNTIME.trim().length > 0}`,
265
+ ].join(" ");
266
+ }
267
+
238
268
  function isFlagEnabled(): boolean {
239
269
  // Read the literal key (not `process.env[CONST]`) so guard:no-env-credentials
240
270
  // can statically verify it against the allowlisted `AGENT_*` prefix. Keep this
@@ -53,9 +53,10 @@ import {
53
53
  import { computeProtectedSegmentIds } from "./context-xray/segments.js";
54
54
  import {
55
55
  AGENT_CHAT_BACKGROUND_RUN_FIELD,
56
+ backgroundRuntimeDiagnosticDetail,
56
57
  isAgentChatDurableBackgroundEnabled,
57
- isInBackgroundFunctionRuntime,
58
58
  resolveAgentChatProcessRunDispatchPath,
59
+ shouldUseBackgroundFunctionTimeoutForWorker,
59
60
  } from "./durable-background.js";
60
61
  import {
61
62
  LLM_MISSING_CREDENTIALS_ERROR_CODE,
@@ -4032,6 +4033,7 @@ export async function claimBackgroundWorkerRunEarly(opts: {
4032
4033
  requestTurnId?: string | null;
4033
4034
  continuationCount: number;
4034
4035
  runsInBackgroundFunction: boolean;
4036
+ backgroundRuntimeDetail?: string;
4035
4037
  deps?: {
4036
4038
  recordRunDiagnostic?: typeof recordRunDiagnostic;
4037
4039
  insertRun?: typeof insertRun;
@@ -4057,7 +4059,13 @@ export async function claimBackgroundWorkerRunEarly(opts: {
4057
4059
  await record(
4058
4060
  opts.runId,
4059
4061
  RUN_DIAG_STAGE.workerEntered,
4060
- `runsInBackgroundFunction=${opts.runsInBackgroundFunction} continuationCount=${opts.continuationCount}`,
4062
+ [
4063
+ `runsInBackgroundFunction=${opts.runsInBackgroundFunction}`,
4064
+ `continuationCount=${opts.continuationCount}`,
4065
+ opts.backgroundRuntimeDetail,
4066
+ ]
4067
+ .filter(Boolean)
4068
+ .join(" "),
4061
4069
  ).catch(() => {});
4062
4070
 
4063
4071
  if (opts.continuationCount > 0) {
@@ -4218,7 +4226,11 @@ export function createProductionAgentHandler(
4218
4226
  // function. Only a true value unlocks the ~13-min soft-timeout budget; a
4219
4227
  // worker on the 60s function keeps the 40s clamp and checkpoints cleanly.
4220
4228
  const runsInBackgroundFunction =
4221
- isBackgroundWorker && isInBackgroundFunctionRuntime();
4229
+ isBackgroundWorker &&
4230
+ shouldUseBackgroundFunctionTimeoutForWorker(backgroundRunMarker);
4231
+ const backgroundRuntimeDetail = isBackgroundWorker
4232
+ ? backgroundRuntimeDiagnosticDetail(backgroundRunMarker)
4233
+ : "";
4222
4234
  // How many server-driven background continuations have already chained into
4223
4235
  // this logical turn (0 on the first chunk). Used to bound the chain.
4224
4236
  const backgroundContinuationCount =
@@ -4239,6 +4251,7 @@ export function createProductionAgentHandler(
4239
4251
  requestTurnId,
4240
4252
  continuationCount: backgroundContinuationCount,
4241
4253
  runsInBackgroundFunction,
4254
+ backgroundRuntimeDetail,
4242
4255
  });
4243
4256
  if (!earlyClaim.claimed) {
4244
4257
  return { ok: true, skipped: earlyClaim.skipped };
@@ -5001,6 +5014,9 @@ export function createProductionAgentHandler(
5001
5014
  }
5002
5015
 
5003
5016
  let dispatched = false;
5017
+ const backgroundDispatchPath = resolveAgentChatProcessRunDispatchPath();
5018
+ const expectsNetlifyBackgroundFunction =
5019
+ backgroundDispatchPath.startsWith("/.netlify/functions/");
5004
5020
  try {
5005
5021
  await fireInternalDispatch({
5006
5022
  event,
@@ -5014,7 +5030,7 @@ export function createProductionAgentHandler(
5014
5030
  // inline. `fireInternalDispatch` strips the app base path for
5015
5031
  // /.netlify/* targets so the request reaches the host-root function url;
5016
5032
  // the Authorization Bearer HMAC is preserved either way.
5017
- path: resolveAgentChatProcessRunDispatchPath(),
5033
+ path: backgroundDispatchPath,
5018
5034
  taskId: runId,
5019
5035
  body: {
5020
5036
  ...body,
@@ -5022,6 +5038,8 @@ export function createProductionAgentHandler(
5022
5038
  [AGENT_CHAT_BACKGROUND_RUN_FIELD]: {
5023
5039
  runId,
5024
5040
  turnId: effectiveTurnId,
5041
+ backgroundFunctionRuntimeExpected:
5042
+ expectsNetlifyBackgroundFunction,
5025
5043
  },
5026
5044
  },
5027
5045
  });
@@ -5302,6 +5320,8 @@ export function createProductionAgentHandler(
5302
5320
  runId: nextRunId,
5303
5321
  turnId: effectiveTurnId,
5304
5322
  continuationCount: backgroundContinuationCount + 1,
5323
+ backgroundFunctionRuntimeExpected:
5324
+ runsInBackgroundFunction,
5305
5325
  },
5306
5326
  },
5307
5327
  });
@@ -5335,7 +5355,13 @@ export function createProductionAgentHandler(
5335
5355
  await recordRunDiagnostic(
5336
5356
  runId,
5337
5357
  RUN_DIAG_STAGE.workerEntered,
5338
- `runsInBackgroundFunction=${runsInBackgroundFunction} continuationCount=${backgroundContinuationCount}`,
5358
+ [
5359
+ `runsInBackgroundFunction=${runsInBackgroundFunction}`,
5360
+ `continuationCount=${backgroundContinuationCount}`,
5361
+ backgroundRuntimeDetail,
5362
+ ]
5363
+ .filter(Boolean)
5364
+ .join(" "),
5339
5365
  ).catch(() => {});
5340
5366
  if (isChainedBackgroundContinuation) {
5341
5367
  await insertRun(runId, effectiveThreadId, effectiveTurnId, {
@@ -5368,7 +5394,9 @@ export function createProductionAgentHandler(
5368
5394
  const setupDetail =
5369
5395
  Object.entries(setupMarks)
5370
5396
  .map(([k, v]) => `${k}=${v}`)
5371
- .join(" ") + ` total=${Date.now() - setupT0}`;
5397
+ .join(" ") +
5398
+ ` total=${Date.now() - setupT0}` +
5399
+ (backgroundRuntimeDetail ? ` ${backgroundRuntimeDetail}` : "");
5372
5400
 
5373
5401
  startRun(
5374
5402
  runId,
@@ -349,11 +349,45 @@ export function startRun(
349
349
  activeRuns.set(runId, run);
350
350
  threadToRun.set(threadId, runId);
351
351
 
352
+ const captureRunPersistenceError = (
353
+ error: unknown,
354
+ phase: "insert-run" | "insert-event",
355
+ extra: Record<string, unknown> = {},
356
+ ) => {
357
+ captureError(error, {
358
+ route: "/_agent-native/agent-chat",
359
+ tags: {
360
+ source: "agent-run-manager",
361
+ phase,
362
+ runStatus: run.status,
363
+ },
364
+ extra: {
365
+ runId,
366
+ threadId,
367
+ eventCount: run.events.length,
368
+ startedAt: run.startedAt,
369
+ ...extra,
370
+ },
371
+ contexts: {
372
+ agentRun: {
373
+ runId,
374
+ threadId,
375
+ status: run.status,
376
+ phase,
377
+ eventCount: run.events.length,
378
+ startedAt: run.startedAt,
379
+ },
380
+ },
381
+ });
382
+ };
383
+
352
384
  // Persist run to SQL without blocking the response. Keep the promise so
353
385
  // final status cannot race ahead of a slow initial INSERT and then get
354
386
  // overwritten by a late row stuck at status='running'.
355
387
  const insertRunPromise = insertRun(runId, threadId, options?.turnId).catch(
356
- () => {},
388
+ (error) => {
389
+ captureRunPersistenceError(error, "insert-run");
390
+ },
357
391
  );
358
392
 
359
393
  // Per-run event persistence chain: events are chained so SQL inserts commit
@@ -368,6 +402,7 @@ export function startRun(
368
402
  // chunk. The stuck-detector threshold is on the order of tens of seconds,
369
403
  // so 1s resolution is plenty.
370
404
  let lastProgressBumpAt = 0;
405
+ let eventPersistenceErrorCaptured = false;
371
406
  const bumpProgressIfDue = () => {
372
407
  const now = Date.now();
373
408
  if (now - lastProgressBumpAt < 1000) return;
@@ -494,7 +529,15 @@ export function startRun(
494
529
  const thisInsert = persistenceChain.then(() =>
495
530
  insertRunEvent(runId, runEvent.seq, JSON.stringify(runEvent.event)),
496
531
  );
497
- persistenceChain = thisInsert.catch(() => {});
532
+ persistenceChain = thisInsert.catch((error) => {
533
+ if (!eventPersistenceErrorCaptured) {
534
+ eventPersistenceErrorCaptured = true;
535
+ captureRunPersistenceError(error, "insert-event", {
536
+ seq: runEvent.seq,
537
+ eventType: runEvent.event.type,
538
+ });
539
+ }
540
+ });
498
541
  const persistence = thisInsert;
499
542
  if (!options?.surfacePersistenceError) {
500
543
  persistence.catch(() => {});
@@ -428,6 +428,29 @@ function backgroundAwareStaleCutoffSql(): string {
428
428
  return `(CAST(? AS BIGINT) - CASE WHEN dispatch_mode LIKE 'background%' THEN ${BACKGROUND_RUN_STALE_MS} ELSE ${RUN_STALE_MS} END)`;
429
429
  }
430
430
 
431
+ /**
432
+ * Liveness basis for the stale reapers: the MOST RECENT of `heartbeat_at`
433
+ * ("process is up", bumped on a 1.5s timer) and `last_progress_at` ("real work
434
+ * is happening", bumped whenever the agent emits an event — including a
435
+ * long-running tool's periodic activity heartbeats, e.g. image generation every
436
+ * 8s), falling back to `started_at`.
437
+ *
438
+ * The reapers previously keyed liveness on `heartbeat_at` alone, so a run that
439
+ * was demonstrably progressing got reaped ('running' → 'errored') the moment the
440
+ * process-liveness write lagged (DB latency, a brief event-loop stall). The
441
+ * producing isolate's SQL-abort check then self-aborted the in-flight action
442
+ * with "Run aborted"; on the durable-background self-chaining path this re-drove
443
+ * the turn in a loop. Honoring progress means a run doing real work is never
444
+ * reaped mid-tool. It can only make reaping MORE conservative — a genuinely dead
445
+ * producer emits neither signal — so a truly-dead run is still reaped.
446
+ *
447
+ * Portable across SQLite and Postgres (CASE + COALESCE only; no GREATEST or
448
+ * scalar MAX, which differ between engines).
449
+ */
450
+ function livenessBasisSql(): string {
451
+ return `(CASE WHEN COALESCE(last_progress_at, started_at) > COALESCE(heartbeat_at, started_at) THEN COALESCE(last_progress_at, started_at) ELSE COALESCE(heartbeat_at, started_at) END)`;
452
+ }
453
+
431
454
  /**
432
455
  * Atomically claim a background-dispatched run for processing. The foreground
433
456
  * POST inserts the run row with `dispatch_mode = 'background'`; the FIRST
@@ -733,8 +756,8 @@ export async function reapIfStale(
733
756
  // override forces a flat window for callers that want one.
734
757
  const staleClause =
735
758
  typeof maxStaleMs === "number"
736
- ? `COALESCE(heartbeat_at, started_at) < ?`
737
- : `COALESCE(heartbeat_at, started_at) < ${backgroundAwareStaleCutoffSql()}`;
759
+ ? `${livenessBasisSql()} < ?`
760
+ : `${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}`;
738
761
  const staleArgs =
739
762
  typeof maxStaleMs === "number" ? [completedAt - maxStaleMs] : [completedAt];
740
763
  const { rowsAffected } = await client.execute({
@@ -1151,7 +1174,7 @@ export async function reapAllStaleRuns(): Promise<number> {
1151
1174
  const stale = await client.execute({
1152
1175
  sql: `SELECT id FROM agent_runs
1153
1176
  WHERE status = 'running'
1154
- AND COALESCE(heartbeat_at, started_at) < ${backgroundAwareStaleCutoffSql()}`,
1177
+ AND ${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}`,
1155
1178
  args: [now],
1156
1179
  });
1157
1180
  const completedAt = Date.now();
@@ -1162,7 +1185,7 @@ export async function reapAllStaleRuns(): Promise<number> {
1162
1185
  error_code = ?,
1163
1186
  error_detail = ?
1164
1187
  WHERE status = 'running'
1165
- AND COALESCE(heartbeat_at, started_at) < ${backgroundAwareStaleCutoffSql()}`,
1188
+ AND ${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}`,
1166
1189
  args: [
1167
1190
  completedAt,
1168
1191
  STALE_RUN_ERROR_EVENT.errorCode,
@@ -1207,7 +1230,7 @@ export async function cleanupOldRuns(
1207
1230
  sql: `SELECT id FROM agent_runs
1208
1231
  WHERE status = 'running'
1209
1232
  AND (
1210
- COALESCE(heartbeat_at, started_at) < ${backgroundAwareStaleCutoffSql()}
1233
+ ${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}
1211
1234
  OR started_at < ?
1212
1235
  )`,
1213
1236
  args: [now, cutoff],
@@ -1236,7 +1259,7 @@ export async function cleanupOldRuns(
1236
1259
  error_code = ?,
1237
1260
  error_detail = ?
1238
1261
  WHERE status = 'running'
1239
- AND COALESCE(heartbeat_at, started_at) < ${backgroundAwareStaleCutoffSql()}`,
1262
+ AND ${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}`,
1240
1263
  args: [
1241
1264
  completedAt,
1242
1265
  STALE_RUN_ERROR_EVENT.errorCode,
@@ -712,6 +712,7 @@ export interface UpdateThreadDataOptions {
712
712
  preserveExistingQueuedMessages?: boolean;
713
713
  preserveExistingTopLevelKeys?: boolean;
714
714
  maxAttempts?: number;
715
+ ignoreConflicts?: boolean;
715
716
  }
716
717
 
717
718
  function parseThreadData(value: string): any {
@@ -792,6 +793,7 @@ export async function updateThreadData(
792
793
  }
793
794
 
794
795
  if (lastConflict) {
796
+ if (options.ignoreConflicts) return;
795
797
  throw new Error(
796
798
  `Failed to update chat thread ${id} after concurrent write conflicts.`,
797
799
  );
@@ -329,15 +329,18 @@ iteration, or a human-in-the-loop choice among design directions.
329
329
  overview board and renders an inline chat choice with one button per screen
330
330
  name. After the user picks, delete the unchosen variant screens and continue
331
331
  from the kept screen by first calling \`get-design-snapshot\` with that
332
- screen's \`fileId\`, then making a bounded single-file pass.
332
+ screen's \`fileId\`, then calling \`edit-design\` on that same \`fileId\` in a
333
+ bounded single-file pass. Use \`mode: "replace-file"\` when expanding the
334
+ representative placeholder into the full chosen direction. Do not call
335
+ \`generate-design\` after a variant pick.
333
336
  - If the chat choice buttons are not available in the host, ask the user to
334
337
  tell you the screen name they prefer. The variants are already real screens
335
338
  on the board, so do not ask them to paste HTML or copy a generated handoff
336
339
  summary.
337
340
  - For direct refinements to an already chosen direction, call
338
341
  \`get-design-snapshot\`, edit from the current tuned HTML, and use
339
- \`edit-design\` for surgical changes. Use \`generate-design\` for new files
340
- or larger structural rewrites.
342
+ \`edit-design\` for surgical changes or \`mode: "replace-file"\` for a bounded
343
+ selected-file replacement. Use \`generate-design\` for new files only.
341
344
  - Use \`export-coding-handoff\` when the user wants to implement the chosen
342
345
  design in a codebase.
343
346
 
@@ -353,9 +356,10 @@ iteration, or a human-in-the-loop choice among design directions.
353
356
  5. After \`present-design-variants\`, wait for the user's pick before
354
357
  generating the next version. Keep the chosen screen, delete the other
355
358
  variant screens, call \`get-design-snapshot\` with \`fileId\` for the kept
356
- screen, then continue from that single file in a bounded pass. Prefer
357
- \`edit-design\`; if \`generate-design\` is needed, update only the kept file
358
- compactly and stop after the first successful save.
359
+ screen, then call \`edit-design\` on that same \`fileId\` in a bounded pass.
360
+ Use \`mode: "replace-file"\` when expanding the representative placeholder
361
+ into the full chosen direction. Do not call \`generate-design\` after a
362
+ variant pick. Stop after the first successful \`edit-design\` save.
359
363
 
360
364
  ## Design Quality Bar
361
365
 
@@ -601,6 +601,31 @@ export function resolveAssistantChatSubmitIntent({
601
601
  return requestedIntent ?? "immediate";
602
602
  }
603
603
 
604
+ export function resolveAssistantChatRunningState({
605
+ forceStopped,
606
+ isRuntimeRunning,
607
+ isReconnecting,
608
+ optimisticRunning,
609
+ isAutoResuming,
610
+ }: {
611
+ forceStopped: boolean;
612
+ isRuntimeRunning: boolean;
613
+ isReconnecting: boolean;
614
+ optimisticRunning: boolean;
615
+ isAutoResuming: boolean;
616
+ }): { isRunning: boolean; showRunningInUI: boolean } {
617
+ const isRunning =
618
+ !forceStopped && (isRuntimeRunning || isReconnecting || optimisticRunning);
619
+ return {
620
+ isRunning,
621
+ // During auto-continuation, assistant-ui can briefly mark the message done
622
+ // between chunks even though the adapter is about to POST the next run.
623
+ // Keep the visible chat state running so the latest assistant message shows
624
+ // Thinking/Resuming and does not expose footer actions prematurely.
625
+ showRunningInUI: !forceStopped && (isRunning || isAutoResuming),
626
+ };
627
+ }
628
+
604
629
  type QueuedMessage = {
605
630
  id: string;
606
631
  text: string;
@@ -1378,14 +1403,17 @@ const AssistantChatInner = forwardRef<
1378
1403
  // queueing forever" state where isReconnecting or isRuntimeRunning gets
1379
1404
  // wedged (e.g. after a tab refresh + stop during reconnect).
1380
1405
  const [forceStopped, setForceStopped] = useState(false);
1381
- // Real running state drives submission/queue gating. Treat reconnecting
1382
- // to an active run the same as running, UNLESS the user has explicitly
1383
- // clicked stop (forceStopped).
1384
- const isRunning =
1385
- !forceStopped && (isRuntimeRunning || isReconnecting || optimisticRunning);
1386
- const textStreaming = isRunning || externalStreaming;
1387
- // UI-only running state — drives the stop button and thinking indicator.
1388
- const showRunningInUI = isRunning;
1406
+ // Real running state drives submission/queue gating; UI running also covers
1407
+ // short auto-continuation gaps so the latest assistant message does not flash
1408
+ // into a done state while the agent is still working.
1409
+ const { isRunning, showRunningInUI } = resolveAssistantChatRunningState({
1410
+ forceStopped,
1411
+ isRuntimeRunning,
1412
+ isReconnecting,
1413
+ optimisticRunning,
1414
+ isAutoResuming,
1415
+ });
1416
+ const textStreaming = showRunningInUI || externalStreaming;
1389
1417
  // A revealed activity label wins; otherwise stay a steady "Thinking" by
1390
1418
  // default. We only surface "Reconnecting" while we are actively replaying
1391
1419
  // recovered content (reconnectContent populated). A bare reconnect with no
@@ -2265,6 +2293,8 @@ const AssistantChatInner = forwardRef<
2265
2293
 
2266
2294
  useEffect(() => {
2267
2295
  if (!authError) return;
2296
+ const shouldCaptureStuckAuthCard =
2297
+ authSessionAvailable || authError.sessionExpired;
2268
2298
  // Auto-recovery (`checkAuthSession`) runs immediately + at 250ms. If the
2269
2299
  // card is still showing 3 seconds later, recovery failed and the user
2270
2300
  // is about to hit "Refresh chat" — that's the "Reload UI required"
@@ -2273,6 +2303,7 @@ const AssistantChatInner = forwardRef<
2273
2303
  void (async () => {
2274
2304
  const hasSession = await checkAuthSession();
2275
2305
  if (hasSession) return;
2306
+ if (!shouldCaptureStuckAuthCard) return;
2276
2307
  captureError(new Error("agent-chat:auth_error_card_stuck"), {
2277
2308
  tags: {
2278
2309
  context: "agent-native-chat",
@@ -3211,7 +3242,7 @@ const AssistantChatInner = forwardRef<
3211
3242
  <CheckpointContext.Provider value={checkpointCtx}>
3212
3243
  <MessageActionsContext.Provider value={messageActionsCtx}>
3213
3244
  <ApprovalContext.Provider value={approvalCtx}>
3214
- <ChatRunningContext.Provider value={isRunning}>
3245
+ <ChatRunningContext.Provider value={showRunningInUI}>
3215
3246
  <TextStreamingContext.Provider value={textStreaming}>
3216
3247
  <div
3217
3248
  data-agent-empty-state={
@@ -807,6 +807,21 @@ function lastActivityTool(
807
807
  return undefined;
808
808
  }
809
809
 
810
+ function formatActivityTrail(
811
+ trail: readonly AgentActivityTrailEntry[],
812
+ ): string | undefined {
813
+ const items = trail
814
+ .slice(-8)
815
+ .map((entry) => {
816
+ const label = entry.label.replace(/\s+/g, " ").trim();
817
+ const tool = entry.tool?.replace(/\s+/g, " ").trim();
818
+ if (label && tool && label !== tool) return `${label} (${tool})`;
819
+ return label || tool || "";
820
+ })
821
+ .filter(Boolean);
822
+ return items.length > 0 ? items.join(" > ") : undefined;
823
+ }
824
+
810
825
  function lastUnresolvedToolActivity(
811
826
  content: ContentPart[],
812
827
  ): string | undefined {
@@ -928,9 +943,9 @@ function incrementalActionGuidance(tool: string): string | undefined {
928
943
  return "create a compact working v1 with `create-extension`, then use focused `update-extension` edits for refinements";
929
944
  case "generate-design":
930
945
  case "update-design":
931
- return "persist a minimal first version (fewer files) with `generate-design`, then refine individual files with `edit-design` search/replace instead of resending everything";
946
+ return 'if an existing design file or snapshot is already in history, especially after a Design variant pick, stop retrying `generate-design`: call `get-design-snapshot` for the selected file, then call `edit-design` once on that same `fileId` (`mode: "replace-file"` for a compact full-file replacement, or search/replace for smaller edits). Use `generate-design` only for a brand-new compact first file when no target file exists yet';
932
947
  case "present-design-variants":
933
- return "call `present-design-variants` with concise labels, descriptions, accent colors, and feature bullets; omit large `content` HTML when needed so the action can render compact representative screens, then refine the chosen direction with `generate-design` or `edit-design`";
948
+ return 'call `present-design-variants` with concise labels, descriptions, accent colors, and feature bullets; omit large `content` HTML when needed so the action can render compact representative screens. After the user picks a direction, delete unchosen screens, snapshot the selected `fileId`, and refine that same file with `edit-design` (`mode: "replace-file"` for placeholder expansion). Do not call `generate-design` after the variant pick';
934
949
  case "create-visual-plan":
935
950
  case "create-ui-plan":
936
951
  case "create-plan-design":
@@ -1439,6 +1454,7 @@ export function createAgentChatAdapter(
1439
1454
  let visibleContinuationPrefix: ContentPart[] = [];
1440
1455
  let lastAutoContinueReason: string | null = null;
1441
1456
  let lastRecoverableRunError: AgentAutoContinueErrorInfo | null = null;
1457
+ let lastActivityTrail: AgentActivityTrailEntry[] = [];
1442
1458
  const attemptedRunIds: string[] = [];
1443
1459
  let authRecoveryAttempted = false;
1444
1460
  let continuationToolCallCounter = 0;
@@ -1468,6 +1484,9 @@ export function createAgentChatAdapter(
1468
1484
  lastPreparingToolName
1469
1485
  ? `last_preparing_tool: ${lastPreparingToolName}`
1470
1486
  : "",
1487
+ formatActivityTrail(lastActivityTrail)
1488
+ ? `activity_trail: ${formatActivityTrail(lastActivityTrail)}`
1489
+ : "",
1471
1490
  `total_transient_continuations: ${totalTransientContinuationAttempts}`,
1472
1491
  attemptedRunIds.length > 0
1473
1492
  ? `attempted_runs: ${attemptedRunIds.join(", ")}`
@@ -1559,6 +1578,7 @@ export function createAgentChatAdapter(
1559
1578
  lastSeq,
1560
1579
  contentParts: content.length,
1561
1580
  attemptedRunIds: [...attemptedRunIds],
1581
+ activityTrail: [...lastActivityTrail],
1562
1582
  startupRecoveryAttempts,
1563
1583
  staleRunContinuationAttempts,
1564
1584
  stalledTransientContinuationAttempts,
@@ -1867,6 +1887,7 @@ export function createAgentChatAdapter(
1867
1887
  completedToolName?: string;
1868
1888
  } => {
1869
1889
  lastAutoContinueReason = signal.reason;
1890
+ lastActivityTrail = [...signal.activityTrail];
1870
1891
  if (signal.errorInfo) {
1871
1892
  lastRecoverableRunError = signal.errorInfo;
1872
1893
  }