@agent-native/core 0.75.1 → 0.75.4

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 (38) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +76 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/durable-background.ts +54 -0
  5. package/corpus/core/src/agent/production-agent.ts +15 -3
  6. package/corpus/core/src/cli/sync-builder-starter-manifest.ts +36 -12
  7. package/corpus/core/src/deploy/build.ts +176 -41
  8. package/corpus/templates/analytics/actions/get-sql-dashboard.ts +0 -8
  9. package/corpus/templates/analytics/actions/update-dashboard.ts +3 -1
  10. package/corpus/templates/analytics/app/components/SqlEditor.tsx +57 -0
  11. package/corpus/templates/analytics/app/components/SqlHighlight.tsx +27 -28
  12. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +130 -8
  13. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +2 -3
  14. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/ViewSqlPopover.tsx +4 -4
  15. package/corpus/templates/analytics/changelog/2026-06-24-active-user-dashboard-panels-classify-templates-more-accurat.md +6 -0
  16. package/corpus/templates/analytics/changelog/2026-06-24-dashboard-panel-deletes-now-stay-deleted-after-refresh.md +6 -0
  17. package/corpus/templates/analytics/changelog/2026-06-24-sql-panel-editors-now-highlight-query-syntax-and-keep-one-cl.md +6 -0
  18. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +2 -2
  19. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +9 -11
  20. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +6 -20
  21. package/corpus/templates/plan/app/components/plan/DocumentArea.tsx +45 -33
  22. package/corpus/templates/plan/changelog/2026-06-24-invalid-generated-recap-blocks-now-show-a-clear-warning-with.md +6 -0
  23. package/corpus/templates/plan/server/plan-content.ts +53 -3
  24. package/dist/agent/durable-background.d.ts +49 -0
  25. package/dist/agent/durable-background.d.ts.map +1 -1
  26. package/dist/agent/durable-background.js +51 -0
  27. package/dist/agent/durable-background.js.map +1 -1
  28. package/dist/agent/production-agent.d.ts.map +1 -1
  29. package/dist/agent/production-agent.js +15 -3
  30. package/dist/agent/production-agent.js.map +1 -1
  31. package/dist/cli/sync-builder-starter-manifest.d.ts.map +1 -1
  32. package/dist/cli/sync-builder-starter-manifest.js +21 -10
  33. package/dist/cli/sync-builder-starter-manifest.js.map +1 -1
  34. package/dist/deploy/build.d.ts +61 -21
  35. package/dist/deploy/build.d.ts.map +1 -1
  36. package/dist/deploy/build.js +164 -41
  37. package/dist/deploy/build.js.map +1 -1
  38. package/package.json +1 -1
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: 4030
@@ -1,5 +1,81 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.75.4
4
+
5
+ ### Patch Changes
6
+
7
+ - dbfbe42: Preserve starter-only manifest fields when syncing builder-agent-native-starter from templates/chat.
8
+
9
+ ## 0.75.3
10
+
11
+ ### Patch Changes
12
+
13
+ - 8a00851: Durable background agent-chat runs now reach Netlify's 15-min async function by
14
+ emitting the background function INTO the scanned functions dir with a real
15
+ `config.path`, and excluding that path from the Nitro `server` `/*` catch-all so
16
+ the match is unambiguous.
17
+
18
+ Grounded in the real Netlify build output: Nitro's `netlify` preset writes no
19
+ `netlify.toml` and no redirects — the `/*` catch-all is an in-code Functions API
20
+ v2 `config.path: "/*"` on `.netlify/functions-internal/server/server.mjs`.
21
+ Netlify scans exactly the configured `functionsDirectory`
22
+ (`.netlify/functions-internal`); `.netlify/functions/` is the build OUTPUT dir
23
+ (where `@netlify/build` later writes the zips + `manifest.json`) and is never
24
+ scanned. On CI, Netlify reads each scanned function's `export const config` to
25
+ build the manifest routes — so per-file `background`/`path` config is honored.
26
+
27
+ The build now emits the background function into
28
+ `.netlify/functions-internal/server-agent-background` (the scanned dir), sharing
29
+ the same `main.mjs` bundle, with `export const config = { background: true, path:
30
+ "/_agent-native/agent-chat/_process-run" }`. It also appends that path to the
31
+ `server` function's `config.excludedPath`, so the `/*` catch-all no longer
32
+ matches the process-run route. Netlify evaluates serverless functions before
33
+ redirects, so a POST to the framework process-run route matches only the async
34
+ background function (immediate 202 ack, 15-min budget) — never the synchronous
35
+ `server` catch-all. The entry sets
36
+ `globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true` at cold start and
37
+ normalizes the request path before delegating to Nitro, preserving the method,
38
+ all headers (the HMAC `Authorization: Bearer` the plugin verifies), and the body.
39
+
40
+ This supersedes the two earlier approaches that failed in production: emitting
41
+ into `functions-internal` with a `config.path` but WITHOUT excluding it from the
42
+ `/*` catch-all (both functions matched the path; the synchronous `server`
43
+ catch-all won, returning a sync 401 instead of a 202), and emitting a standalone
44
+ function into `.netlify/functions/` (never scanned, returned 404). The foreground
45
+ self-dispatch now always targets the framework process-run route on every host
46
+ via `resolveAgentChatProcessRunDispatchPath`. The graceful inline 40s fallback on
47
+ a dispatch fast-fail is unchanged.
48
+
49
+ ## 0.75.2
50
+
51
+ ### Patch Changes
52
+
53
+ - 4b3543d: Durable background agent-chat runs now actually receive Netlify's 15-min async
54
+ budget by reaching a STANDALONE background function at its DIRECT url, bypassing
55
+ Nitro's synchronous `/*` catch-all.
56
+
57
+ The previous emit wrote the background function into `.netlify/functions-internal`
58
+ with a custom `config.path` of the `_process-run` route. A custom `config.path`
59
+ makes a function reachable ONLY at that path (not at its default function url),
60
+ `functions-internal` is not exposed at a default url, and Netlify routed
61
+ `/_agent-native/agent-chat/_process-run` to the synchronous Nitro `server`
62
+ catch-all instead — so the worker was capped at the ~60s wall and degraded to
63
+ 40s-chunked runs (confirmed live: a POST to the process-run path returned a
64
+ synchronous 401 from the handler rather than a 202 async ack).
65
+
66
+ The build now emits a standalone function into the STANDARD
67
+ `.netlify/functions/server-agent-background` dir with `background: true` and NO
68
+ custom `path`, so Netlify exposes it at `/.netlify/functions/server-agent-background`
69
+ and invokes it asynchronously (immediate 202 ack, 15-min budget). Its entry sets
70
+ `globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true` at cold start and rewrites
71
+ the incoming request path back to the `_process-run` route before delegating to
72
+ the Nitro handler, preserving the method, all headers (the HMAC
73
+ `Authorization: Bearer` the plugin verifies survives the rewrite), and the body.
74
+ The foreground self-dispatch (and server-driven continuation chunks) now target
75
+ that direct url on hosted Netlify via a shared `resolveAgentChatProcessRunDispatchPath`
76
+ helper, and stay on the framework route everywhere else. The graceful inline
77
+ 40s fallback on a dispatch fast-fail is unchanged.
78
+
3
79
  ## 0.75.1
