@agent-native/core 0.84.67 → 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 (145) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +14 -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/core-routes-plugin.ts +91 -4
  30. package/corpus/core/src/server/http-response-telemetry.ts +189 -0
  31. package/corpus/core/src/server/self-dispatch.ts +29 -0
  32. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +53 -1
  33. package/corpus/templates/analytics/AGENTS.md +13 -0
  34. package/corpus/templates/analytics/actions/view-screen.ts +25 -0
  35. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +20 -0
  36. package/corpus/templates/analytics/app/global.css +55 -0
  37. package/corpus/templates/analytics/app/i18n/zh-TW.ts +100 -1
  38. package/corpus/templates/analytics/app/i18n-data.ts +1226 -0
  39. package/corpus/templates/analytics/app/pages/Settings.tsx +3 -0
  40. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +182 -144
  41. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +11 -18
  42. package/corpus/templates/analytics/app/pages/analyses/AnalysisDetail.tsx +11 -18
  43. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +49 -1
  44. package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +557 -0
  45. package/corpus/templates/analytics/app/pages/sessions/session-replay-devtools.ts +274 -0
  46. package/corpus/templates/analytics/app/pages/settings/AlertRulesSettingsCard.tsx +974 -0
  47. package/corpus/templates/analytics/changelog/2026-07-02-alert-rules-can-now-be-viewed-and-managed-from-settings.md +6 -0
  48. package/corpus/templates/analytics/changelog/2026-07-02-recent-ask-chats-show-a-loading-placeholder-while-your-chat-.md +6 -0
  49. package/corpus/templates/analytics/changelog/2026-07-02-session-replays-now-capture-console-logs-and-network-request.md +6 -0
  50. package/corpus/templates/analytics/changelog/2026-07-02-sharing-status-labels-use-neutral-icons-instead-of-colored-d.md +6 -0
  51. package/corpus/templates/analytics/changelog/2026-07-02-the-session-replay-viewer-has-a-dev-tools-panel-with-console.md +6 -0
  52. package/corpus/templates/analytics/server/db/schema.ts +3 -0
  53. package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +4 -0
  54. package/corpus/templates/analytics/server/lib/analytics-alerts.ts +142 -1
  55. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +369 -8
  56. package/corpus/templates/analytics/server/lib/session-replay.ts +76 -2
  57. package/corpus/templates/analytics/server/routes/api/session-replay/agent-diagnostics.json.get.ts +101 -0
  58. package/corpus/templates/analytics/shared/session-replay-diagnostics.ts +78 -0
  59. package/corpus/templates/design/app/pages/DesignEditor.tsx +24 -12
  60. package/corpus/templates/design/changelog/2026-07-02-the-temporary-code-tab-is-hidden-from-the-design-editor-side.md +6 -0
  61. package/corpus/templates/plan/actions/create-visual-recap.ts +1 -1
  62. package/corpus/templates/plan/actions/update-local-plan-folder.ts +3 -1
  63. package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
  64. package/corpus/templates/plan/changelog/2026-07-02-trying-to-edit-a-recap-or-plan-you-can-only-view-now-explain.md +6 -0
  65. package/corpus/templates/plan/server/plans.ts +26 -9
  66. package/dist/agent/production-agent.d.ts.map +1 -1
  67. package/dist/agent/production-agent.js +155 -34
  68. package/dist/agent/production-agent.js.map +1 -1
  69. package/dist/agent/run-manager.d.ts +29 -0
  70. package/dist/agent/run-manager.d.ts.map +1 -1
  71. package/dist/agent/run-manager.js +73 -0
  72. package/dist/agent/run-manager.js.map +1 -1
  73. package/dist/agent/run-store.d.ts +41 -0
  74. package/dist/agent/run-store.d.ts.map +1 -1
  75. package/dist/agent/run-store.js +100 -4
  76. package/dist/agent/run-store.js.map +1 -1
  77. package/dist/agent/types.d.ts +13 -0
  78. package/dist/agent/types.d.ts.map +1 -1
  79. package/dist/agent/types.js.map +1 -1
  80. package/dist/client/RunStuckBanner.d.ts.map +1 -1
  81. package/dist/client/RunStuckBanner.js +13 -0
  82. package/dist/client/RunStuckBanner.js.map +1 -1
  83. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  84. package/dist/client/agent-chat-adapter.js +410 -0
  85. package/dist/client/agent-chat-adapter.js.map +1 -1
  86. package/dist/client/analytics.d.ts +1 -1
  87. package/dist/client/analytics.d.ts.map +1 -1
  88. package/dist/client/analytics.js.map +1 -1
  89. package/dist/client/error-format.d.ts.map +1 -1
  90. package/dist/client/error-format.js +2 -0
  91. package/dist/client/error-format.js.map +1 -1
  92. package/dist/client/extensions/ExtensionsSidebarSection.js +1 -1
  93. package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
  94. package/dist/client/session-replay.d.ts +36 -0
  95. package/dist/client/session-replay.d.ts.map +1 -1
  96. package/dist/client/session-replay.js +579 -1
  97. package/dist/client/session-replay.js.map +1 -1
  98. package/dist/client/sse-event-processor.d.ts +20 -1
  99. package/dist/client/sse-event-processor.d.ts.map +1 -1
  100. package/dist/client/sse-event-processor.js +42 -11
  101. package/dist/client/sse-event-processor.js.map +1 -1
  102. package/dist/client/use-run-stuck-detection.d.ts +14 -4
  103. package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
  104. package/dist/client/use-run-stuck-detection.js +18 -4
  105. package/dist/client/use-run-stuck-detection.js.map +1 -1
  106. package/dist/collab/awareness.d.ts +2 -2
  107. package/dist/collab/awareness.d.ts.map +1 -1
  108. package/dist/collab/routes.d.ts +2 -2
  109. package/dist/db/runtime-diagnostics.d.ts +52 -0
  110. package/dist/db/runtime-diagnostics.d.ts.map +1 -0
  111. package/dist/db/runtime-diagnostics.js +250 -0
  112. package/dist/db/runtime-diagnostics.js.map +1 -0
  113. package/dist/notifications/routes.d.ts +1 -1
  114. package/dist/observability/routes.d.ts +3 -3
  115. package/dist/progress/routes.d.ts +1 -1
  116. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  117. package/dist/resources/handlers.d.ts +1 -1
  118. package/dist/secrets/routes.d.ts +3 -3
  119. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  120. package/dist/server/agent-chat-plugin.js +101 -2
  121. package/dist/server/agent-chat-plugin.js.map +1 -1
  122. package/dist/server/core-routes-plugin.d.ts +17 -0
  123. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  124. package/dist/server/core-routes-plugin.js +65 -5
  125. package/dist/server/core-routes-plugin.js.map +1 -1
  126. package/dist/server/http-response-telemetry.d.ts +4 -0
  127. package/dist/server/http-response-telemetry.d.ts.map +1 -0
  128. package/dist/server/http-response-telemetry.js +174 -0
  129. package/dist/server/http-response-telemetry.js.map +1 -0
  130. package/dist/server/self-dispatch.d.ts +17 -0
  131. package/dist/server/self-dispatch.d.ts.map +1 -1
  132. package/dist/server/self-dispatch.js +11 -0
  133. package/dist/server/self-dispatch.js.map +1 -1
  134. package/docs/content/locales/ar-SA/tracking.mdx +55 -0
  135. package/docs/content/locales/de-DE/tracking.mdx +55 -0
  136. package/docs/content/locales/es-ES/tracking.mdx +55 -0
  137. package/docs/content/locales/fr-FR/tracking.mdx +55 -0
  138. package/docs/content/locales/hi-IN/tracking.mdx +55 -0
  139. package/docs/content/locales/ja-JP/tracking.mdx +55 -0
  140. package/docs/content/locales/ko-KR/tracking.mdx +55 -0
  141. package/docs/content/locales/pt-BR/tracking.mdx +55 -0
  142. package/docs/content/locales/zh-CN/tracking.mdx +55 -0
  143. package/docs/content/locales/zh-TW/tracking.mdx +55 -0
  144. package/docs/content/tracking.mdx +2 -0
  145. package/package.json +1 -1
