@agent-native/core 0.84.66 → 0.85.0

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 (155) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +20 -0
  3. package/corpus/core/docs/content/locales/ar-SA/tracking.mdx +55 -0
  4. package/corpus/core/docs/content/locales/de-DE/tracking.mdx +55 -0
  5. package/corpus/core/docs/content/locales/es-ES/tracking.mdx +55 -0
  6. package/corpus/core/docs/content/locales/fr-FR/tracking.mdx +55 -0
  7. package/corpus/core/docs/content/locales/hi-IN/tracking.mdx +55 -0
  8. package/corpus/core/docs/content/locales/ja-JP/tracking.mdx +55 -0
  9. package/corpus/core/docs/content/locales/ko-KR/tracking.mdx +55 -0
  10. package/corpus/core/docs/content/locales/pt-BR/tracking.mdx +55 -0
  11. package/corpus/core/docs/content/locales/zh-CN/tracking.mdx +55 -0
  12. package/corpus/core/docs/content/locales/zh-TW/tracking.mdx +55 -0
  13. package/corpus/core/docs/content/tracking.mdx +2 -0
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/agent/production-agent.ts +199 -38
  16. package/corpus/core/src/agent/run-manager.ts +79 -0
  17. package/corpus/core/src/agent/run-store.ts +117 -5
  18. package/corpus/core/src/agent/types.ts +13 -0
  19. package/corpus/core/src/client/RunStuckBanner.tsx +14 -0
  20. package/corpus/core/src/client/agent-chat-adapter.ts +475 -0
  21. package/corpus/core/src/client/analytics.ts +2 -0
  22. package/corpus/core/src/client/error-format.ts +4 -0
  23. package/corpus/core/src/client/extensions/ExtensionsSidebarSection.tsx +1 -1
  24. package/corpus/core/src/client/session-replay.ts +774 -2
  25. package/corpus/core/src/client/sse-event-processor.ts +65 -9
  26. package/corpus/core/src/client/use-run-stuck-detection.ts +31 -7
  27. package/corpus/core/src/db/runtime-diagnostics.ts +331 -0
  28. package/corpus/core/src/server/agent-chat-plugin.ts +115 -2
  29. package/corpus/core/src/server/analytics.ts +1 -34
  30. package/corpus/core/src/server/core-routes-plugin.ts +91 -4
  31. package/corpus/core/src/server/http-response-telemetry.ts +189 -0
  32. package/corpus/core/src/server/self-dispatch.ts +29 -0
  33. package/corpus/core/src/server/ssr-handler.ts +18 -377
  34. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +53 -1
  35. package/corpus/templates/analytics/AGENTS.md +13 -0
  36. package/corpus/templates/analytics/actions/view-screen.ts +25 -0
  37. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +20 -0
  38. package/corpus/templates/analytics/app/global.css +55 -0
  39. package/corpus/templates/analytics/app/i18n/zh-TW.ts +100 -1
  40. package/corpus/templates/analytics/app/i18n-data.ts +1226 -0
  41. package/corpus/templates/analytics/app/pages/Settings.tsx +3 -0
  42. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +182 -144
  43. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +11 -18
  44. package/corpus/templates/analytics/app/pages/analyses/AnalysisDetail.tsx +11 -18
  45. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +49 -1
  46. package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +557 -0
  47. package/corpus/templates/analytics/app/pages/sessions/session-replay-devtools.ts +274 -0
  48. package/corpus/templates/analytics/app/pages/settings/AlertRulesSettingsCard.tsx +974 -0
  49. package/corpus/templates/analytics/changelog/2026-07-02-alert-rules-can-now-be-viewed-and-managed-from-settings.md +6 -0
  50. package/corpus/templates/analytics/changelog/2026-07-02-recent-ask-chats-show-a-loading-placeholder-while-your-chat-.md +6 -0
  51. package/corpus/templates/analytics/changelog/2026-07-02-session-replays-now-capture-console-logs-and-network-request.md +6 -0
  52. package/corpus/templates/analytics/changelog/2026-07-02-sharing-status-labels-use-neutral-icons-instead-of-colored-d.md +6 -0
  53. package/corpus/templates/analytics/changelog/2026-07-02-the-session-replay-viewer-has-a-dev-tools-panel-with-console.md +6 -0
  54. package/corpus/templates/analytics/server/db/schema.ts +3 -0
  55. package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +4 -0
  56. package/corpus/templates/analytics/server/lib/analytics-alerts.ts +142 -1
  57. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +369 -8
  58. package/corpus/templates/analytics/server/lib/session-replay.ts +76 -2
  59. package/corpus/templates/analytics/server/routes/api/session-replay/agent-diagnostics.json.get.ts +101 -0
  60. package/corpus/templates/analytics/shared/session-replay-diagnostics.ts +78 -0
  61. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +0 -2
  62. package/corpus/templates/design/app/pages/DesignEditor.tsx +24 -12
  63. package/corpus/templates/design/changelog/2026-07-02-the-temporary-code-tab-is-hidden-from-the-design-editor-side.md +6 -0
  64. package/corpus/templates/forms/server/lib/public-form-ssr.ts +0 -1
  65. package/corpus/templates/plan/actions/create-visual-recap.ts +1 -1
  66. package/corpus/templates/plan/actions/update-local-plan-folder.ts +3 -1
  67. package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
  68. package/corpus/templates/plan/changelog/2026-07-02-trying-to-edit-a-recap-or-plan-you-can-only-view-now-explain.md +6 -0
  69. package/corpus/templates/plan/server/plans.ts +26 -9
  70. package/dist/agent/production-agent.d.ts.map +1 -1
  71. package/dist/agent/production-agent.js +155 -34
  72. package/dist/agent/production-agent.js.map +1 -1
  73. package/dist/agent/run-manager.d.ts +29 -0
  74. package/dist/agent/run-manager.d.ts.map +1 -1
  75. package/dist/agent/run-manager.js +73 -0
  76. package/dist/agent/run-manager.js.map +1 -1
  77. package/dist/agent/run-store.d.ts +41 -0
  78. package/dist/agent/run-store.d.ts.map +1 -1
  79. package/dist/agent/run-store.js +100 -4
  80. package/dist/agent/run-store.js.map +1 -1
  81. package/dist/agent/types.d.ts +13 -0
  82. package/dist/agent/types.d.ts.map +1 -1
  83. package/dist/agent/types.js.map +1 -1
  84. package/dist/client/RunStuckBanner.d.ts.map +1 -1
  85. package/dist/client/RunStuckBanner.js +13 -0
  86. package/dist/client/RunStuckBanner.js.map +1 -1
  87. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  88. package/dist/client/agent-chat-adapter.js +410 -0
  89. package/dist/client/agent-chat-adapter.js.map +1 -1
  90. package/dist/client/analytics.d.ts +1 -1
  91. package/dist/client/analytics.d.ts.map +1 -1
  92. package/dist/client/analytics.js.map +1 -1
  93. package/dist/client/error-format.d.ts.map +1 -1
  94. package/dist/client/error-format.js +2 -0
  95. package/dist/client/error-format.js.map +1 -1
  96. package/dist/client/extensions/ExtensionsSidebarSection.js +1 -1
  97. package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
  98. package/dist/client/session-replay.d.ts +36 -0
  99. package/dist/client/session-replay.d.ts.map +1 -1
  100. package/dist/client/session-replay.js +579 -1
  101. package/dist/client/session-replay.js.map +1 -1
  102. package/dist/client/sse-event-processor.d.ts +20 -1
  103. package/dist/client/sse-event-processor.d.ts.map +1 -1
  104. package/dist/client/sse-event-processor.js +42 -11
  105. package/dist/client/sse-event-processor.js.map +1 -1
  106. package/dist/client/use-run-stuck-detection.d.ts +14 -4
  107. package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
  108. package/dist/client/use-run-stuck-detection.js +18 -4
  109. package/dist/client/use-run-stuck-detection.js.map +1 -1
  110. package/dist/collab/awareness.d.ts +2 -2
  111. package/dist/collab/awareness.d.ts.map +1 -1
  112. package/dist/collab/routes.d.ts +1 -1
  113. package/dist/db/runtime-diagnostics.d.ts +52 -0
  114. package/dist/db/runtime-diagnostics.d.ts.map +1 -0
  115. package/dist/db/runtime-diagnostics.js +250 -0
  116. package/dist/db/runtime-diagnostics.js.map +1 -0
  117. package/dist/observability/routes.d.ts +5 -5
  118. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  119. package/dist/resources/handlers.d.ts +2 -2
  120. package/dist/secrets/routes.d.ts +3 -3
  121. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  122. package/dist/server/agent-chat-plugin.js +101 -2
  123. package/dist/server/agent-chat-plugin.js.map +1 -1
  124. package/dist/server/analytics.d.ts +1 -19
  125. package/dist/server/analytics.d.ts.map +1 -1
  126. package/dist/server/analytics.js +1 -31
  127. package/dist/server/analytics.js.map +1 -1
  128. package/dist/server/core-routes-plugin.d.ts +17 -0
  129. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  130. package/dist/server/core-routes-plugin.js +65 -5
  131. package/dist/server/core-routes-plugin.js.map +1 -1
  132. package/dist/server/http-response-telemetry.d.ts +4 -0
  133. package/dist/server/http-response-telemetry.d.ts.map +1 -0
  134. package/dist/server/http-response-telemetry.js +174 -0
  135. package/dist/server/http-response-telemetry.js.map +1 -0
  136. package/dist/server/self-dispatch.d.ts +17 -0
  137. package/dist/server/self-dispatch.d.ts.map +1 -1
  138. package/dist/server/self-dispatch.js +11 -0
  139. package/dist/server/self-dispatch.js.map +1 -1
  140. package/dist/server/ssr-handler.d.ts.map +1 -1
  141. package/dist/server/ssr-handler.js +18 -271
  142. package/dist/server/ssr-handler.js.map +1 -1
  143. package/dist/server/transcribe-voice.d.ts +1 -1
  144. package/docs/content/locales/ar-SA/tracking.mdx +55 -0
  145. package/docs/content/locales/de-DE/tracking.mdx +55 -0
  146. package/docs/content/locales/es-ES/tracking.mdx +55 -0
  147. package/docs/content/locales/fr-FR/tracking.mdx +55 -0
  148. package/docs/content/locales/hi-IN/tracking.mdx +55 -0
  149. package/docs/content/locales/ja-JP/tracking.mdx +55 -0
  150. package/docs/content/locales/ko-KR/tracking.mdx +55 -0
  151. package/docs/content/locales/pt-BR/tracking.mdx +55 -0
  152. package/docs/content/locales/zh-CN/tracking.mdx +55 -0
  153. package/docs/content/locales/zh-TW/tracking.mdx +55 -0
  154. package/docs/content/tracking.mdx +2 -0
  155. package/package.json +1 -1
