@agent-native/core 0.84.67 → 0.85.1

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 (149) 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/action-continuation-guidance.ts +1 -1
  16. package/corpus/core/src/agent/production-agent.ts +222 -38
  17. package/corpus/core/src/agent/run-manager.ts +79 -0
  18. package/corpus/core/src/agent/run-store.ts +122 -8
  19. package/corpus/core/src/agent/types.ts +13 -0
  20. package/corpus/core/src/client/RunStuckBanner.tsx +14 -0
  21. package/corpus/core/src/client/agent-chat-adapter.ts +475 -0
  22. package/corpus/core/src/client/analytics.ts +2 -0
  23. package/corpus/core/src/client/error-format.ts +4 -0
  24. package/corpus/core/src/client/extensions/ExtensionsSidebarSection.tsx +1 -1
  25. package/corpus/core/src/client/session-replay.ts +774 -2
  26. package/corpus/core/src/client/sse-event-processor.ts +65 -9
  27. package/corpus/core/src/client/use-run-stuck-detection.ts +31 -7
  28. package/corpus/core/src/db/runtime-diagnostics.ts +331 -0
  29. package/corpus/core/src/server/agent-chat-plugin.ts +115 -2
  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/templates/analytics/.agents/skills/session-replay/SKILL.md +53 -1
  34. package/corpus/templates/analytics/AGENTS.md +13 -0
  35. package/corpus/templates/analytics/actions/view-screen.ts +25 -0
  36. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +20 -0
  37. package/corpus/templates/analytics/app/global.css +55 -0
  38. package/corpus/templates/analytics/app/i18n/zh-TW.ts +100 -1
  39. package/corpus/templates/analytics/app/i18n-data.ts +1226 -0
  40. package/corpus/templates/analytics/app/pages/Settings.tsx +3 -0
  41. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +182 -144
  42. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +11 -18
  43. package/corpus/templates/analytics/app/pages/analyses/AnalysisDetail.tsx +11 -18
  44. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +49 -1
  45. package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +557 -0
  46. package/corpus/templates/analytics/app/pages/sessions/session-replay-devtools.ts +274 -0
  47. package/corpus/templates/analytics/app/pages/settings/AlertRulesSettingsCard.tsx +974 -0
  48. package/corpus/templates/analytics/changelog/2026-07-02-alert-rules-can-now-be-viewed-and-managed-from-settings.md +6 -0
  49. package/corpus/templates/analytics/changelog/2026-07-02-recent-ask-chats-show-a-loading-placeholder-while-your-chat-.md +6 -0
  50. package/corpus/templates/analytics/changelog/2026-07-02-session-replays-now-capture-console-logs-and-network-request.md +6 -0
  51. package/corpus/templates/analytics/changelog/2026-07-02-sharing-status-labels-use-neutral-icons-instead-of-colored-d.md +6 -0
  52. package/corpus/templates/analytics/changelog/2026-07-02-the-session-replay-viewer-has-a-dev-tools-panel-with-console.md +6 -0
  53. package/corpus/templates/analytics/server/db/schema.ts +3 -0
  54. package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +4 -0
  55. package/corpus/templates/analytics/server/lib/analytics-alerts.ts +142 -1
  56. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +369 -8
  57. package/corpus/templates/analytics/server/lib/session-replay.ts +76 -2
  58. package/corpus/templates/analytics/server/routes/api/session-replay/agent-diagnostics.json.get.ts +101 -0
  59. package/corpus/templates/analytics/shared/session-replay-diagnostics.ts +78 -0
  60. package/corpus/templates/design/app/pages/DesignEditor.tsx +24 -12
  61. package/corpus/templates/design/changelog/2026-07-02-the-temporary-code-tab-is-hidden-from-the-design-editor-side.md +6 -0
  62. package/corpus/templates/plan/actions/create-visual-recap.ts +1 -1
  63. package/corpus/templates/plan/actions/update-local-plan-folder.ts +3 -1
  64. package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
  65. package/corpus/templates/plan/changelog/2026-07-02-trying-to-edit-a-recap-or-plan-you-can-only-view-now-explain.md +6 -0
  66. package/corpus/templates/plan/server/plans.ts +26 -9
  67. package/dist/agent/action-continuation-guidance.js +1 -1
  68. package/dist/agent/action-continuation-guidance.js.map +1 -1
  69. package/dist/agent/production-agent.d.ts +9 -1
  70. package/dist/agent/production-agent.d.ts.map +1 -1
  71. package/dist/agent/production-agent.js +168 -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 +105 -7
  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 +2 -2
  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/notifications/routes.d.ts +1 -1
  118. package/dist/observability/routes.d.ts +3 -3
  119. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  120. package/dist/resources/handlers.d.ts +3 -3
  121. package/dist/secrets/routes.d.ts +3 -3
  122. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  123. package/dist/server/agent-chat-plugin.js +101 -2
  124. package/dist/server/agent-chat-plugin.js.map +1 -1
  125. package/dist/server/core-routes-plugin.d.ts +17 -0
  126. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  127. package/dist/server/core-routes-plugin.js +65 -5
  128. package/dist/server/core-routes-plugin.js.map +1 -1
  129. package/dist/server/http-response-telemetry.d.ts +4 -0
  130. package/dist/server/http-response-telemetry.d.ts.map +1 -0
  131. package/dist/server/http-response-telemetry.js +174 -0
  132. package/dist/server/http-response-telemetry.js.map +1 -0
  133. package/dist/server/self-dispatch.d.ts +17 -0
  134. package/dist/server/self-dispatch.d.ts.map +1 -1
  135. package/dist/server/self-dispatch.js +11 -0
  136. package/dist/server/self-dispatch.js.map +1 -1
  137. package/dist/server/transcribe-voice.d.ts +1 -1
  138. package/docs/content/locales/ar-SA/tracking.mdx +55 -0
  139. package/docs/content/locales/de-DE/tracking.mdx +55 -0
  140. package/docs/content/locales/es-ES/tracking.mdx +55 -0
  141. package/docs/content/locales/fr-FR/tracking.mdx +55 -0
  142. package/docs/content/locales/hi-IN/tracking.mdx +55 -0
  143. package/docs/content/locales/ja-JP/tracking.mdx +55 -0
  144. package/docs/content/locales/ko-KR/tracking.mdx +55 -0
  145. package/docs/content/locales/pt-BR/tracking.mdx +55 -0
  146. package/docs/content/locales/zh-CN/tracking.mdx +55 -0
  147. package/docs/content/locales/zh-TW/tracking.mdx +55 -0
  148. package/docs/content/tracking.mdx +2 -0
  149. package/package.json +1 -1
