@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
@@ -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) {
@@ -42,42 +42,9 @@ function getGaMeasurementId(): string | null {
42
42
  );
43
43
  }
44
44
 
45
- /**
46
- * Script hosts the injected GA loader pulls executable code from. Google Tag
47
- * Manager serves `gtag/js`, and GA4 can lazy-load additional collectors from
48
- * `www.google-analytics.com`. These must be listed in the document `script-src`
49
- * so the CSP reflects the code the framework itself injects (see
50
- * `applyDocumentCsp` in `ssr-handler.ts`).
51
- */
52
- export const GA_CSP_SCRIPT_HOSTS = [
53
- "https://www.googletagmanager.com",
54
- "https://www.google-analytics.com",
55
- ] as const;
56
-
57
- /**
58
- * Network/image hosts used by the GA4 loader when it sends page-view and event
59
- * beacons. These are separate from `script-src`: a stricter deployment CSP with
60
- * `connect-src 'self'` or `img-src 'self'` can load gtag.js but still drop all
61
- * analytics events unless these hosts are present too.
62
- */
63
- export const GA_CSP_CONNECT_HOSTS = [
64
- "https://www.google-analytics.com",
65
- "https://analytics.google.com",
66
- "https://stats.g.doubleclick.net",
67
- "https://region1.google-analytics.com",
68
- ] as const;
69
-
70
- export const GA_CSP_IMG_HOSTS = [
71
- "https://www.google-analytics.com",
72
- "https://www.googletagmanager.com",
73
- "https://stats.g.doubleclick.net",
74
- ] as const;
75
-
76
45
  /**
77
46
  * The exact JS body (no surrounding `<script>` tags) of the inline gtag config
78
- * block injected next to the gtag.js loader. Returned so the SSR handler can
79
- * hash it for the `script-src` CSP directive — the hash must be computed from
80
- * the identical string that `getGaScript()` embeds, so both call this helper.
47
+ * block injected next to the gtag.js loader.
81
48
  * Returns `null` when GA is not configured.
82
49
  */
83
50
  export function getGaInlineConfigScriptBody(): string | null {
@@ -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,