@agent-native/core 0.75.4 → 0.75.5

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 (36) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +40 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/durable-background.ts +62 -28
  5. package/corpus/core/src/agent/production-agent.ts +16 -12
  6. package/corpus/core/src/cli/skills.ts +22 -4
  7. package/corpus/core/src/deploy/build.ts +47 -113
  8. package/corpus/core/src/deploy/workspace-deploy.ts +62 -30
  9. package/corpus/core/src/server/self-dispatch.ts +29 -2
  10. package/corpus/templates/clips/changelog/2026-06-24-chrome-extension-setup-now-shows-a-clear-all-done-confirmati.md +6 -0
  11. package/corpus/templates/clips/chrome-extension/src/permission.html +60 -0
  12. package/corpus/templates/clips/chrome-extension/src/permission.ts +28 -15
  13. package/corpus/templates/plan/.agents/skills/visual-plan/references/wireframe.md +22 -4
  14. package/corpus/templates/plan/.agents/skills/visual-recap/references/wireframe.md +22 -4
  15. package/dist/agent/durable-background.d.ts +32 -28
  16. package/dist/agent/durable-background.d.ts.map +1 -1
  17. package/dist/agent/durable-background.js +59 -28
  18. package/dist/agent/durable-background.js.map +1 -1
  19. package/dist/agent/production-agent.d.ts.map +1 -1
  20. package/dist/agent/production-agent.js +16 -12
  21. package/dist/agent/production-agent.js.map +1 -1
  22. package/dist/cli/skills.d.ts +1 -1
  23. package/dist/cli/skills.d.ts.map +1 -1
  24. package/dist/cli/skills.js +22 -4
  25. package/dist/cli/skills.js.map +1 -1
  26. package/dist/deploy/build.d.ts +32 -36
  27. package/dist/deploy/build.d.ts.map +1 -1
  28. package/dist/deploy/build.js +47 -104
  29. package/dist/deploy/build.js.map +1 -1
  30. package/dist/deploy/workspace-deploy.js +62 -30
  31. package/dist/deploy/workspace-deploy.js.map +1 -1
  32. package/dist/server/self-dispatch.d.ts +0 -10
  33. package/dist/server/self-dispatch.d.ts.map +1 -1
  34. package/dist/server/self-dispatch.js +27 -2
  35. package/dist/server/self-dispatch.js.map +1 -1
  36. 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: 4030
31
+ - template files: 4031
@@ -1,5 +1,45 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.75.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 25802f2: Durable background agent-chat runs now reach Netlify's 15-min async function via
8
+ the function's DEFAULT url (`/.netlify/functions/<name>`) with NO custom
9
+ `config.path` and NO catch-all patch — the doc-correct approach per the Netlify
10
+ docs.
11
+
12
+ Every Netlify function is reachable at `/.netlify/functions/<name>` by default;
13
+ a custom `config.path` REMOVES that default url. The build now emits the
14
+ background function into the scanned dir
15
+ (`.netlify/functions-internal/server-agent-background`, or per-app
16
+ `<app>-agent-background` for workspaces), sharing the same `main.mjs` bundle,
17
+ with `export const config = { background: true, ... }` and NO custom path. The
18
+ function therefore keeps its default url, and `background: true` makes any
19
+ invocation of that url asynchronous (immediate 202 ack, 15-min budget). The
20
+ Nitro `server` function's `/*` catch-all already excludes `/.netlify/*`, so the
21
+ default-url namespace is never shadowed by the synchronous function — there is
22
+ nothing to patch.
23
+
24
+ The function entry rewrites the incoming pathname to the framework
25
+ `_process-run` route (base-path-prefixed for workspaces) before delegating to
26
+ Nitro, preserving the method, all headers (the HMAC `Authorization: Bearer` the
27
+ plugin verifies), and the body. It also sets
28
+ `globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true` at cold start so the
29
+ worker takes the 15-min soft-timeout. The foreground self-dispatch resolves the
30
+ function's default url on hosted Netlify (per-app name from
31
+ `AGENT_NATIVE_WORKSPACE_APP_ID`), and `fireInternalDispatch` strips the app base
32
+ path for `/.netlify/*` targets so the request reaches the host-root function
33
+ url. Off-Netlify (local dev, `netlify dev`, non-Netlify hosts), the foreground
34
+ dispatches to the framework process-run route, handled inline by the same
35
+ in-process catch-all.
36
+
37
+ This supersedes the earlier attempt that gave the function a custom
38
+ `config.path` (the framework route) plus a `server` `excludedPath` patch — that
39
+ custom path was not honored as a route in production (a probe of
40
+ `POST /_agent-native/agent-chat/_process-run` returned 404). The graceful inline
41
+ 40s fallback on a dispatch fast-fail is unchanged.
42
+
3
43
  ## 0.75.4