@@ -132,6 +132,44 @@ const LARGE_INPUT_TOOL_NAMES = new Set([
132
132
  const MAX_HISTORY_LARGE_TOOL_ARGS_CHARS = 200_000;
133
133
  const STARTUP_RESPONSE_TIMEOUT_MS = 45_000;
134
134
 
135
+ // ── Background follow mode ──────────────────────────────────────────────────
136
+ // For background-dispatched runs (dispatchMode starts with "background") the
137
+ // SERVER is the sole recovery brain: it chains continuation chunks itself
138
+ // (fresh runId, same turnId), pre-inserts the successor run row BEFORE the old
139
+ // chunk completes (so /runs/active shows an active run continuously across
140
+ // chunk boundaries), and a server sweep reaps lost handoffs into loud terminal
141
+ // errors. The client therefore never POSTs synthetic continuations for these
142
+ // runs — it demotes itself to a READER of server state: poll /runs/active,
143
+ // (re)attach to whichever run of this turn is live, and fold its events into
144
+ // the same assistant message. Read-only, so multiple tabs following the same
145
+ // run are safe (no localStorage/Web-Locks claim needed).
146
+ const BACKGROUND_FOLLOW_POLL_INTERVAL_MS = 1_000;
147
+ // How long the follow loop tolerates seeing NO active run for this turn before
148
+ // treating the turn as ended. The server pre-inserts the successor row before
149
+ // the old chunk completes, so a healthy chain never shows an idle gap; 45s
150
+ // also comfortably covers the server's 25s unclaimed-handoff grace + sweep
151
+ // cadence so a reaped handoff resurfaces as a terminal errored run (which the
152
+ // follow loop replays) before this window expires.
153
+ const BACKGROUND_FOLLOW_IDLE_TIMEOUT_MS = 45_000;
154
+
155
+ /**
156
+ * User-facing copy for the server's background terminal reasons
157
+ * (`agent_runs.terminal_reason`, surfaced by /runs/active). These runs died in
158
+ * server-side handoff machinery where no richer error event may exist, so the
159
+ * client owns the message. Keys are matched after stripping an optional
160
+ * `error:` prefix (`terminalReasonForEvent` records `error:<code>`).
161
+ */
162
+ const BACKGROUND_TERMINAL_REASON_MESSAGES: Record<string, string> = {
163
+ background_worker_never_started:
164
+ "The agent run was handed off to a background worker that never started. It was recovered so you can try again.",
165
+ background_continuation_dispatch_failed:
166
+ "The agent's background worker could not hand off the next step of this run. Retry to continue from the preserved context.",
167
+ dispatch_payload_missing:
168
+ "The agent's background run lost its saved request data and could not continue. Retry to start a fresh run.",
169
+ turn_continuation_budget_exhausted:
170
+ "This request needed more automatic continuations than allowed and was stopped. Try breaking it into smaller steps.",
171
+ };
172
+
135
173
  function normalizeMentions(text: string): string {
136
174
  return text.replace(/@\[([^\]|]+)\|[^\]]+\]/g, "@$1");
137
175
  }