@@ -8088,6 +8088,9 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
8088
8088
  stages: m.RUN_DIAG_STAGE,
8089
8089
  }))
8090
8090
  .catch(() => null);
8091
+ const runtimeDetail = await import("../db/runtime-diagnostics.js")
8092
+ .then((m) => m.formatRuntimeDebugFingerprint())
8093
+ .catch(() => "");
8091
8094
 
8092
8095
  // Record "the route handler was entered" against the run after auth
8093
8096
  // succeeds. This is the proof the bg-fn invocation actually reached
@@ -8097,14 +8100,74 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
8097
8100
  .record(prepared.runId, diag.stages.routeEntered)
8098
8101
  .catch(() => {});
8099
8102
  await diag
8100
- .record(prepared.runId, diag.stages.authPassed)
8103
+ .record(prepared.runId, diag.stages.authPassed, runtimeDetail)
8101
8104
  .catch(() => {});
8102
8105
  }
8103
8106
 
8107
+ // PAYLOAD REHYDRATION: a `payloadRef` marker means the dispatch
8108
+ // carried ONLY the marker (Netlify caps background-function request
8109
+ // bodies at 256KB — a large chat history silently exceeded it) and
8110
+ // the full request body is persisted on the run row. Rehydrate it
8111
+ // here. The payload is NOT cleared on read: a Netlify at-least-once
8112
+ // retry of a failed invocation must be able to rehydrate again (the
8113
+ // claim CAS still dedupes execution); terminal status writes clear it.
8114
+ let workerBody: Record<string, unknown> = prepared.body;
8115
+ const preparedMarkerRecord =
8116
+ preparedMarker && typeof preparedMarker === "object"
8117
+ ? (preparedMarker as Record<string, unknown>)
8118
+ : null;
8119
+ if (preparedMarkerRecord?.payloadRef === true) {
8120
+ const runStore = await import("../agent/run-store.js");
8121
+ const rawPayload = await runStore
8122
+ .readRunDispatchPayload(prepared.runId)
8123
+ .catch(() => null);
8124
+ let parsedPayload: Record<string, unknown> | null = null;
8125
+ if (rawPayload) {
8126
+ try {
8127
+ const candidate = JSON.parse(rawPayload);
8128
+ if (candidate && typeof candidate === "object") {
8129
+ parsedPayload = candidate as Record<string, unknown>;
8130
+ }
8131
+ } catch {
8132
+ // Corrupt payload — treated as missing below.
8133
+ }
8134
+ }
8135
+ if (!parsedPayload) {
8136
+ // Row missing / reaped / already terminal — there is nothing to
8137
+ // run. Fail the run loudly (if it is still running) and ack the
8138
+ // dispatch with a 200 so Netlify does not retry a dead handoff.
8139
+ if (diag) {
8140
+ await diag
8141
+ .record(
8142
+ prepared.runId,
8143
+ diag.stages.workerThrew,
8144
+ "dispatch payload missing — cannot rehydrate background run body",
8145
+ )
8146
+ .catch(() => {});
8147
+ }
8148
+ const statusUpdated = await runStore
8149
+ .updateRunStatusIfRunning(prepared.runId, "errored")
8150
+ .catch(() => false);
8151
+ if (statusUpdated) {
8152
+ await runStore
8153
+ .setRunTerminalReason(
8154
+ prepared.runId,
8155
+ "dispatch_payload_missing",
8156
+ )
8157
+ .catch(() => {});
8158
+ }
8159
+ return { ok: false, skipped: "dispatch-payload-missing" };
8160
+ }
8161
+ workerBody = {
8162
+ ...parsedPayload,
8163
+ [AGENT_CHAT_BACKGROUND_RUN_FIELD]: preparedMarker,
8164
+ };
8165
+ }
8166
+
8104
8167
  // Stash the verified+augmented body for the handler — the body stream