4
44
 
5
45
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.75.4",
3
+ "version": "0.75.5",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -63,45 +63,79 @@ export const AGENT_CHAT_PROCESS_RUN_PATH =
63
63
  export const AGENT_BACKGROUND_FUNCTION_NAME = "server-agent-background";
64
64
 
65
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.
66
+ * Default function URL of the background function on Netlify. Every Netlify
67
+ * function is reachable at `/.netlify/functions/<name>` BY DEFAULT; that default
68
+ * url is removed ONLY if the function declares a custom `config.path`. The
69
+ * emitted background function declares NO custom `config.path` (it sets
70
+ * `background: true` and nothing else routing-related), so it KEEPS this default
71
+ * url and the Nitro `server` function already excludes `/.netlify/*` from its
72
+ * `/*` catch-all, so this namespace is never shadowed. The foreground therefore
73
+ * dispatches HERE on hosted Netlify (see `resolveAgentChatProcessRunDispatchPath`).
75
74
  */
76
75
  export const AGENT_BACKGROUND_FUNCTION_URL_PATH = `/.netlify/functions/${AGENT_BACKGROUND_FUNCTION_NAME}`;
77
76
 
77
+ /**
78
+ * The per-app workspace background function URL path. Workspace deploy emits one
79
+ * background function per app named `<app>-agent-background`, reachable at its
80
+ * DEFAULT url `/.netlify/functions/<app>-agent-background` (no custom
81
+ * `config.path`). The foreground resolves the current workspace app id from
82
+ * `AGENT_NATIVE_WORKSPACE_APP_ID` (set by the workspace function entry) so it can
83
+ * dispatch to the right per-app function url. Returns `null` when no workspace
84
+ * app id is configured (single-template deploy).
85
+ */
86
+ function resolveWorkspaceBackgroundFunctionUrlPath(): string | null {
87
+ const raw = process.env.AGENT_NATIVE_WORKSPACE_APP_ID;
88
+ if (typeof raw !== "string") return null;
89
+ // Mirror the workspace app-id normalization (resources/store.ts): take the
90
+ // first path segment and accept only the safe slug shape used for function
91
+ // names. Anything else falls back to the single-template name.
92
+ const candidate = raw.trim().replace(/^\/+/, "").split("/")[0] ?? "";
93
+ if (!/^[a-z0-9][a-z0-9-]{0,127}$/.test(candidate)) return null;
94
+ return `/.netlify/functions/${candidate}-agent-background`;
95
+ }
96
+
78
97
  /**
79
98
  * Resolve the path the foreground POST should self-dispatch the chat background
80
99
  * worker to.
81
100
  *
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.
101
+ * GROUNDED IN THE REAL NETLIFY BUILD OUTPUT + THE NETLIFY DOCS DEFAULT-URL RULE:
102
+ * the background function is emitted INTO the scanned dir
103
+ * (`.netlify/functions-internal/server-agent-background`, or per-app
104
+ * `<app>-agent-background` for workspaces) with `export const config = {
105
+ * background: true, ... }` and NO custom `config.path`. Because it has no custom
106
+ * path, Netlify keeps its DEFAULT function url `/.netlify/functions/<name>`, and
107
+ * `background: true` makes any invocation of that url ASYNC (immediate 202,
108
+ * 15-min budget). The Nitro `server` function already excludes `/.netlify/*`
109
+ * from its `/*` catch-all, so the default-url namespace is NEVER shadowed by the
110
+ * synchronous function.
90
111
  *
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.
112
+ * Therefore on hosted Netlify the foreground dispatches to the function's DEFAULT
113
+ * url (`/.netlify/functions/<name>`); the function entry then rewrites the
114
+ * incoming pathname to `AGENT_CHAT_PROCESS_RUN_PATH` (base-path-prefixed for
115
+ * workspaces) before delegating to the Nitro router, so the `_process-run`
116
+ * plugin runs with the async 15-min budget. Everywhere else (local dev, `netlify
117
+ * dev`, non-Netlify hosts where no second function exists) there is no second
118
+ * function, so the foreground dispatches to the framework route
119
+ * `AGENT_CHAT_PROCESS_RUN_PATH` and the same in-process catch-all handles it
120
+ * inline. The HMAC token (signed over the runId) is unchanged either way.
97
121
  *
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.
122
+ * NOTE: this is the DOC-CORRECT approach. An earlier attempt gave the function a
123
+ * custom `config.path` + a catch-all `excludedPath` patch; the custom path was
124
+ * NOT honored as a route in prod (probe 404). Using the default function url
125
+ * (no custom path) is what Netlify documents and is simpler there is nothing
126
+ * to shadow because `/.netlify/*` is already excluded from the `server` catch-all.
103
127
  */
