@agent-native/core 0.75.0 → 0.75.2

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 (48) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +46 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/durable-background.ts +71 -3
  5. package/corpus/core/src/agent/production-agent.ts +16 -3
  6. package/corpus/core/src/cli/skills.ts +15 -11
  7. package/corpus/core/src/deploy/build.ts +100 -30
  8. package/corpus/core/src/templates/default/_gitignore +1 -0
  9. package/corpus/core/src/templates/headless/_gitignore +1 -0
  10. package/corpus/core/src/templates/workspace-root/_gitignore +1 -0
  11. package/corpus/templates/analytics/actions/get-sql-dashboard.ts +0 -8
  12. package/corpus/templates/analytics/actions/update-dashboard.ts +3 -1
  13. package/corpus/templates/analytics/app/components/SqlEditor.tsx +57 -0
  14. package/corpus/templates/analytics/app/components/SqlHighlight.tsx +27 -28
  15. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +130 -8
  16. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +2 -3
  17. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/ViewSqlPopover.tsx +4 -4
  18. package/corpus/templates/analytics/changelog/2026-06-24-active-user-dashboard-panels-classify-templates-more-accurat.md +6 -0
  19. package/corpus/templates/analytics/changelog/2026-06-24-dashboard-panel-deletes-now-stay-deleted-after-refresh.md +6 -0
  20. package/corpus/templates/analytics/changelog/2026-06-24-sql-panel-editors-now-highlight-query-syntax-and-keep-one-cl.md +6 -0
  21. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +2 -2
  22. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +9 -11
  23. package/corpus/templates/clips/actions/list-recordings.ts +21 -0
  24. package/corpus/templates/clips/app/components/library/library-layout.tsx +21 -2
  25. package/corpus/templates/clips/app/hooks/use-library.ts +18 -0
  26. package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +7 -5
  27. package/dist/agent/durable-background.d.ts +51 -3
  28. package/dist/agent/durable-background.d.ts.map +1 -1
  29. package/dist/agent/durable-background.js +64 -3
  30. package/dist/agent/durable-background.js.map +1 -1
  31. package/dist/agent/production-agent.d.ts.map +1 -1
  32. package/dist/agent/production-agent.js +16 -3
  33. package/dist/agent/production-agent.js.map +1 -1
  34. package/dist/cli/skills.d.ts +1 -1
  35. package/dist/cli/skills.d.ts.map +1 -1
  36. package/dist/cli/skills.js +15 -11
  37. package/dist/cli/skills.js.map +1 -1
  38. package/dist/deploy/build.d.ts +38 -17
  39. package/dist/deploy/build.d.ts.map +1 -1
  40. package/dist/deploy/build.js +96 -29
  41. package/dist/deploy/build.js.map +1 -1
  42. package/dist/templates/default/_gitignore +1 -0
  43. package/dist/templates/headless/_gitignore +1 -0
  44. package/dist/templates/workspace-root/_gitignore +1 -0
  45. package/package.json +1 -1
  46. package/src/templates/default/_gitignore +1 -0
  47. package/src/templates/headless/_gitignore +1 -0
  48. package/src/templates/workspace-root/_gitignore +1 -0
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 1160
31
- - template files: 4025
31
+ - template files: 4029
@@ -1,5 +1,51 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.75.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 4b3543d: Durable background agent-chat runs now actually receive Netlify's 15-min async
8
+ budget by reaching a STANDALONE background function at its DIRECT url, bypassing
9
+ Nitro's synchronous `/*` catch-all.
10
+
11
+ The previous emit wrote the background function into `.netlify/functions-internal`
12
+ with a custom `config.path` of the `_process-run` route. A custom `config.path`
13
+ makes a function reachable ONLY at that path (not at its default function url),
14
+ `functions-internal` is not exposed at a default url, and Netlify routed
15
+ `/_agent-native/agent-chat/_process-run` to the synchronous Nitro `server`
16
+ catch-all instead — so the worker was capped at the ~60s wall and degraded to
17
+ 40s-chunked runs (confirmed live: a POST to the process-run path returned a
18
+ synchronous 401 from the handler rather than a 202 async ack).
19
+
20
+ The build now emits a standalone function into the STANDARD
21
+ `.netlify/functions/server-agent-background` dir with `background: true` and NO
22
+ custom `path`, so Netlify exposes it at `/.netlify/functions/server-agent-background`
23
+ and invokes it asynchronously (immediate 202 ack, 15-min budget). Its entry sets
24
+ `globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true` at cold start and rewrites
25
+ the incoming request path back to the `_process-run` route before delegating to
26
+ the Nitro handler, preserving the method, all headers (the HMAC
27
+ `Authorization: Bearer` the plugin verifies survives the rewrite), and the body.
28
+ The foreground self-dispatch (and server-driven continuation chunks) now target
29
+ that direct url on hosted Netlify via a shared `resolveAgentChatProcessRunDispatchPath`
30
+ helper, and stay on the framework route everywhere else. The graceful inline
31
+ 40s fallback on a dispatch fast-fail is unchanged.
32
+
33
+ ## 0.75.1
34
+
35
+ ### Patch Changes
36
+
37
+ - 98b89e2: Durable background agent-chat runs now actually receive Netlify's 15-min async
38
+ budget. The emitted `-background` function declared a custom `config.path` but
39
+ omitted `background: true`, so Netlify served it SYNCHRONOUSLY (~60s) — the
40
+ `config` object overrides the legacy `-background` filename convention — and the
41
+ durable worker was capped at the 60s wall (it degraded to 40s-chunked runs and
42
+ never used the 15-min budget). The emit now sets `background: true` (immediate
43
+ 202 ack + 15-min execution) and force-marks the background runtime in the
44
+ function entry so the worker reliably takes the ~13-min soft-timeout regardless
45
+ of the deployed Lambda name. Root cause confirmed with a live prod routing probe
46
+ (POST to the process-run path returned a synchronous 401 from the handler
47
+ instead of a 202 async ack).
48
+
3
49
  ## 0.75.0
