@agent-native/core 0.92.2 → 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 (170) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +26 -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/client/AssistantChat.tsx +314 -96
  25. package/corpus/core/src/client/agent-chat-adapter.ts +17 -1
  26. package/corpus/core/src/client/chat/repo-helpers.ts +45 -0
  27. package/corpus/core/src/client/chat/tool-call-display.tsx +36 -27
  28. package/corpus/core/src/client/chat-model-groups.ts +1 -5
  29. package/corpus/core/src/client/composer/TiptapComposer.tsx +0 -2
  30. package/corpus/core/src/client/embed-auth.ts +20 -0
  31. package/corpus/core/src/client/settings/SettingsSection.tsx +66 -5
  32. package/corpus/core/src/client/sse-event-processor.ts +49 -17
  33. package/corpus/core/src/client/use-db-sync.ts +31 -9
  34. package/corpus/core/src/collab/agent-presence.ts +6 -1
  35. package/corpus/core/src/collab/awareness-store.ts +185 -0
  36. package/corpus/core/src/collab/awareness.ts +58 -2
  37. package/corpus/core/src/scripts/agent-engines/manage-agent-engine.ts +7 -8
  38. package/corpus/core/src/scripts/agent-engines/set-agent-engine.ts +7 -18
  39. package/corpus/core/src/server/core-routes-plugin.ts +21 -2
  40. package/corpus/core/src/server/embed-session.ts +20 -4
  41. package/corpus/core/src/styles/agent-native.css +51 -0
  42. package/corpus/core/src/tracking/providers.ts +33 -2
  43. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +8 -1
  44. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +9 -1
  45. package/corpus/templates/analytics/app/pages/monitoring/uptime/MonitorDetail.tsx +41 -11
  46. package/corpus/templates/analytics/app/pages/monitoring/uptime/types.ts +44 -0
  47. package/corpus/templates/analytics/changelog/2026-07-09-dashboard-action-menus-now-close-cleanly-before-opening-hist.md +6 -0
  48. package/corpus/templates/analytics/changelog/2026-07-09-dashboard-email-reports-still-send-when-the-screenshot-captu.md +6 -0
  49. package/corpus/templates/analytics/changelog/2026-07-09-uptime-alerts-now-suppress-recovery-noise-during-flapping-an.md +6 -0
  50. package/corpus/templates/analytics/changelog/2026-07-09-uptime-check-history-now-shows-timing-diagnostics-so-slow-or.md +6 -0
  51. package/corpus/templates/analytics/changelog/2026-07-09-uptime-recovery-alerts-now-send-when-the-original-outage-not.md +6 -0
  52. package/corpus/templates/analytics/docs/uptime-monitoring.md +29 -16
  53. package/corpus/templates/analytics/netlify.toml +3 -0
  54. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +126 -0
  55. package/corpus/templates/analytics/server/db/schema-monitoring.ts +5 -0
  56. package/corpus/templates/analytics/server/jobs/uptime-monitors.ts +5 -1
  57. package/corpus/templates/analytics/server/lib/uptime-monitors.ts +305 -31
  58. package/corpus/templates/analytics/server/plugins/db.ts +15 -0
  59. package/corpus/templates/analytics/server/plugins/uptime-monitor-jobs.ts +1 -1
  60. package/corpus/templates/analytics/server/routes/api/uptime-monitors/run.post.ts +57 -0
  61. package/corpus/templates/clips/actions/list-meetings.ts +12 -0
  62. package/corpus/templates/clips/changelog/2026-07-09-meeting-reminders-now-ignore-obvious-solo-personal-calendar-.md +6 -0
  63. package/corpus/templates/clips/desktop/src-tauri/src/meetings_watcher.rs +1 -0
  64. package/corpus/templates/clips/server/lib/calendar-event-classification.ts +79 -0
  65. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  66. package/dist/agent/engine/ai-sdk-engine.js +13 -4
  67. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  68. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  69. package/dist/agent/engine/anthropic-engine.js +14 -3
  70. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  71. package/dist/agent/engine/builder-engine.d.ts +1 -1
  72. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  73. package/dist/agent/engine/builder-engine.js +26 -4
  74. package/dist/agent/engine/builder-engine.js.map +1 -1
  75. package/dist/agent/engine/registry.d.ts.map +1 -1
  76. package/dist/agent/engine/registry.js +47 -2
  77. package/dist/agent/engine/registry.js.map +1 -1
  78. package/dist/agent/model-config.d.ts +2 -2
  79. package/dist/agent/model-config.d.ts.map +1 -1
  80. package/dist/agent/model-config.js +0 -2
  81. package/dist/agent/model-config.js.map +1 -1
  82. package/dist/agent/production-agent.d.ts.map +1 -1
  83. package/dist/agent/production-agent.js +12 -1
  84. package/dist/agent/production-agent.js.map +1 -1
  85. package/dist/agent/run-manager.d.ts.map +1 -1
  86. package/dist/agent/run-manager.js +52 -33
  87. package/dist/agent/run-manager.js.map +1 -1
  88. package/dist/agent/run-store.d.ts +24 -0
  89. package/dist/agent/run-store.d.ts.map +1 -1
  90. package/dist/agent/run-store.js +64 -2
  91. package/dist/agent/run-store.js.map +1 -1
  92. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  93. package/dist/agent/thread-data-builder.js +28 -2
  94. package/dist/agent/thread-data-builder.js.map +1 -1
  95. package/dist/client/AssistantChat.d.ts +6 -0
  96. package/dist/client/AssistantChat.d.ts.map +1 -1
  97. package/dist/client/AssistantChat.js +243 -36
  98. package/dist/client/AssistantChat.js.map +1 -1
  99. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  100. package/dist/client/agent-chat-adapter.js +15 -1
  101. package/dist/client/agent-chat-adapter.js.map +1 -1
  102. package/dist/client/chat/repo-helpers.d.ts.map +1 -1
  103. package/dist/client/chat/repo-helpers.js +43 -0
  104. package/dist/client/chat/repo-helpers.js.map +1 -1
  105. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  106. package/dist/client/chat/tool-call-display.js +4 -4
  107. package/dist/client/chat/tool-call-display.js.map +1 -1
  108. package/dist/client/chat-model-groups.d.ts.map +1 -1
  109. package/dist/client/chat-model-groups.js +1 -3
  110. package/dist/client/chat-model-groups.js.map +1 -1
  111. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  112. package/dist/client/composer/TiptapComposer.js +0 -2
  113. package/dist/client/composer/TiptapComposer.js.map +1 -1
  114. package/dist/client/embed-auth.d.ts.map +1 -1
  115. package/dist/client/embed-auth.js +14 -0
  116. package/dist/client/embed-auth.js.map +1 -1
  117. package/dist/client/settings/SettingsSection.d.ts.map +1 -1
  118. package/dist/client/settings/SettingsSection.js +32 -3
  119. package/dist/client/settings/SettingsSection.js.map +1 -1
  120. package/dist/client/sse-event-processor.d.ts.map +1 -1
  121. package/dist/client/sse-event-processor.js +44 -17
  122. package/dist/client/sse-event-processor.js.map +1 -1
  123. package/dist/client/use-db-sync.d.ts.map +1 -1
  124. package/dist/client/use-db-sync.js +29 -9
  125. package/dist/client/use-db-sync.js.map +1 -1
  126. package/dist/collab/agent-presence.d.ts.map +1 -1
  127. package/dist/collab/agent-presence.js +5 -2
  128. package/dist/collab/agent-presence.js.map +1 -1
  129. package/dist/collab/awareness-store.d.ts +1 -1
  130. package/dist/collab/awareness-store.d.ts.map +1 -1
  131. package/dist/collab/awareness-store.js +0 -0
  132. package/dist/collab/awareness-store.js.map +1 -1
  133. package/dist/collab/awareness.d.ts +2 -0
  134. package/dist/collab/awareness.d.ts.map +1 -1
  135. package/dist/collab/awareness.js +46 -2
  136. package/dist/collab/awareness.js.map +1 -1
  137. package/dist/collab/routes.d.ts +1 -1
  138. package/dist/notifications/routes.d.ts +1 -1
  139. package/dist/observability/routes.d.ts +5 -5
  140. package/dist/progress/routes.d.ts +1 -1
  141. package/dist/scripts/agent-engines/manage-agent-engine.d.ts.map +1 -1
  142. package/dist/scripts/agent-engines/manage-agent-engine.js +6 -6
  143. package/dist/scripts/agent-engines/manage-agent-engine.js.map +1 -1
  144. package/dist/scripts/agent-engines/set-agent-engine.d.ts.map +1 -1
  145. package/dist/scripts/agent-engines/set-agent-engine.js +5 -13
  146. package/dist/scripts/agent-engines/set-agent-engine.js.map +1 -1
  147. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  148. package/dist/server/core-routes-plugin.d.ts +5 -0
  149. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  150. package/dist/server/core-routes-plugin.js +10 -3
  151. package/dist/server/core-routes-plugin.js.map +1 -1
  152. package/dist/server/embed-session.d.ts.map +1 -1
  153. package/dist/server/embed-session.js +17 -4
  154. package/dist/server/embed-session.js.map +1 -1
  155. package/dist/styles/agent-native.css +51 -0
  156. package/dist/tracking/providers.d.ts.map +1 -1
  157. package/dist/tracking/providers.js +23 -5
  158. package/dist/tracking/providers.js.map +1 -1
  159. package/docs/content/locales/ar-SA/server.mdx +35 -0
  160. package/docs/content/locales/de-DE/server.mdx +35 -0
  161. package/docs/content/locales/es-ES/server.mdx +35 -0
  162. package/docs/content/locales/fr-FR/server.mdx +35 -0
  163. package/docs/content/locales/hi-IN/server.mdx +35 -0
  164. package/docs/content/locales/ja-JP/server.mdx +35 -0
  165. package/docs/content/locales/ko-KR/server.mdx +35 -0
  166. package/docs/content/locales/pt-BR/server.mdx +35 -0
  167. package/docs/content/locales/zh-CN/server.mdx +35 -0
  168. package/docs/content/locales/zh-TW/server.mdx +35 -0
  169. package/docs/content/server.mdx +35 -0
  170. package/package.json +1 -1