104
128
  export function resolveAgentChatProcessRunDispatchPath(): string {
129
+ if (
130
+ process.env.NETLIFY &&
131
+ process.env.NETLIFY !== "false" &&
132
+ process.env.NETLIFY_LOCAL !== "true"
133
+ ) {
134
+ return (
135
+ resolveWorkspaceBackgroundFunctionUrlPath() ??
136
+ AGENT_BACKGROUND_FUNCTION_URL_PATH
137
+ );
138
+ }
105
139
  return AGENT_CHAT_PROCESS_RUN_PATH;
106
140
  }
107
141
 
@@ -4394,13 +4394,16 @@ export function createProductionAgentHandler(
4394
4394
  try {
4395
4395
  await fireInternalDispatch({
4396
4396
  event,
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.
4397
+ // On hosted Netlify this resolves to the background function's DEFAULT
4398
+ // url (/.netlify/functions/<name>, or per-app <app>-agent-background for
4399
+ // workspaces) the function declares NO custom config.path, so it keeps
4400
+ // its default url, and `background: true` makes that url async (202,
4401
+ // 15-min budget). The `server` /* catch-all already excludes /.netlify/*
4402
+ // so it never shadows it. Off-Netlify this resolves to the framework
4403
+ // `_process-run` route and the same in-process catch-all handles it
4404
+ // inline. `fireInternalDispatch` strips the app base path for
4405
+ // /.netlify/* targets so the request reaches the host-root function url;
4406
+ // the Authorization Bearer HMAC is preserved either way.
4404
4407
  path: resolveAgentChatProcessRunDispatchPath(),
4405
4408
  taskId: runId,
4406
4409
  body: {
@@ -4643,11 +4646,12 @@ export function createProductionAgentHandler(
4643
4646
  try {
4644
4647
  await fireInternalDispatch({
4645
4648
  event,
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.
4649
+ // Continuation chunks use the same path resolution as the
4650
+ // initial dispatch: on hosted Netlify the background
4651
+ // function's DEFAULT url (no custom config.path; async via
4652
+ // background:true; never shadowed because /.netlify/* is
4653
+ // excluded from the /* catch-all) so each chunk keeps the
4654
+ // 15-min budget; off-Netlify the in-process framework route.
4651
4655
  path: resolveAgentChatProcessRunDispatchPath(),
4652
4656
  taskId: nextRunId,
4653
4657
  body: {
@@ -526,6 +526,13 @@ themes. For any inline border, background, or text color, reference a token:
526
526
  and \`--wf-radius\`. Never hard-code a hex color and never set \`font-family\` — the
527
527
  renderer owns the sketch/clean font.
528
528
 
529
+ **Use literal CSS lengths for spacing.** The \`--wf-*\` tokens are for colors and
530
+ renderer-owned visual styling, not layout spacing. Do not use guessed spacing
531
+ tokens such as \`var(--wf-space-4)\`, Tailwind spacing classes, or theme spacing
532
+ variables inside wireframe HTML; if a token is unavailable in the Plan renderer,
533
+ padding collapses and content hugs the border. Use explicit CSS lengths for
534
+ layout: \`padding:16px\`, \`gap:12px\`, \`margin-top:18px\`, \`minmax(0,1fr)\`.
535
+
529
536
  **Lay out with inline \`style\` flex/grid.** You write the real layout —
530
537
  \`display:flex; flex-direction:column; gap:10px; padding:16px\` and so on — and the
531
538
  renderer never repositions anything. Compose the actual product: reproduce the
@@ -610,10 +617,21 @@ check/serve or verify command for \`<plan-dir>\`.
610
617
  **Treat the wireframe border as part of the visible design.** Always wrap HTML
611
618
  wireframe content in a root container with real inner padding before drawing
612
619
  cards, fields, pills, labels, or controls. Use at least 14-16px of padding,
613
- \`box-sizing: border-box\`, \`height: 100%\`, and \`gap\` between child rows so the
614
- first row never sits flush against the screen border. Keep text away from
615
- borders: every container, field, button, menu item, and annotation needs enough
616
- padding and line-height to read cleanly in the rendered Plan view.
620
+ \`box-sizing: border-box\`, \`height: 100%\`, and \`gap\` between child rows on the
621
+ root node itself so the first row never sits flush against the screen border. Do
622
+ not rely on padding on a nested page section as the first visible inset; the
623
+ outermost element must create the breathing room. Keep text away from borders:
624
+ every container, field, button, menu item, and annotation needs enough padding
625
+ and line-height to read cleanly in the rendered Plan view.
626
+
627
+ **For feature-cloud or abundance visuals, optimize the composition over line-by-line
628
+ reading.** Some marketing/product sections need to feel like a large surface area
629
+ of capability rather than a precise app workflow. In those cases, use one padded
630
+ root with a short headline and a dense, aesthetic cloud of short feature labels,
631
+ chips, rings, or columns. Vary scale and opacity with tokens, cluster by meaning,
632
+ and let many labels be glanceable rather than individually essential. Do not
633
+ force dozens of features into equal cards with long wrapped sentences; that
634
+ usually creates a messy unreadable mockup.
617
635
 
618
636
  **Lay out children safely so they never collide.** Use HTML flex/grid with
619
637
  \`gap\`, \`min-width: 0\`, and sensible overflow. Avoid negative margins, absolute
@@ -1510,61 +1510,57 @@ export function isDurableBackgroundDeployEnabled(): boolean {
1510
1510
  * 15-min async function instead of the synchronous `/*` catch-all.
1511
1511
  * Additive + flag-gated (see `isDurableBackgroundDeployEnabled`).
1512
1512
  *
1513
- * GROUNDED IN THE REAL NETLIFY BUILD OUTPUT (verified from a local Nitro build):
1513
+ * GROUNDED IN THE REAL NETLIFY BUILD OUTPUT (verified from a local Nitro build)
1514
+ * AND THE NETLIFY DOCS DEFAULT-URL RULE:
1514
1515
  * - Nitro's `netlify` preset emits exactly ONE function source at
1515
1516
  * `.netlify/functions-internal/server/`. `server.mjs` re-exports `main.mjs`
1516
1517
  * 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`.
1518
+ * ["/.netlify/*"], preferStatic: true, ... }`. The `/*` catch-all is an
1519
+ * IN-CODE Functions-API-v2 `config.path` and it ALREADY EXCLUDES
1520
+ * `/.netlify/*`.
1520
1521
  * - The generated `.netlify/netlify.toml` sets
1521
1522
  * `functionsDirectory = ".netlify/functions-internal"`. Netlify scans EXACTLY
1522
1523
  * that dir; functions placed anywhere else (e.g. `.netlify/functions/`, which
1523
1524
  * is the BUILD OUTPUT dir where `@netlify/build` later writes the zipped
1524
1525
  * 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`.
1526
+ * - Every scanned function is reachable at its DEFAULT url
1527
+ * `/.netlify/functions/<name>` BY DEFAULT. A custom `config.path` REMOVES
1528
+ * that default url; declaring NO custom `config.path` KEEPS it.
1531
1529
  *
1532
1530
  * THEREFORE we:
1533
1531
  * 1. Emit the background function INTO the scanned dir
1534
1532
  * (`.netlify/functions-internal/server-agent-background/`), sharing the same
1535
1533
  * 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.
1534
+ * 2. Give its `export const config` `background: true` (→ async invoke,
1535
+ * immediate 202, 15-min budget) and NO custom `config.path`. With no custom
1536
+ * path the function keeps its DEFAULT url
1537
+ * `/.netlify/functions/server-agent-background`, and because the Nitro
1538
+ * `server` function's `/*` catch-all already excludes `/.netlify/*`, that
1539
+ * default-url namespace is NEVER shadowed by the synchronous function — no
1540
+ * catch-all patch is needed.
1541
+ * 3. The entry NORMALIZES/rewrites the incoming request pathname to
1542
+ * `AGENT_CHAT_PROCESS_RUN_PATH` before delegating to `./main.mjs`. The
1543
+ * function is reached at its default url
1544
+ * (`/.netlify/functions/server-agent-background`), so the Nitro router needs
1545
+ * the path rewritten to the framework `_process-run` route, preserving the
1546
+ * method, ALL headers (the HMAC `Authorization: Bearer` MUST survive), and
1547
+ * the body.
1546
1548
  * 4. Set `globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true` at cold start
1547
1549
  * (read back by `isInBackgroundFunctionRuntime()` so the worker takes the
1548
1550
  * ~13-min soft-timeout). A `globalThis` flag — NOT `process.env` — keeps the
1549
1551
  * no-env-mutation guard satisfied and carries no cross-request state.
1550
1552
  *
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).
1553
+ * The foreground dispatches to this DEFAULT url on hosted Netlify
1554
+ * (`resolveAgentChatProcessRunDispatchPath` `AGENT_BACKGROUND_FUNCTION_URL_PATH`).
1557
1555
  *
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.
1556
+ * WHY THIS IS THE DOC-CORRECT FIX: a prior attempt gave the function a custom
1557
+ * `config.path` (= the framework route) plus a catch-all `excludedPath` patch.
1558
+ * The custom `config.path` was NOT honored as a route in prod — a probe of
1559
+ * `POST /_agent-native/agent-chat/_process-run` returned 404. The doc-correct
1560
+ * approach (confirmed against the Netlify docs) is to use the DEFAULT function
1561
+ * url with no custom path: the function stays reachable at
1562
+ * `/.netlify/functions/<name>` and is never shadowed because `/.netlify/*` is
1563
+ * already excluded from the `server` catch-all.
1568
1564
  *
1569
1565
  * Safety net regardless of Netlify routing nuance: if the dispatch fast-fails
1570
1566
  * (e.g. the function was not emitted), the foreground handler degrades to an
@@ -1596,11 +1592,6 @@ export function emitSingleTemplateNetlifyBackgroundFunction(
1596
1592
  // copied bundle does NOT re-register the catch-all `config.path`.
1597
1593
  fs.rmSync(path.join(dest, "server.mjs"), { force: true });
1598
1594
 
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
1595
  const processRunPath = JSON.stringify(AGENT_CHAT_PROCESS_RUN_PATH);
1605
1596
  const entry = `// Mark this isolate as the durable background runtime BEFORE the handler
1606
1597
  // bundle is imported, so isInBackgroundFunctionRuntime() reliably returns true
@@ -1618,11 +1609,10 @@ let cachedHandler;
1618
1609
 
1619
1610
  // Netlify v2 invokes this as (request, context). The Nitro netlify handler is a
1620
1611
  // 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
1612
+ // This function declares NO custom \`config.path\`, so it is reached at its
1613
+ // DEFAULT url (/.netlify/functions/${backgroundName}). The Nitro router only
1614
+ // knows the framework route, so we REWRITE the incoming pathname to
1615
+ // PROCESS_RUN_PATH before delegating. Method, ALL headers (the HMAC
1626
1616
  // Authorization: Bearer MUST survive — the plugin verifies it) and the body are
1627
1617
  // preserved by cloning the incoming Request with only its URL pathname set.
1628
1618
  export default async function handler(request) {
@@ -1646,13 +1636,12 @@ export const config = {
1646
1636
  generator: "agent-native build",
1647
1637
  // background: true makes Netlify invoke this ASYNCHRONOUSLY (immediate HTTP
1648
1638
  // 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.
1639
+ // build/functions/background-functions + build/functions/api). We declare NO
1640
+ // custom path, so the function keeps its DEFAULT url
1641
+ // /.netlify/functions/${backgroundName}; the Nitro \`server\` /* catch-all
1642
+ // already excludes /.netlify/* so that default url is never shadowed by the
1643
+ // synchronous function. The foreground dispatches to that default url.
1654
1644
  background: true,
1655
- path: PROCESS_RUN_PATH,
1656
1645
  nodeBundler: "none",
1657
1646
  includedFiles: ["**"],
1658
1647
  preferStatic: false,
@@ -1661,67 +1650,12 @@ export const config = {
1661
1650
  fs.writeFileSync(path.join(dest, `${backgroundName}.mjs`), entry);
1662
1651
  console.log(
1663
1652
  `[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).",
1653
+ `scanned dir .netlify/functions-internal with config { background:true } ` +
1654
+ `and NO custom path reachable at its default url ` +
1655
+ `/.netlify/functions/${backgroundName} (never shadowed; the server /* ` +
1656
+ `catch-all already excludes /.netlify/*). REQUIRES real-deploy ` +
1657
+ `verification of Netlify async (202) invocation — see ` +
1658
+ `docs/design/durable-agent-runs.md.`,
1725
1659
  );
1726
1660
  }
1727
1661
 
@@ -696,24 +696,30 @@ function isDurableBackgroundDeployEnabled(): boolean {
696
696
  /**
697
697
  * Emit a SECOND Netlify function for `app` whose name ends in `-background`,
698
698
  * re-exporting the SAME `main.mjs` handler bundle. Netlify invokes any function
699
- * whose deployed name ends in `-background` asynchronously (202 immediately, up
700
- * to 15-min budget), which is exactly what the durable-background chat
701
- * dispatch (`fireInternalDispatch` → AGENT_CHAT_PROCESS_RUN_PATH) needs.
699
+ * with `config.background: true` asynchronously (202 immediately, up to 15-min
700
+ * budget), which is exactly what the durable-background chat dispatch
701
+ * (`fireInternalDispatch` → the function's default url) needs.
702
702
  *
703
- * The function is given a `config.path` of the chat process-run route so
704
- * Netlify routes that POST to this async function instead of the synchronous
705
- * `<app>-server` function. It shares the same bundle (`includedFiles: ["**"]`)
706
- * so `A2A_SECRET`, the DB URL, and the rest of the env/bundle are present.
703
+ * DOC-CORRECT DEFAULT-URL APPROACH (mirrors the single-template emit in
704
+ * deploy/build.ts): the function declares NO custom `config.path`, so it keeps
705
+ * its DEFAULT url `/.netlify/functions/<app>-agent-background`. The `<app>-server`
706
+ * function's catch-all already excludes `/.netlify/*`, so that default-url
707
+ * namespace is NEVER shadowed by the synchronous function — no overlapping
708
+ * `config.path` and no catch-all patch are needed. The foreground dispatches to
709
+ * that default url (`resolveAgentChatProcessRunDispatchPath` resolves the per-app
710
+ * name from `AGENT_NATIVE_WORKSPACE_APP_ID`); `fireInternalDispatch` strips the
711
+ * app base path for `/.netlify/*` targets so the request reaches the host-root
712
+ * function url. The entry then REWRITES the incoming pathname to the
713
+ * base-path-prefixed `_process-run` route before delegating to the Nitro router.
707
714
  *
708
- * ⚠️ REAL-DEPLOY VERIFICATION REQUIRED. The exact Netlify routing/precedence
709
- * between this `-background` function's `config.path` and the synchronous
710
- * `<app>-server` function's broader `config.path` (which also matches
711
- * `/_agent-native/*` for the dispatch app, or `/<app>/*` otherwise) cannot be
712
- * verified in this environment. It is emitted additively and only under the
713
- * flag; if Netlify resolves the overlap to the wrong function, the dispatch
714
- * would land on the synchronous function (no 15-min budget) the run would
715
- * still work via the existing 40s soft-timeout path, just without the durable
716
- * win. See packages/core/docs/design/durable-agent-runs.md.
715
+ * It shares the same bundle (`includedFiles: ["**"]`) so `A2A_SECRET`, the DB
716
+ * URL, and the rest of the env/bundle are present. A prior attempt gave the
717
+ * function a custom `config.path` (the framework route) that overlapped the
718
+ * synchronous `<app>-server` catch-all; that path was not honored as a route in
719
+ * prod (probe 404). The default url is the doc-correct fix.
720
+ *
721
+ * Safety net: if the dispatch fast-fails the foreground degrades to an inline
722
+ * 40s synchronous run (see production-agent.ts).
717
723
  */
718
724
  function emitNetlifyBackgroundFunction(
719
725
  workspaceRoot: string,
@@ -721,7 +727,9 @@ function emitNetlifyBackgroundFunction(
721
727
  srcServerDir: string,
722
728
  workspaceApps: WorkspaceAppManifestEntry[],
723
729
  ): void {
724
- // Name MUST end in `-background` for Netlify async invocation.
730
+ // Name MUST end in `-background` (Netlify async convention + the runtime guard
731
+ // reads the -background Lambda-name suffix as a fallback). It is reached at its
732
+ // DEFAULT url /.netlify/functions/<app>-agent-background.
725
733
  const backgroundName = `${app}-agent-background`;
726
734
  const dest = path.join(netlifyFunctionsDir(workspaceRoot), backgroundName);
727
735
  fs.rmSync(dest, { recursive: true, force: true });
@@ -733,14 +741,20 @@ function emitNetlifyBackgroundFunction(
733
741
  workspaceApps,
734
742
  app,
735
743
  );
736
- // Only the chat process-run route needs the async function. Keeping the path
737
- // narrow avoids stealing any other traffic from the synchronous function.
738
- // The dispatch URL is app-base-path-prefixed (same as the agent-teams
739
- // processor dispatch), so the route Netlify must map to this async function
740
- // is `/<app>/_agent-native/agent-chat/_process-run`.
744
+ // The Nitro router for this app expects the base-path-prefixed framework route.
745
+ // The function is reached at its default url, so the entry rewrites the
746
+ // incoming pathname to `/<app>/_agent-native/agent-chat/_process-run`.
741
747
  const processRunPath = `${basePath}${AGENT_CHAT_PROCESS_RUN_PATH}`;
742
- const pathConfig = [processRunPath];
743
- const server = `const basePath = ${JSON.stringify(basePath)};
748
+ const server = `// Mark this isolate as the durable background runtime BEFORE the handler bundle
749
+ // is imported, so isInBackgroundFunctionRuntime() reliably returns true in this
750
+ // function (the deployed Lambda name is not guaranteed to end in -background). A
751
+ // globalThis flag (NOT process.env) avoids the no-env-mutation guard and carries
752
+ // no cross-request state.
753
+ globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true;
754
+
755
+ const basePath = ${JSON.stringify(basePath)};
756
+ // The base-path-prefixed framework route the Nitro router dispatches to.
757
+ const PROCESS_RUN_PATH = ${JSON.stringify(processRunPath)};
744
758
 
745
759
  function setBasePathEnv() {
746
760
  const processRef = globalThis.process ??= { env: {} };
@@ -767,16 +781,33 @@ setBasePathEnv();
767
781
 
768
782
  let cachedHandler;
769
783
 
770
- export default async function handler(...args) {
784
+ // Reached at the DEFAULT url /.netlify/functions/${backgroundName}; REWRITE the
785
+ // incoming pathname to the base-path-prefixed _process-run route so the Nitro
786
+ // router runs the plugin. Method, ALL headers (the HMAC Authorization: Bearer
787
+ // MUST survive) and the body are preserved.
788
+ export default async function handler(request) {
771
789
  setBasePathEnv();
772
790
  cachedHandler ??= (await import("./main.mjs")).default;
773
- return cachedHandler(...args);
791
+ const url = new URL(request.url);
792
+ url.pathname = PROCESS_RUN_PATH;
793
+ const method = request.method || "POST";
794
+ const hasBody = method !== "GET" && method !== "HEAD";
795
+ const body = hasBody ? await request.text() : undefined;
796
+ const rewritten = new Request(url.toString(), {
797
+ method,
798
+ headers: request.headers,
799
+ body,
800
+ });
801
+ return cachedHandler(rewritten);
774
802
  }
775
803
 
776
804
  export const config = {
777
805
  name: ${JSON.stringify(`${app} agent background handler`)},
778
806
  generator: "agent-native workspace deploy",
779
- path: ${JSON.stringify(pathConfig)},
807
+ // background: true → async invoke (202, 15-min budget). NO custom path: the
808
+ // function keeps its default url /.netlify/functions/${backgroundName}, which
809
+ // the <app>-server catch-all never shadows (it excludes /.netlify/*).
810
+ background: true,
780
811
  nodeBundler: "none",
781
812
  includedFiles: ["**"],
782
813
  preferStatic: false,
@@ -788,9 +819,10 @@ export const config = {
788
819
  fs.writeFileSync(path.join(dest, `${backgroundName}.mjs`), server);
789
820
  console.log(
790
821
  `[workspace-deploy] Emitted durable-background function "${backgroundName}" ` +
791
- `for app "${app}" (path ${processRunPath}). ` +
792
- `REQUIRES real-deploy verification of Netlify async routing — see ` +
793
- `docs/design/durable-agent-runs.md.`,
822
+ `for app "${app}" with config { background:true } and NO custom path — ` +
823
+ `reachable at its default url /.netlify/functions/${backgroundName} ` +
824
+ `(rewrites to ${processRunPath}). REQUIRES real-deploy verification of ` +
825
+ `Netlify async (202) invocation — see docs/design/durable-agent-runs.md.`,
794
826
  );
795
827
  }
796
828