@agent-native/core 0.75.2 → 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.
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: 4029
31
+ - template files: 4030
@@ -1,5 +1,51 @@
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
+
3
49
  ## 0.75.2
4
50
 
5
51
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.75.2",
3
+ "version": "0.75.4",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -63,17 +63,15 @@ export const AGENT_CHAT_PROCESS_RUN_PATH =
63
63
  export const AGENT_BACKGROUND_FUNCTION_NAME = "server-agent-background";
64
64
 
65
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).
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.
77
75
  */
78
76
  export const AGENT_BACKGROUND_FUNCTION_URL_PATH = `/.netlify/functions/${AGENT_BACKGROUND_FUNCTION_NAME}`;
79
77
 
@@ -81,29 +79,29 @@ export const AGENT_BACKGROUND_FUNCTION_URL_PATH = `/.netlify/functions/${AGENT_B
81
79
  * Resolve the path the foreground POST should self-dispatch the chat background
82
80
  * worker to.
83
81
  *
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.
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.
92
97
  *
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
+ * 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.
98
103
  */
99
104
  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
105
  return AGENT_CHAT_PROCESS_RUN_PATH;
108
106
  }
109
107
 
@@ -4394,14 +4394,13 @@ export function createProductionAgentHandler(
4394
4394
  try {
4395
4395
  await fireInternalDispatch({
4396
4396
  event,
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).
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.
4405
4404
  path: resolveAgentChatProcessRunDispatchPath(),
4406
4405
  taskId: runId,
4407
4406
  body: {
@@ -4644,11 +4643,11 @@ export function createProductionAgentHandler(
4644
4643
  try {
4645
4644
  await fireInternalDispatch({
4646
4645
  event,
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.
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.
4652
4651
  path: resolveAgentChatProcessRunDispatchPath(),
4653
4652
  taskId: nextRunId,
4654
4653
  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
  }
@@ -1505,46 +1505,70 @@ export function isDurableBackgroundDeployEnabled(): boolean {
1505
1505
  }
1506
1506
 
1507
1507
  /**
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.
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
1511
  * Additive + flag-gated (see `isDurableBackgroundDeployEnabled`).
1512
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
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`.
1531
+ *
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
1518
1547
  * (read back by `isInBackgroundFunctionRuntime()` so the worker takes the
1519
1548
  * ~13-min soft-timeout). A `globalThis` flag — NOT `process.env` — keeps the
1520
1549
  * 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.
1535
1550
  *
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.
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).
1557
+ *
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.
1544
1568
  *
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).
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).
1548
1572
  */
1549
1573
  export function emitSingleTemplateNetlifyBackgroundFunction(
1550
1574
  projectCwd: string,
@@ -1561,17 +1585,22 @@ export function emitSingleTemplateNetlifyBackgroundFunction(
1561
1585
  return;
1562
1586
  }
1563
1587
  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");
1567
- const dest = path.join(functionsDir, backgroundName);
1568
- fs.mkdirSync(functionsDir, { recursive: true });
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);
1569
1593
  fs.rmSync(dest, { recursive: true, force: true });
1570
1594
  copyDir(serverDir, dest);
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`.
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`.
1573
1597
  fs.rmSync(path.join(dest, "server.mjs"), { force: true });
1574
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
+
1575
1604
  const processRunPath = JSON.stringify(AGENT_CHAT_PROCESS_RUN_PATH);
1576
1605
  const entry = `// Mark this isolate as the durable background runtime BEFORE the handler
1577
1606
  // bundle is imported, so isInBackgroundFunctionRuntime() reliably returns true
@@ -1589,12 +1618,13 @@ let cachedHandler;
1589
1618
 
1590
1619
  // Netlify v2 invokes this as (request, context). The Nitro netlify handler is a
1591
1620
  // 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.
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.
1598
1628
  export default async function handler(request) {
1599
1629
  cachedHandler ??= (await import("./main.mjs")).default;
1600
1630
  const url = new URL(request.url);
@@ -1615,14 +1645,14 @@ export const config = {
1615
1645
  name: "agent background handler",
1616
1646
  generator: "agent-native build",
1617
1647
  // 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.
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.
1625
1654
  background: true,
1655
+ path: PROCESS_RUN_PATH,
1626
1656
  nodeBundler: "none",
1627
1657
  includedFiles: ["**"],
1628
1658
  preferStatic: false,
@@ -1630,10 +1660,68 @@ export const config = {
1630
1660
  `;
1631
1661
  fs.writeFileSync(path.join(dest, `${backgroundName}.mjs`), entry);
1632
1662
  console.log(
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.`,
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).",
1637
1725
  );
1638
1726
  }
1639
1727
 
@@ -2349,7 +2349,6 @@ export function AddProperty({
2349
2349
  },
2350
2350
  });
2351
2351
  const [open, setOpen] = useState(false);
2352
- const [name, setName] = useState("");
2353
2352
  const [typeQuery, setTypeQuery] = useState("");
2354
2353
  const filteredPropertyTypes = filterDocumentPropertyTypes(typeQuery);
2355
2354
  const firstFilteredPropertyType = filteredPropertyTypes[0] ?? null;
@@ -2377,19 +2376,18 @@ export function AddProperty({
2377
2376
  }),
2378
2377
  }))
2379
2378
  .filter((group) => group.fields.length > 0);
2380
- const addPropertyNameInputRef = useRef<HTMLInputElement>(null);
2379
+ const addPropertySearchInputRef = useRef<HTMLInputElement>(null);
2381
2380
 
2382
2381
  useEffect(() => {
2383
2382
  if (!open) return;
2384
2383
  const frame = requestAnimationFrame(() => {
2385
- addPropertyNameInputRef.current?.focus();
2386
- addPropertyNameInputRef.current?.select();
2384
+ addPropertySearchInputRef.current?.focus();
2385
+ addPropertySearchInputRef.current?.select();
2387
2386
  });
2388
2387
  return () => cancelAnimationFrame(frame);
2389
2388
  }, [open]);
2390
2389
 
2391
2390
  function closeAddPropertyPicker() {
2392
- setName("");
2393
2391
  setTypeQuery("");
2394
2392
  setOpen(false);
2395
2393
  }
@@ -2398,7 +2396,7 @@ export function AddProperty({
2398
2396
  const label = DOCUMENT_PROPERTY_TYPE_LABELS[type];
2399
2397
  await configure.mutateAsync({
2400
2398
  documentId,
2401
- name: name.trim() || label,
2399
+ name: label,
2402
2400
  type,
2403
2401
  options: defaultPropertyOptions(type),
2404
2402
  });
@@ -2445,23 +2443,11 @@ export function AddProperty({
2445
2443
  className="w-80 p-2"
2446
2444
  >
2447
2445
  <div className="grid gap-2">
2448
- <Input
2449
- ref={addPropertyNameInputRef}
2450
- aria-label="New property name"
2451
- autoFocus
2452
- value={name}
2453
- placeholder="Property name"
2454
- onChange={(event) => setName(event.target.value)}
2455
- onKeyDown={(event) => {
2456
- if (event.key === "Escape") {
2457
- event.preventDefault();
2458
- closeAddPropertyPicker();
2459
- }
2460
- }}
2461
- />
2462
2446
  <div className="flex h-8 items-center gap-1 rounded border border-border bg-background px-2">
2463
2447
  <IconSearch className="size-3.5 shrink-0 text-muted-foreground" />
2464
2448
  <Input
2449
+ ref={addPropertySearchInputRef}
2450
+ autoFocus
2465
2451
  value={typeQuery}
2466
2452
  placeholder="Search property types"
2467
2453
  aria-label="Search property types"