4
80
 
5
81
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.75.1",
3
+ "version": "0.75.4",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -51,6 +51,60 @@ 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
+ * Default function URL of the background function on Netlify, kept for
67
+ * diagnostics/tests. Every Netlify function is ALSO reachable at
68
+ * `/.netlify/functions/<name>` unless a custom `config.path` removes the default
69
+ * url. The emitted background function declares `config.path =
70
+ * AGENT_CHAT_PROCESS_RUN_PATH`, which means Netlify routes the process-run path
71
+ * to it directly AND (per Netlify docs) removes this default url — so the
72
+ * foreground does NOT dispatch here; it dispatches to the framework route (see
73
+ * `resolveAgentChatProcessRunDispatchPath`). This constant is retained only so
74
+ * the name/url shape stays asserted and discoverable.
75
+ */
76
+ export const AGENT_BACKGROUND_FUNCTION_URL_PATH = `/.netlify/functions/${AGENT_BACKGROUND_FUNCTION_NAME}`;
77
+
78
+ /**
79
+ * Resolve the path the foreground POST should self-dispatch the chat background
80
+ * worker to.
81
+ *
82
+ * GROUNDED IN THE REAL NETLIFY BUILD OUTPUT: the background function is emitted
83
+ * INTO the scanned dir (`.netlify/functions-internal/server-agent-background`)
84
+ * with `export const config = { background: true, path:
85
+ * AGENT_CHAT_PROCESS_RUN_PATH }`. Netlify evaluates serverless functions BEFORE
86
+ * redirects (request-chain step 10 vs 11), and the build excludes this exact path
87
+ * from the `server` `/*` catch-all — so a POST to `AGENT_CHAT_PROCESS_RUN_PATH`
88
+ * matches ONLY the async background function (immediate 202, 15-min budget),
89
+ * never the synchronous `server` catch-all.
90
+ *
91
+ * So the dispatch path is the SAME framework route on every host. On hosted
92
+ * Netlify it lands on the async function (because of the exclude + the
93
+ * background function's `config.path`); everywhere else (local dev, `netlify
94
+ * dev`, non-Netlify hosts where no second function exists) the same in-process
95
+ * catch-all handles it inline. The HMAC token (signed over the runId) is
96
+ * unchanged.
97
+ *
98
+ * NOTE: this is a deliberate change from the earlier "dispatch to the direct
99
+ * `/.netlify/functions/<name>` url" attempt, which only worked if the function
100
+ * was reachable at its default url. With a custom `config.path` the default url
101
+ * is removed, and there is no shadowing to bypass anyway, so dispatching to the
102
+ * framework route is both correct and simpler.
103
+ */
104
+ export function resolveAgentChatProcessRunDispatchPath(): string {
105
+ return AGENT_CHAT_PROCESS_RUN_PATH;
106
+ }
107
+
54
108
  /**
55
109
  * Env flag for durable background runs. DEFAULT-ON: unset means enabled; an app
56
110
  * opts OUT with an explicit falsy value (`false`/`0`/`no`/`off`).
@@ -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,14 @@ export function createProductionAgentHandler(
4394
4394
  try {
4395
4395
  await fireInternalDispatch({
4396
4396
  event,
4397
- path: AGENT_CHAT_PROCESS_RUN_PATH,
4397
+ // The framework `_process-run` route on every host. On hosted Netlify
4398
+ // the build emits an async background function (in the scanned dir)
4399
+ // that CLAIMS this exact path via `config.path` AND excludes it from
4400
+ // the `server` /* catch-all, so this POST matches ONLY the async
4401
+ // function (immediate 202, 15-min budget) — Netlify matches functions
4402
+ // before redirects. Off-Netlify the same in-process catch-all handles
4403
+ // it. The Authorization Bearer HMAC is preserved either way.
4404
+ path: resolveAgentChatProcessRunDispatchPath(),
4398
4405
  taskId: runId,
4399
4406
  body: {
4400
4407
  ...body,
@@ -4636,7 +4643,12 @@ export function createProductionAgentHandler(
4636
4643
  try {
4637
4644
  await fireInternalDispatch({
4638
4645
  event,
4639
- path: AGENT_CHAT_PROCESS_RUN_PATH,
4646
+ // Continuation chunks dispatch to the same framework
4647
+ // `_process-run` route; on hosted Netlify it matches the
4648
+ // async background function (config.path + excluded from the
4649
+ // /* catch-all) so each chunk keeps the 15-min budget. Same
4650
+ // path-resolution as the initial dispatch.
4651
+ path: resolveAgentChatProcessRunDispatchPath(),
4640
4652
  taskId: nextRunId,
4641
4653
  body: {
4642
4654
  ...body,
@@ -59,6 +59,26 @@ export function generateStandaloneChatManifest(repoRoot?: string): {
59
59
  }
60
60
  }
61
61
 
62
+ function mergePackageJsonRecords(
63
+ canonical: Record<string, string> | undefined,
64
+ starter: Record<string, string> | undefined,
65
+ starterPinnedKeys: string[] = [],
66
+ ): Record<string, string> {
67
+ const merged = { ...(canonical ?? {}) };
68
+ for (const [key, value] of Object.entries(starter ?? {})) {
69
+ if (!(key in merged)) {
70
+ merged[key] = value;
71
+ }
72
+ }
73
+ for (const key of starterPinnedKeys) {
74
+ const pinned = starter?.[key];
75
+ if (pinned) {
76
+ merged[key] = pinned;
77
+ }
78
+ }
79
+ return merged;
80
+ }
81
+
62
82
  export function mergeStarterManifest(
63
83
  starterPackageJson: PackageJson,
64
84
  canonicalPackageJson: PackageJson,
@@ -75,19 +95,23 @@ export function mergeStarterManifest(
75
95
  if (starterPackageJson.private !== undefined) {
76
96
  merged.private = starterPackageJson.private;
77
97
  }
98
+ if (typeof starterPackageJson.packageManager === "string") {
99
+ merged.packageManager = starterPackageJson.packageManager;
100
+ }
78
101
 
79
- const starterDeps =
80
- (starterPackageJson.dependencies as Record<string, string> | undefined) ??
81
- {};
82
- const canonicalDeps =
83
- (canonicalPackageJson.dependencies as Record<string, string> | undefined) ??
84
- {};
85
- merged.dependencies = {
86
- ...canonicalDeps,
87
- ...(starterDeps["@agent-native/core"]
88
- ? { "@agent-native/core": starterDeps["@agent-native/core"] }
89
- : {}),
90
- };
102
+ merged.dependencies = mergePackageJsonRecords(
103
+ canonicalPackageJson.dependencies as Record<string, string> | undefined,
104
+ starterPackageJson.dependencies as Record<string, string> | undefined,
105
+ ["@agent-native/core"],
106
+ );
107
+ merged.devDependencies = mergePackageJsonRecords(
108
+ canonicalPackageJson.devDependencies as Record<string, string> | undefined,
109
+ starterPackageJson.devDependencies as Record<string, string> | undefined,
110
+ );
111
+ merged.scripts = mergePackageJsonRecords(
112
+ canonicalPackageJson.scripts as Record<string, string> | undefined,
113
+ starterPackageJson.scripts as Record<string, string> | undefined,
114
+ );
91
115
 
92
116
  return merged;
93
117
  }
@@ -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,36 +1505,76 @@ 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 an async (background) function INSIDE the
1509
+ * scanned functions dir so the chat `_process-run` worker runs on Netlify's
1510
+ * 15-min async function instead of the synchronous `/*` catch-all.
1511
+ * Additive + flag-gated (see `isDurableBackgroundDeployEnabled`).
1512
+ *
1513
+ * GROUNDED IN THE REAL NETLIFY BUILD OUTPUT (verified from a local Nitro build):
1514
+ * - Nitro's `netlify` preset emits exactly ONE function source at
1515
+ * `.netlify/functions-internal/server/`. `server.mjs` re-exports `main.mjs`
1516
+ * and declares `export const config = { path: "/*", excludedPath:
1517
+ * ["/.netlify/*"], preferStatic: true, ... }`. Nitro writes NO `netlify.toml`
1518
+ * and NO `[[redirects]]`; the `/*` catch-all is an IN-CODE Functions-API-v2
1519
+ * `config.path`.
1520
+ * - The generated `.netlify/netlify.toml` sets
1521
+ * `functionsDirectory = ".netlify/functions-internal"`. Netlify scans EXACTLY
1522
+ * that dir; functions placed anywhere else (e.g. `.netlify/functions/`, which
1523
+ * is the BUILD OUTPUT dir where `@netlify/build` later writes the zipped
1524
+ * functions + `manifest.json`) are NEVER deployed.
1525
+ * - On Netlify CI, `@netlify/build` reads each scanned function's
1526
+ * `export const config`, zips it, and materializes
1527
+ * `.netlify/functions/manifest.json` with `routes` derived from each
1528
+ * `config.path`. So per-file `export const config` (including `background` and
1529
+ * `path`) IS honored — the `server` manifest entry's `routes: [{ pattern:
1530
+ * "/*" }]` came straight from `server.mjs`'s `config.path`.
1509
1531
  *
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.
1532
+ * THEREFORE we:
1533
+ * 1. Emit the background function INTO the scanned dir
1534
+ * (`.netlify/functions-internal/server-agent-background/`), sharing the same
1535
+ * built `main.mjs` bundle, so Netlify discovers it and honors its config.
1536
+ * 2. Give its `export const config` BOTH `background: true` (→ async invoke,
1537
+ * immediate 202, 15-min budget) AND `path: AGENT_CHAT_PROCESS_RUN_PATH` (so
1538
+ * it claims that exact path at function-matching time, which Netlify
1539
+ * evaluates BEFORE redirects — step 10 vs 11 in the request chain).
1540
+ * 3. PATCH the Nitro `server` function's own `server.mjs` so its catch-all
1541
+ * `config.path: "/*"` EXCLUDES `AGENT_CHAT_PROCESS_RUN_PATH` (append it to
1542
+ * `excludedPath`). Netlify does NOT define a winner when two serverless
1543
+ * functions both match a path; rather than rely on that undocumented order,
1544
+ * we make the match UNAMBIGUOUS — only the background function matches
1545
+ * `_process-run`, and `server` matches everything else exactly as before.
1546
+ * 4. Set `globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true` at cold start
1547
+ * (read back by `isInBackgroundFunctionRuntime()` so the worker takes the
1548
+ * ~13-min soft-timeout). A `globalThis` flag — NOT `process.env` — keeps the
1549
+ * no-env-mutation guard satisfied and carries no cross-request state.
1514
1550
  *
1515
- * The function declares `background: true` so Netlify invokes it ASYNC (HTTP
1516
- * 202 ack) with the 15-min budget, and a `config.path` of the process-run route
1517
- * so the `_process-run` self-dispatch lands on it. The earlier version set the
1518
- * path but omitted `background: true` and relied on the legacy `-background`
1519
- * filename which the `config` object overrides so Netlify served it
1520
- * SYNCHRONOUSLY (~60s) and the durable worker never got the 15-min budget
1521
- * (confirmed live: a POST to the process-run path returned a synchronous 401
1522
- * from the handler instead of a 202 async ack).
1551
+ * With a real `config.path` the function is reachable at that path directly, so
1552
+ * the foreground POSTs to `AGENT_CHAT_PROCESS_RUN_PATH`
1553
+ * (`resolveAgentChatProcessRunDispatchPath`) and the request entry no longer
1554
+ * needs to rewrite the path it already arrives at the framework route. We keep
1555
+ * a defensive normalize-to-PROCESS_RUN_PATH in the entry anyway (cheap, and it
1556
+ * makes the function correct even if reached via its default function url).
1523
1557
  *
1524
- * ⚠️ One Netlify runtime behavior still resolves only on a real deploy: the
1525
- * routing precedence between this function's `config.path` and Nitro's catch-all
1526
- * for that exact path. If the catch-all wins, the run still completes via the
1527
- * 40s soft-timeout path on the sync function (no durable win, no regression).
1528
- * See docs/design/durable-agent-runs.md (Open risks #1).
1558
+ * WHY THIS BEATS ALL THREE PRIOR FAILURES:
1559
+ * - Attempts 1 & 2 emitted into `functions-internal` (correct dir) with a
1560
+ * `config.path` but did NOT exclude that path from the `server` `/*`
1561
+ * catch-all. Two functions matched `_process-run`; the order is undocumented
1562
+ * and the catch-all `server` (priority 0, synchronous) won → SYNC 401, not a
1563
+ * 202. We now exclude the path from `server`, so only the async function
1564
+ * matches.
1565
+ * - Attempt 3 emitted a standalone function into `.netlify/functions/` — the
1566
+ * OUTPUT dir, which Netlify does not scan — so it never entered the manifest
1567
+ * → 404. We now emit into the SCANNED `functions-internal` dir.
1568
+ *
1569
+ * Safety net regardless of Netlify routing nuance: if the dispatch fast-fails
1570
+ * (e.g. the function was not emitted), the foreground handler degrades to an
1571
+ * inline 40s synchronous run (see production-agent.ts).
1529
1572
  */