8105
8168
  // is already consumed, so the handler reads this instead.
8106
8169
  (event as any).context = (event as any).context ?? {};
8107
- (event as any).context.__agentChatBackgroundBody = prepared.body;
8170
+ (event as any).context.__agentChatBackgroundBody = workerBody;
8108
8171
 
8109
8172
  // Durable owner context: this self-dispatch is cookieless (HMAC-only).
8110
8173
  // Resolve the owner from the persisted run row, never the request
@@ -8287,6 +8350,56 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
8287
8350
  }, 15_000); // Start 15s after init (after the scheduler)
8288
8351
  })();
8289
8352
 
8353
+ // ─── Unclaimed background-run sweep ────────────────────────────────
8354
+ // Backstop for LOST background handoffs. The foreground circuit-breaker
8355
+ // covers the initial dispatch (a connected client is polling the claim),
8356
+ // but a server-chained CONTINUATION handoff has no foreground watching
8357
+ // it: if the dispatch is lost after the successor row was inserted, the
8358
+ // row would sit at dispatch_mode='background' forever and the turn hangs
8359
+ // silently. This sweep reaps such rows into a loud, attributable error
8360
+ // (`background_worker_never_started`) that the client renders, instead
8361
+ // of an idle spinner. Cheap: one indexed-ish query per 2-min window.
8362
+ (() => {
8363
+ let lastSweep = 0;
8364
+ const SWEEP_INTERVAL_MS = 2 * 60 * 1000;
8365
+
8366
+ setTimeout(() => {
8367
+ setInterval(() => {
8368
+ const now = Date.now();
8369
+ if (now - lastSweep < SWEEP_INTERVAL_MS) return;
8370
+ lastSweep = now;
8371
+
8372
+ (async () => {
8373
+ const {
8374
+ listUnclaimedBackgroundRunIds,
8375
+ reapUnclaimedBackgroundRun,
8376
+ } = await import("../agent/run-store.js");
8377
+ let runIds: string[];
8378
+ try {
8379
+ runIds = await listUnclaimedBackgroundRunIds();
8380
+ } catch {
8381
+ return; // Table may not exist yet on first boot
8382
+ }
8383
+ for (const staleRunId of runIds) {
8384
+ try {
8385
+ const reaped = await reapUnclaimedBackgroundRun(staleRunId);
8386
+ if (reaped) {
8387
+ console.error(
8388
+ "[agent-chat] swept unclaimed background run (handoff lost):",
8389
+ staleRunId,
8390
+ );
8391
+ }
8392
+ } catch {
8393
+ // best-effort per run
8394
+ }
8395
+ }
8396
+ })().catch(() => {
8397
+ // best-effort — never break the server
8398
+ });
8399
+ }, 30_000); // Check every 30s but only sweep once per 2min
8400
+ }, 20_000); // Start 20s after init (after the agent-teams sweep)
8401
+ })();
8402
+
8290
8403
  // ─── Trigger Dispatcher (event-based automations) ─────────────────