4
50
 
5
51
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.75.0",
3
+ "version": "0.75.2",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -51,6 +51,62 @@ import {
51
51
  export const AGENT_CHAT_PROCESS_RUN_PATH =
52
52
  "/_agent-native/agent-chat/_process-run";
53
53
 
54
+ /**
55
+ * Name of the standalone Netlify background function the build emits (see
56
+ * `emitSingleTemplateNetlifyBackgroundFunction` in deploy/build.ts). Shared so
57
+ * the emit and the dispatch-path helper below can never drift on the name.
58
+ *
59
+ * MUST end in `-background` — both because that is the conventional Netlify
60
+ * async-function suffix and because `isInBackgroundFunctionRuntime()` reads the
61
+ * `AWS_LAMBDA_FUNCTION_NAME` `-background` suffix as a secondary runtime signal.
62
+ */
63
+ export const AGENT_BACKGROUND_FUNCTION_NAME = "server-agent-background";
64
+
65
+ /**
66
+ * Direct invocation URL of the standalone background function on Netlify.
67
+ *
68
+ * The function is emitted into the STANDARD functions dir with `background:true`
69
+ * and NO custom `config.path`, so Netlify exposes it (and ONLY it) at the
70
+ * default function URL `/.netlify/functions/<name>` and invokes it
71
+ * asynchronously (immediate HTTP 202 ack, 15-min budget). Hitting this URL
72
+ * directly BYPASSES Nitro's `/*` catch-all `server` function — which is the
73
+ * whole point: the previous approach put a custom `config.path` on the function
74
+ * and Netlify routed `/_agent-native/agent-chat/_process-run` to the synchronous
75
+ * `server` catch-all instead (verified live: a synchronous 401 from the handler
76
+ * rather than a 202 async ack).
77
+ */
78
+ export const AGENT_BACKGROUND_FUNCTION_URL_PATH = `/.netlify/functions/${AGENT_BACKGROUND_FUNCTION_NAME}`;
79
+
80
+ /**
81
+ * Resolve the path the foreground POST should self-dispatch the chat background
82
+ * worker to.
83
+ *
84
+ * On hosted Netlify (`NETLIFY` truthy and not `netlify dev`) the standalone
85
+ * async background function is reachable ONLY at its direct function URL
86
+ * (`/.netlify/functions/server-agent-background`); POSTing the framework route
87
+ * `AGENT_CHAT_PROCESS_RUN_PATH` would land on Nitro's synchronous `/*` catch-all
88
+ * (the ~60s `server` function) and NEVER get the 15-min budget. The standalone
89
+ * function's entry rewrites the incoming path back to `AGENT_CHAT_PROCESS_RUN_PATH`
90
+ * before delegating to the Nitro handler, so the `_process-run` plugin still
91
+ * dispatches it — only the OUTER URL differs.
92
+ *
93
+ * Everywhere else (local dev, `netlify dev`, non-Netlify hosts where the
94
+ * standalone function does not exist) we keep dispatching to the framework route
95
+ * directly — the same in-process catch-all handles it, and there is no separate
96
+ * async function to reach. The HMAC token (signed over the runId) is unchanged
97
+ * either way; only the URL path differs.
98
+ */
99
+ export function resolveAgentChatProcessRunDispatchPath(): string {
100
+ if (
101
+ process.env.NETLIFY &&
102
+ process.env.NETLIFY !== "false" &&
103
+ process.env.NETLIFY_LOCAL !== "true"
104
+ ) {
105
+ return AGENT_BACKGROUND_FUNCTION_URL_PATH;
106
+ }
107
+ return AGENT_CHAT_PROCESS_RUN_PATH;
108
+ }
109
+
54
110
  /**
55
111
  * Env flag for durable background runs. DEFAULT-ON: unset means enabled; an app
56
112
  * opts OUT with an explicit falsy value (`false`/`0`/`no`/`off`).
@@ -112,11 +168,23 @@ export function isHostedRuntimeForDurableBackground(): boolean {
112
168
  * hard wall and get killed at 60s, then re-dispatch/resume in a wasteful loop.
113
169
  * So the 13-min budget is taken ONLY when this returns true.
114
170
  *
115
- * Falls back to the explicit `AGENT_CHAT_FORCE_BACKGROUND_RUNTIME` env (truthy)
116
- * for hosts that don't expose a `-background`-suffixed function name but where
117
- * the operator has confirmed a long async budget. Off by default.
171
+ * The PRIMARY signal is a `globalThis` marker the emitted background function's
172
+ * entry sets at cold start the deployed Lambda name is not guaranteed to end
173
+ * in `-background` on Netlify, so the entry marks its own runtime. A `globalThis`
174
+ * flag (not `process.env`) keeps the no-env-mutation guard satisfied and carries
175
+ * no cross-request state (set once per isolate). The `AWS_LAMBDA_FUNCTION_NAME`
176
+ * suffix and the explicit `AGENT_CHAT_FORCE_BACKGROUND_RUNTIME` env (truthy) are
177
+ * additional signals — the latter an operator escape hatch. Off by default.
118
178
  */
119
179
  export function isInBackgroundFunctionRuntime(): boolean {
180
+ // Set by the emitted `-background` function entry at cold start (the primary,
181
+ // most reliable signal — see the emit in deploy/build.ts).
182
+ if (
183
+ (globalThis as Record<string, unknown>)
184
+ .__AGENT_NATIVE_BACKGROUND_RUNTIME__ === true
185
+ ) {
186
+ return true;
187
+ }
120
188
  const lambdaName = process.env.AWS_LAMBDA_FUNCTION_NAME;
121
189
  if (
122
190
  typeof lambdaName === "string" &&
@@ -65,10 +65,10 @@ import {
65
65
  } from "./run-manager.js";
66
66
  import type { ActiveRun } from "./run-manager.js";
67
67
  import {
68
- AGENT_CHAT_PROCESS_RUN_PATH,
69
68
  AGENT_CHAT_BACKGROUND_RUN_FIELD,
70
69
  isAgentChatDurableBackgroundEnabled,
71
70
  isInBackgroundFunctionRuntime,
71
+ resolveAgentChatProcessRunDispatchPath,
72
72
  } from "./durable-background.js";
73
73
  import { fireInternalDispatch } from "../server/self-dispatch.js";
74
74
  import { readBody } from "../server/h3-helpers.js";
@@ -4394,7 +4394,15 @@ export function createProductionAgentHandler(
4394
4394
  try {
4395
4395
  await fireInternalDispatch({
4396
4396
  event,
4397
- path: AGENT_CHAT_PROCESS_RUN_PATH,
4397
+ // On hosted Netlify this is the standalone background function's DIRECT
4398
+ // url (`/.netlify/functions/server-agent-background`) — POSTing the
4399
+ // framework `_process-run` path would land on Nitro's synchronous `/*`
4400
+ // catch-all and never get the 15-min budget. The function's entry
4401
+ // rewrites the path back to AGENT_CHAT_PROCESS_RUN_PATH before
4402
+ // delegating to Nitro, so the `_process-run` plugin still runs and the
4403
+ // Authorization Bearer HMAC survives. Off-Netlify it stays the
4404
+ // framework path (handled in-process).
4405
+ path: resolveAgentChatProcessRunDispatchPath(),
4398
4406
  taskId: runId,
4399
4407
  body: {
4400
4408
  ...body,
@@ -4636,7 +4644,12 @@ export function createProductionAgentHandler(
4636
4644
  try {
4637
4645
  await fireInternalDispatch({
4638
4646
  event,
4639
- path: AGENT_CHAT_PROCESS_RUN_PATH,
4647
+ // Continuation chunks must also land on the standalone async
4648
+ // background function (its direct url on hosted Netlify) so
4649
+ // each chunk keeps the 15-min budget; same path-resolution as
4650
+ // the initial dispatch. The function entry rewrites the path
4651
+ // back to `_process-run` for the Nitro router.
4652
+ path: resolveAgentChatProcessRunDispatchPath(),
4640
4653
  taskId: nextRunId,
4641
4654
  body: {
4642
4655
  ...body,
@@ -394,14 +394,16 @@ sign-in at setup — this is intended), so the first tool call in that client do
394
394
  not hit an OAuth wall:
395
395
 
396
396
  \`\`\`bash
397
- npx @agent-native/core@latest skills add visual-plan
397
+ npx @agent-native/core@latest skills add visual-plans
398
398
  \`\`\`
399
399
 
400
400
  After that, \`/visual-plan\` and \`/visual-recap\` are the two installed slash
401
- commands. The other planning modes (\`create-ui-plan\`, \`create-prototype-plan\`,
402
- \`create-plan-design\`, \`create-visual-questions\`) are MCP tools reachable from
403
- \`/visual-plan\`, not separate slash commands. Pass \`--no-connect\` to register
404
- the connector without authenticating, then run
401
+ commands. If you only need one command, use \`skills add visual-plan\` or
402
+ \`skills add visual-recap\` instead. The other planning modes
403
+ (\`create-ui-plan\`, \`create-prototype-plan\`, \`create-plan-design\`,
404
+ \`create-visual-questions\`) are MCP tools reachable from \`/visual-plan\`, not
405
+ separate slash commands. Pass \`--no-connect\` to register the connector without
406
+ authenticating, then run
405
407
  \`npx @agent-native/core@latest connect https://plan.agent-native.com --client all\`
406
408
  whenever you are ready, or choose a narrower \`--client\`. Auth and MCP tool
407
409
  loading are per client config/session.
@@ -410,7 +412,7 @@ loading are per client config/session.
410
412
  install with \`--mode local-files\`:
411
413
 
412
414
  \`\`\`bash
413
- npx @agent-native/core@latest skills add visual-plan --mode local-files
415
+ npx @agent-native/core@latest skills add visual-plans --mode local-files
414
416
  \`\`\`
415
417
 
416
418
  This mode does not register the Plan MCP connector. Before authoring structured
@@ -1648,14 +1650,16 @@ sign-in at setup — this is intended), so the first tool call in that client do
1648
1650
  not hit an OAuth wall:
1649
1651
 
1650
1652
  \`\`\`bash
1651
- npx @agent-native/core@latest skills add visual-plan
1653
+ npx @agent-native/core@latest skills add visual-plans
1652
1654
  \`\`\`
1653
1655
 
1654
1656
  After that, \`/visual-plan\` and \`/visual-recap\` are the two installed slash
1655
- commands. The other planning modes (\`create-ui-plan\`, \`create-prototype-plan\`,
1656
- \`create-plan-design\`, \`create-visual-questions\`) are MCP tools reachable from
1657
- \`/visual-plan\`, not separate slash commands. Pass \`--no-connect\` to register
1658
- the connector without authenticating, then run
1657
+ commands. If you only need one command, use \`skills add visual-plan\` or
1658
+ \`skills add visual-recap\` instead. The other planning modes
1659
+ (\`create-ui-plan\`, \`create-prototype-plan\`, \`create-plan-design\`,
1660
+ \`create-visual-questions\`) are MCP tools reachable from \`/visual-plan\`, not
1661
+ separate slash commands. Pass \`--no-connect\` to register the connector without
1662
+ authenticating, then run
1659
1663
  \`npx @agent-native/core@latest connect https://plan.agent-native.com --client all\`
1660
1664
  whenever you are ready, or choose a narrower \`--client\`. Auth and MCP tool
1661
1665
  loading are per client config/session.
@@ -34,7 +34,10 @@ import {
34
34
  } from "./workspace-core.js";
35
35
  import { generateActionRegistryForProject } from "../vite/action-types-plugin.js";
36
36
  import { mcpEmbedStaticAssetRouteRules } from "../shared/mcp-embed-headers.js";
37
- import { AGENT_CHAT_PROCESS_RUN_PATH } from "../agent/durable-background.js";
37
+ import {
38
+ AGENT_BACKGROUND_FUNCTION_NAME,
39
+ AGENT_CHAT_PROCESS_RUN_PATH,
40
+ } from "../agent/durable-background.js";
38
41
  import {
39
42
  AGENT_NATIVE_SOCIAL_IMAGE_ALT,
40
43
  AGENT_NATIVE_SOCIAL_IMAGE_CACHE_BUSTER,
@@ -1502,31 +1505,52 @@ export function isDurableBackgroundDeployEnabled(): boolean {
1502
1505
  }
1503
1506
 
1504
1507
  /**
1505
- * Single-template Netlify build: emit a SECOND function whose name ends in
1506
- * `-background`, re-exporting the same `main.mjs` handler bundle, so the chat
1507
- * `_process-run` POST lands on Netlify's async (15-min) function instead of the
1508
- * synchronous one. Additive + flag-gated (see `isDurableBackgroundDeployEnabled`).
1508
+ * Single-template Netlify build: emit a STANDALONE async background function so
1509
+ * the chat `_process-run` worker runs on Netlify's async (15-min) function
1510
+ * reached at its DIRECT url, instead of the synchronous `/*` catch-all.
1511
+ * Additive + flag-gated (see `isDurableBackgroundDeployEnabled`).
1512
+ *
1513
+ * Nitro's `netlify` preset emits a single synchronous function at
1514
+ * `.netlify/functions-internal/server` (`server.mjs` → `main.mjs`, `config.path`
1515
+ * `/*`). We copy that bundle to a STANDALONE function in the STANDARD functions
1516
+ * dir `.netlify/functions/server-agent-background` and write an entry that:
1517
+ * 1. Sets `globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true` at cold start
1518
+ * (read back by `isInBackgroundFunctionRuntime()` so the worker takes the
1519
+ * ~13-min soft-timeout). A `globalThis` flag — NOT `process.env` — keeps the
1520
+ * no-env-mutation guard satisfied and carries no cross-request state.
1521
+ * 2. Declares `export const config = { background: true }` with NO `path`. With
1522
+ * no custom `path`, Netlify exposes the function at the DEFAULT url
1523
+ * `/.netlify/functions/server-agent-background` and `background:true` makes
1524
+ * it ASYNC (immediate HTTP 202 ack, 15-min budget). The foreground POST
1525
+ * self-dispatches to that direct url
1526
+ * (`resolveAgentChatProcessRunDispatchPath`), BYPASSING Nitro's `/*`
1527
+ * catch-all entirely.
1528
+ * 3. REWRITES the incoming request path to `AGENT_CHAT_PROCESS_RUN_PATH` before
1529
+ * delegating to the Nitro handler (`./main.mjs` default export — a Netlify
1530
+ * v2 / Web-standard `async (Request) => Response` handler), so the Nitro
1531
+ * router dispatches it to the `_process-run` plugin. Method, ALL headers
1532
+ * (critically the HMAC `Authorization: Bearer` the plugin verifies) and the
1533
+ * body are preserved by cloning the incoming `Request` with only its URL
1534
+ * pathname rewritten.
1509
1535
  *
1510
- * Nitro's `netlify` preset emits a single function at
1511
- * `.netlify/functions-internal/server` (`server.mjs` `main.mjs`). We copy
1512
- * that directory to a sibling `<...>-background` function and write an entry
1513
- * with a `config.path` of the process-run route.
1536
+ * WHY this replaced the previous `config.path` approach: a function with a custom
1537
+ * `config.path` is reachable ONLY at that path (not at its default function url)
1538
+ * and `functions-internal` is not exposed at the default url at all. In prod,
1539
+ * Netlify routed `/_agent-native/agent-chat/_process-run` to the synchronous
1540
+ * Nitro `server` catch-all, NOT to the background function — verified live: a
1541
+ * POST to the process-run path returned a SYNCHRONOUS 401 from the handler
1542
+ * (the Nitro plugin ran) instead of a 202 async ack. A standalone function at
1543
+ * its direct url cannot be shadowed by the catch-all.
1514
1544
  *
1515
- * ⚠️ REAL-DEPLOY VERIFICATION REQUIRED. Whether Netlify routes
1516
- * `/_agent-native/agent-chat/_process-run` to this `-background` function (and
1517
- * invokes it asynchronously with the 15-min budget) vs the synchronous Nitro
1518
- * function cannot be verified in this environment — Nitro owns the primary
1519
- * function's routing manifest, and the precedence between the two functions for
1520
- * that path is a Netlify runtime behavior. If routing resolves to the
1521
- * synchronous function, the run still completes via the existing 40s
1522
- * soft-timeout path (no durable win, no regression). See
1523
- * docs/design/durable-agent-runs.md (Open risks #1).
1545
+ * Safety net regardless of Netlify routing nuance: if the direct-url dispatch
1546
+ * fast-fails (e.g. 404 when this function was not emitted), the foreground
1547
+ * handler degrades to an inline 40s synchronous run (see production-agent.ts).
1524
1548
  */
1525
1549
  export function emitSingleTemplateNetlifyBackgroundFunction(
1526
1550
  projectCwd: string,
1527
1551
  ): void {
1528
- const functionsDir = path.join(projectCwd, ".netlify", "functions-internal");
1529
- const serverDir = path.join(functionsDir, "server");
1552
+ const internalDir = path.join(projectCwd, ".netlify", "functions-internal");
1553
+ const serverDir = path.join(internalDir, "server");
1530
1554
  if (!fs.existsSync(path.join(serverDir, "main.mjs"))) {
1531
1555
  // Nitro output layout differs from what we expected — skip rather than
1532
1556
  // guess. The single-function deploy is unaffected.
@@ -1536,24 +1560,69 @@ export function emitSingleTemplateNetlifyBackgroundFunction(
1536
1560
  );
1537
1561
  return;
1538
1562
  }
1539
- const backgroundName = "server-agent-background";
1563
+ const backgroundName = AGENT_BACKGROUND_FUNCTION_NAME;
1564
+ // Emit into the STANDARD functions dir (NOT functions-internal) so Netlify
1565
+ // exposes the function at its default url `/.netlify/functions/<name>`.
1566
+ const functionsDir = path.join(projectCwd, ".netlify", "functions");
1540
1567
  const dest = path.join(functionsDir, backgroundName);
1568
+ fs.mkdirSync(functionsDir, { recursive: true });
1541
1569
  fs.rmSync(dest, { recursive: true, force: true });
1542
1570
  copyDir(serverDir, dest);
1543
- // Drop the original Nitro entry so our background entry is the entrypoint.
1571
+ // Drop the original Nitro `/*` entry so our standalone entry is the entrypoint
1572
+ // and the copied bundle does NOT re-register the catch-all `config.path`.
1544
1573
  fs.rmSync(path.join(dest, "server.mjs"), { force: true });
1545
1574
 
1546
- const entry = `let cachedHandler;
1547
-
1548
- export default async function handler(...args) {
1575
+ const processRunPath = JSON.stringify(AGENT_CHAT_PROCESS_RUN_PATH);
1576
+ const entry = `// Mark this isolate as the durable background runtime BEFORE the handler
1577
+ // bundle is imported, so isInBackgroundFunctionRuntime() reliably returns true
1578
+ // in this function. The deployed Lambda name is NOT guaranteed to end in
1579
+ // "-background" (Netlify may mangle/prefix it), so we cannot depend on
1580
+ // AWS_LAMBDA_FUNCTION_NAME alone. A globalThis flag (NOT process.env) avoids the
1581
+ // no-env-mutation guard and carries no cross-request state — it is a static,
1582
+ // set-once isolate marker read back by isInBackgroundFunctionRuntime().
1583
+ globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true;
1584
+
1585
+ // The framework route the Nitro router dispatches to (the _process-run plugin).
1586
+ const PROCESS_RUN_PATH = ${processRunPath};
1587
+
1588
+ let cachedHandler;
1589
+
1590
+ // Netlify v2 invokes this as (request, context). The Nitro netlify handler is a
1591
+ // Web-standard \`async (Request) => Response\` (see nitro/presets/netlify/runtime).
1592
+ // We are reached at this function's DIRECT url
1593
+ // (/.netlify/functions/${backgroundName}); rewrite the path to PROCESS_RUN_PATH
1594
+ // so Nitro routes it to the _process-run plugin, preserving method, ALL headers
1595
+ // (the HMAC Authorization: Bearer MUST survive — the plugin verifies it) and the
1596
+ // body. We clone the incoming Request with only its URL pathname rewritten;
1597
+ // query + origin are preserved.
1598
+ export default async function handler(request) {
1549
1599
  cachedHandler ??= (await import("./main.mjs")).default;
1550
- return cachedHandler(...args);
1600
+ const url = new URL(request.url);
1601
+ url.pathname = PROCESS_RUN_PATH;
1602
+ // Read the body once and pass it through. GET/HEAD have no body.
1603
+ const method = request.method || "POST";
1604
+ const hasBody = method !== "GET" && method !== "HEAD";
1605
+ const body = hasBody ? await request.text() : undefined;
1606
+ const rewritten = new Request(url.toString(), {
1607
+ method,
1608
+ headers: request.headers,
1609
+ body,
1610
+ });
1611
+ return cachedHandler(rewritten);
1551
1612
  }
1552
1613
 
1553
1614
  export const config = {
1554
1615
  name: "agent background handler",
1555
1616
  generator: "agent-native build",
1556
- path: ${JSON.stringify([AGENT_CHAT_PROCESS_RUN_PATH])},
1617
+ // background: true makes Netlify invoke this ASYNCHRONOUSLY (immediate HTTP
1618
+ // 202 ack) with the 15-minute budget. NO custom \`path\`: that keeps the
1619
+ // function reachable at its DEFAULT url /.netlify/functions/${backgroundName},
1620
+ // which BYPASSES Nitro's /* catch-all. The previous version set a custom
1621
+ // \`config.path\` and Netlify routed the process-run path to the SYNCHRONOUS
1622
+ // catch-all instead (verified live: a synchronous 401 from the handler, not a
1623
+ // 202 async ack). See Netlify docs: build/functions/background-functions +
1624
+ // build/functions/configuration.
1625
+ background: true,
1557
1626
  nodeBundler: "none",
1558
1627
  includedFiles: ["**"],
1559
1628
  preferStatic: false,
@@ -1561,9 +1630,10 @@ export const config = {
1561
1630
  `;
1562
1631
  fs.writeFileSync(path.join(dest, `${backgroundName}.mjs`), entry);
1563
1632
  console.log(
1564
- `[build] Emitted durable-background function "${backgroundName}" ` +
1565
- `(path ${AGENT_CHAT_PROCESS_RUN_PATH}). REQUIRES real-deploy verification ` +
1566
- `of Netlify async routing see docs/design/durable-agent-runs.md.`,
1633
+ `[build] Emitted standalone durable-background function "${backgroundName}" ` +
1634
+ `at /.netlify/functions/${backgroundName} (async, rewrites to ` +
1635
+ `${AGENT_CHAT_PROCESS_RUN_PATH}). REQUIRES real-deploy verification of ` +
1636
+ `Netlify async invocation — see docs/design/durable-agent-runs.md.`,
1567
1637
  );
1568
1638
  }
1569
1639
 
@@ -10,6 +10,7 @@ npm-debug.log*
10
10
  pnpm-debug.log*
11
11
 
12
12
  node_modules
13
+ node-compile-cache/
13
14
  dist
14
15
  dist-ssr
15
16
  *.local
@@ -7,6 +7,7 @@ npm-debug.log*
7
7
  pnpm-debug.log*
8
8
 
9
9
  node_modules
10
+ node-compile-cache/
10
11
  dist
11
12
  *.local
12
13
 
@@ -28,6 +28,7 @@ packages/*/.env
28
28
  # pnpm
29
29
  .pnpm-store/
30
30
  pnpm-debug.log*
31
+ node-compile-cache/
31
32
 
32
33
  # Local workspace data
33
34
  data/
@@ -24,10 +24,6 @@ function seededResponse(
24
24
  };
25
25
  }
26
26
 
27
- function isEmptyConfig(config: Record<string, unknown>): boolean {
28
- return !Array.isArray(config.panels) || config.panels.length === 0;
29
- }
30
-
31
27
  export default defineAction({
32
28
  description:
33
29
  "Get a SQL analytics dashboard by ID, including its full panel config, visibility, and access metadata.",
@@ -78,10 +74,6 @@ export default defineAction({
78
74
  });
79
75
  }
80
76
  const config = dash.config as Record<string, unknown>;
81
- if (isEmptyConfig(config)) {
82
- const seed = loadDashboardSeed(args.id);
83
- if (seed) return seededResponse(args.id, seed);
84
- }
85
77
  return {
86
78
  id: args.id,
87
79
  ...config,
@@ -466,7 +466,9 @@ export default defineAction({
466
466
  .optional()
467
467
  .describe("Replace the whole dashboard config (or a JSON string)."),
468
468
  }),
469
- http: false,
469
+ // The SQL dashboard editor persists user edits through callAction(), which
470
+ // needs this action mounted under /_agent-native/actions/update-dashboard.
471
+ http: { method: "POST" },
470
472
  mcpApp: {
471
473
  compactCatalog: true,
472
474
  resource: embedApp({
@@ -0,0 +1,57 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+ import { SqlHighlight } from "@/components/SqlHighlight";
4
+
5
+ export interface SqlEditorProps extends Omit<
6
+ React.TextareaHTMLAttributes<HTMLTextAreaElement>,
7
+ "value"
8
+ > {
9
+ value: string;
10
+ }
11
+
12
+ export const SqlEditor = React.forwardRef<HTMLTextAreaElement, SqlEditorProps>(
13
+ ({ className, onScroll, value, disabled, readOnly, ...props }, ref) => {
14
+ const highlightRef = React.useRef<HTMLPreElement | null>(null);
15
+
16
+ const handleScroll = (event: React.UIEvent<HTMLTextAreaElement>) => {
17
+ const highlight = highlightRef.current;
18
+ if (highlight) {
19
+ highlight.scrollTop = event.currentTarget.scrollTop;
20
+ highlight.scrollLeft = event.currentTarget.scrollLeft;
21
+ }
22
+ onScroll?.(event);
23
+ };
24
+
25
+ return (
26
+ <div
27
+ className={cn(
28
+ "relative min-h-[200px] rounded-md border border-input bg-background ring-offset-background focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2",
29
+ disabled && "cursor-not-allowed opacity-50",
30
+ className,
31
+ )}
32
+ >
33
+ <SqlHighlight
34
+ ref={highlightRef}
35
+ aria-hidden="true"
36
+ sql={value || " "}
37
+ preClassName="pointer-events-none absolute inset-0 overflow-hidden rounded-md bg-transparent p-3 text-xs leading-5"
38
+ className="text-foreground"
39
+ />
40
+ <textarea
41
+ ref={ref}
42
+ value={value}
43
+ disabled={disabled}
44
+ readOnly={readOnly}
45
+ onScroll={handleScroll}
46
+ spellCheck={false}
47
+ className={cn(
48
+ "relative z-10 min-h-[inherit] w-full resize-y rounded-md border-0 bg-transparent p-3 font-mono text-xs leading-5 text-transparent caret-foreground outline-none selection:bg-primary/30 placeholder:text-muted-foreground focus:outline-none disabled:cursor-not-allowed",
49
+ readOnly && "cursor-default",
50
+ )}
51
+ {...props}
52
+ />
53
+ </div>
54
+ );
55
+ },
56
+ );
57
+ SqlEditor.displayName = "SqlEditor";
@@ -1,4 +1,4 @@
1
- import { type ReactNode } from "react";
1
+ import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
2
2
  import { cn } from "@/lib/utils";
3
3
 
4
4
  const KEYWORDS = new Set([
@@ -266,36 +266,35 @@ const TOKEN_CLASS: Record<TokenType, string> = {
266
266
  plain: "",
267
267
  };
268
268
 
269
- interface SqlHighlightProps {
269
+ interface SqlHighlightProps extends HTMLAttributes<HTMLPreElement> {
270
270
  sql: string;
271
- className?: string;
272
271
  preClassName?: string;
273
272
  }
274
273
 
275
- export function SqlHighlight({
276
- sql,
277
- className,
278
- preClassName,
279
- }: SqlHighlightProps) {
280
- const tokens = tokenize(sql);
281
- const nodes: ReactNode[] = tokens.map((tok, idx) => {
282
- const cls = TOKEN_CLASS[tok.type];
283
- if (!cls) return tok.value;
274
+ export const SqlHighlight = forwardRef<HTMLPreElement, SqlHighlightProps>(
275
+ function SqlHighlight({ sql, className, preClassName, ...props }, ref) {
276
+ const tokens = tokenize(sql);
277
+ const nodes: ReactNode[] = tokens.map((tok, idx) => {
278
+ const cls = TOKEN_CLASS[tok.type];
279
+ if (!cls) return tok.value;
280
+ return (
281
+ <span key={idx} className={cls}>
282
+ {tok.value}
283
+ </span>
284
+ );
285
+ });
284
286
  return (
285
- <span key={idx} className={cls}>
286
- {tok.value}
287
- </span>
287
+ <pre
288
+ ref={ref}
289
+ {...props}
290
+ className={cn(
291
+ "font-mono text-xs leading-5 whitespace-pre-wrap break-words",
292
+ preClassName,
293
+ className,
294
+ )}
295
+ >
296
+ <code>{nodes}</code>
297
+ </pre>
288
298
  );
289
- });
290
- return (
291
- <pre
292
- className={cn(
293
- "font-mono text-xs leading-5 whitespace-pre-wrap break-words",
294
- preClassName,
295
- className,
296
- )}
297
- >
298
- <code>{nodes}</code>
299
- </pre>
300
- );
301
- }
299
+ },
300
+ );