@agent-native/core 0.92.1 → 0.92.3

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 (181) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +32 -0
  3. package/corpus/core/docs/content/locales/ar-SA/server.mdx +35 -0
  4. package/corpus/core/docs/content/locales/de-DE/server.mdx +35 -0
  5. package/corpus/core/docs/content/locales/es-ES/server.mdx +35 -0
  6. package/corpus/core/docs/content/locales/fr-FR/server.mdx +35 -0
  7. package/corpus/core/docs/content/locales/hi-IN/server.mdx +35 -0
  8. package/corpus/core/docs/content/locales/ja-JP/server.mdx +35 -0
  9. package/corpus/core/docs/content/locales/ko-KR/server.mdx +35 -0
  10. package/corpus/core/docs/content/locales/pt-BR/server.mdx +35 -0
  11. package/corpus/core/docs/content/locales/zh-CN/server.mdx +35 -0
  12. package/corpus/core/docs/content/locales/zh-TW/server.mdx +35 -0
  13. package/corpus/core/docs/content/server.mdx +35 -0
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +14 -4
  16. package/corpus/core/src/agent/engine/anthropic-engine.ts +15 -3
  17. package/corpus/core/src/agent/engine/builder-engine.ts +30 -4
  18. package/corpus/core/src/agent/engine/registry.ts +63 -2
  19. package/corpus/core/src/agent/model-config.ts +0 -2
  20. package/corpus/core/src/agent/production-agent.ts +12 -1
  21. package/corpus/core/src/agent/run-manager.ts +57 -36
  22. package/corpus/core/src/agent/run-store.ts +81 -2
  23. package/corpus/core/src/agent/thread-data-builder.ts +33 -2
  24. package/corpus/core/src/cli/sync-builder-starter-manifest.ts +29 -1
  25. package/corpus/core/src/client/AssistantChat.tsx +314 -96
  26. package/corpus/core/src/client/agent-chat-adapter.ts +17 -1
  27. package/corpus/core/src/client/chat/repo-helpers.ts +45 -0
  28. package/corpus/core/src/client/chat/tool-call-display.tsx +36 -27
  29. package/corpus/core/src/client/chat-model-groups.ts +1 -5
  30. package/corpus/core/src/client/composer/TiptapComposer.tsx +0 -2
  31. package/corpus/core/src/client/embed-auth.ts +20 -0
  32. package/corpus/core/src/client/settings/SettingsSection.tsx +66 -5
  33. package/corpus/core/src/client/sse-event-processor.ts +49 -17
  34. package/corpus/core/src/client/use-db-sync.ts +31 -9
  35. package/corpus/core/src/collab/agent-presence.ts +6 -1
  36. package/corpus/core/src/collab/awareness-store.ts +185 -0
  37. package/corpus/core/src/collab/awareness.ts +58 -2
  38. package/corpus/core/src/notifications/channels.ts +1 -22
  39. package/corpus/core/src/scripts/agent-engines/manage-agent-engine.ts +7 -8
  40. package/corpus/core/src/scripts/agent-engines/set-agent-engine.ts +7 -18
  41. package/corpus/core/src/server/core-routes-plugin.ts +21 -2
  42. package/corpus/core/src/server/embed-session.ts +20 -4
  43. package/corpus/core/src/styles/agent-native.css +51 -0
  44. package/corpus/core/src/tracking/providers.ts +33 -2
  45. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +8 -1
  46. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +9 -1
  47. package/corpus/templates/analytics/app/pages/monitoring/uptime/MonitorDetail.tsx +41 -11
  48. package/corpus/templates/analytics/app/pages/monitoring/uptime/types.ts +44 -0
  49. package/corpus/templates/analytics/changelog/2026-07-08-alert-emails-now-show-the-message-without-raw-metadata-json.md +6 -0
  50. package/corpus/templates/analytics/changelog/2026-07-08-uptime-checks-no-longer-count-ssrf-setup-time-against-the-re.md +6 -0
  51. package/corpus/templates/analytics/changelog/2026-07-08-uptime-checks-reuse-one-keep-alive-connection-so-recorded-la.md +6 -0
  52. package/corpus/templates/analytics/changelog/2026-07-08-uptime-monitors-now-confirm-transient-timeout-failures-befor.md +6 -0
  53. package/corpus/templates/analytics/changelog/2026-07-09-dashboard-action-menus-now-close-cleanly-before-opening-hist.md +6 -0
  54. package/corpus/templates/analytics/changelog/2026-07-09-dashboard-email-reports-still-send-when-the-screenshot-captu.md +6 -0
  55. package/corpus/templates/analytics/changelog/2026-07-09-uptime-alerts-now-suppress-recovery-noise-during-flapping-an.md +6 -0
  56. package/corpus/templates/analytics/changelog/2026-07-09-uptime-check-history-now-shows-timing-diagnostics-so-slow-or.md +6 -0
  57. package/corpus/templates/analytics/changelog/2026-07-09-uptime-recovery-alerts-now-send-when-the-original-outage-not.md +6 -0
  58. package/corpus/templates/analytics/docs/uptime-monitoring.md +47 -22
  59. package/corpus/templates/analytics/netlify.toml +3 -0
  60. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +126 -0
  61. package/corpus/templates/analytics/server/db/schema-monitoring.ts +5 -0
  62. package/corpus/templates/analytics/server/jobs/uptime-monitors.ts +5 -1
  63. package/corpus/templates/analytics/server/lib/uptime-monitors.ts +538 -54
  64. package/corpus/templates/analytics/server/plugins/db.ts +15 -0
  65. package/corpus/templates/analytics/server/plugins/uptime-monitor-jobs.ts +1 -1
  66. package/corpus/templates/analytics/server/routes/api/uptime-monitors/run.post.ts +57 -0
  67. package/corpus/templates/clips/actions/list-meetings.ts +12 -0
  68. package/corpus/templates/clips/changelog/2026-07-09-meeting-reminders-now-ignore-obvious-solo-personal-calendar-.md +6 -0
  69. package/corpus/templates/clips/desktop/src-tauri/src/meetings_watcher.rs +1 -0
  70. package/corpus/templates/clips/server/lib/calendar-event-classification.ts +79 -0
  71. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  72. package/dist/agent/engine/ai-sdk-engine.js +13 -4
  73. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  74. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  75. package/dist/agent/engine/anthropic-engine.js +14 -3
  76. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  77. package/dist/agent/engine/builder-engine.d.ts +1 -1
  78. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  79. package/dist/agent/engine/builder-engine.js +26 -4
  80. package/dist/agent/engine/builder-engine.js.map +1 -1
  81. package/dist/agent/engine/registry.d.ts.map +1 -1
  82. package/dist/agent/engine/registry.js +47 -2
  83. package/dist/agent/engine/registry.js.map +1 -1
  84. package/dist/agent/model-config.d.ts +2 -2
  85. package/dist/agent/model-config.d.ts.map +1 -1
  86. package/dist/agent/model-config.js +0 -2
  87. package/dist/agent/model-config.js.map +1 -1
  88. package/dist/agent/production-agent.d.ts.map +1 -1
  89. package/dist/agent/production-agent.js +12 -1
  90. package/dist/agent/production-agent.js.map +1 -1
  91. package/dist/agent/run-manager.d.ts.map +1 -1
  92. package/dist/agent/run-manager.js +52 -33
  93. package/dist/agent/run-manager.js.map +1 -1
  94. package/dist/agent/run-store.d.ts +24 -0
  95. package/dist/agent/run-store.d.ts.map +1 -1
  96. package/dist/agent/run-store.js +64 -2
  97. package/dist/agent/run-store.js.map +1 -1
  98. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  99. package/dist/agent/thread-data-builder.js +28 -2
  100. package/dist/agent/thread-data-builder.js.map +1 -1
  101. package/dist/cli/sync-builder-starter-manifest.d.ts.map +1 -1
  102. package/dist/cli/sync-builder-starter-manifest.js +24 -1
  103. package/dist/cli/sync-builder-starter-manifest.js.map +1 -1
  104. package/dist/client/AssistantChat.d.ts +6 -0
  105. package/dist/client/AssistantChat.d.ts.map +1 -1
  106. package/dist/client/AssistantChat.js +243 -36
  107. package/dist/client/AssistantChat.js.map +1 -1
  108. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  109. package/dist/client/agent-chat-adapter.js +15 -1
  110. package/dist/client/agent-chat-adapter.js.map +1 -1
  111. package/dist/client/chat/repo-helpers.d.ts.map +1 -1
  112. package/dist/client/chat/repo-helpers.js +43 -0
  113. package/dist/client/chat/repo-helpers.js.map +1 -1
  114. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  115. package/dist/client/chat/tool-call-display.js +4 -4
  116. package/dist/client/chat/tool-call-display.js.map +1 -1
  117. package/dist/client/chat-model-groups.d.ts.map +1 -1
  118. package/dist/client/chat-model-groups.js +1 -3
  119. package/dist/client/chat-model-groups.js.map +1 -1
  120. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  121. package/dist/client/composer/TiptapComposer.js +0 -2
  122. package/dist/client/composer/TiptapComposer.js.map +1 -1
  123. package/dist/client/embed-auth.d.ts.map +1 -1
  124. package/dist/client/embed-auth.js +14 -0
  125. package/dist/client/embed-auth.js.map +1 -1
  126. package/dist/client/settings/SettingsSection.d.ts.map +1 -1
  127. package/dist/client/settings/SettingsSection.js +32 -3
  128. package/dist/client/settings/SettingsSection.js.map +1 -1
  129. package/dist/client/sse-event-processor.d.ts.map +1 -1
  130. package/dist/client/sse-event-processor.js +44 -17
  131. package/dist/client/sse-event-processor.js.map +1 -1
  132. package/dist/client/use-db-sync.d.ts.map +1 -1
  133. package/dist/client/use-db-sync.js +29 -9
  134. package/dist/client/use-db-sync.js.map +1 -1
  135. package/dist/collab/agent-presence.d.ts.map +1 -1
  136. package/dist/collab/agent-presence.js +5 -2
  137. package/dist/collab/agent-presence.js.map +1 -1
  138. package/dist/collab/awareness-store.d.ts +1 -1
  139. package/dist/collab/awareness-store.d.ts.map +1 -1
  140. package/dist/collab/awareness-store.js +0 -0
  141. package/dist/collab/awareness-store.js.map +1 -1
  142. package/dist/collab/awareness.d.ts +2 -0
  143. package/dist/collab/awareness.d.ts.map +1 -1
  144. package/dist/collab/awareness.js +46 -2
  145. package/dist/collab/awareness.js.map +1 -1
  146. package/dist/collab/routes.d.ts +1 -1
  147. package/dist/notifications/channels.js +1 -17
  148. package/dist/notifications/channels.js.map +1 -1
  149. package/dist/notifications/routes.d.ts +1 -1
  150. package/dist/observability/routes.d.ts +5 -5
  151. package/dist/progress/routes.d.ts +1 -1
  152. package/dist/resources/handlers.d.ts +1 -1
  153. package/dist/scripts/agent-engines/manage-agent-engine.d.ts.map +1 -1
  154. package/dist/scripts/agent-engines/manage-agent-engine.js +6 -6
  155. package/dist/scripts/agent-engines/manage-agent-engine.js.map +1 -1
  156. package/dist/scripts/agent-engines/set-agent-engine.d.ts.map +1 -1
  157. package/dist/scripts/agent-engines/set-agent-engine.js +5 -13
  158. package/dist/scripts/agent-engines/set-agent-engine.js.map +1 -1
  159. package/dist/server/core-routes-plugin.d.ts +5 -0
  160. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  161. package/dist/server/core-routes-plugin.js +10 -3
  162. package/dist/server/core-routes-plugin.js.map +1 -1
  163. package/dist/server/embed-session.d.ts.map +1 -1
  164. package/dist/server/embed-session.js +17 -4
  165. package/dist/server/embed-session.js.map +1 -1
  166. package/dist/styles/agent-native.css +51 -0
  167. package/dist/tracking/providers.d.ts.map +1 -1
  168. package/dist/tracking/providers.js +23 -5
  169. package/dist/tracking/providers.js.map +1 -1
  170. package/docs/content/locales/ar-SA/server.mdx +35 -0
  171. package/docs/content/locales/de-DE/server.mdx +35 -0
  172. package/docs/content/locales/es-ES/server.mdx +35 -0
  173. package/docs/content/locales/fr-FR/server.mdx +35 -0
  174. package/docs/content/locales/hi-IN/server.mdx +35 -0
  175. package/docs/content/locales/ja-JP/server.mdx +35 -0
  176. package/docs/content/locales/ko-KR/server.mdx +35 -0
  177. package/docs/content/locales/pt-BR/server.mdx +35 -0
  178. package/docs/content/locales/zh-CN/server.mdx +35 -0
  179. package/docs/content/locales/zh-TW/server.mdx +35 -0
  180. package/docs/content/server.mdx +35 -0
  181. package/package.json +1 -1