1530
1573
  export function emitSingleTemplateNetlifyBackgroundFunction(
1531
1574
  projectCwd: string,
1532
1575
  ): void {
1533
- const functionsDir = path.join(projectCwd, ".netlify", "functions-internal");
1534
- const serverDir = path.join(functionsDir, "server");
1576
+ const internalDir = path.join(projectCwd, ".netlify", "functions-internal");
1577
+ const serverDir = path.join(internalDir, "server");
1535
1578
  if (!fs.existsSync(path.join(serverDir, "main.mjs"))) {
1536
1579
  // Nitro output layout differs from what we expected — skip rather than
1537
1580
  // guess. The single-function deploy is unaffected.
@@ -1541,13 +1584,24 @@ export function emitSingleTemplateNetlifyBackgroundFunction(
1541
1584
  );
1542
1585
  return;
1543
1586
  }
1544
- const backgroundName = "server-agent-background";
1545
- const dest = path.join(functionsDir, backgroundName);
1587
+ const backgroundName = AGENT_BACKGROUND_FUNCTION_NAME;
1588
+ // Emit INTO the SCANNED functions dir (functions-internal) so Netlify discovers
1589
+ // the function and honors its `export const config`. `.netlify/functions/` is
1590
+ // the build OUTPUT dir (where @netlify/build writes the zip + manifest) and is
1591
+ // NOT scanned — emitting there is why the standalone attempt 404'd.
1592
+ const dest = path.join(internalDir, backgroundName);
1546
1593
  fs.rmSync(dest, { recursive: true, force: true });
1547
1594
  copyDir(serverDir, dest);
1548
- // Drop the original Nitro entry so our background entry is the entrypoint.
1595
+ // Drop the original Nitro `/*` entry so our entry is the entrypoint and the
1596
+ // copied bundle does NOT re-register the catch-all `config.path`.
1549
1597
  fs.rmSync(path.join(dest, "server.mjs"), { force: true });
1550
1598
 
1599
+ // Make the `server` `/*` catch-all NOT match the process-run path, so only the
1600
+ // async background function matches it (function-vs-function path order is
1601
+ // undocumented on Netlify — don't rely on it).
1602
+ excludeProcessRunPathFromServerCatchAll(serverDir);
1603
+
1604
+ const processRunPath = JSON.stringify(AGENT_CHAT_PROCESS_RUN_PATH);
1551
1605
  const entry = `// Mark this isolate as the durable background runtime BEFORE the handler
1552
1606
  // bundle is imported, so isInBackgroundFunctionRuntime() reliably returns true
1553
1607
  // in this function. The deployed Lambda name is NOT guaranteed to end in
@@ -1557,26 +1611,48 @@ export function emitSingleTemplateNetlifyBackgroundFunction(
1557
1611
  // set-once isolate marker read back by isInBackgroundFunctionRuntime().
1558
1612
  globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true;
1559
1613
 
1614
+ // The framework route the Nitro router dispatches to (the _process-run plugin).
1615
+ const PROCESS_RUN_PATH = ${processRunPath};
1616
+
1560
1617
  let cachedHandler;
1561
1618
 
1562
- export default async function handler(...args) {
1619
+ // Netlify v2 invokes this as (request, context). The Nitro netlify handler is a
1620
+ // Web-standard \`async (Request) => Response\` (see nitro/presets/netlify/runtime).
1621
+ // Because this function declares \`config.path = PROCESS_RUN_PATH\`, Netlify routes
1622
+ // that exact path here and the request already arrives as PROCESS_RUN_PATH — no
1623
+ // rewrite is needed. We still NORMALIZE the pathname to PROCESS_RUN_PATH so the
1624
+ // function stays correct even if it is ever reached via its default function url
1625
+ // (/.netlify/functions/${backgroundName}). Method, ALL headers (the HMAC
1626
+ // Authorization: Bearer MUST survive — the plugin verifies it) and the body are
1627
+ // preserved by cloning the incoming Request with only its URL pathname set.
1628
+ export default async function handler(request) {
1563
1629
  cachedHandler ??= (await import("./main.mjs")).default;
1564
- return cachedHandler(...args);
1630
+ const url = new URL(request.url);
1631
+ url.pathname = PROCESS_RUN_PATH;
1632
+ // Read the body once and pass it through. GET/HEAD have no body.
1633
+ const method = request.method || "POST";
1634
+ const hasBody = method !== "GET" && method !== "HEAD";
1635
+ const body = hasBody ? await request.text() : undefined;
1636
+ const rewritten = new Request(url.toString(), {
1637
+ method,
1638
+ headers: request.headers,
1639
+ body,
1640
+ });
1641
+ return cachedHandler(rewritten);
1565
1642
  }
1566
1643
 
1567
1644
  export const config = {
1568
1645
  name: "agent background handler",
1569
1646
  generator: "agent-native build",
1570
- // background: true is what actually makes Netlify invoke this ASYNCHRONOUSLY
1571
- // (immediate HTTP 202 ack) with the 15-minute budget. Without it, a function
1572
- // that declares a custom \`path\` is served SYNCHRONOUSLY (~60s) even when its
1573
- // file name ends in "-background" the \`config\` object overrides the legacy
1574
- // filename convention. That omission is why the durable worker was capped at
1575
- // ~60s in prod (verified live: POST to the process-run path returned a
1576
- // synchronous 401 from the handler, not a 202 async ack). See Netlify docs:
1577
- // build/functions/background-functions + build/functions/configuration.
1647
+ // background: true makes Netlify invoke this ASYNCHRONOUSLY (immediate HTTP
1648
+ // 202 ack) with the 15-minute budget (Netlify docs:
1649
+ // build/functions/background-functions + build/functions/api). path claims the
1650
+ // process-run route directly; Netlify evaluates serverless functions BEFORE
1651
+ // redirects (request-chain step 10 vs 11), and we exclude this path from the
1652
+ // \`server\` /* catch-all so only THIS function matches it no ambiguous
1653
+ // function-vs-function order.
1578
1654
  background: true,
1579
- path: ${JSON.stringify([AGENT_CHAT_PROCESS_RUN_PATH])},
1655
+ path: PROCESS_RUN_PATH,
1580
1656
  nodeBundler: "none",
1581
1657
  includedFiles: ["**"],
1582
1658
  preferStatic: false,
@@ -1584,9 +1660,68 @@ export const config = {
1584
1660
  `;
1585
1661
  fs.writeFileSync(path.join(dest, `${backgroundName}.mjs`), entry);
1586
1662
  console.log(
1587
- `[build] Emitted durable-background function "${backgroundName}" ` +
1588
- `(path ${AGENT_CHAT_PROCESS_RUN_PATH}). REQUIRES real-deploy verification ` +
1589
- `of Netlify async routing see docs/design/durable-agent-runs.md.`,
1663
+ `[build] Emitted durable-background function "${backgroundName}" into the ` +
1664
+ `scanned dir .netlify/functions-internal with config { background:true, ` +
1665
+ `path:"${AGENT_CHAT_PROCESS_RUN_PATH}" } and excluded that path from the ` +
1666
+ `server /* catch-all. REQUIRES real-deploy verification of Netlify async ` +
1667
+ `(202) invocation — see docs/design/durable-agent-runs.md.`,
1668
+ );
1669
+ }
1670
+
1671
+ /**
1672
+ * Append `AGENT_CHAT_PROCESS_RUN_PATH` to the Nitro `server` function's
1673
+ * `config.excludedPath` so its `/*` catch-all does NOT match the process-run
1674
+ * path. That guarantees the async background function (which declares
1675
+ * `config.path = AGENT_CHAT_PROCESS_RUN_PATH`) is the ONLY function that matches
1676
+ * that path — Netlify does not define a winner when two serverless functions
1677
+ * both match, so we make the match unambiguous instead of relying on order.
1678
+ *
1679
+ * The Nitro-generated `server/server.mjs` is small and deterministic:
1680
+ * export { default } from "./main.mjs";
1681
+ * export const config = { ... excludedPath: ["/.netlify/*"], ... };
1682
+ * We parse the `excludedPath: [...]` array literal and add our path if absent.
1683
+ * If the shape ever changes and we can't find/parse it, we log and leave the
1684
+ * file untouched (the inline-40s fallback still keeps chat working).
1685
+ */
1686
+ function excludeProcessRunPathFromServerCatchAll(serverDir: string): void {
1687
+ const serverEntry = path.join(serverDir, "server.mjs");
1688
+ if (!fs.existsSync(serverEntry)) {
1689
+ console.warn(
1690
+ "[build] Durable-background: server/server.mjs not found; cannot exclude " +
1691
+ `${AGENT_CHAT_PROCESS_RUN_PATH} from the /* catch-all.`,
1692
+ );
1693
+ return;
1694
+ }
1695
+ const original = fs.readFileSync(serverEntry, "utf8");
1696
+ if (original.includes(AGENT_CHAT_PROCESS_RUN_PATH)) {
1697
+ // Already excluded (idempotent — emit may run on a re-used output tree).
1698
+ return;
1699
+ }
1700
+ const excludedPathRe = /excludedPath:\s*\[([^\]]*)\]/;
1701
+ const match = original.match(excludedPathRe);
1702
+ const quotedPath = JSON.stringify(AGENT_CHAT_PROCESS_RUN_PATH);
1703
+ if (match) {
1704
+ const existing = match[1].trim();
1705
+ const next = existing
1706
+ ? `excludedPath: [${existing.replace(/,\s*$/, "")}, ${quotedPath}]`
1707
+ : `excludedPath: [${quotedPath}]`;
1708
+ fs.writeFileSync(serverEntry, original.replace(excludedPathRe, next));
1709
+ return;
1710
+ }
1711
+ // No existing `excludedPath` — inject one into the config object. Match the
1712
+ // `path: "/*"` line and add `excludedPath` right after it.
1713
+ const pathLineRe = /(path:\s*("\/\*"|'\/\*'),?)/;
1714
+ if (pathLineRe.test(original)) {
1715
+ fs.writeFileSync(
1716
+ serverEntry,
1717
+ original.replace(pathLineRe, `$1\n excludedPath: [${quotedPath}],`),
1718
+ );
1719
+ return;
1720
+ }
1721
+ console.warn(
1722
+ "[build] Durable-background: could not locate excludedPath/path in " +
1723
+ "server/server.mjs; leaving the /* catch-all unchanged (the background " +
1724
+ "function may be shadowed — the inline-40s fallback still applies).",
1590
1725
  );
1591
1726
  }
1592
1727
 
@@ -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";