@@ -485,11 +485,14 @@ async function* parseJsonlStream(
485
485
  let pendingText = "";
486
486
  let pendingThinking: { text: string; signature?: string } | null = null;
487
487
 
488
- const flushPending = () => {
488
+ const flushPendingText = () => {
489
489
  if (pendingText) {
490
490
  parts.push({ type: "text", text: pendingText });
491
491
  pendingText = "";
492
492
  }
493
+ };
494
+
495
+ const flushPendingThinking = () => {
493
496
  if (pendingThinking) {
494
497
  parts.push({
495
498
  type: "thinking",
@@ -502,6 +505,11 @@ async function* parseJsonlStream(
502
505
  }
503
506
  };
504
507
 
508
+ const flushPending = () => {
509
+ flushPendingText();
510
+ flushPendingThinking();
511
+ };
512
+
505
513
  try {
506
514
  for await (const line of readJsonlLines(
507
515
  reader,
@@ -527,13 +535,16 @@ async function* parseJsonlStream(
527
535
  switch (event.type) {
528
536
  case "text-delta": {
529
537
  const text = event.text ?? "";
538
+ flushPendingThinking();
530
539
  pendingText += text;
531
540
  yield { type: "text-delta", text };
532
541
  break;
533
542
  }
534
543
 
535
- case "thinking-delta": {
544
+ case "thinking-delta":
545
+ case "reasoning-delta": {
536
546
  const text = event.text ?? "";
547
+ flushPendingText();
537
548
  if (!pendingThinking) pendingThinking = { text: "" };
538
549
  pendingThinking.text += text;
539
550
  if (event.signature) pendingThinking.signature = event.signature;
@@ -645,10 +656,18 @@ async function* parseJsonlStream(
645
656
  const isCredentialAuthError =
646
657
  Boolean(explicitErrMsg) &&
647
658
  isBuilderCredentialAuthError(String(errMsg));
659
+ // Anthropic's bare "Connection error." often arrives here with no
660
+ // gateway code. Tag it as a network error so in-run retries and
661
+ // run-level resume treat it as transient instead of terminal.
662
+ const isProviderConnectionError =
663
+ typeof explicitErrMsg === "string" &&
664
+ isProviderConnectionErrorMessage(String(explicitErrMsg));
648
665
  const errCode = isCredentialAuthError
649
666
  ? "builder_auth_error"
650
- : (gatewayErrCode ??
651
- (!explicitErrMsg ? "builder_gateway_error" : undefined));
667
+ : isProviderConnectionError
668
+ ? BUILDER_GATEWAY_NETWORK_ERROR_CODE
669
+ : (gatewayErrCode ??
670
+ (!explicitErrMsg ? "builder_gateway_error" : undefined));
652
671
  console.error(
653
672
  `[builder-engine] stop reason=error model=${model} code=${errCode ?? "(none)"} error=${errMsg}`,
654
673
  );
@@ -987,6 +1006,9 @@ function isBuilderGatewayNetworkError(err: unknown): boolean {
987
1006
  text.includes("econnaborted") ||
988
1007
  text.includes("fetch failed") ||
989
1008
  text.includes("network error") ||
1009
+ // Anthropic SDK's APIConnectionError default ("Connection error.") is
1010
+ // often forwarded by the Builder gateway as a stop event with no code.
1011
+ text.includes("connection error") ||
990
1012
  text.includes("connection reset") ||
991
1013
  text.includes("connection closed") ||
992
1014
  text.includes("stream closed") ||
@@ -994,6 +1016,10 @@ function isBuilderGatewayNetworkError(err: unknown): boolean {
994
1016
  );
995
1017
  }
996
1018
 
1019
+ function isProviderConnectionErrorMessage(message: string): boolean {
1020
+ return message.trim().toLowerCase() === "connection error.";
1021
+ }
1022
+
997
1023
  function captureBuilderGatewayTransportError(
998
1024
  err: unknown,
999
1025
  context: {
@@ -121,6 +121,64 @@ export function isAgentEnginePackageInstalled(
121
121
  return packageNames.every(canResolvePackage);
122
122
  }
123
123
 
124
+ interface ParsedVersionedModelId {
125
+ family: string;
126
+ version: number[];
127
+ suffix: string;
128
+ }
129
+
130
+ function parseVersionedModelId(model: string): ParsedVersionedModelId | null {
131
+ const match =
132
+ /^(?<family>.+?)[-.](?<version>\d+(?:[-.]\d+)*)(?<suffix>(?:[-.][a-z][a-z0-9]*)*)$/i.exec(
133
+ model.trim().toLowerCase(),
134
+ );
135
+ const groups = match?.groups;
136
+ if (!groups?.family || !groups.version) return null;
137
+
138
+ const version = groups.version.split(/[-.]/).map((part) => Number(part));
139
+ if (version.some((part) => !Number.isSafeInteger(part))) return null;
140
+
141
+ return {
142
+ family: groups.family,
143
+ version,
144
+ suffix: groups.suffix ?? "",
145
+ };
146
+ }
147
+
148
+ function compareModelVersions(left: number[], right: number[]): number {
149
+ const length = Math.max(left.length, right.length);
150
+ for (let index = 0; index < length; index += 1) {
151
+ const delta = (left[index] ?? 0) - (right[index] ?? 0);
152
+ if (delta !== 0) return delta;
153
+ }
154
+ return 0;
155
+ }
156
+
157
+ function findLatestSupportedVersionMatch(
158
+ candidate: string,
159
+ supportedModels: readonly string[],
160
+ ): string | undefined {
161
+ const parsedCandidate = parseVersionedModelId(candidate);
162
+ if (!parsedCandidate) return undefined;
163
+
164
+ let best: { model: string; version: number[] } | undefined;
165
+ for (const supportedModel of supportedModels) {
166
+ const parsedSupported = parseVersionedModelId(supportedModel);
167
+ if (!parsedSupported) continue;
168
+ if (parsedSupported.family !== parsedCandidate.family) continue;
169
+ if (parsedSupported.suffix !== parsedCandidate.suffix) continue;
170
+ if (
171
+ best &&
172
+ compareModelVersions(parsedSupported.version, best.version) <= 0
173
+ ) {
174
+ continue;
175
+ }
176
+ best = { model: supportedModel, version: parsedSupported.version };
177
+ }
178
+
179
+ return best?.model;
180
+ }
181
+
124
182
  export function normalizeModelForEngine(
125
183
  engine: Pick<AgentEngine, "name" | "defaultModel" | "supportedModels">,
126
184
  model: string | null | undefined,
@@ -128,13 +186,16 @@ export function normalizeModelForEngine(
128
186
  const candidate = typeof model === "string" ? model.trim() : "";
129
187
  if (!candidate) return engine.defaultModel;
130
188
 
131
- if (engine.name !== "builder") return candidate;
189
+ if (engine.supportedModels.length === 0) return candidate;
132
190
 
133
191
  if (candidate === "auto" || engine.supportedModels.includes(candidate)) {
134
192
  return candidate;
135
193
  }
136
194
 
137
- return engine.supportedModels.includes("auto") ? "auto" : engine.defaultModel;
195
+ return (
196
+ findLatestSupportedVersionMatch(candidate, engine.supportedModels) ??
197
+ engine.defaultModel
198
+ );
138
199
  }
139
200
 
140
201
  function assertAgentEnginePackageInstalled(entry: AgentEngineEntry): void {
@@ -267,8 +267,6 @@ export const AGENT_MODEL_CONFIG = {
267
267
  "gemini-3-1-pro",
268
268
  "gemini-3-5-flash",
269
269
  "gemini-3-1-flash-lite",
270
- "grok-code-fast",
271
- "qwen3-coder",
272
270
  ],
273
271
  },
274
272
  anthropic: {
@@ -1181,6 +1181,7 @@ export function isRetryableError(err: unknown): boolean {
1181
1181
  return (
1182
1182
  code === "builder_gateway_error" ||
1183
1183
  code === "builder_gateway_network_error" ||
1184
+ code === "provider_network_error" ||
1184
1185
  code === "http_429" ||
1185
1186
  code === "http_500" ||
1186
1187
  code === "http_502" ||
@@ -1206,6 +1207,11 @@ export function isRetryableError(err: unknown): boolean {
1206
1207
  msg.includes("gateway error") ||
1207
1208
  msg.includes("socket hang up") ||
1208
1209
  msg.includes("connection reset") ||
1210
+ // Anthropic SDK APIConnectionError default message is exactly
1211
+ // "Connection error." — Builder gateway often forwards it as a stop
1212
+ // event with no structured code. Without this match the run fails in
1213
+ // ~3s and the client recovery loop storms POSTs.
1214
+ msg.includes("connection error") ||
1209
1215
  msg.includes("too many requests") ||
1210
1216
  msg.includes("timeout") ||
1211
1217
  msg.includes("gateway timeout") ||
@@ -1853,7 +1859,8 @@ export function isResumableEngineError(err: unknown): boolean {
1853
1859
  err instanceof EngineError ? (err.errorCode ?? "").toLowerCase() : "";
1854
1860
  if (
1855
1861
  code === "builder_gateway_timeout" ||
1856
- code === "builder_gateway_network_error"
1862
+ code === "builder_gateway_network_error" ||
1863
+ code === "provider_network_error"
1857
1864
  ) {
1858
1865
  return true;
1859
1866
  }
@@ -1873,6 +1880,7 @@ export function isResumableEngineError(err: unknown): boolean {
1873
1880
  text.includes("econnaborted") ||
1874
1881
  text.includes("fetch failed") ||
1875
1882
  text.includes("network error") ||
1883
+ text.includes("connection error") ||
1876
1884
  text.includes("connection reset") ||
1877
1885
  text.includes("connection closed") ||
1878
1886
  text.includes("stream closed") ||
@@ -4455,6 +4463,8 @@ function isRecoverableContinuationError(event: {
4455
4463
  return (
4456
4464
  event.recoverable === true ||
4457
4465
  code === "builder_gateway_timeout" ||
4466
+ code === "builder_gateway_network_error" ||
4467
+ code === "provider_network_error" ||
4458
4468
  code === "stale_run" ||
4459
4469
  code === "timeout" ||
4460
4470
  code === "timeout_error" ||
@@ -4463,6 +4473,7 @@ function isRecoverableContinuationError(event: {
4463
4473
  code === "http_529" ||
4464
4474
  code === "run_timeout" ||
4465
4475
  message.includes("timeout") ||
4476
+ message.includes("connection error") ||
4466
4477
  message.includes("temporarily unavailable")
4467
4478
  );
4468
4479
  }
@@ -18,9 +18,10 @@ import {
18
18
  reapUnclaimedBackgroundRun,
19
19
  reconcileTerminalRunFromEvents,
20
20
  ensureTerminalRunEvent,
21
+ getLastTerminalRunEvent,
22
+ resolveErroredRunTerminalEvent,
21
23
  setRunError,
22
24
  setRunTerminalReason,
23
- STALE_RUN_ERROR_EVENT,
24
25
  } from "./run-store.js";
25
26
  import type { AgentChatEvent, RunEvent, RunStatus } from "./types.js";
26
27
 
@@ -681,28 +682,43 @@ export function startRun(
681
682
  // reap the row. Paired with RUN_STALE_MS (15s) — 10x the interval to
682
683
  // tolerate transient DB slowness without false positives.
683
684
  let consecutiveHeartbeatFailures = 0;
685
+ // Single-flight the heartbeat write. The timer fires every 1.5s but a write
686
+ // can take up to the DB op timeout (~8s) when the Neon pooler is saturated.
687
+ // Firing a fresh write each tick regardless piled up ~5 concurrent writes
688
+ // under contention, each holding a pooler connection — ADDING to the exact
689
+ // connection-cap exhaustion that starves the heartbeat and false-reaps the
690
+ // run as stale. Skip a tick's write while one is still outstanding so a run
691
+ // holds at most one heartbeat connection. The abort/backstop checks below
692
+ // still run every tick (they don't touch the DB on the hot path).
693
+ let heartbeatInFlight = false;
684
694
  const heartbeatTimer: ReturnType<typeof setInterval> = setInterval(() => {
685
- updateRunHeartbeat(runId)
686
- .then(() => {
687
- consecutiveHeartbeatFailures = 0;
688
- })
689
- .catch((error) => {
690
- consecutiveHeartbeatFailures += 1;
691
- // Swallow routine single-tick blips; escalate once failures approach
692
- // the stale window so false-positive stale_run from silent write
693
- // failures is diagnosable.
694
- if (consecutiveHeartbeatFailures >= 3) {
695
- captureError(error, {
696
- route: "/_agent-native/agent-chat",
697
- tags: {
698
- source: "agent-run-manager",
699
- phase: "heartbeat",
700
- consecutiveFailures: String(consecutiveHeartbeatFailures),
701
- },
702
- extra: { runId, threadId },
703
- });
704
- }
705
- });
695
+ if (!heartbeatInFlight) {
696
+ heartbeatInFlight = true;
697
+ updateRunHeartbeat(runId)
698
+ .then(() => {
699
+ consecutiveHeartbeatFailures = 0;
700
+ })
701
+ .catch((error) => {
702
+ consecutiveHeartbeatFailures += 1;
703
+ // Swallow routine single-tick blips; escalate once failures approach
704
+ // the stale window so false-positive stale_run from silent write
705
+ // failures is diagnosable.
706
+ if (consecutiveHeartbeatFailures >= 3) {
707
+ captureError(error, {
708
+ route: "/_agent-native/agent-chat",
709
+ tags: {
710
+ source: "agent-run-manager",
711
+ phase: "heartbeat",
712
+ consecutiveFailures: String(consecutiveHeartbeatFailures),
713
+ },
714
+ extra: { runId, threadId },
715
+ });
716
+ }
717
+ })
718
+ .finally(() => {
719
+ heartbeatInFlight = false;
720
+ });
721
+ }
706
722
  checkSqlAbort();
707
723
  checkNoProgressBackstop();
708
724
  }, 1500);
@@ -1289,24 +1305,29 @@ function subscribeFromSQL(
1289
1305
  return;
1290
1306
  }
1291
1307
  } else if (run?.status === "errored") {
1292
- // The run row was flipped to `errored` but no terminal event
1293
- // was ever persisted almost always means a reaper's silent
1294
- // `appendTerminalRunEvent(...).catch(() => {})` swallowed a
1295
- // transient DB error, so the user-facing situation is the
1296
- // same as a stale-run reap. Send the friendly event AND try
1297
- // to persist it so future reconnects replay it from SQL
1298
- // rather than regenerating it (the user used to see a bare
1299
- // "run_terminal_event_missing" debug string here).
1300
- await ensureTerminalRunEvent(
1301
- runId,
1302
- STALE_RUN_ERROR_EVENT,
1303
- ).catch(() => {});
1308
+ // The run row is terminal but this subscriber's cursor is
1309
+ // already past (or never saw) the terminal event. Prefer the
1310
+ // REAL last terminal event / row error_detail over inventing
1311
+ // a stale_run card slides prod showed Connection error.
1312
+ // rows being mislabeled as stale_run on reconnect because
1313
+ // this path always synthesized STALE_RUN_ERROR_EVENT.
1314
+ const existing = await getLastTerminalRunEvent(runId).catch(
1315
+ () => null,
1316
+ );
1317
+ const resolved = existing
1318
+ ? { event: existing.event, shouldPersist: false }
1319
+ : resolveErroredRunTerminalEvent(run);
1320
+ if (resolved.shouldPersist) {
1321
+ await ensureTerminalRunEvent(runId, resolved.event).catch(
1322
+ () => {},
1323
+ );
1324
+ }
1304
1325
  try {
1305
1326
  controller.enqueue(
1306
1327
  encoder.encode(
1307
1328
  `data: ${JSON.stringify({
1308
- ...STALE_RUN_ERROR_EVENT,
1309
- seq: lastSeq,
1329
+ ...resolved.event,
1330
+ seq: existing?.seq ?? lastSeq,
1310
1331
  })}\n\n`,
1311
1332
  ),
1312
1333
  );
@@ -994,8 +994,13 @@ export async function recordRunDiagnostic(
994
994
  export async function updateRunHeartbeat(runId: string): Promise<void> {
995
995
  await ensureRunTables();
996
996
  const client = getDbExec();
997
+ // Only bump liveness while the row is still running. Zombie producers that
998
+ // keep their setInterval after status flips to errored/completed used to
999
+ // rewrite heartbeat_at for minutes after the turn died (seen on slides
1000
+ // prod: heartbeat continued ~400s past completed_at), which confuses
1001
+ // triage and can keep /runs/active looking "fresh" after failure.
997
1002
  await client.execute({
998
- sql: `UPDATE agent_runs SET heartbeat_at = ? WHERE id = ?`,
1003
+ sql: `UPDATE agent_runs SET heartbeat_at = ? WHERE id = ? AND status = 'running'`,
999
1004
  args: [Date.now(), runId],
1000
1005
  });
1001
1006
  }
@@ -1259,11 +1264,13 @@ export async function getRunById(runId: string): Promise<{
1259
1264
  threadId: string;
1260
1265
  status: string;
1261
1266
  startedAt: number;
1267
+ errorCode: string | null;
1268
+ errorDetail: string | null;
1262
1269
  } | null> {
1263
1270
  await ensureRunTables();
1264
1271
  const client = getDbExec();
1265
1272
  const { rows } = await client.execute({
1266
- sql: `SELECT id, thread_id, status, started_at FROM agent_runs WHERE id = ?`,
1273
+ sql: `SELECT id, thread_id, status, started_at, error_code, error_detail FROM agent_runs WHERE id = ?`,
1267
1274
  args: [runId],
1268
1275
  });
1269
1276
  if (rows.length === 0) return null;
@@ -1272,15 +1279,87 @@ export async function getRunById(runId: string): Promise<{
1272
1279
  thread_id: string;
1273
1280
  status: string;
1274
1281
  started_at: number | string;
1282
+ error_code?: string | null;
1283
+ error_detail?: string | null;
1275
1284
  };
1276
1285
  return {
1277
1286
  id: r.id,
1278
1287
  threadId: r.thread_id,
1279
1288
  status: r.status,
1280
1289
  startedAt: Number(r.started_at),
1290
+ errorCode: r.error_code ?? null,
1291
+ errorDetail: r.error_detail ?? null,
1281
1292
  };
1282
1293
  }
1283
1294
 
1295
+ /**
1296
+ * Read the latest terminal event already persisted for a run, if any.
1297
+ * Used by SSE reconnect when the client cursor is already past that event
1298
+ * (so `getRunEventsSince` returns empty) but the row is terminal — we must
1299
+ * replay the REAL error instead of inventing a stale_run card.
1300
+ */
1301
+ export async function getLastTerminalRunEvent(
1302
+ runId: string,
1303
+ ): Promise<{ seq: number; event: Record<string, unknown> } | null> {
1304
+ await ensureRunTables();
1305
+ const client = getDbExec();
1306
+ const { rows } = await client.execute({
1307
+ sql: `SELECT seq, event_data FROM agent_run_events WHERE run_id = ? ORDER BY seq DESC LIMIT 1`,
1308
+ args: [runId],
1309
+ });
1310
+ const last = rows[0] as
1311
+ | { seq?: number | string; event_data?: string }
1312
+ | undefined;
1313
+ if (!last?.event_data) return null;
1314
+ try {
1315
+ const parsed = JSON.parse(last.event_data) as Record<string, unknown>;
1316
+ if (
1317
+ parsed?.type === "done" ||
1318
+ parsed?.type === "error" ||
1319
+ parsed?.type === "missing_api_key" ||
1320
+ parsed?.type === "loop_limit" ||
1321
+ parsed?.type === "auto_continue"
1322
+ ) {
1323
+ return { seq: Number(last.seq ?? 0), event: parsed };
1324
+ }
1325
+ } catch {
1326
+ return null;
1327
+ }
1328
+ return null;
1329
+ }
1330
+
1331
+ /**
1332
+ * Build the terminal error payload to stream when an `errored` run has no
1333
+ * in-cursor terminal event. Prefer the real last terminal event, then the
1334
+ * row's error_code/error_detail, and only then the generic stale_run card.
1335
+ */
1336
+ export function resolveErroredRunTerminalEvent(run: {
1337
+ errorCode?: string | null;
1338
+ errorDetail?: string | null;
1339
+ }): {
1340
+ event: Record<string, unknown>;
1341
+ shouldPersist: boolean;
1342
+ } {
1343
+ const code = typeof run.errorCode === "string" ? run.errorCode.trim() : "";
1344
+ const detail =
1345
+ typeof run.errorDetail === "string" ? run.errorDetail.trim() : "";
1346
+ if (code === STALE_RUN_ERROR_EVENT.errorCode) {
1347
+ return { event: { ...STALE_RUN_ERROR_EVENT }, shouldPersist: true };
1348
+ }
1349
+ if (detail || (code && code !== "unknown")) {
1350
+ return {
1351
+ event: {
1352
+ type: "error",
1353
+ error: detail || "The agent run failed.",
1354
+ ...(code && code !== "unknown" ? { errorCode: code } : {}),
1355
+ recoverable: true,
1356
+ },
1357
+ shouldPersist: true,
1358
+ };
1359
+ }
1360
+ return { event: { ...STALE_RUN_ERROR_EVENT }, shouldPersist: true };
1361
+ }
1362
+
1284
1363
  export async function getRunByThread(
1285
1364
  threadId: string,
1286
1365
  options?: { includeTerminal?: boolean },
@@ -3,6 +3,7 @@ import {
3
3
  normalizeCodeAgentTranscript,
4
4
  type CodeAgentTranscriptEvent as CoreCodeAgentTranscriptEvent,
5
5
  type NormalizedCodeAgentStatusEvent,
6
+ type NormalizedCodeAgentThinkingEvent,
6
7
  type NormalizedCodeAgentToolEvent,
7
8
  type NormalizedCodeAgentTranscriptItem,
8
9
  } from "../code-agents/transcript-normalizer.js";
@@ -124,6 +125,15 @@ export function buildAssistantMessage(
124
125
  }
125
126
  };
126
127
 
128
+ const appendReasoning = (text: string) => {
129
+ const last = content[content.length - 1];
130
+ if (last && last.type === "reasoning") {
131
+ last.text = (last.text ?? "") + text;
132
+ } else {
133
+ content.push({ type: "reasoning", text });
134
+ }
135
+ };
136
+
127
137
  for (const { event } of events) {
128
138
  if (event.type === "clear") {
129
139
  clearAssistantDraftContent(content);
@@ -135,6 +145,11 @@ export function buildAssistantMessage(
135
145
  continue;
136
146
  }
137
147
 
148
+ if (event.type === "thinking") {
149
+ appendReasoning(event.text ?? "");
150
+ continue;
151
+ }
152
+
138
153
  if (event.type === "tool_start") {
139
154
  toolCallCounter += 1;
140
155
  const toolCallId = runId
@@ -255,12 +270,18 @@ function clearAssistantDraftContent(content: ContentPart[]): void {
255
270
  for (let index = content.length - 1; index >= 0; index--) {
256
271
  const part = content[index];
257
272
  if (!part) continue;
258
- if (part.type === "text") {
273
+ if (part.type === "text" || part.type === "reasoning") {
259
274
  content.splice(index, 1);
260
275
  continue;
261
276
  }
262
277
  if (part.type === "tool-call" && part.result === undefined) {
263
- content.splice(index, 1);
278
+ // Keep materialized in-flight tool cards across retry clears so persisted
279
+ // thread rebuilds match the live SSE processor and avoid hide→show flicker.
280
+ const isEphemeral =
281
+ (part as { activity?: boolean }).activity === true ||
282
+ part.argsText === "" ||
283
+ Object.keys(part.args ?? {}).length === 0;
284
+ if (isEphemeral) content.splice(index, 1);
264
285
  }
265
286
  }
266
287
  }
@@ -1062,6 +1083,9 @@ function contentPartForCodeAgentTranscriptItem(
1062
1083
  if (item.type === "tool") {
1063
1084
  return toolContentPartForCodeAgentTranscriptItem(item);
1064
1085
  }
1086
+ if (item.type === "thinking") {
1087
+ return thinkingContentPartForCodeAgentTranscriptItem(item);
1088
+ }
1065
1089
  if (item.type === "status") {
1066
1090
  const text = statusTextForCodeAgentTranscriptItem(item, options);
1067
1091
  return text ? { type: "text", text } : null;
@@ -1069,6 +1093,13 @@ function contentPartForCodeAgentTranscriptItem(
1069
1093
  return null;
1070
1094
  }
1071
1095
 
1096
+ function thinkingContentPartForCodeAgentTranscriptItem(
1097
+ item: NormalizedCodeAgentThinkingEvent,
1098
+ ): ContentPart | null {
1099
+ const text = item.text.trim();
1100
+ return text ? { type: "reasoning", text } : null;
1101
+ }
1102
+
1072
1103
  function toolContentPartForCodeAgentTranscriptItem(
1073
1104
  item: NormalizedCodeAgentToolEvent,
1074
1105
  ): ContentPart {