@@ -1239,6 +1277,77 @@ export interface CreateAgentChatAdapterOptions {
1239
1277
  surface?: AgentChatSurfaceKind;
1240
1278
  }
1241
1279
 
1280
+ function runtimeDebugUrlForApiUrl(apiUrl: string): string | null {
1281
+ if (typeof window === "undefined") return null;
1282
+ try {
1283
+ const url = new URL(apiUrl, window.location.href);
1284
+ const marker = "/_agent-native/";
1285
+ const markerIndex = url.pathname.indexOf(marker);
1286
+ if (markerIndex < 0) return null;
1287
+ url.pathname = `${url.pathname.slice(0, markerIndex)}${marker}debug/runtime`;
1288
+ url.search = "";
1289
+ return url.toString();
1290
+ } catch {
1291
+ return null;
1292
+ }
1293
+ }
1294
+
1295
+ function stringValue(value: unknown): string {
1296
+ return typeof value === "string" ? value : "";
1297
+ }
1298
+
1299
+ function formatRuntimeDebugDetails(payload: unknown): string {
1300
+ if (!payload || typeof payload !== "object") return "";
1301
+ const root = payload as Record<string, any>;
1302
+ const runtime =
1303
+ root.runtime && typeof root.runtime === "object" ? root.runtime : {};
1304
+ const database =
1305
+ runtime.database && typeof runtime.database === "object"
1306
+ ? runtime.database
1307
+ : {};
1308
+ const schema =
1309
+ root.schema && typeof root.schema === "object" ? root.schema : {};
1310
+ const lines = [
1311
+ stringValue(runtime.app) ? `runtime_app: ${runtime.app}` : "",
1312
+ stringValue(runtime.environment)
1313
+ ? `runtime_environment: ${runtime.environment}`
1314
+ : "",
1315
+ stringValue(runtime.deployContext)
1316
+ ? `deploy_context: ${runtime.deployContext}`
1317
+ : "",
1318
+ stringValue(runtime.deployId) ? `deploy_id: ${runtime.deployId}` : "",
1319
+ stringValue(runtime.commitRef) ? `commit_ref: ${runtime.commitRef}` : "",
1320
+ stringValue(runtime.branch) ? `branch: ${runtime.branch}` : "",
1321
+ stringValue(runtime.siteName) ? `site_name: ${runtime.siteName}` : "",
1322
+ typeof database.configured === "boolean"
1323
+ ? `db_configured: ${database.configured}`
1324
+ : "",
1325
+ stringValue(database.source) ? `db_source: ${database.source}` : "",
1326
+ stringValue(database.dialect) ? `db_dialect: ${database.dialect}` : "",
1327
+ stringValue(database.protocol) ? `db_protocol: ${database.protocol}` : "",
1328
+ stringValue(database.host) ? `db_host: ${database.host}` : "",
1329
+ stringValue(database.database) ? `db_database: ${database.database}` : "",
1330
+ stringValue(database.urlHash) ? `db_url_hash: ${database.urlHash}` : "",
1331
+ database.neon?.endpointId
1332
+ ? `db_neon_endpoint: ${database.neon.endpointId}`
1333
+ : "",
1334
+ typeof database.neon?.pooled === "boolean"
1335
+ ? `db_neon_pooled: ${database.neon.pooled}`
1336
+ : "",
1337
+ typeof schema.ok === "boolean" ? `schema_ok: ${schema.ok}` : "",
1338
+ Array.isArray(schema.missingTables) && schema.missingTables.length
1339
+ ? `schema_missing_tables: ${schema.missingTables.join(", ")}`
1340
+ : "",
1341
+ Array.isArray(schema.missingColumns) && schema.missingColumns.length
1342
+ ? `schema_missing_columns: ${schema.missingColumns
1343
+ .map((entry: any) => `${entry.table}.${entry.column}`)
1344
+ .join(", ")}`
1345
+ : "",
1346
+ stringValue(schema.error) ? `schema_error: ${schema.error}` : "",
1347
+ ].filter(Boolean);
1348
+ return lines.join("\n");
1349
+ }
1350
+
1242
1351
  export function createAgentChatAdapter(
1243
1352
  options?: CreateAgentChatAdapterOptions,
1244
1353
  ): ChatModelAdapter {
@@ -1253,6 +1362,16 @@ export function createAgentChatAdapter(
1253
1362
  const browserTabId = options?.browserTabId;
1254
1363
  const scopeRef = options?.scopeRef;
1255
1364
  const surface = options?.surface ?? "app";
1365
+ let runtimeDebugDetails = "";
1366
+ const runtimeDebugUrl = runtimeDebugUrlForApiUrl(apiUrl);
1367
+ if (runtimeDebugUrl && typeof fetch === "function") {
1368
+ void fetch(runtimeDebugUrl, { credentials: "include" })
1369
+ .then((res) => (res.ok ? res.json() : null))
1370
+ .then((payload) => {
1371
+ runtimeDebugDetails = formatRuntimeDebugDetails(payload);
1372
+ })
1373
+ .catch(() => {});
1374
+ }
1256
1375
 
1257
1376
  return {
1258
1377
  async *run({ messages, abortSignal, runConfig }) {
@@ -1449,6 +1568,7 @@ export function createAgentChatAdapter(
1449
1568
  attemptedRunIds.length > 0
1450
1569
  ? `attempted_runs: ${attemptedRunIds.join(", ")}`
1451
1570
  : "",
1571
+ runtimeDebugDetails,
1452
1572
  ]
1453
1573
  .filter(Boolean)
1454
1574
  .join("\n");
@@ -1566,6 +1686,13 @@ export function createAgentChatAdapter(
1566
1686
  if (mode) currentRunDispatchMode = mode;
1567
1687
  };
1568
1688
 
1689
+ // Mode switch for recovery ownership: background-dispatched runs are
1690
+ // recovered by the SERVER (chained continuations, sweeps); the client
1691
+ // only follows. Foreground (null/"foreground"/unknown) keeps the full
1692
+ // client-side continuation machinery unchanged.
1693
+ const isBackgroundDispatch = () =>
1694
+ currentRunDispatchMode?.startsWith("background") === true;
1695
+
1569
1696
  const rememberRunSeq = (seq: number) => {
1570
1697
  lastSeq = seq;
1571
1698
  if (runId) {
@@ -1929,6 +2056,331 @@ export function createAgentChatAdapter(
1929
2056
  return false;
1930
2057
  };
1931
2058
 
2059
+ // ── Background follow mode (see module comment on the constants) ──
2060
+ // Emits a terminal chat error using the same runError/content/yield
2061
+ // shape as the exhausted-recovery paths, so background failures render
2062
+ // identically to foreground ones.
2063
+ const emitBackgroundTerminalError = function* (args: {
2064
+ message: string;
2065
+ errorCode: string;
2066
+ details?: string;
2067
+ }): Generator<ChatModelRunResult, void, unknown> {
2068
+ const runError = {
2069
+ message: args.message,
2070
+ details: [args.details, connectionRecoveryDetails()]
2071
+ .filter(Boolean)
2072
+ .join("\n\n"),
2073
+ errorCode: args.errorCode,
2074
+ recoverable: true,
2075
+ ...(runId ? { runId } : {}),
2076
+ };
2077
+ if (typeof window !== "undefined") {
2078
+ window.dispatchEvent(
2079
+ new CustomEvent("agent-chat:run-error", {
2080
+ detail: { ...runError, tabId },
2081
+ }),
2082
+ );
2083
+ }
2084
+ settleInterruptedToolCalls(content, undefined, {
2085
+ includeActivity: true,
2086
+ });
2087
+ content.push({
2088
+ type: "text",
2089
+ text: formatChatErrorText(args.message, undefined, args.errorCode),
2090
+ });
2091
+ yield {
2092
+ content: [...content],
2093
+ status: { type: "incomplete" as const, reason: "error" as const },
2094
+ metadata: { custom: { ...(runId ? { runId } : {}), runError } },
2095
+ } as ChatModelRunResult;
2096
+ clearActiveRun();
2097
+ };
2098
+
2099
+ // Final outcome for a background turn the follow loop can no longer
2100
+ // follow: either the run went terminal server-side (surface its
2101
+ // error, mapping terminal_reason to clear copy) or it vanished
2102
+ // (finalize with received content when it completed, loud error
2103
+ // otherwise — never a silent stop).
2104
+ const emitBackgroundTerminalOutcome = function* (
2105
+ lastKnown: Record<string, unknown> | null,
2106
+ ): Generator<ChatModelRunResult, void, unknown> {
2107
+ const status =
2108
+ typeof lastKnown?.status === "string" ? lastKnown.status : "";
2109
+ const rawTerminalReason =
2110
+ typeof lastKnown?.terminalReason === "string"
2111
+ ? lastKnown.terminalReason
2112
+ : "";
2113
+ // terminal_reason is either a bare reason ("dispatch_payload_missing")
2114
+ // or "error:<errorCode>" when derived from a terminal error event.
2115
+ const terminalReason = rawTerminalReason.replace(/^error:/, "");
2116
+
2117
+ if (status === "completed") {
2118
+ // The turn finished server-side; the events we managed to fold are
2119
+ // the durable transcript. Finalize with them.
2120
+ settleInterruptedToolCalls(content, undefined, {
2121
+ includeActivity: true,
2122
+ });
2123
+ yield {
2124
+ content: [...content],
2125
+ status: { type: "complete" as const, reason: "stop" as const },
2126
+ metadata: { custom: { ...(runId ? { runId } : {}) } },
2127
+ } as ChatModelRunResult;
2128
+ clearActiveRun();
2129
+ return;
2130
+ }
2131
+
2132
+ const mappedMessage = terminalReason
2133
+ ? BACKGROUND_TERMINAL_REASON_MESSAGES[terminalReason]
2134
+ : undefined;
2135
+ if (mappedMessage) {
2136
+ yield* emitBackgroundTerminalError({
2137
+ message: mappedMessage,
2138
+ errorCode: terminalReason,
2139
+ details: `terminal_reason: ${rawTerminalReason}`,
2140
+ });
2141
+ return;
2142
+ }
2143
+ if (lastRecoverableRunError) {
2144
+ // A replayed terminal error event already carried the real
2145
+ // message (recoverable errors replay as auto-continue signals
2146
+ // whose errorInfo we captured while following).
2147
+ yield* emitBackgroundTerminalError({
2148
+ message: lastRecoverableRunError.message,
2149
+ errorCode:
2150
+ lastRecoverableRunError.errorCode ?? "connection_error",
2151
+ details: lastRecoverableRunError.details,
2152
+ });
2153
+ return;
2154
+ }
2155
+ yield* emitBackgroundTerminalError({
2156
+ message:
2157
+ "The agent's background run stopped before finishing and no continuation appeared. You can retry from the preserved chat context.",
2158
+ errorCode: terminalReason || "background_run_lost",
2159
+ details: rawTerminalReason
2160
+ ? `terminal_reason: ${rawTerminalReason}`
2161
+ : undefined,
2162
+ });
2163
+ };
2164
+
2165
+ // One read-only attach to the current run's event stream. Unlike
2166
+ // reconnectCurrentRun this does NOT retry internally and lets the
2167
+ // follow loop see every auto-continue signal (with errorInfo intact),
2168
+ // because the loop — not this reader — decides what a detach means.
2169
+ // Return values: "completed" = terminal event consumed (turn over);
2170
+ // "aborted" = user abort; "detached" = we ATTACHED to a live stream
2171
+ // that ended/stalled without a terminal event (resets the follow
2172
+ // loop's idle window); "gone" = could not attach at all (404, HTTP
2173
+ // error, network failure — the idle window keeps accumulating so a
2174
+ // persistently unattachable run still terminates loudly).
2175
+ const followAttachOnce = async function* (): AsyncGenerator<
2176
+ ChatModelRunResult,
2177
+ "completed" | "aborted" | "detached" | "gone",
2178
+ unknown
2179
+ > {
2180
+ if (!runId) return "gone";
2181
+ let attached = false;
2182
+ try {
2183
+ const eventsRes = await fetch(
2184
+ `${apiUrl}/runs/${encodeURIComponent(runId)}/events?after=${lastSeq + 1}`,
2185
+ { signal: abortSignal },
2186
+ );
2187
+ if (!eventsRes.ok || !eventsRes.body) {
2188
+ return "gone";
2189
+ }
2190
+ attached = true;
2191
+ updateCurrentRunDispatchMode(
2192
+ eventsRes.headers.get("X-Dispatch-Mode"),
2193
+ );
2194
+ for await (const result of readSSEStream(
2195
+ eventsRes.body,
2196
+ content,
2197
+ toolCallCounter,
2198
+ tabId,
2199
+ (seq) => {
2200
+ rememberRunSeq(seq);
2201
+ if (threadId) updateActiveRunSeq(seq);
2202
+ },
2203
+ runId,
2204
+ currentSSEOptions(),
2205
+ )) {
2206
+ yield withRequestModeMetadata(result);
2207
+ }
2208
+ // readSSEStream returned normally: a terminal done/error was
2209
+ // consumed and rendered — the turn is over.
2210
+ clearActiveRun();
2211
+ return "completed";
2212
+ } catch (attachErr: unknown) {
2213
+ if (attachErr instanceof Error && attachErr.name === "AbortError") {
2214
+ clearActiveRun();
2215
+ return "aborted";
2216
+ }
2217
+ if (attachErr instanceof AgentAutoContinueSignal) {
2218
+ lastAutoContinueReason = attachErr.reason;
2219
+ if (attachErr.activityTrail.length > 0) {
2220
+ lastActivityTrail = [...attachErr.activityTrail];
2221
+ }
2222
+ if (attachErr.errorInfo) {
2223
+ lastRecoverableRunError = attachErr.errorInfo;
2224
+ }
2225
+ }
2226
+ // A fetch that never yielded a response body counts as "gone" for
2227
+ // the idle window; a broken mid-stream read counts as attached.
2228
+ return attached ? "detached" : "gone";
2229
+ }
2230
+ };
2231
+
2232
+ // The background follow loop. Entered instead of ANY synthetic
2233
+ // continuation POST when the current run is background-dispatched.
2234
+ // Keeps following as long as an active run for this turn exists — the
2235
+ // server keeps chaining until its own per-turn budget or a real
2236
+ // terminal error, so a successor that stalls again is simply
2237
+ // re-followed, not counted against any client budget. Only a
2238
+ // continuous BACKGROUND_FOLLOW_IDLE_TIMEOUT_MS with no active run
2239
+ // ends the turn from the client side.
2240
+ const followBackgroundTurn = async function* (
2241
+ initialSignal: AgentAutoContinueSignal,
2242
+ ): AsyncGenerator<ChatModelRunResult, void, unknown> {
2243
+ lastAutoContinueReason = initialSignal.reason;
2244
+ if (initialSignal.activityTrail.length > 0) {
2245
+ lastActivityTrail = [...initialSignal.activityTrail];
2246
+ }
2247
+ if (initialSignal.errorInfo) {
2248
+ lastRecoverableRunError = initialSignal.errorInfo;
2249
+ }
2250
+ // Show "Resuming…" instead of a frozen Thinking label while waiting
2251
+ // for the server-chained successor chunk.
2252
+ const dispatchResumingUiEvent = () => {
2253
+ if (typeof window !== "undefined") {
2254
+ window.dispatchEvent(
2255
+ new CustomEvent("agent-chat:auto-continue", {
2256
+ detail: { tabId },
2257
+ }),
2258
+ );
2259
+ }
2260
+ };
2261
+ dispatchResumingUiEvent();
2262
+
2263
+ let idleSince: number | null = null;
2264
+ let lastSeenActive: Record<string, unknown> | null = null;
2265
+ // Terminal runs already replayed once. A recoverable terminal error
2266
+ // event replays as an auto-continue signal (isAutoRecoverableError),
2267
+ // which used to re-drive a POST in foreground mode. In follow mode
2268
+ // the server owns recovery, so a SECOND visit to the same terminal
2269
+ // run means the turn is over — surface the outcome instead of
2270
+ // re-replaying it every poll for the whole reconnect window.
2271
+ const replayedTerminalRunIds = new Set<string>();
2272
+
2273
+ while (true) {
2274
+ if (abortSignal.aborted) {
2275
+ clearActiveRun();
2276
+ return;
2277
+ }
2278
+ let active: Record<string, unknown> | null = null;
2279
+ try {
2280
+ const activeRes = await fetch(
2281
+ `${apiUrl}/runs/active?threadId=${encodeURIComponent(threadId!)}`,
2282
+ { signal: abortSignal },
2283
+ );
2284
+ if (activeRes.ok) {
2285
+ active = await activeRes.json().catch(() => null);
2286
+ }
2287
+ } catch (pollErr: unknown) {
2288
+ if (pollErr instanceof Error && pollErr.name === "AbortError") {
2289
+ clearActiveRun();
2290
+ return;
2291
+ }
2292
+ // Transient poll failure — counts as "no active run" this tick.
2293
+ }
2294
+
2295
+ const activeRunId =
2296
+ active?.active === true && active.runId
2297
+ ? String(active.runId)
2298
+ : null;
2299
+ const activeTurnId =
2300
+ typeof active?.turnId === "string" ? active.turnId : "";
2301
+ const activeStatus =
2302
+ typeof active?.status === "string" ? active.status : "";
2303
+ // Only follow runs belonging to THIS turn (server-chained
2304
+ // successors reuse the turnId) or runs we already attached to.
2305
+ const isOurRun =
2306
+ activeRunId !== null &&
2307
+ (attemptedRunIds.includes(activeRunId) ||
2308
+ !activeTurnId ||
2309
+ activeTurnId === turnId);
2310
+
2311
+ if (activeRunId && isOurRun) {
2312
+ lastSeenActive = active;
2313
+ updateCurrentRunDispatchMode(active?.dispatchMode);
2314
+ const isTerminal =
2315
+ activeStatus === "completed" || activeStatus === "errored";
2316
+ if (isTerminal && replayedTerminalRunIds.has(activeRunId)) {
2317
+ yield* emitBackgroundTerminalOutcome(active);
2318
+ return;
2319
+ }
2320
+ const previousRunId = runId;
2321
+ runId = activeRunId;
2322
+ if (!attemptedRunIds.includes(activeRunId)) {
2323
+ attemptedRunIds.push(activeRunId);
2324
+ }
2325
+ reconnectCursorForRun(activeRunId, previousRunId);
2326
+ if (threadId) {
2327
+ setActiveRun({ threadId, runId: activeRunId, lastSeq });
2328
+ }
2329
+ const attach = yield* followAttachOnce();
2330
+ if (attach === "completed" || attach === "aborted") {
2331
+ return;
2332
+ }
2333
+ if (isTerminal) {
2334
+ replayedTerminalRunIds.add(activeRunId);
2335
+ }
2336
+ if (attach === "detached") {
2337
+ // We really attached to a live stream (chunk boundary or
2338
+ // transport blip) — the run demonstrably exists; keep
2339
+ // following with a fresh idle window.
2340
+ idleSince = null;
2341
+ } else {
2342
+ // "gone": /runs/active reported the run but its event stream
2343
+ // 404s (reaped row / cross-isolate lag). Let the idle window
2344
+ // ACCUMULATE instead of resetting, so a persistently
2345
+ // inconsistent state still terminates loudly instead of
2346
+ // hot-looping for the whole reconnect window.
2347
+ if (idleSince === null) idleSince = Date.now();
2348
+ if (
2349
+ Date.now() - idleSince >=
2350
+ BACKGROUND_FOLLOW_IDLE_TIMEOUT_MS
2351
+ ) {
2352
+ yield* emitBackgroundTerminalOutcome(lastSeenActive);
2353
+ return;
2354
+ }
2355
+ }
2356
+ dispatchResumingUiEvent();
2357
+ } else {
2358
+ if (idleSince === null) idleSince = Date.now();
2359
+ if (Date.now() - idleSince >= BACKGROUND_FOLLOW_IDLE_TIMEOUT_MS) {
2360
+ captureChatClientError(
2361
+ new Error(
2362
+ "Background run went idle with no successor run appearing",
2363
+ ),
2364
+ "background-follow-idle",
2365
+ {
2366
+ lastSeenStatus:
2367
+ typeof lastSeenActive?.status === "string"
2368
+ ? lastSeenActive.status
2369
+ : null,
2370
+ },
2371
+ );
2372
+ yield* emitBackgroundTerminalOutcome(lastSeenActive);
2373
+ return;
2374
+ }
2375
+ }
2376
+ await delay(BACKGROUND_FOLLOW_POLL_INTERVAL_MS, abortSignal);
2377
+ if (abortSignal.aborted) {
2378
+ clearActiveRun();
2379
+ return;
2380
+ }
2381
+ }
2382
+ };
2383
+
1932
2384
  const visibleContentForContinuation = (): ContentPart[] => {
1933
2385
  return contentAfterContinuationPrefix(
1934
2386
  content,
@@ -2506,6 +2958,17 @@ export function createAgentChatAdapter(
2506
2958
  }
2507
2959
 
2508
2960
  if (err instanceof AgentAutoContinueSignal) {
2961
+ // Background-dispatched runs: the server chains continuations
2962
+ // itself (successor row pre-inserted before the old chunk
2963
+ // completes). Never POST a synthetic continuation and never
2964
+ // abort the live server-side run — switch to read-only
2965
+ // following of server state instead. This is the fix for the
2966
+ // client/server recovery race: client watchdog signals here are
2967
+ // just "reattach", not "recover".
2968
+ if (isBackgroundDispatch() && threadId) {
2969
+ yield* followBackgroundTurn(err);
2970
+ return;
2971
+ }
2509
2972
  if (err.reason === "no_progress") {
2510
2973
  await abortCurrentRun();
2511
2974
  }
@@ -2676,6 +3139,18 @@ export function createAgentChatAdapter(
2676
3139
  const activeReconnected = yield* reconnectActiveRunForThread();
2677
3140
  if (activeReconnected) return;
2678
3141
 
3142
+ // Background-dispatched run whose transport failed and could not
3143
+ // be reconnected above: follow server state instead of the
3144
+ // synthetic-continuation POST below. (An initial POST that failed
3145
+ // before any response headers arrived has no dispatch mode yet
3146
+ // and keeps the foreground startup-retry path.)
3147
+ if (isBackgroundDispatch() && threadId) {
3148
+ yield* followBackgroundTurn(
3149
+ new AgentAutoContinueSignal({ reason: "stream_ended" }),
3150
+ );
3151
+ return;
3152
+ }
3153
+
2679
3154
  if (err instanceof AgentStartupTimeoutError) {
2680
3155
  if (startupRecoveryAttempts < MAX_STARTUP_RECOVERY_ATTEMPTS) {
2681
3156
  await retryDelay(startupRecoveryAttempts++, abortSignal);
@@ -13,6 +13,8 @@ import type {
13
13
  import { scrubUrl } from "./url-scrub.js";
14
14
  export { scrubUrl } from "./url-scrub.js";
15
15
  export type {
16
+ SessionReplayConsoleOptions,
17
+ SessionReplayNetworkOptions,
16
18
  SessionReplayOptions,
17
19
  SessionReplayStartResult,
18
20
  SessionReplayUrlMatcher,
@@ -90,6 +90,10 @@ function isProviderAuthenticationError(
90
90
  return (
91
91
  code === "authentication_error" ||
92
92
  code === "http_401" ||
93
+ /^401 status code(?:\s*\(no body\))?$/i.test(text) ||
94
+ /\b(?:http\s*)?401\b.*\b(?:status|unauthorized|authentication|auth|no body)\b/i.test(
95
+ text,
96
+ ) ||
93
97
  lower.includes("invalid x-api-key") ||
94
98
  lower.includes("invalid api key") ||
95
99
  lower.includes("incorrect api key") ||
@@ -940,7 +940,7 @@ export function ExtensionsSidebarSection() {
940
940
  <PopoverTrigger asChild>
941
941
  <button
942
942
  type="button"
943
- className="inline-flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground/70 transition-colors hover:bg-accent hover:text-accent-foreground"
943
+ className="pointer-events-none inline-flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground/70 opacity-0 transition-all hover:bg-accent hover:text-accent-foreground focus-visible:pointer-events-auto focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-hover/extensions-section:pointer-events-auto group-hover/extensions-section:opacity-100 group-focus-within/extensions-section:pointer-events-auto group-focus-within/extensions-section:opacity-100 data-[state=open]:pointer-events-auto data-[state=open]:opacity-100"
944
944
  aria-label={copy.newExtension}
945
945
  >
946
946
  <IconPlus className="h-3.5 w-3.5" />