@@ -78,6 +78,38 @@ const LARGE_INPUT_TOOL_NAMES = new Set([
78
78
  ]);
79
79
  const MAX_HISTORY_LARGE_TOOL_ARGS_CHARS = 200_000;
80
80
  const STARTUP_RESPONSE_TIMEOUT_MS = 45_000;
81
+ // ── Background follow mode ──────────────────────────────────────────────────
82
+ // For background-dispatched runs (dispatchMode starts with "background") the
83
+ // SERVER is the sole recovery brain: it chains continuation chunks itself
84
+ // (fresh runId, same turnId), pre-inserts the successor run row BEFORE the old
85
+ // chunk completes (so /runs/active shows an active run continuously across
86
+ // chunk boundaries), and a server sweep reaps lost handoffs into loud terminal
87
+ // errors. The client therefore never POSTs synthetic continuations for these
88
+ // runs — it demotes itself to a READER of server state: poll /runs/active,
89
+ // (re)attach to whichever run of this turn is live, and fold its events into
90
+ // the same assistant message. Read-only, so multiple tabs following the same
91
+ // run are safe (no localStorage/Web-Locks claim needed).
92
+ const BACKGROUND_FOLLOW_POLL_INTERVAL_MS = 1_000;
93
+ // How long the follow loop tolerates seeing NO active run for this turn before
94
+ // treating the turn as ended. The server pre-inserts the successor row before
95
+ // the old chunk completes, so a healthy chain never shows an idle gap; 45s
96
+ // also comfortably covers the server's 25s unclaimed-handoff grace + sweep
97
+ // cadence so a reaped handoff resurfaces as a terminal errored run (which the
98
+ // follow loop replays) before this window expires.
99
+ const BACKGROUND_FOLLOW_IDLE_TIMEOUT_MS = 45_000;
100
+ /**
101
+ * User-facing copy for the server's background terminal reasons
102
+ * (`agent_runs.terminal_reason`, surfaced by /runs/active). These runs died in
103
+ * server-side handoff machinery where no richer error event may exist, so the
104
+ * client owns the message. Keys are matched after stripping an optional
105
+ * `error:` prefix (`terminalReasonForEvent` records `error:<code>`).
106
+ */
107
+ const BACKGROUND_TERMINAL_REASON_MESSAGES = {
108
+ background_worker_never_started: "The agent run was handed off to a background worker that never started. It was recovered so you can try again.",
109
+ background_continuation_dispatch_failed: "The agent's background worker could not hand off the next step of this run. Retry to continue from the preserved context.",
110
+ dispatch_payload_missing: "The agent's background run lost its saved request data and could not continue. Retry to start a fresh run.",
111
+ turn_continuation_budget_exhausted: "This request needed more automatic continuations than allowed and was stopped. Try breaking it into smaller steps.",
112
+ };
81
113
  function normalizeMentions(text) {
82
114
  return text.replace(/@\[([^\]|]+)\|[^\]]+\]/g, "@$1");