@@ -135,6 +135,36 @@ function cloneContentParts(content) {
135
135
  ...(part.chatUI ? { chatUI: { ...part.chatUI } } : {}),
136
136
  });
137
137
  }
138
+ export function settleInterruptedAssistantToolCallsInRepo(repo) {
139
+ if (!Array.isArray(repo.messages))
140
+ return { repo, changed: false };
141
+ let changed = false;
142
+ const nextMessages = repo.messages.map((entry) => {
143
+ const wrapper = entry && typeof entry === "object" && "message" in entry
144
+ ? entry
145
+ : null;
146
+ const message = (wrapper?.message ?? entry);
147
+ if (message?.role !== "assistant" || !Array.isArray(message.content)) {
148
+ return entry;
149
+ }
150
+ const content = cloneContentParts(message.content);
151
+ if (!settleInterruptedToolCalls(content, undefined, { includeActivity: true })) {
152
+ return entry;
153
+ }
154
+ changed = true;
155
+ const nextMessage = {
156
+ ...message,
157
+ content,
158
+ status: { type: "incomplete", reason: "error" },
159
+ };
160
+ return wrapper
161
+ ? { ...entry, message: nextMessage }
162
+ : nextMessage;
163
+ });
164
+ return changed
165
+ ? { repo: { ...repo, messages: nextMessages }, changed }
166
+ : { repo, changed };
167
+ }
138
168
  function clearPendingSelection() {
139
169
  fetch(agentNativePath(`/_agent-native/application-state/${PENDING_SELECTION_KEY}`), {
140
170
  method: "DELETE",
@@ -259,6 +289,26 @@ function toolCallPartHasResult(part) {
259
289
  const candidate = part;
260
290
  return candidate.type === "tool-call" && "result" in candidate;
261
291
  }
292
+ /**
293
+ * Monotonic progress rank for a tool-call part. Higher means the UI should
294
+ * prefer this copy over a lower-ranked duplicate of the same logical call.
295
+ * Used so reconnect overlays that are ahead of lagging thread messages stay
296
+ * visible instead of flickering back to an older pending spinner.
297
+ */
298
+ function toolCallProgressRank(part) {
299
+ if (!part || typeof part !== "object")
300
+ return 0;
301
+ const candidate = part;
302
+ if (candidate.type !== "tool-call")
303
+ return 0;
304
+ if ("result" in candidate)
305
+ return 4;
306
+ if (candidate.activity === true)
307
+ return 1;
308
+ return typeof candidate.argsText === "string" && candidate.argsText.length > 0
309
+ ? 2
310
+ : 1;
311
+ }
262
312
  /**
263
313
  * Identity fingerprint for a tool-call part that survives across readers: two
264
314
  * readers of the same run assign unrelated synthetic toolCallIds until both
@@ -294,10 +344,10 @@ function collectRenderedToolCallStates(messages) {
294
344
  const id = toolCallIdFromContentPart(part);
295
345
  if (!id)
296
346
  continue;
297
- const hasResult = toolCallPartHasResult(part);
347
+ const rank = toolCallProgressRank(part);
298
348
  const existing = byId.get(id);
299
349
  byId.set(id, {
300
- hasResult: Boolean(existing?.hasResult || hasResult),
350
+ rank: Math.max(existing?.rank ?? 0, rank),
301
351
  });
302
352
  }
303
353
  }
@@ -310,10 +360,10 @@ function collectRenderedToolCallStates(messages) {
310
360
  const fingerprint = toolCallFingerprintFromContentPart(part);
311
361
  if (!fingerprint)
312
362
  continue;
313
- const hasResult = toolCallPartHasResult(part);
363
+ const rank = toolCallProgressRank(part);
314
364
  const existing = latestAssistantByFingerprint.get(fingerprint);
315
365
  latestAssistantByFingerprint.set(fingerprint, {
316
- hasResult: Boolean(existing?.hasResult || hasResult),
366
+ rank: Math.max(existing?.rank ?? 0, rank),
317
367
  });
318
368
  }
319
369
  }
@@ -402,24 +452,33 @@ export function dedupeReconnectContentAgainstMessages(content, messages) {
402
452
  }
403
453
  const filtered = byId.size > 0 || latestAssistantByFingerprint.size > 0
404
454
  ? snapshotDeduped.filter((part) => {
455
+ const reconnectRank = toolCallProgressRank(part);
405
456
  const id = toolCallIdFromContentPart(part);
406
457
  const existing = id ? byId.get(id) : undefined;
407
458
  if (existing) {
408
- changed = true;
409
- return false;
459
+ // Keep reconnect copies that are strictly ahead of the rendered
460
+ // message (e.g. completed overlay vs lagging pending thread data).
461
+ // Same-or-behind ranks are duplicates and should stay hidden.
462
+ if (reconnectRank <= existing.rank) {
463
+ changed = true;
464
+ return false;
465
+ }
466
+ return true;
410
467
  }
411
468
  // Fingerprint fallback for the id-convergence window: two readers of
412
469
  // the same active run can assign unrelated ids to the same logical tool
413
470
  // call before the server id converges. Suppress the reconnect overlay
414
- // whenever it matches the latest rendered assistant tool call and either
415
- // side is still pending; completed-vs-completed remains visible so a
416
- // legitimately repeated identical call is not hidden.
471
+ // only when the rendered message is at least as far along; completed
472
+ // reconnect copies stay visible over pending message copies so the UI
473
+ // does not pop back to an older spinner.
417
474
  const fingerprint = toolCallFingerprintFromContentPart(part);
418
475
  const latestByFingerprint = fingerprint
419
476
  ? latestAssistantByFingerprint.get(fingerprint)
420
477
  : undefined;
478
+ const isCompletedRepeat = reconnectRank >= 4 && latestByFingerprint?.rank === 4;
421
479
  if (latestByFingerprint &&
422
- (!toolCallPartHasResult(part) || !latestByFingerprint.hasResult)) {
480
+ !isCompletedRepeat &&
481
+ reconnectRank <= latestByFingerprint.rank) {
423
482
  changed = true;
424
483
  return false;
425
484
  }
@@ -834,6 +893,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
834
893
  const queueDirtyRef = useRef(false);
835
894
  const queueMutationVersionRef = useRef(0);
836
895
  const dequeueInFlightRef = useRef(false);
896
+ const queueStopVersionRef = useRef(0);
837
897
  const [composerContextItems, setComposerContextItems] = useState([]);
838
898
  const composerContextItemsRef = useRef([]);
839
899
  const isActiveComposerRef = useRef(isActiveComposer);
@@ -975,6 +1035,9 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
975
1035
  const [reconnectContent, setReconnectContent] = useState([]);
976
1036
  // When stop is clicked during reconnect, keep content visible (don't wipe it)
977
1037
  const [reconnectFrozen, setReconnectFrozen] = useState(false);
1038
+ // Adapter took over while reconnect still had visible tool cards — keep the
1039
+ // overlay until the adapter message catches up so we don't flash an empty gap.
1040
+ const [adapterHandoffPending, setAdapterHandoffPending] = useState(false);
978
1041
  const reconnectRunIdRef = useRef(null);
979
1042
  const reconnectTailOnlyRef = useRef(false);
980
1043
  const reconnectCanMaterializeRef = useRef(false);
@@ -1026,9 +1089,14 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1026
1089
  isReconnecting,
1027
1090
  hasReconnectContent: reconnectContent.length > 0,
1028
1091
  });
1029
- const reconnectActivityContent = useMemo(() => isReconnecting || reconnectFrozen
1092
+ const reconnectActivityContent = useMemo(() => isReconnecting || reconnectFrozen || adapterHandoffPending
1030
1093
  ? reconnectActivityFallbackContent(runningActivityTool)
1031
- : [], [isReconnecting, reconnectFrozen, runningActivityTool]);
1094
+ : [], [
1095
+ adapterHandoffPending,
1096
+ isReconnecting,
1097
+ reconnectFrozen,
1098
+ runningActivityTool,
1099
+ ]);
1032
1100
  const lastBroadcastRunningRef = useRef(isRunning);
1033
1101
  const tiptapRef = useRef(null);
1034
1102
  // Stable ref to the "stop active run" action so addToQueue can abort
@@ -1113,14 +1181,21 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1113
1181
  let settledRepo = repo;
1114
1182
  if (repo?.messages?.length > 0) {
1115
1183
  let shouldImport = true;
1116
- try {
1117
- shouldImport = shouldImportServerThreadData(normalizeThreadRepository(threadRuntime.export()), repo);
1118
- }
1119
- catch {
1120
- // If the runtime/export comparison itself fails, do not fail open by
1121
- // importing a server snapshot that may be stale relative to local UI.
1184
+ // Adapter owns the live turn (including auto-continuation gaps). Do not
1185
+ // let a lagging server thread snapshot clobber in-flight tool cards.
1186
+ if (isRuntimeRunningRef.current || isAutoResumingRef.current) {
1122
1187
  shouldImport = false;
1123
1188
  }
1189
+ else {
1190
+ try {
1191
+ shouldImport = shouldImportServerThreadData(normalizeThreadRepository(threadRuntime.export()), repo);
1192
+ }
1193
+ catch {
1194
+ // If the runtime/export comparison itself fails, do not fail open by
1195
+ // importing a server snapshot that may be stale relative to local UI.
1196
+ shouldImport = false;
1197
+ }
1198
+ }
1124
1199
  if (shouldImport) {
1125
1200
  if (options?.markTitleGenerated) {
1126
1201
  titleGeneratedRef.current = true;
@@ -1264,6 +1339,25 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1264
1339
  if (isRuntimeRunningRef.current || isAutoResumingRef.current) {
1265
1340
  return false;
1266
1341
  }
1342
+ // SUPERSEDE THE PREVIOUS RECONNECT GENERATION. A turn that keeps failing
1343
+ // (e.g. repeated stale_run at "Contacting model") produces a new runId
1344
+ // every few seconds; each call here used to OVERWRITE the single-slot
1345
+ // refs below without tearing down the prior closure, leaving its
1346
+ // watchdog (1s) + idleCheck (1s) + thread poll (2s) + SSE retry loop all
1347
+ // running. 4-5 stacked generations = a ~20 req/s request storm that
1348
+ // hammers the same Neon pool the server needs for heartbeats, deepening
1349
+ // the DB saturation that causes the failures in the first place. Abort
1350
+ // the prior generation's AbortController so its stream loop exits and its
1351
+ // `finally` clears every interval before we start a fresh one.
1352
+ if (reconnectAbortRef.current) {
1353
+ try {
1354
+ reconnectAbortRef.current.abort();
1355
+ }
1356
+ catch {
1357
+ // Already aborted / detached — nothing to unwind.
1358
+ }
1359
+ reconnectAbortRef.current = null;
1360
+ }
1267
1361
  reconnectRunIdRef.current = runId;
1268
1362
  const afterSeq = resolveReconnectAfterSeq(threadId, runId);
1269
1363
  reconnectTailOnlyRef.current = afterSeq > 0;
@@ -1278,6 +1372,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1278
1372
  });
1279
1373
  setIsReconnecting(true);
1280
1374
  setReconnectFrozen(false);
1375
+ setAdapterHandoffPending(false);
1281
1376
  setReconnectContent([]);
1282
1377
  window.dispatchEvent(new CustomEvent("agentNative.chatRunning", {
1283
1378
  detail: { isRunning: true, tabId: tabId || threadId },
@@ -1349,6 +1444,19 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1349
1444
  let noProgressDuringReconnect = false;
1350
1445
  let latestContent = [];
1351
1446
  const preparingActionState = {};
1447
+ // Exponential backoff for the reattach retry loop. A run that is
1448
+ // "active" server-side but producing no stream (the exact stuck state
1449
+ // that triggers a reconnect) would otherwise re-fetch /runs/:id/events
1450
+ // every ~250ms — several req/s per generation. Back off 250ms → 5s so a
1451
+ // genuinely stuck run stops hammering the server; reset the moment real
1452
+ // progress streams in (see the onSeq callback) so a recovering run
1453
+ // re-tightens immediately.
1454
+ let reconnectRetryCount = 0;
1455
+ const backoffRetryDelay = () => {
1456
+ const ms = Math.min(250 * 2 ** reconnectRetryCount, 5000);
1457
+ reconnectRetryCount += 1;
1458
+ return new Promise((resolve) => window.setTimeout(resolve, ms));
1459
+ };
1352
1460
  const sameRunStillActive = async () => {
1353
1461
  try {
1354
1462
  const res = await fetch(`${apiUrl}/runs/active?threadId=${encodeURIComponent(threadId)}`, { signal: abortCtrl.signal });
@@ -1370,6 +1478,11 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1370
1478
  ? window.setInterval(() => {
1371
1479
  if (reconnectRunIdRef.current !== runId)
1372
1480
  return;
1481
+ // Adapter took over mid-poll — skip imports that would race the
1482
+ // live stream and flicker tool cards back to older snapshots.
1483
+ if (isRuntimeRunningRef.current || isAutoResumingRef.current) {
1484
+ return;
1485
+ }
1373
1486
  void refreshThreadFromServer();
1374
1487
  }, 2000)
1375
1488
  : undefined;
@@ -1385,7 +1498,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1385
1498
  if (!sseRes.ok || !sseRes.body) {
1386
1499
  const activeState = await sameRunStillActive();
1387
1500
  if (activeState !== "inactive") {
1388
- await new Promise((resolve) => window.setTimeout(resolve, 250));
1501
+ await backoffRetryDelay();
1389
1502
  continue;
1390
1503
  }
1391
1504
  break;
@@ -1405,6 +1518,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1405
1518
  try {
1406
1519
  await readSSEStreamRaw(sseRes.body, content, toolCallCounter, tabId, scheduleUpdate, (seq) => {
1407
1520
  markReconnectProgress();
1521
+ reconnectRetryCount = 0;
1408
1522
  updateActiveRunSeq(seq);
1409
1523
  }, { preparingActionState });
1410
1524
  if (reconnectAfterSeq === 0) {
@@ -1420,7 +1534,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1420
1534
  }
1421
1535
  const activeState = await sameRunStillActive();
1422
1536
  if (activeState !== "inactive") {
1423
- await new Promise((resolve) => window.setTimeout(resolve, 250));
1537
+ await backoffRetryDelay();
1424
1538
  continue;
1425
1539
  }
1426
1540
  }
@@ -1768,7 +1882,8 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1768
1882
  if (!loadHistoryRepository ||
1769
1883
  !hasRestoredRef.current ||
1770
1884
  isRestoring ||
1771
- isRunning) {
1885
+ isRunning ||
1886
+ isAutoResuming) {
1772
1887
  return;
1773
1888
  }
1774
1889
  let cancelled = false;
@@ -1785,6 +1900,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1785
1900
  }, [
1786
1901
  historyReloadKey,
1787
1902
  importThreadData,
1903
+ isAutoResuming,
1788
1904
  isRestoring,
1789
1905
  isRunning,
1790
1906
  loadHistoryRepository,
@@ -2187,6 +2303,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2187
2303
  const next = queuedMessages[0];
2188
2304
  if (!next)
2189
2305
  return;
2306
+ const stopVersion = queueStopVersionRef.current;
2190
2307
  dequeueInFlightRef.current = true;
2191
2308
  let cancelled = false;
2192
2309
  let started = false;
@@ -2203,7 +2320,8 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2203
2320
  await waitForThreadRunToClear(apiUrl, threadId);
2204
2321
  if (cancelled)
2205
2322
  return;
2206
- if (queuedMessagesRef.current[0]?.id !== next.id) {
2323
+ if (queueStopVersionRef.current !== stopVersion ||
2324
+ queuedMessagesRef.current[0]?.id !== next.id) {
2207
2325
  return;
2208
2326
  }
2209
2327
  // Keep the placeholder visible while waiting. Remove it only when the
@@ -2227,6 +2345,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2227
2345
  }
2228
2346
  catch (err) {
2229
2347
  if (removedForAppend &&
2348
+ queueStopVersionRef.current === stopVersion &&
2230
2349
  !queuedMessagesRef.current.some((message) => message.id === next.id)) {
2231
2350
  applyLocalQueuedMessages((prev) => [next, ...prev]);
2232
2351
  }
@@ -2280,30 +2399,66 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2280
2399
  prevIsRuntimeRunningRef.current = isRuntimeRunning;
2281
2400
  if (isRuntimeRunning && !wasRunning) {
2282
2401
  // SINGLE-READER OWNERSHIP: the adapter runtime just took over (a new run
2283
- // started or an adopted run resumed). Any live reconnect reader is now a
2284
- // duplicate accumulator abort it and DISCARD its content; the adapter's
2285
- // stream renders the canonical message. Null the run ref first so the
2286
- // reader's own cleanup paths (which all check reconnectRunIdRef) no-op.
2402
+ // started or an adopted run resumed). Abort the reconnect reader, but keep
2403
+ // its visible content until the adapter message has tool/text parts so the
2404
+ // UI does not flash an empty gap between readers.
2287
2405
  if (reconnectRunIdRef.current !== null) {
2406
+ const keepOverlay = reconnectContent.length > 0;
2288
2407
  reconnectRunIdRef.current = null;
2289
2408
  reconnectAbortRef.current?.abort();
2290
2409
  reconnectAbortRef.current = null;
2291
2410
  setIsReconnecting(false);
2292
- setReconnectContent([]);
2293
2411
  setReconnectFrozen(false);
2294
2412
  reconnectCanMaterializeRef.current = false;
2295
2413
  reconnectTailOnlyRef.current = false;
2414
+ if (keepOverlay) {
2415
+ setAdapterHandoffPending(true);
2416
+ }
2417
+ else {
2418
+ setReconnectContent([]);
2419
+ setAdapterHandoffPending(false);
2420
+ }
2296
2421
  }
2297
2422
  else if (reconnectFrozen) {
2298
2423
  setReconnectFrozen(false);
2299
2424
  setReconnectContent([]);
2425
+ setAdapterHandoffPending(false);
2300
2426
  reconnectCanMaterializeRef.current = false;
2301
2427
  }
2302
2428
  if (forceStopped) {
2303
2429
  setForceStopped(false);
2304
2430
  }
2305
2431
  }
2306
- }, [isRuntimeRunning, reconnectFrozen, forceStopped]);
2432
+ }, [
2433
+ isRuntimeRunning,
2434
+ reconnectFrozen,
2435
+ forceStopped,
2436
+ reconnectContent.length,
2437
+ ]);
2438
+ // Release the deferred reconnect overlay once thread messages have caught
2439
+ // up enough that dedupe would hide the overlay, or after a short timeout so
2440
+ // a stuck handoff cannot leave duplicate tool cards forever.
2441
+ useEffect(() => {
2442
+ if (!adapterHandoffPending)
2443
+ return;
2444
+ if (!isRuntimeRunning) {
2445
+ setReconnectContent([]);
2446
+ setAdapterHandoffPending(false);
2447
+ return;
2448
+ }
2449
+ const stillNeeded = dedupeReconnectContentAgainstMessages(reconnectContent, messages).length >
2450
+ 0;
2451
+ if (!stillNeeded) {
2452
+ setReconnectContent([]);
2453
+ setAdapterHandoffPending(false);
2454
+ return;
2455
+ }
2456
+ const timer = window.setTimeout(() => {
2457
+ setReconnectContent([]);
2458
+ setAdapterHandoffPending(false);
2459
+ }, 2500);
2460
+ return () => window.clearTimeout(timer);
2461
+ }, [adapterHandoffPending, isRuntimeRunning, messages, reconnectContent]);
2307
2462
  // Same transition guard for isReconnecting: only clear forceStopped on
2308
2463
  // the false→true edge (a new reconnect starting on page load).
2309
2464
  const prevIsReconnectingRef = useRef(isReconnecting);
@@ -2320,6 +2475,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2320
2475
  if (!reconnectCanMaterializeRef.current) {
2321
2476
  setReconnectFrozen(false);
2322
2477
  setReconnectContent([]);
2478
+ setAdapterHandoffPending(false);
2323
2479
  return;
2324
2480
  }
2325
2481
  try {
@@ -2359,6 +2515,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2359
2515
  threadRuntime.import(ensureMessageMetadata(repo));
2360
2516
  setReconnectFrozen(false);
2361
2517
  setReconnectContent([]);
2518
+ setAdapterHandoffPending(false);
2362
2519
  reconnectCanMaterializeRef.current = false;
2363
2520
  }
2364
2521
  catch (err) {
@@ -2382,13 +2539,47 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2382
2539
  threadId,
2383
2540
  threadRuntime,
2384
2541
  ]);
2542
+ const settleVisibleInterruptedTools = useCallback(() => {
2543
+ try {
2544
+ const repo = normalizeThreadRepository(threadRuntime.export());
2545
+ const settled = settleInterruptedAssistantToolCallsInRepo(repo);
2546
+ if (settled.changed) {
2547
+ threadRuntime.import(ensureMessageMetadata(settled.repo));
2548
+ }
2549
+ }
2550
+ catch (err) {
2551
+ captureError(err, {
2552
+ tags: {
2553
+ source: "agent-chat-client",
2554
+ phase: "settle-stopped-tool-calls",
2555
+ },
2556
+ extra: {
2557
+ threadId: threadId ?? null,
2558
+ tabId: tabId ?? null,
2559
+ },
2560
+ });
2561
+ }
2562
+ }, [tabId, threadId, threadRuntime]);
2385
2563
  // Abort the active server run (identical to what the Stop button does) so
2386
2564
  // an immediate-while-running send can proceed cleanly without a 409 race.
2387
2565
  // Captured in a stable ref so addToQueue can call it without listing
2388
2566
  // all the stop-related state in its own dep array.
2389
- const stopActiveRun = useCallback(() => {
2567
+ const stopActiveRun = useCallback((options) => {
2390
2568
  setForceStopped(true);
2391
2569
  setOptimisticRunning(false);
2570
+ setHasActiveServerRun(false);
2571
+ setPendingReconnectRecovery(null);
2572
+ setIsAutoResuming(false);
2573
+ if (autoResumeTimerRef.current !== null) {
2574
+ window.clearTimeout(autoResumeTimerRef.current);
2575
+ autoResumeTimerRef.current = null;
2576
+ }
2577
+ resetRunningActivity();
2578
+ if (!options?.preserveQueuedMessages) {
2579
+ queueStopVersionRef.current += 1;
2580
+ dequeueInFlightRef.current = false;
2581
+ applyLocalQueuedMessages(() => []);
2582
+ }
2392
2583
  const activeRun = getActiveRun();
2393
2584
  const runIdToAbort = reconnectRunIdRef.current ?? activeRun?.runId;
2394
2585
  userStoppedRunRef.current = {
@@ -2409,10 +2600,16 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2409
2600
  reconnectAbortRef.current = null;
2410
2601
  reconnectRunIdRef.current = null;
2411
2602
  setIsReconnecting(false);
2603
+ setAdapterHandoffPending(false);
2412
2604
  const shouldFreezeReconnectContent = !reconnectTailOnlyRef.current &&
2413
2605
  reconnectCanMaterializeRef.current &&
2414
2606
  reconnectContent.length > 0;
2415
2607
  if (shouldFreezeReconnectContent) {
2608
+ const frozenContent = cloneContentParts(reconnectContent);
2609
+ settleInterruptedToolCalls(frozenContent, undefined, {
2610
+ includeActivity: true,
2611
+ });
2612
+ setReconnectContent(frozenContent);
2416
2613
  setReconnectFrozen(true);
2417
2614
  }
2418
2615
  else {
@@ -2423,6 +2620,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2423
2620
  reconnectTailOnlyRef.current = false;
2424
2621
  }
2425
2622
  threadRuntime.cancelRun();
2623
+ settleVisibleInterruptedTools();
2426
2624
  if (typeof window !== "undefined") {
2427
2625
  window.dispatchEvent(new CustomEvent("agentNative.chatRunning", {
2428
2626
  detail: { isRunning: false, tabId: tabId || threadId },
@@ -2430,8 +2628,11 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2430
2628
  }
2431
2629
  }, [
2432
2630
  apiUrl,
2631
+ applyLocalQueuedMessages,
2433
2632
  isReconnecting,
2434
- reconnectContent.length,
2633
+ resetRunningActivity,
2634
+ reconnectContent,
2635
+ settleVisibleInterruptedTools,
2435
2636
  tabId,
2436
2637
  threadId,
2437
2638
  threadRuntime,
@@ -2439,7 +2640,8 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2439
2640
  // Keep the ref current so addToQueue can call it without a stale closure.
2440
2641
  stopActiveRunRef.current = stopActiveRun;
2441
2642
  const addToQueue = useCallback(async (text, images, references, attachments, requestMode, intent = "queued", recoveryAction, includeComposerContext = false, trackInRunsTray = false, preserveReconnectAutoRecoveryBudget = false, hideUserMessage = false) => {
2442
- if (!(await ensureAgentEngineReadyForSubmit())) {
2643
+ if (agentEngineConfigured.state === "missing") {
2644
+ void ensureAgentEngineReadyForSubmit();
2443
2645
  return;
2444
2646
  }
2445
2647
  if (!preserveReconnectAutoRecoveryBudget) {
@@ -2578,7 +2780,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2578
2780
  hideUserMessage,
2579
2781
  },
2580
2782
  ]);
2581
- stopActiveRunRef.current();
2783
+ stopActiveRunRef.current({ preserveQueuedMessages: true });
2582
2784
  }
2583
2785
  else if (isRunning && intent === "queued") {
2584
2786
  applyLocalQueuedMessages((prev) => [
@@ -2620,6 +2822,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2620
2822
  }
2621
2823
  }, [
2622
2824
  applyLocalQueuedMessages,
2825
+ agentEngineConfigured.state,
2623
2826
  buildComposerContextSubmission,
2624
2827
  ensureAgentEngineReadyForSubmit,
2625
2828
  execMode,
@@ -2883,8 +3086,12 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2883
3086
  setDismissedRunErrorKey(visibleRunErrorKey);
2884
3087
  }
2885
3088
  setRunErrorInfo(null);
2886
- } }) })), (isReconnecting || reconnectFrozen) &&
2887
- visibleReconnectContent.length > 0 && (_jsx(MessageScrollerItem, { children: _jsx(ReconnectStreamMessage, { content: visibleReconnectContent }) })), (isReconnecting || reconnectFrozen) &&
3089
+ } }) })), (isReconnecting ||
3090
+ reconnectFrozen ||
3091
+ adapterHandoffPending) &&
3092
+ visibleReconnectContent.length > 0 && (_jsx(MessageScrollerItem, { children: _jsx(ReconnectStreamMessage, { content: visibleReconnectContent }) })), (isReconnecting ||
3093
+ reconnectFrozen ||
3094
+ adapterHandoffPending) &&
2888
3095
  visibleReconnectContent.length === 0 &&
2889
3096
  reconnectContent.length === 0 &&
2890
3097
  reconnectActivityContent.length > 0 && (_jsx(MessageScrollerItem, { children: _jsx(ReconnectStreamMessage, { content: reconnectActivityContent }) })), showRunningInUI && (_jsx(MessageScrollerItem, { children: _jsx(RunningActivityStatus, { label: runningStatusLabel }) })), queuedMessages
@@ -2920,9 +3127,9 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2920
3127
  isRunning,
2921
3128
  requestedIntent: options?.intent,
2922
3129
  }), undefined, true)
2923
- : undefined, onSlashCommand: onSlashCommand, onBeforeSubmit: ensureAgentEngineReadyForSubmit, execMode: execMode, onExecModeChange: onExecModeChange, planModeDisabled: planModeDisabled, planModeDisabledReason: planModeDisabledReason, selectedModel: selectedModel ?? defaultModel, selectedEffort: selectedEffort, availableModels: availableModels, onModelChange: onModelChange, onEffortChange: onEffortChange, imageModelMenu: imageModelMenu, onConnectProvider: onConnectProvider, toolbarSlot: composerToolbarSlot, contextItems: composerContextItems, onRemoveContextItem: removeComposerContextItem, plusMenuMode: plusMenuMode, layoutVariant: composerLayoutVariant, providerConnectStatusEnabled: providerStatusChecksEnabled, draftScope: threadId || tabId, interceptBuildRequestsForBuilder: true, onAttachmentError: setComposerError, extraActionButton: contextXRayEnabled ||
3130
+ : undefined, onSlashCommand: onSlashCommand, execMode: execMode, onExecModeChange: onExecModeChange, planModeDisabled: planModeDisabled, planModeDisabledReason: planModeDisabledReason, selectedModel: selectedModel ?? defaultModel, selectedEffort: selectedEffort, availableModels: availableModels, onModelChange: onModelChange, onEffortChange: onEffortChange, imageModelMenu: imageModelMenu, onConnectProvider: onConnectProvider, toolbarSlot: composerToolbarSlot, contextItems: composerContextItems, onRemoveContextItem: removeComposerContextItem, plusMenuMode: plusMenuMode, layoutVariant: composerLayoutVariant, providerConnectStatusEnabled: providerStatusChecksEnabled, draftScope: threadId || tabId, interceptBuildRequestsForBuilder: true, onAttachmentError: setComposerError, extraActionButton: contextXRayEnabled ||
2924
3131
  composerExtraActionButton ||
2925
- showRunningInUI ? (_jsxs(_Fragment, { children: [contextXRayEnabled && (_jsx(ContextMeter, { threadId: threadId })), composerExtraActionButton, showRunningInUI && (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", onClick: stopActiveRun, className: "shrink-0 flex h-7 w-7 items-center justify-center rounded-md bg-muted text-foreground hover:bg-muted/80", children: _jsx(IconPlayerStop, { className: "h-3.5 w-3.5" }) }) }), _jsx(TooltipContent, { children: "Stop generating" })] }))] })) : undefined })] }), missingApiKey &&
3132
+ showRunningInUI ? (_jsxs(_Fragment, { children: [contextXRayEnabled && (_jsx(ContextMeter, { threadId: threadId })), composerExtraActionButton, showRunningInUI && (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", onClick: () => stopActiveRun(), className: "shrink-0 flex h-7 w-7 items-center justify-center rounded-md bg-muted text-foreground hover:bg-muted/80", children: _jsx(IconPlayerStop, { className: "h-3.5 w-3.5" }) }) }), _jsx(TooltipContent, { children: "Stop generating" })] }))] })) : undefined })] }), missingApiKey &&
2926
3133
  !authError &&
2927
3134
  !missingApiKeySetupAboveComposer ? (_jsx("div", { className: "agent-composer-setup-card", "data-agent-composer-setup-position": "below", children: _jsx(BuilderSetupCard, { onConnected: handleBuilderConnected, bouncePulse: missingKeyBouncePulse, layout: missingApiKeySetupLayout }) })) : null] })] }) }) }) }) }) }));
2928
3135
  });