8291
8404
  if (disableRecurringJobsRuntime) {
8292
8405
  if (process.env.DEBUG) {
@@ -45,6 +45,12 @@ import {
45
45
  import { mountBrowserSessionRoutes } from "../browser-sessions/routes.js";
46
46
  import { mountDbAdminRoutes } from "../db-admin/routes.js";
47
47
  import { getDbExec } from "../db/client.js";
48
+ import {
49
+ getDatabaseRuntimeFingerprint,
50
+ getRuntimeDebugFingerprint,
51
+ runDatabaseSchemaHealthCheck,
52
+ type DatabaseSchemaHealthResult,
53
+ } from "../db/runtime-diagnostics.js";
48
54
  import {
49
55
  uploadFile,
50
56
  getActiveFileUploadProviderForRequest,
@@ -143,6 +149,7 @@ import {
143
149
  DEFAULT_UPLOAD_MAX_FILE_BYTES,
144
150
  isAllowedUploadMimeType,
145
151
  } from "./h3-helpers.js";
152
+ import { createHttpResponseTelemetryMiddleware } from "./http-response-telemetry.js";
146
153
  import { isIdentitySsoEnabled } from "./identity-sso-store.js";
147
154
  import { handleIdentitySso } from "./identity-sso.js";
148
155
  import { createOpenRouteHandler } from "./open-route.js";
@@ -204,10 +211,24 @@ export function getFrameworkEnvKeys(): EnvKeyConfig[] {
204
211
  export interface DbHealthProbeResult {
205
212
  /** The serverless function is live and served the request. */
206
213
  ok: true;
214
+ /** Database + optional schema readiness for stricter production monitors. */
215
+ ready: boolean;
207
216
  /** A trivial `SELECT 1` reached the database (false = no DB or unreachable). */
208
217
  db: boolean;
209
218
  /** Round-trip time of the probe in milliseconds. */
210
219
  ms: number;
220
+ /** Redacted database routing details useful for deploy/runtime checks. */
221
+ database: {
222
+ configured: boolean;
223
+ source: string;
224
+ dialect: string;
225
+ urlHash?: string;
226
+ appName?: string;
227
+ authTokenConfigured: boolean;
228
+ netlifyDatabaseUrlConfigured: boolean;
229
+ };
230
+ /** Optional metadata-only schema compatibility check. */
231
+ schema?: DatabaseSchemaHealthResult;
211
232
  }
212
233
 
213
234
  /**
@@ -222,16 +243,40 @@ export interface DbHealthProbeResult {
222
243
  */
223
244
  export async function runDbHealthProbe(
224
245
  exec: () => { execute: (sql: string) => Promise<unknown> } = getDbExec,
246
+ options: { schema?: boolean } = {},
225
247
  ): Promise<DbHealthProbeResult> {
226
248
  const startedAt = Date.now();
227
249
  let db = false;
250
+ let schema: DatabaseSchemaHealthResult | undefined;
251
+ const dbExec = exec();
228
252
  try {
229
- await exec().execute("SELECT 1");
253
+ await dbExec.execute("SELECT 1");
230
254
  db = true;
231
255
  } catch {
232
256
  // Live even when the DB is unreachable or the app has no database.
233
257
  }
234
- return { ok: true, db, ms: Date.now() - startedAt };
258
+ if (db && options.schema) {
259
+ schema = await runDatabaseSchemaHealthCheck({
260
+ exec: dbExec as ReturnType<typeof getDbExec>,
261
+ });
262
+ }
263
+ const database = getDatabaseRuntimeFingerprint();
264
+ return {
265
+ ok: true,
266
+ ready: db && (!schema || schema.ok),
267
+ db,
268
+ ms: Date.now() - startedAt,
269
+ database: {
270
+ configured: database.configured,
271
+ source: database.source,
272
+ dialect: database.dialect,
273
+ urlHash: database.urlHash,
274
+ appName: database.appName,
275
+ authTokenConfigured: database.authTokenConfigured,
276
+ netlifyDatabaseUrlConfigured: database.netlifyDatabaseUrlConfigured,
277
+ },
278
+ ...(schema ? { schema } : {}),
279
+ };
235
280
  }
236
281
  const DEFAULT_BUILDER_WAITLIST_FORM_ID = "DYTHuM0jlV";
237
282
  const DEFAULT_BUILDER_WAITLIST_FORMS_ORIGIN = "https://forms.agent-native.com";
@@ -863,6 +908,8 @@ export function createCoreRoutesPlugin(
863
908
 
864
909
  const P = FRAMEWORK_ROUTE_PREFIX;
865
910
 
911
+ getH3App(nitroApp).use(createHttpResponseTelemetryMiddleware());
912
+
866
913
  // Security response headers — emitted on every framework response.
867
914
  // Mounted before route handlers so 4xx/5xx error pages also carry the
868
915
  // headers. Routes that need to tighten a specific header override via
@@ -1071,17 +1118,57 @@ export function createCoreRoutesPlugin(
1071
1118
  // Health + DB warmup — liveness probe that touches the database so
1072
1119
  // uptime monitors and the keep-warm cron prevent a scale-to-zero
1073
1120
  // serverless DB (e.g. Neon) from cold-starting on the next real
1074
- // request. Public, side-effect free, and never cached.
1121
+ // request. Public, side-effect free, and never cached. Add ?schema=1
1122
+ // for metadata-only schema checks, and ?strict=1 to turn a not-ready
1123
+ // DB/schema probe into a failing HTTP status for monitors.
1075
1124
  if (!options.disableHealth) {
1076
1125
  getH3App(nitroApp).use(
1077
1126
  `${P}/health`,
1078
1127
  defineEventHandler(async (event) => {
1079
1128
  setResponseHeader(event, "cache-control", "no-store");
1080
- return runDbHealthProbe();
1129
+ const schema =
1130
+ event.url?.searchParams.get("schema") === "1" ||
1131
+ event.url?.searchParams.get("schema") === "true";
1132
+ const strict =
1133
+ event.url?.searchParams.get("strict") === "1" ||
1134
+ event.url?.searchParams.get("strict") === "true" ||
1135
+ process.env.AGENT_NATIVE_HEALTH_STRICT_SCHEMA === "true";
1136
+ const result = await runDbHealthProbe(getDbExec, { schema });
1137
+ if (strict && !result.ready) setResponseStatus(event, 503);
1138
+ return result;
1081
1139
  }),
1082
1140
  );
1083
1141
  }
1084
1142
 
1143
+ getH3App(nitroApp).use(
1144
+ `${P}/debug/runtime`,
1145
+ defineEventHandler(async (event) => {
1146
+ setResponseHeader(event, "cache-control", "no-store");
1147
+ const session = await getSession(event).catch(() => null);
1148
+ const productionLike =
1149
+ process.env.NODE_ENV === "production" ||
1150
+ process.env.NETLIFY === "true" ||
1151
+ process.env.VERCEL === "1";
1152
+ if (!session?.email && productionLike) {
1153
+ setResponseStatus(event, 401);
1154
+ return { error: "Authentication required" };
1155
+ }
1156
+ const schema = await runDatabaseSchemaHealthCheck().catch((err) => ({
1157
+ ok: false,
1158
+ checked: false,
1159
+ dialect: getDatabaseRuntimeFingerprint().dialect,
1160
+ missingTables: [],
1161
+ missingColumns: [],
1162
+ error: err instanceof Error ? err.message : String(err),
1163
+ }));
1164
+ return {
1165
+ ok: true,
1166
+ runtime: getRuntimeDebugFingerprint(),
1167
+ schema,
1168
+ };
1169
+ }),
1170
+ );
1171
+
1085
1172
  getH3App(nitroApp).use(
1086
1173
  `${P}/speculation-rules.json`,
1087
1174
  defineEventHandler((event) => {
@@ -0,0 +1,189 @@
1
+ import { defineEventHandler, getHeader, getMethod } from "h3";
2
+ import type { EventHandler, H3Event } from "h3";
3
+
4
+ import { getDatabaseRuntimeFingerprint } from "../db/runtime-diagnostics.js";
5
+ import { track } from "../tracking/index.js";
6
+ import { getAppName } from "./app-name.js";
7
+
8
+ const TELEMETRY_EVENT_NAME = "http.response";
9
+ const TRACKING_INGEST_PATHS = new Set([
10
+ "/track",
11
+ "/api/analytics/track",
12
+ "/api/events/track",
13
+ "/_agent-native/track",
14
+ ]);
15
+
16
+ function envValue(key: string): string | undefined {
17
+ const value = process.env[key]?.trim();
18
+ return value || undefined;
19
+ }
20
+
21
+ function boolEnv(key: string): boolean {
22
+ return ["1", "true", "yes", "on"].includes(
23
+ (process.env[key] ?? "").trim().toLowerCase(),
24
+ );
25
+ }
26
+
27
+ function sampleRate(): number {
28
+ const raw = envValue("AGENT_NATIVE_HTTP_TELEMETRY_SAMPLE_RATE");
29
+ if (!raw) return 1;
30
+ const parsed = Number.parseFloat(raw);
31
+ if (!Number.isFinite(parsed)) return 1;
32
+ return Math.max(0, Math.min(1, parsed));
33
+ }
34
+
35
+ function shouldDisableTelemetry(): boolean {
36
+ return boolEnv("AGENT_NATIVE_HTTP_TELEMETRY_DISABLED");
37
+ }
38
+
39
+ function requestPath(event: H3Event): string {
40
+ const raw =
41
+ event.url?.pathname ??
42
+ String(event.node?.req?.url ?? event.path ?? "/").split("?")[0] ??
43
+ "/";
44
+ return raw || "/";
45
+ }
46
+
47
+ function normalizeSegment(segment: string): string {
48
+ if (!segment) return segment;
49
+ if (/^[0-9]+$/.test(segment)) return ":id";
50
+ if (/^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(segment)) return ":id";
51
+ if (
52
+ /^(run|turn|thread|design|screen|file|msg|key|tok)_[a-z0-9_-]+$/i.test(
53
+ segment,
54
+ )
55
+ ) {
56
+ return ":id";
57
+ }
58
+ if (/^(run|turn)-[0-9]{10,}-[a-z0-9]+$/i.test(segment)) return ":id";
59
+ if (segment.length > 36 && /^[a-z0-9_-]+$/i.test(segment)) return ":id";
60
+ return segment;
61
+ }
62
+
63
+ export function normalizeHttpTelemetryPath(pathname: string): string {
64
+ const normalized = pathname.startsWith("/") ? pathname : `/${pathname}`;
65
+ return normalized
66
+ .split("/")
67
+ .map((segment, index) => (index === 0 ? "" : normalizeSegment(segment)))
68
+ .join("/");
69
+ }
70
+
71
+ function statusClass(statusCode: number): string {
72
+ if (!Number.isFinite(statusCode) || statusCode < 100) return "unknown";
73
+ return `${Math.floor(statusCode / 100)}xx`;
74
+ }
75
+
76
+ function routeKind(pathname: string): string {
77
+ if (pathname === "/_agent-native" || pathname.startsWith("/_agent-native/")) {
78
+ return "framework";
79
+ }
80
+ if (pathname === "/api" || pathname.startsWith("/api/")) return "api";
81
+ if (pathname.startsWith("/.well-known/")) return "well-known";
82
+ return "app";
83
+ }
84
+
85
+ function hostForEvent(event: H3Event): string | undefined {
86
+ return (
87
+ getHeader(event, "x-forwarded-host") ??
88
+ getHeader(event, "host") ??
89
+ undefined
90
+ );
91
+ }
92
+
93
+ function organizationForHost(host: string | undefined): string | undefined {
94
+ const configured =
95
+ envValue("AGENT_NATIVE_ANALYTICS_ORG_NAME") ??
96
+ envValue("AGENT_NATIVE_ORG_NAME");
97
+ if (configured) return configured;
98
+ const normalized = host?.split(":")[0]?.toLowerCase();
99
+ return normalized?.endsWith(".agent-native.com") ||
100
+ normalized === "agent-native.com"
101
+ ? "Builder.io"
102
+ : undefined;
103
+ }
104
+
105
+ function shouldTrack(pathname: string, statusCode: number): boolean {
106
+ if (shouldDisableTelemetry()) return false;
107
+ if (TRACKING_INGEST_PATHS.has(pathname)) return false;
108
+ if (pathname.startsWith("/api/analytics/replay")) return false;
109
+ if (statusCode >= 500) return true;
110
+ const rate = sampleRate();
111
+ if (rate <= 0) return false;
112
+ if (rate >= 1) return true;
113
+ return Math.random() < rate;
114
+ }
115
+
116
+ function responseStatusCode(event: H3Event): number {
117
+ const raw =
118
+ (event.node?.res as any)?.statusCode ??
119
+ (event.node?.res as any)?.status ??
120
+ (event as any).res?.status ??
121
+ 200;
122
+ const parsed = Number(raw);
123
+ return Number.isFinite(parsed) ? parsed : 200;
124
+ }
125
+
126
+ function emitTelemetry(event: H3Event, startedAt: number): void {
127
+ const statusCode = responseStatusCode(event);
128
+ const pathname = requestPath(event);
129
+ if (!shouldTrack(pathname, statusCode)) return;
130
+
131
+ try {
132
+ const host = hostForEvent(event);
133
+ const db = getDatabaseRuntimeFingerprint();
134
+ track(TELEMETRY_EVENT_NAME, {
135
+ source: "server",
136
+ app: getAppName(),
137
+ template: envValue("AGENT_NATIVE_TEMPLATE") ?? getAppName(),
138
+ organization: organizationForHost(host),
139
+ method: getMethod(event),
140
+ path: normalizeHttpTelemetryPath(pathname),
141
+ route_kind: routeKind(pathname),
142
+ status_code: statusCode,
143
+ status_class: statusClass(statusCode),
144
+ duration_ms: Math.max(0, Date.now() - startedAt),
145
+ host,
146
+ environment: envValue("NODE_ENV"),
147
+ deploy_context: envValue("CONTEXT") ?? envValue("VERCEL_ENV"),
148
+ deploy_id: envValue("DEPLOY_ID") ?? envValue("VERCEL_DEPLOYMENT_ID"),
149
+ commit_ref:
150
+ envValue("COMMIT_REF") ??
151
+ envValue("NETLIFY_COMMIT_REF") ??
152
+ envValue("VERCEL_GIT_COMMIT_SHA") ??
153
+ envValue("GIT_COMMIT_SHA"),
154
+ db_source: db.source,
155
+ db_dialect: db.dialect,
156
+ db_url_hash: db.urlHash,
157
+ db_neon_endpoint: db.neon?.endpointId,
158
+ db_neon_pooled: db.neon?.pooled,
159
+ });
160
+ } catch {
161
+ // Response telemetry is best-effort. Never perturb request handling.
162
+ }
163
+ }
164
+
165
+ export function createHttpResponseTelemetryMiddleware(): EventHandler {
166
+ return defineEventHandler((event) => {
167
+ const startedAt = Date.now();
168
+ const res = event.node?.res as
169
+ | {
170
+ once?: (event: "finish" | "close", cb: () => void) => void;
171
+ on?: (event: "finish" | "close", cb: () => void) => void;
172
+ }
173
+ | undefined;
174
+ let emitted = false;
175
+ const emitOnce = () => {
176
+ if (emitted) return;
177
+ emitted = true;
178
+ emitTelemetry(event, startedAt);
179
+ };
180
+
181
+ if (typeof res?.once === "function") {
182
+ res.once("finish", emitOnce);
183
+ res.once("close", emitOnce);
184
+ } else if (typeof res?.on === "function") {
185
+ res.on("finish", emitOnce);
186
+ res.on("close", emitOnce);
187
+ }
188
+ });
189
+ }
@@ -94,6 +94,23 @@ export interface FireInternalDispatchOptions {
94
94
  body?: Record<string, unknown>;
95
95
  /** Max ms to wait for the outbound request to leave the box. Default 250ms. */
96
96
  settleMs?: number;
97
+ /**
98
+ * Await the dispatch response fully instead of racing the settle timer.
99
+ *
100
+ * The 250ms settle race is correct for a synchronous handler that must
101
+ * respond to its own caller quickly — but it is WRONG for a handoff fired
102
+ * from a function that is about to finish (e.g. a background worker chaining
103
+ * its continuation chunk): once the handler's promise resolves, the Lambda
104
+ * freezes and a still-in-flight dispatch fetch is killed WITHOUT rejecting,
105
+ * so the handoff is lost silently — the error path never fires. With
106
+ * `awaitResponse: true` the call resolves only after the target confirmed
107
+ * receipt (Netlify background functions 202 on enqueue, normally well under
108
+ * a second) and throws on any network error or non-2xx, bounded by
109
+ * `responseTimeoutMs`.
110
+ */
111
+ awaitResponse?: boolean;
112
+ /** Max ms to await the dispatch response when `awaitResponse` is set. Default 15s. */
113
+ responseTimeoutMs?: number;
97
114
  }
98
115
 
99
116
  async function dispatchResponseError(
@@ -167,10 +184,14 @@ export async function fireInternalDispatch(
167
184
  }
168
185
  }
169
186
 
187
+ const awaitResponse = options.awaitResponse === true;
170
188
  const dispatchPromise = fetch(url, {
171
189
  method: "POST",
172
190
  headers,
173
191
  body: JSON.stringify({ taskId: options.taskId, ...(options.body ?? {}) }),
192
+ ...(awaitResponse
193
+ ? { signal: AbortSignal.timeout(options.responseTimeoutMs ?? 15_000) }
194
+ : {}),
174
195
  }).then(async (res) => {
175
196
  if (!res.ok) {
176
197
  throw await dispatchResponseError(options.path, res);
@@ -187,6 +208,14 @@ export async function fireInternalDispatch(
187
208
  );
188
209
  });
189
210
 
211
+ if (awaitResponse) {
212
+ // Confirmed handoff: resolve only once the target acknowledged the
213
+ // dispatch (throws on network error / timeout / non-2xx). Used by callers
214
+ // whose own invocation is about to end — see the option doc above.
215
+ await dispatchPromise;
216
+ return;
217
+ }
218
+
190
219
  const settleMs = options.settleMs ?? DEFAULT_DISPATCH_SETTLE_MS;
191
220
  await Promise.race([
192
221
  dispatchPromise,
@@ -33,10 +33,62 @@ agent answers about browser recordings in the Analytics template.
33
33
  scoped to the recording, embeds a small SSR discovery payload on
34
34
  `/sessions/:recordingId`, and advertises
35
35
  `/api/session-replay/agent-context.json` plus bounded
36
- `/api/session-replay/agent-events.json` reads.
36
+ `/api/session-replay/agent-events.json` and
37
+ `/api/session-replay/agent-diagnostics.json` reads.
37
38
  - Do not make session recordings public just so an agent can inspect them.
38
39
  Tokenized agent links are the intended handoff path.
39
40
 
41
+ ## Console And Network Capture
42
+
43
+ - While recording, the core client (`session-replay.ts` in
44
+ `@agent-native/core`) patches console (`log`/`info`/`warn`/`error`/`debug`),
45
+ window `error` / `unhandledrejection`, `fetch`, and XHR, and emits rrweb
46
+ custom events tagged `agent-native.console` and `agent-native.network`.
47
+ - Capture is on by default whenever session replay is enabled. Tune or disable
48
+ it with the `console` / `network` options on the session replay config; each
49
+ accepts a boolean or an options object.
50
+ - Privacy bounds: request/response bodies and headers are never captured, URLs
51
+ are scrubbed, messages are truncated, and recorder self-traffic (the replay
52
+ ingest and tracking endpoints) is excluded.
53
+ - Per-session budgets: 1000 console events and 2000 network events, with a
54
+ truncation notice event once a budget is hit.
55
+ - On ingest, `deriveReplaySignals` computes the real `errorCount` from tagged
56
+ console events plus the additive `networkErrorCount` column on
57
+ `session_recordings`. Keep new columns additive.
58
+
59
+ ## Agent Diagnostics Surface
60
+
61
+ - `buildSessionReplayAgentContext` includes a `diagnostics` section: up to 50
62
+ console entries and 50 network entries, errors/failures first, with totals
63
+ and truncated flags. Agent-context instructions steer agents to diagnostics
64
+ as the primary debugging signal.
65
+ - The agent timeline includes `console-error` / `network-error` markers; error
66
+ markers are kept preferentially under the 200-marker cap.
67
+ - `apis.diagnostics` advertises the fuller bounded list:
68
+ `GET /api/session-replay/agent-diagnostics.json?id=<recordingId>&agent_access=<token>&kind=console|network|all&level=<level>&limit=<n>`
69
+ (limit defaults to 200, max 500). It uses the same recording-scoped
70
+ `agent_access` token as the other agent JSON APIs.
71
+
72
+ ## Dev Tools Panel
73
+
74
+ - The `/sessions/:recordingId` replay player has a Dev Tools toggle that opens
75
+ a panel with Console and Network tabs: filter chips, search, an error-count
76
+ badge, and playback-time highlighting.
77
+ - Rows are Jump-to-seek: clicking one seeks the player to that moment. Extend
78
+ this panel instead of adding a separate debugging surface.
79
+
80
+ ## Debugging A User-Reported Bug
81
+
82
+ 1. Search the reporting user's email on `/sessions` to find their recordings.
83
+ 2. Open the relevant session at `/sessions/:recordingId` and click
84
+ **Copy for agent** to mint the two-hour tokenized link.
85
+ 3. Paste the link to an agent. The agent fetches
86
+ `/api/session-replay/agent-context.json`, reads the `diagnostics` section
87
+ and timeline markers first, then drills into `apis.diagnostics` (filtered
88
+ by `kind`/`level`) and `apis.events` for the fuller bounded lists as needed.
89
+ 4. For human verification, open the Dev Tools panel in the replay player and
90
+ jump-to-seek from the failing console or network row.
91
+
40
92
  ## Capture Defaults
41
93
 
42
94
  - Replay is off unless enabled by config/env.
@@ -128,6 +128,8 @@ details live in `.agents/skills/`.
128
128
  runtimes use the in-process scheduler unless `ANALYTICS_ALERT_JOBS=0` is set.
129
129
  External cron callers can POST `/api/analytics-alerts/run` with
130
130
  `Authorization: Bearer $ANALYTICS_ALERTS_CRON_SECRET`.
131
+ Users can view and manage these rules in Settings under the Alerts card, which
132
+ uses the same action surface as the agent.
131
133
 
132
134
  ## Application State
133
135
 
@@ -153,6 +155,17 @@ details live in `.agents/skills/`.
153
155
  scoped to one recording for two hours; SSR embeds an agent discovery payload
154
156
  and the JSON APIs expose only summary/timeline metadata plus bounded event
155
157
  reads.
158
+ - Recordings also capture console logs and network request metadata (no
159
+ bodies/headers, scrubbed URLs, truncated messages, per-session budgets);
160
+ ingest derives `errorCount`/`networkErrorCount` and adds
161
+ `console-error`/`network-error` timeline markers.
162
+ - The agent context includes a bounded `diagnostics` section (errors first) and
163
+ advertises `GET /api/session-replay/agent-diagnostics.json` with
164
+ `kind`/`level`/`limit` params (limit max 500) under the same `agent_access`
165
+ token — the primary signal when debugging a user-reported issue.
166
+ - The replay player has a Dev Tools panel with Console and Network tabs
167
+ (filters, search, jump-to-seek, error badge); extend it rather than adding a
168
+ separate replay debugging surface. See the `session-replay` skill.
156
169
  - Dashboard rows that include `recording_id` should link to
157
170
  `/sessions/:recordingId`; rows that only include `session_id` can link to a
158
171
  filtered `/sessions` search.
@@ -9,6 +9,7 @@ import {
9
9
  } from "@agent-native/core/server";
10
10
  import { z } from "zod";
11
11
 
12
+ import { listAnalyticsAlertRules } from "../server/lib/analytics-alerts";
12
13
  import { listDashboardCatalog } from "../server/lib/dashboard-catalog";
13
14
  import { getAnalysis, getDashboard } from "../server/lib/dashboards-store";
14
15
  import { listAnalyticsPublicKeys } from "../server/lib/first-party-analytics.js";
@@ -195,6 +196,30 @@ export default defineAction({
195
196
  }
196
197
  } else if (nav?.view === "settings") {
197
198
  screen.page = "settings";
199
+ const email = getRequestUserEmail();
200
+ if (email) {
201
+ const orgId = getRequestOrgId() || null;
202
+ const alertRules = await listAnalyticsAlertRules({ email, orgId });
203
+ screen.analyticsAlerts = alertRules.map((rule) => ({
204
+ id: rule.id,
205
+ name: rule.name,
206
+ enabled: rule.enabled,
207
+ severity: rule.severity,
208
+ eventName: rule.eventName,
209
+ filters: rule.filters,
210
+ thresholdMode: rule.thresholdMode,
211
+ distinctBy: rule.distinctBy,
212
+ threshold: rule.threshold,
213
+ windowMinutes: rule.windowMinutes,
214
+ cooldownMinutes: rule.cooldownMinutes,
215
+ channels: rule.channels,
216
+ emailRecipients: rule.emailRecipients,
217
+ lastStatus: rule.lastStatus,
218
+ lastEvaluatedAt: rule.lastEvaluatedAt,
219
+ lastTriggeredAt: rule.lastTriggeredAt,
220
+ lastError: rule.lastError,
221
+ }));
222
+ }
198
223
  }
199
224
 
200
225
  if (Object.keys(screen).length === 0) {