83
115
  }
@@ -939,6 +971,75 @@ function missingCredentialFailure(message) {
939
971
  };
940
972
  }
941
973
  }
974
+ function runtimeDebugUrlForApiUrl(apiUrl) {
975
+ if (typeof window === "undefined")
976
+ return null;
977
+ try {
978
+ const url = new URL(apiUrl, window.location.href);
979
+ const marker = "/_agent-native/";
980
+ const markerIndex = url.pathname.indexOf(marker);
981
+ if (markerIndex < 0)
982
+ return null;
983
+ url.pathname = `${url.pathname.slice(0, markerIndex)}${marker}debug/runtime`;
984
+ url.search = "";
985
+ return url.toString();
986
+ }
987
+ catch {
988
+ return null;
989
+ }
990
+ }
991
+ function stringValue(value) {
992
+ return typeof value === "string" ? value : "";
993
+ }
994
+ function formatRuntimeDebugDetails(payload) {
995
+ if (!payload || typeof payload !== "object")
996
+ return "";
997
+ const root = payload;
998
+ const runtime = root.runtime && typeof root.runtime === "object" ? root.runtime : {};
999
+ const database = runtime.database && typeof runtime.database === "object"
1000
+ ? runtime.database
1001
+ : {};
1002
+ const schema = root.schema && typeof root.schema === "object" ? root.schema : {};
1003
+ const lines = [
1004
+ stringValue(runtime.app) ? `runtime_app: ${runtime.app}` : "",
1005
+ stringValue(runtime.environment)
1006
+ ? `runtime_environment: ${runtime.environment}`
1007
+ : "",
1008
+ stringValue(runtime.deployContext)
1009
+ ? `deploy_context: ${runtime.deployContext}`
1010
+ : "",
1011
+ stringValue(runtime.deployId) ? `deploy_id: ${runtime.deployId}` : "",
1012
+ stringValue(runtime.commitRef) ? `commit_ref: ${runtime.commitRef}` : "",
1013
+ stringValue(runtime.branch) ? `branch: ${runtime.branch}` : "",
1014
+ stringValue(runtime.siteName) ? `site_name: ${runtime.siteName}` : "",
1015
+ typeof database.configured === "boolean"
1016
+ ? `db_configured: ${database.configured}`
1017
+ : "",
1018
+ stringValue(database.source) ? `db_source: ${database.source}` : "",
1019
+ stringValue(database.dialect) ? `db_dialect: ${database.dialect}` : "",
1020
+ stringValue(database.protocol) ? `db_protocol: ${database.protocol}` : "",
1021
+ stringValue(database.host) ? `db_host: ${database.host}` : "",
1022
+ stringValue(database.database) ? `db_database: ${database.database}` : "",
1023
+ stringValue(database.urlHash) ? `db_url_hash: ${database.urlHash}` : "",
1024
+ database.neon?.endpointId
1025
+ ? `db_neon_endpoint: ${database.neon.endpointId}`
1026
+ : "",
1027
+ typeof database.neon?.pooled === "boolean"
1028
+ ? `db_neon_pooled: ${database.neon.pooled}`
1029
+ : "",
1030
+ typeof schema.ok === "boolean" ? `schema_ok: ${schema.ok}` : "",
1031
+ Array.isArray(schema.missingTables) && schema.missingTables.length
1032
+ ? `schema_missing_tables: ${schema.missingTables.join(", ")}`
1033
+ : "",
1034
+ Array.isArray(schema.missingColumns) && schema.missingColumns.length
1035
+ ? `schema_missing_columns: ${schema.missingColumns
1036
+ .map((entry) => `${entry.table}.${entry.column}`)
1037
+ .join(", ")}`
1038
+ : "",
1039
+ stringValue(schema.error) ? `schema_error: ${schema.error}` : "",
1040
+ ].filter(Boolean);
1041
+ return lines.join("\n");
1042
+ }
942
1043
  export function createAgentChatAdapter(options) {
943
1044
  const apiUrl = options?.apiUrl ?? agentNativePath("/_agent-native/agent-chat");
944
1045
  const tabId = options?.tabId;
@@ -950,6 +1051,16 @@ export function createAgentChatAdapter(options) {
950
1051
  const browserTabId = options?.browserTabId;
951
1052
  const scopeRef = options?.scopeRef;
952
1053
  const surface = options?.surface ?? "app";
1054
+ let runtimeDebugDetails = "";
1055
+ const runtimeDebugUrl = runtimeDebugUrlForApiUrl(apiUrl);
1056
+ if (runtimeDebugUrl && typeof fetch === "function") {
1057
+ void fetch(runtimeDebugUrl, { credentials: "include" })
1058
+ .then((res) => (res.ok ? res.json() : null))
1059
+ .then((payload) => {
1060
+ runtimeDebugDetails = formatRuntimeDebugDetails(payload);
1061
+ })
1062
+ .catch(() => { });
1063
+ }
953
1064
  return {
954
1065
  async *run({ messages, abortSignal, runConfig }) {
955
1066
  // Extract latest user message and build history from prior messages
@@ -1114,6 +1225,7 @@ export function createAgentChatAdapter(options) {
1114
1225
  attemptedRunIds.length > 0
1115
1226
  ? `attempted_runs: ${attemptedRunIds.join(", ")}`
1116
1227
  : "",
1228
+ runtimeDebugDetails,
1117
1229
  ]
1118
1230
  .filter(Boolean)
1119
1231
  .join("\n");
@@ -1220,6 +1332,11 @@ export function createAgentChatAdapter(options) {
1220
1332
  if (mode)
1221
1333
  currentRunDispatchMode = mode;
1222
1334
  };
1335
+ // Mode switch for recovery ownership: background-dispatched runs are
1336
+ // recovered by the SERVER (chained continuations, sweeps); the client
1337
+ // only follows. Foreground (null/"foreground"/unknown) keeps the full
1338
+ // client-side continuation machinery unchanged.
1339
+ const isBackgroundDispatch = () => currentRunDispatchMode?.startsWith("background") === true;
1223
1340
  const rememberRunSeq = (seq) => {
1224
1341
  lastSeq = seq;
1225
1342
  if (runId) {
@@ -1518,6 +1635,279 @@ export function createAgentChatAdapter(options) {
1518
1635
  lastSeq = interruptedLastSeq;
1519
1636
  return false;
1520
1637
  };
1638
+ // ── Background follow mode (see module comment on the constants) ──
1639
+ // Emits a terminal chat error using the same runError/content/yield
1640
+ // shape as the exhausted-recovery paths, so background failures render
1641
+ // identically to foreground ones.
1642
+ const emitBackgroundTerminalError = function* (args) {
1643
+ const runError = {
1644
+ message: args.message,
1645
+ details: [args.details, connectionRecoveryDetails()]
1646
+ .filter(Boolean)
1647
+ .join("\n\n"),
1648
+ errorCode: args.errorCode,
1649
+ recoverable: true,
1650
+ ...(runId ? { runId } : {}),
1651
+ };
1652
+ if (typeof window !== "undefined") {
1653
+ window.dispatchEvent(new CustomEvent("agent-chat:run-error", {
1654
+ detail: { ...runError, tabId },
1655
+ }));
1656
+ }
1657
+ settleInterruptedToolCalls(content, undefined, {
1658
+ includeActivity: true,
1659
+ });
1660
+ content.push({
1661
+ type: "text",
1662
+ text: formatChatErrorText(args.message, undefined, args.errorCode),
1663
+ });
1664
+ yield {
1665
+ content: [...content],
1666
+ status: { type: "incomplete", reason: "error" },
1667
+ metadata: { custom: { ...(runId ? { runId } : {}), runError } },
1668
+ };
1669
+ clearActiveRun();
1670
+ };
1671
+ // Final outcome for a background turn the follow loop can no longer
1672
+ // follow: either the run went terminal server-side (surface its
1673
+ // error, mapping terminal_reason to clear copy) or it vanished
1674
+ // (finalize with received content when it completed, loud error
1675
+ // otherwise — never a silent stop).
1676
+ const emitBackgroundTerminalOutcome = function* (lastKnown) {
1677
+ const status = typeof lastKnown?.status === "string" ? lastKnown.status : "";
1678
+ const rawTerminalReason = typeof lastKnown?.terminalReason === "string"
1679
+ ? lastKnown.terminalReason
1680
+ : "";
1681
+ // terminal_reason is either a bare reason ("dispatch_payload_missing")
1682
+ // or "error:<errorCode>" when derived from a terminal error event.
1683
+ const terminalReason = rawTerminalReason.replace(/^error:/, "");
1684
+ if (status === "completed") {
1685
+ // The turn finished server-side; the events we managed to fold are
1686
+ // the durable transcript. Finalize with them.
1687
+ settleInterruptedToolCalls(content, undefined, {
1688
+ includeActivity: true,
1689
+ });
1690
+ yield {
1691
+ content: [...content],
1692
+ status: { type: "complete", reason: "stop" },
1693
+ metadata: { custom: { ...(runId ? { runId } : {}) } },
1694
+ };
1695
+ clearActiveRun();
1696
+ return;
1697
+ }
1698
+ const mappedMessage = terminalReason
1699
+ ? BACKGROUND_TERMINAL_REASON_MESSAGES[terminalReason]
1700
+ : undefined;
1701
+ if (mappedMessage) {
1702
+ yield* emitBackgroundTerminalError({
1703
+ message: mappedMessage,
1704
+ errorCode: terminalReason,
1705
+ details: `terminal_reason: ${rawTerminalReason}`,
1706
+ });
1707
+ return;
1708
+ }
1709
+ if (lastRecoverableRunError) {
1710
+ // A replayed terminal error event already carried the real
1711
+ // message (recoverable errors replay as auto-continue signals
1712
+ // whose errorInfo we captured while following).
1713
+ yield* emitBackgroundTerminalError({
1714
+ message: lastRecoverableRunError.message,
1715
+ errorCode: lastRecoverableRunError.errorCode ?? "connection_error",
1716
+ details: lastRecoverableRunError.details,
1717
+ });
1718
+ return;
1719
+ }
1720
+ yield* emitBackgroundTerminalError({
1721
+ message: "The agent's background run stopped before finishing and no continuation appeared. You can retry from the preserved chat context.",
1722
+ errorCode: terminalReason || "background_run_lost",
1723
+ details: rawTerminalReason
1724
+ ? `terminal_reason: ${rawTerminalReason}`
1725
+ : undefined,
1726
+ });
1727
+ };
1728
+ // One read-only attach to the current run's event stream. Unlike
1729
+ // reconnectCurrentRun this does NOT retry internally and lets the
1730
+ // follow loop see every auto-continue signal (with errorInfo intact),
1731
+ // because the loop — not this reader — decides what a detach means.
1732
+ // Return values: "completed" = terminal event consumed (turn over);
1733
+ // "aborted" = user abort; "detached" = we ATTACHED to a live stream
1734
+ // that ended/stalled without a terminal event (resets the follow
1735
+ // loop's idle window); "gone" = could not attach at all (404, HTTP
1736
+ // error, network failure — the idle window keeps accumulating so a
1737
+ // persistently unattachable run still terminates loudly).
1738
+ const followAttachOnce = async function* () {
1739
+ if (!runId)
1740
+ return "gone";
1741
+ let attached = false;
1742
+ try {
1743
+ const eventsRes = await fetch(`${apiUrl}/runs/${encodeURIComponent(runId)}/events?after=${lastSeq + 1}`, { signal: abortSignal });
1744
+ if (!eventsRes.ok || !eventsRes.body) {
1745
+ return "gone";
1746
+ }
1747
+ attached = true;
1748
+ updateCurrentRunDispatchMode(eventsRes.headers.get("X-Dispatch-Mode"));
1749
+ for await (const result of readSSEStream(eventsRes.body, content, toolCallCounter, tabId, (seq) => {
1750
+ rememberRunSeq(seq);
1751
+ if (threadId)
1752
+ updateActiveRunSeq(seq);
1753
+ }, runId, currentSSEOptions())) {
1754
+ yield withRequestModeMetadata(result);
1755
+ }
1756
+ // readSSEStream returned normally: a terminal done/error was
1757
+ // consumed and rendered — the turn is over.
1758
+ clearActiveRun();
1759
+ return "completed";
1760
+ }
1761
+ catch (attachErr) {
1762
+ if (attachErr instanceof Error && attachErr.name === "AbortError") {
1763
+ clearActiveRun();
1764
+ return "aborted";
1765
+ }
1766
+ if (attachErr instanceof AgentAutoContinueSignal) {
1767
+ lastAutoContinueReason = attachErr.reason;
1768
+ if (attachErr.activityTrail.length > 0) {
1769
+ lastActivityTrail = [...attachErr.activityTrail];
1770
+ }
1771
+ if (attachErr.errorInfo) {
1772
+ lastRecoverableRunError = attachErr.errorInfo;
1773
+ }
1774
+ }
1775
+ // A fetch that never yielded a response body counts as "gone" for
1776
+ // the idle window; a broken mid-stream read counts as attached.
1777
+ return attached ? "detached" : "gone";
1778
+ }
1779
+ };
1780
+ // The background follow loop. Entered instead of ANY synthetic
1781
+ // continuation POST when the current run is background-dispatched.
1782
+ // Keeps following as long as an active run for this turn exists — the
1783
+ // server keeps chaining until its own per-turn budget or a real
1784
+ // terminal error, so a successor that stalls again is simply
1785
+ // re-followed, not counted against any client budget. Only a
1786
+ // continuous BACKGROUND_FOLLOW_IDLE_TIMEOUT_MS with no active run
1787
+ // ends the turn from the client side.
1788
+ const followBackgroundTurn = async function* (initialSignal) {
1789
+ lastAutoContinueReason = initialSignal.reason;
1790
+ if (initialSignal.activityTrail.length > 0) {
1791
+ lastActivityTrail = [...initialSignal.activityTrail];
1792
+ }
1793
+ if (initialSignal.errorInfo) {
1794
+ lastRecoverableRunError = initialSignal.errorInfo;
1795
+ }
1796
+ // Show "Resuming…" instead of a frozen Thinking label while waiting
1797
+ // for the server-chained successor chunk.
1798
+ const dispatchResumingUiEvent = () => {
1799
+ if (typeof window !== "undefined") {
1800
+ window.dispatchEvent(new CustomEvent("agent-chat:auto-continue", {
1801
+ detail: { tabId },
1802
+ }));
1803
+ }
1804
+ };
1805
+ dispatchResumingUiEvent();
1806
+ let idleSince = null;
1807
+ let lastSeenActive = null;
1808
+ // Terminal runs already replayed once. A recoverable terminal error
1809
+ // event replays as an auto-continue signal (isAutoRecoverableError),
1810
+ // which used to re-drive a POST in foreground mode. In follow mode
1811
+ // the server owns recovery, so a SECOND visit to the same terminal
1812
+ // run means the turn is over — surface the outcome instead of
1813
+ // re-replaying it every poll for the whole reconnect window.
1814
+ const replayedTerminalRunIds = new Set();
1815
+ while (true) {
1816
+ if (abortSignal.aborted) {
1817
+ clearActiveRun();
1818
+ return;
1819
+ }
1820
+ let active = null;
1821
+ try {
1822
+ const activeRes = await fetch(`${apiUrl}/runs/active?threadId=${encodeURIComponent(threadId)}`, { signal: abortSignal });
1823
+ if (activeRes.ok) {
1824
+ active = await activeRes.json().catch(() => null);
1825
+ }
1826
+ }
1827
+ catch (pollErr) {
1828
+ if (pollErr instanceof Error && pollErr.name === "AbortError") {
1829
+ clearActiveRun();
1830
+ return;
1831
+ }
1832
+ // Transient poll failure — counts as "no active run" this tick.
1833
+ }
1834
+ const activeRunId = active?.active === true && active.runId
1835
+ ? String(active.runId)
1836
+ : null;
1837
+ const activeTurnId = typeof active?.turnId === "string" ? active.turnId : "";
1838
+ const activeStatus = typeof active?.status === "string" ? active.status : "";
1839
+ // Only follow runs belonging to THIS turn (server-chained
1840
+ // successors reuse the turnId) or runs we already attached to.
1841
+ const isOurRun = activeRunId !== null &&
1842
+ (attemptedRunIds.includes(activeRunId) ||
1843
+ !activeTurnId ||
1844
+ activeTurnId === turnId);
1845
+ if (activeRunId && isOurRun) {
1846
+ lastSeenActive = active;
1847
+ updateCurrentRunDispatchMode(active?.dispatchMode);
1848
+ const isTerminal = activeStatus === "completed" || activeStatus === "errored";
1849
+ if (isTerminal && replayedTerminalRunIds.has(activeRunId)) {
1850
+ yield* emitBackgroundTerminalOutcome(active);
1851
+ return;
1852
+ }
1853
+ const previousRunId = runId;
1854
+ runId = activeRunId;
1855
+ if (!attemptedRunIds.includes(activeRunId)) {
1856
+ attemptedRunIds.push(activeRunId);
1857
+ }
1858
+ reconnectCursorForRun(activeRunId, previousRunId);
1859
+ if (threadId) {
1860
+ setActiveRun({ threadId, runId: activeRunId, lastSeq });
1861
+ }
1862
+ const attach = yield* followAttachOnce();
1863
+ if (attach === "completed" || attach === "aborted") {
1864
+ return;
1865
+ }
1866
+ if (isTerminal) {
1867
+ replayedTerminalRunIds.add(activeRunId);
1868
+ }
1869
+ if (attach === "detached") {
1870
+ // We really attached to a live stream (chunk boundary or
1871
+ // transport blip) — the run demonstrably exists; keep
1872
+ // following with a fresh idle window.
1873
+ idleSince = null;
1874
+ }
1875
+ else {
1876
+ // "gone": /runs/active reported the run but its event stream
1877
+ // 404s (reaped row / cross-isolate lag). Let the idle window
1878
+ // ACCUMULATE instead of resetting, so a persistently
1879
+ // inconsistent state still terminates loudly instead of
1880
+ // hot-looping for the whole reconnect window.
1881
+ if (idleSince === null)
1882
+ idleSince = Date.now();
1883
+ if (Date.now() - idleSince >=
1884
+ BACKGROUND_FOLLOW_IDLE_TIMEOUT_MS) {
1885
+ yield* emitBackgroundTerminalOutcome(lastSeenActive);
1886
+ return;
1887
+ }
1888
+ }
1889
+ dispatchResumingUiEvent();
1890
+ }
1891
+ else {
1892
+ if (idleSince === null)
1893
+ idleSince = Date.now();
1894
+ if (Date.now() - idleSince >= BACKGROUND_FOLLOW_IDLE_TIMEOUT_MS) {
1895
+ captureChatClientError(new Error("Background run went idle with no successor run appearing"), "background-follow-idle", {
1896
+ lastSeenStatus: typeof lastSeenActive?.status === "string"
1897
+ ? lastSeenActive.status
1898
+ : null,
1899
+ });
1900
+ yield* emitBackgroundTerminalOutcome(lastSeenActive);
1901
+ return;
1902
+ }
1903
+ }
1904
+ await delay(BACKGROUND_FOLLOW_POLL_INTERVAL_MS, abortSignal);
1905
+ if (abortSignal.aborted) {
1906
+ clearActiveRun();
1907
+ return;
1908
+ }
1909
+ }
1910
+ };
1521
1911
  const visibleContentForContinuation = () => {
1522
1912
  return contentAfterContinuationPrefix(content, visibleContinuationPrefix);
1523
1913
  };
@@ -2027,6 +2417,17 @@ export function createAgentChatAdapter(options) {
2027
2417
  return;
2028
2418
  }
2029
2419
  if (err instanceof AgentAutoContinueSignal) {
2420
+ // Background-dispatched runs: the server chains continuations
2421
+ // itself (successor row pre-inserted before the old chunk
2422
+ // completes). Never POST a synthetic continuation and never
2423
+ // abort the live server-side run — switch to read-only
2424
+ // following of server state instead. This is the fix for the
2425
+ // client/server recovery race: client watchdog signals here are
2426
+ // just "reattach", not "recover".
2427
+ if (isBackgroundDispatch() && threadId) {
2428
+ yield* followBackgroundTurn(err);
2429
+ return;
2430
+ }
2030
2431
  if (err.reason === "no_progress") {
2031
2432
  await abortCurrentRun();
2032
2433
  }
@@ -2181,6 +2582,15 @@ export function createAgentChatAdapter(options) {
2181
2582
  const activeReconnected = yield* reconnectActiveRunForThread();
2182
2583
  if (activeReconnected)
2183
2584
  return;
2585
+ // Background-dispatched run whose transport failed and could not
2586
+ // be reconnected above: follow server state instead of the
2587
+ // synthetic-continuation POST below. (An initial POST that failed
2588
+ // before any response headers arrived has no dispatch mode yet
2589
+ // and keeps the foreground startup-retry path.)
2590
+ if (isBackgroundDispatch() && threadId) {
2591
+ yield* followBackgroundTurn(new AgentAutoContinueSignal({ reason: "stream_ended" }));
2592
+ return;
2593
+ }
2184
2594
  if (err instanceof AgentStartupTimeoutError) {
2185
2595
  if (startupRecoveryAttempts < MAX_STARTUP_RECOVERY_ATTEMPTS) {
2186
2596
  await retryDelay(startupRecoveryAttempts++, abortSignal);