@agent-native/core 0.75.0 → 0.75.1

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 (31) hide show
  1. package/corpus/core/CHANGELOG.md +16 -0
  2. package/corpus/core/package.json +1 -1
  3. package/corpus/core/src/agent/durable-background.ts +15 -3
  4. package/corpus/core/src/cli/skills.ts +15 -11
  5. package/corpus/core/src/deploy/build.ts +33 -10
  6. package/corpus/core/src/templates/default/_gitignore +1 -0
  7. package/corpus/core/src/templates/headless/_gitignore +1 -0
  8. package/corpus/core/src/templates/workspace-root/_gitignore +1 -0
  9. package/corpus/templates/clips/actions/list-recordings.ts +21 -0
  10. package/corpus/templates/clips/app/components/library/library-layout.tsx +21 -2
  11. package/corpus/templates/clips/app/hooks/use-library.ts +18 -0
  12. package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +7 -5
  13. package/dist/agent/durable-background.d.ts +7 -3
  14. package/dist/agent/durable-background.d.ts.map +1 -1
  15. package/dist/agent/durable-background.js +13 -3
  16. package/dist/agent/durable-background.js.map +1 -1
  17. package/dist/cli/skills.d.ts +1 -1
  18. package/dist/cli/skills.d.ts.map +1 -1
  19. package/dist/cli/skills.js +15 -11
  20. package/dist/cli/skills.js.map +1 -1
  21. package/dist/deploy/build.d.ts +14 -9
  22. package/dist/deploy/build.d.ts.map +1 -1
  23. package/dist/deploy/build.js +33 -10
  24. package/dist/deploy/build.js.map +1 -1
  25. package/dist/templates/default/_gitignore +1 -0
  26. package/dist/templates/headless/_gitignore +1 -0
  27. package/dist/templates/workspace-root/_gitignore +1 -0
  28. package/package.json +1 -1
  29. package/src/templates/default/_gitignore +1 -0
  30. package/src/templates/headless/_gitignore +1 -0
  31. package/src/templates/workspace-root/_gitignore +1 -0
@@ -1,5 +1,21 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.75.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 98b89e2: Durable background agent-chat runs now actually receive Netlify's 15-min async
8
+ budget. The emitted `-background` function declared a custom `config.path` but
9
+ omitted `background: true`, so Netlify served it SYNCHRONOUSLY (~60s) — the
10
+ `config` object overrides the legacy `-background` filename convention — and the
11
+ durable worker was capped at the 60s wall (it degraded to 40s-chunked runs and
12
+ never used the 15-min budget). The emit now sets `background: true` (immediate
13
+ 202 ack + 15-min execution) and force-marks the background runtime in the
14
+ function entry so the worker reliably takes the ~13-min soft-timeout regardless
15
+ of the deployed Lambda name. Root cause confirmed with a live prod routing probe
16
+ (POST to the process-run path returned a synchronous 401 from the handler
17
+ instead of a 202 async ack).
18
+
3
19
  ## 0.75.0
4
20
 
5
21
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.75.0",
3
+ "version": "0.75.1",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -112,11 +112,23 @@ export function isHostedRuntimeForDurableBackground(): boolean {
112
112
  * hard wall and get killed at 60s, then re-dispatch/resume in a wasteful loop.
113
113
  * So the 13-min budget is taken ONLY when this returns true.
114
114
  *
115
- * Falls back to the explicit `AGENT_CHAT_FORCE_BACKGROUND_RUNTIME` env (truthy)
116
- * for hosts that don't expose a `-background`-suffixed function name but where
117
- * the operator has confirmed a long async budget. Off by default.
115
+ * The PRIMARY signal is a `globalThis` marker the emitted background function's
116
+ * entry sets at cold start the deployed Lambda name is not guaranteed to end
117
+ * in `-background` on Netlify, so the entry marks its own runtime. A `globalThis`
118
+ * flag (not `process.env`) keeps the no-env-mutation guard satisfied and carries
119
+ * no cross-request state (set once per isolate). The `AWS_LAMBDA_FUNCTION_NAME`
120
+ * suffix and the explicit `AGENT_CHAT_FORCE_BACKGROUND_RUNTIME` env (truthy) are
121
+ * additional signals — the latter an operator escape hatch. Off by default.
118
122
  */
119
123
  export function isInBackgroundFunctionRuntime(): boolean {
124
+ // Set by the emitted `-background` function entry at cold start (the primary,
125
+ // most reliable signal — see the emit in deploy/build.ts).
126
+ if (
127
+ (globalThis as Record<string, unknown>)
128
+ .__AGENT_NATIVE_BACKGROUND_RUNTIME__ === true
129
+ ) {
130
+ return true;
131
+ }
120
132
  const lambdaName = process.env.AWS_LAMBDA_FUNCTION_NAME;
121
133
  if (
122
134
  typeof lambdaName === "string" &&
@@ -394,14 +394,16 @@ sign-in at setup — this is intended), so the first tool call in that client do
394
394
  not hit an OAuth wall:
395
395
 
396
396
  \`\`\`bash
397
- npx @agent-native/core@latest skills add visual-plan
397
+ npx @agent-native/core@latest skills add visual-plans
398
398
  \`\`\`
399
399
 
400
400
  After that, \`/visual-plan\` and \`/visual-recap\` are the two installed slash
401
- commands. The other planning modes (\`create-ui-plan\`, \`create-prototype-plan\`,
402
- \`create-plan-design\`, \`create-visual-questions\`) are MCP tools reachable from
403
- \`/visual-plan\`, not separate slash commands. Pass \`--no-connect\` to register
404
- the connector without authenticating, then run
401
+ commands. If you only need one command, use \`skills add visual-plan\` or
402
+ \`skills add visual-recap\` instead. The other planning modes
403
+ (\`create-ui-plan\`, \`create-prototype-plan\`, \`create-plan-design\`,
404
+ \`create-visual-questions\`) are MCP tools reachable from \`/visual-plan\`, not
405
+ separate slash commands. Pass \`--no-connect\` to register the connector without
406
+ authenticating, then run
405
407
  \`npx @agent-native/core@latest connect https://plan.agent-native.com --client all\`
406
408
  whenever you are ready, or choose a narrower \`--client\`. Auth and MCP tool
407
409
  loading are per client config/session.
@@ -410,7 +412,7 @@ loading are per client config/session.
410
412
  install with \`--mode local-files\`:
411
413
 
412
414
  \`\`\`bash
413
- npx @agent-native/core@latest skills add visual-plan --mode local-files
415
+ npx @agent-native/core@latest skills add visual-plans --mode local-files
414
416
  \`\`\`
415
417
 
416
418
  This mode does not register the Plan MCP connector. Before authoring structured
@@ -1648,14 +1650,16 @@ sign-in at setup — this is intended), so the first tool call in that client do
1648
1650
  not hit an OAuth wall:
1649
1651
 
1650
1652
  \`\`\`bash
1651
- npx @agent-native/core@latest skills add visual-plan
1653
+ npx @agent-native/core@latest skills add visual-plans
1652
1654
  \`\`\`
1653
1655
 
1654
1656
  After that, \`/visual-plan\` and \`/visual-recap\` are the two installed slash
1655
- commands. The other planning modes (\`create-ui-plan\`, \`create-prototype-plan\`,
1656
- \`create-plan-design\`, \`create-visual-questions\`) are MCP tools reachable from
1657
- \`/visual-plan\`, not separate slash commands. Pass \`--no-connect\` to register
1658
- the connector without authenticating, then run
1657
+ commands. If you only need one command, use \`skills add visual-plan\` or
1658
+ \`skills add visual-recap\` instead. The other planning modes
1659
+ (\`create-ui-plan\`, \`create-prototype-plan\`, \`create-plan-design\`,
1660
+ \`create-visual-questions\`) are MCP tools reachable from \`/visual-plan\`, not
1661
+ separate slash commands. Pass \`--no-connect\` to register the connector without
1662
+ authenticating, then run
1659
1663
  \`npx @agent-native/core@latest connect https://plan.agent-native.com --client all\`
1660
1664
  whenever you are ready, or choose a narrower \`--client\`. Auth and MCP tool
1661
1665
  loading are per client config/session.
@@ -1512,15 +1512,20 @@ export function isDurableBackgroundDeployEnabled(): boolean {
1512
1512
  * that directory to a sibling `<...>-background` function and write an entry
1513
1513
  * with a `config.path` of the process-run route.
1514
1514
  *
1515
- * ⚠️ REAL-DEPLOY VERIFICATION REQUIRED. Whether Netlify routes
1516
- * `/_agent-native/agent-chat/_process-run` to this `-background` function (and
1517
- * invokes it asynchronously with the 15-min budget) vs the synchronous Nitro
1518
- * function cannot be verified in this environment Nitro owns the primary
1519
- * function's routing manifest, and the precedence between the two functions for
1520
- * that path is a Netlify runtime behavior. If routing resolves to the
1521
- * synchronous function, the run still completes via the existing 40s
1522
- * soft-timeout path (no durable win, no regression). See
1523
- * docs/design/durable-agent-runs.md (Open risks #1).
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).
1523
+ *
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).
1524
1529
  */
1525
1530
  export function emitSingleTemplateNetlifyBackgroundFunction(
1526
1531
  projectCwd: string,
@@ -1543,7 +1548,16 @@ export function emitSingleTemplateNetlifyBackgroundFunction(
1543
1548
  // Drop the original Nitro entry so our background entry is the entrypoint.
1544
1549
  fs.rmSync(path.join(dest, "server.mjs"), { force: true });
1545
1550
 
1546
- const entry = `let cachedHandler;
1551
+ const entry = `// Mark this isolate as the durable background runtime BEFORE the handler
1552
+ // bundle is imported, so isInBackgroundFunctionRuntime() reliably returns true
1553
+ // in this function. The deployed Lambda name is NOT guaranteed to end in
1554
+ // "-background" (Netlify may mangle/prefix it), so we cannot depend on
1555
+ // AWS_LAMBDA_FUNCTION_NAME alone. A globalThis flag (NOT process.env) avoids the
1556
+ // no-env-mutation guard and carries no cross-request state — it is a static,
1557
+ // set-once isolate marker read back by isInBackgroundFunctionRuntime().
1558
+ globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true;
1559
+
1560
+ let cachedHandler;
1547
1561
 
1548
1562
  export default async function handler(...args) {
1549
1563
  cachedHandler ??= (await import("./main.mjs")).default;
@@ -1553,6 +1567,15 @@ export default async function handler(...args) {
1553
1567
  export const config = {
1554
1568
  name: "agent background handler",
1555
1569
  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.
1578
+ background: true,
1556
1579
  path: ${JSON.stringify([AGENT_CHAT_PROCESS_RUN_PATH])},
1557
1580
  nodeBundler: "none",
1558
1581
  includedFiles: ["**"],
@@ -10,6 +10,7 @@ npm-debug.log*
10
10
  pnpm-debug.log*
11
11
 
12
12
  node_modules
13
+ node-compile-cache/
13
14
  dist
14
15
  dist-ssr
15
16
  *.local
@@ -7,6 +7,7 @@ npm-debug.log*
7
7
  pnpm-debug.log*
8
8
 
9
9
  node_modules
10
+ node-compile-cache/
10
11
  dist
11
12
  *.local
12
13
 
@@ -28,6 +28,7 @@ packages/*/.env
28
28
  # pnpm
29
29
  .pnpm-store/
30
30
  pnpm-debug.log*
31
+ node-compile-cache/
31
32
 
32
33
  # Local workspace data
33
34
  data/
@@ -55,6 +55,15 @@ export default defineAction({
55
55
  .describe("Sort order"),
56
56
  limit: z.coerce.number().int().min(1).max(500).default(100),
57
57
  offset: z.coerce.number().int().min(0).default(0),
58
+ countOnly: z
59
+ // Robust coercion: a GET query param arrives as the string "true"/"false",
60
+ // and z.coerce.boolean would treat "false" as true. Map strings explicitly.
61
+ .preprocess(
62
+ (v) => (typeof v === "string" ? v === "true" : v),
63
+ z.boolean(),
64
+ )
65
+ .default(false)
66
+ .describe("Return only the total count, skipping the row payload"),
58
67
  }),
59
68
  http: { method: "GET" },
60
69
  run: async (args) => {
@@ -135,6 +144,18 @@ export default defineAction({
135
144
  );
136
145
  }
137
146
 
147
+ // Count-only callers (e.g. the sidebar badge) need just the total for the
148
+ // same filters, ignoring limit/offset. Run the COUNT and short-circuit
149
+ // before the row select, joins, and tag/view subqueries. Keeping it inside
150
+ // this branch means the normal list path doesn't pay for an extra query.
151
+ if (args.countOnly) {
152
+ const totalRows = await db
153
+ .select({ count: sql<number>`COUNT(1)` })
154
+ .from(schema.recordings)
155
+ .where(and(...whereClauses));
156
+ return { recordings: [], total: Number(totalRows[0]?.count ?? 0) };
157
+ }
158
+
138
159
  // Sort
139
160
  const viewCountOrder = sql<number>`(
140
161
  SELECT COUNT(1)
@@ -46,6 +46,7 @@ import {
46
46
  useSpaces,
47
47
  useOrganizations,
48
48
  useCreateFolder,
49
+ useRecordingsCount,
49
50
  } from "@/hooks/use-library";
50
51
  import { useIsMobile } from "@/hooks/use-mobile";
51
52
  import { useDesktopPromo } from "@/hooks/use-desktop-promo";
@@ -124,6 +125,10 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
124
125
  { enabled: hasActiveOrg && Boolean(currentOrganizationId) },
125
126
  );
126
127
 
128
+ // Clip count for the "Library" nav item — count-only, no row payload or
129
+ // title polling across the app shell.
130
+ const { data: libraryCount } = useRecordingsCount({ view: "library" });
131
+
127
132
  const libFolderList: FolderNode[] = useMemo(
128
133
  () =>
129
134
  (libFolders?.folders ?? [])
@@ -176,12 +181,14 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
176
181
  label: string;
177
182
  icon: React.ComponentType<{ className?: string }>;
178
183
  match: (path: string) => boolean;
184
+ count?: number;
179
185
  }[] = [
180
186
  {
181
187
  to: "/library",
182
188
  label: "Library",
183
189
  icon: IconInbox,
184
190
  match: (p) => p.startsWith("/library"),
191
+ count: libraryCount,
185
192
  },
186
193
  {
187
194
  to: "/spaces",
@@ -374,7 +381,7 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
374
381
  </div>
375
382
 
376
383
  <nav className="mt-3 space-y-0.5 px-2">
377
- {navItems.map(({ to, label, icon: Icon, match }) => {
384
+ {navItems.map(({ to, label, icon: Icon, match, count }) => {
378
385
  const active = match(location.pathname);
379
386
  return (
380
387
  <NavLink
@@ -388,7 +395,19 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
388
395
  )}
389
396
  >
390
397
  <Icon className="h-4 w-4" />
391
- {label}
398
+ <span className="flex-1 truncate">{label}</span>
399
+ {count !== undefined && count > 0 && (
400
+ <span
401
+ className={cn(
402
+ "shrink-0 tabular-nums text-[11px]",
403
+ active
404
+ ? "text-primary/80"
405
+ : "text-muted-foreground",
406
+ )}
407
+ >
408
+ {count}
409
+ </span>
410
+ )}
392
411
  </NavLink>
393
412
  );
394
413
  })}
@@ -87,6 +87,24 @@ export function useRecordings(args: ListRecordingsArgs = {}) {
87
87
  );
88
88
  }
89
89
 
90
+ /**
91
+ * Count-only variant for surfaces like the sidebar badge that need a total but
92
+ * not the rows. Hits `list-recordings` with `countOnly`, so it skips the row
93
+ * payload server-side and doesn't share (or pay for) the full-list query or its
94
+ * title polling.
95
+ */
96
+ export function useRecordingsCount(
97
+ args: Omit<ListRecordingsArgs, "limit" | "offset"> = {},
98
+ ) {
99
+ return useActionQuery<number>(
100
+ "list-recordings",
101
+ { ...args, countOnly: true } as any,
102
+ {
103
+ select: (data: any) => (typeof data?.total === "number" ? data.total : 0),
104
+ },
105
+ );
106
+ }
107
+
90
108
  export interface SearchHit {
91
109
  id: string;
92
110
  title: string;
@@ -489,14 +489,16 @@ sign-in at setup — this is intended), so the first tool call in that client do
489
489
  not hit an OAuth wall:
490
490
 
491
491
  ```bash
492
- npx @agent-native/core@latest skills add visual-plan
492
+ npx @agent-native/core@latest skills add visual-plans
493
493
  ```
494
494
 
495
495
  After that, `/visual-plan` and `/visual-recap` are the two installed slash
496
- commands. The other planning modes (`create-ui-plan`, `create-prototype-plan`,
497
- `create-plan-design`, `create-visual-questions`) are MCP tools reachable from
498
- `/visual-plan`, not separate slash commands. Pass `--no-connect` to register
499
- the connector without authenticating, then run
496
+ commands. If you only need one command, use `skills add visual-plan` or
497
+ `skills add visual-recap` instead. The other planning modes
498
+ (`create-ui-plan`, `create-prototype-plan`, `create-plan-design`,
499
+ `create-visual-questions`) are MCP tools reachable from `/visual-plan`, not
500
+ separate slash commands. Pass `--no-connect` to register the connector without
501
+ authenticating, then run
500
502
  `npx @agent-native/core@latest connect https://plan.agent-native.com --client all`
501
503
  whenever you are ready, or choose a narrower `--client`. Auth and MCP tool
502
504
  loading are per client config/session.
@@ -39,9 +39,13 @@ export declare function isHostedRuntimeForDurableBackground(): boolean;
39
39
  * hard wall and get killed at 60s, then re-dispatch/resume in a wasteful loop.
40
40
  * So the 13-min budget is taken ONLY when this returns true.
41
41
  *
42
- * Falls back to the explicit `AGENT_CHAT_FORCE_BACKGROUND_RUNTIME` env (truthy)
43
- * for hosts that don't expose a `-background`-suffixed function name but where
44
- * the operator has confirmed a long async budget. Off by default.
42
+ * The PRIMARY signal is a `globalThis` marker the emitted background function's
43
+ * entry sets at cold start the deployed Lambda name is not guaranteed to end
44
+ * in `-background` on Netlify, so the entry marks its own runtime. A `globalThis`
45
+ * flag (not `process.env`) keeps the no-env-mutation guard satisfied and carries
46
+ * no cross-request state (set once per isolate). The `AWS_LAMBDA_FUNCTION_NAME`
47
+ * suffix and the explicit `AGENT_CHAT_FORCE_BACKGROUND_RUNTIME` env (truthy) are
48
+ * additional signals — the latter an operator escape hatch. Off by default.
45
49
  */
46
50
  export declare function isInBackgroundFunctionRuntime(): boolean;
47
51
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"durable-background.d.ts","sourceRoot":"","sources":["../../src/agent/durable-background.ts"],"names":[],"mappings":"AA6CA;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,2CACE,CAAC;AAE3C;;;GAGG;AACH,eAAO,MAAM,iCAAiC,kCACb,CAAC;AAElC;;;;;;GAMG;AACH,eAAO,MAAM,+BAA+B,oBAAoB,CAAC;AAEjE;;;;GAIG;AACH,wBAAgB,mCAAmC,IAAI,OAAO,CAsB7D;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,6BAA6B,IAAI,OAAO,CAcvD;AAyBD;;;;;;GAMG;AACH,wBAAgB,mCAAmC,IAAI,OAAO,CAM7D;AAED,uDAAuD;AACvD,MAAM,MAAM,qBAAqB,GAC7B;IACE,EAAE,EAAE,IAAI,CAAC;IACT,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B,GACD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEN;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,OAAO,EACb,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,qBAAqB,CA0CvB"}
1
+ {"version":3,"file":"durable-background.d.ts","sourceRoot":"","sources":["../../src/agent/durable-background.ts"],"names":[],"mappings":"AA6CA;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,2CACE,CAAC;AAE3C;;;GAGG;AACH,eAAO,MAAM,iCAAiC,kCACb,CAAC;AAElC;;;;;;GAMG;AACH,eAAO,MAAM,+BAA+B,oBAAoB,CAAC;AAEjE;;;;GAIG;AACH,wBAAgB,mCAAmC,IAAI,OAAO,CAsB7D;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,6BAA6B,IAAI,OAAO,CAsBvD;AAyBD;;;;;;GAMG;AACH,wBAAgB,mCAAmC,IAAI,OAAO,CAM7D;AAED,uDAAuD;AACvD,MAAM,MAAM,qBAAqB,GAC7B;IACE,EAAE,EAAE,IAAI,CAAC;IACT,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B,GACD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEN;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,OAAO,EACb,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,qBAAqB,CA0CvB"}
@@ -93,11 +93,21 @@ export function isHostedRuntimeForDurableBackground() {
93
93
  * hard wall and get killed at 60s, then re-dispatch/resume in a wasteful loop.
94
94
  * So the 13-min budget is taken ONLY when this returns true.
95
95
  *
96
- * Falls back to the explicit `AGENT_CHAT_FORCE_BACKGROUND_RUNTIME` env (truthy)
97
- * for hosts that don't expose a `-background`-suffixed function name but where
98
- * the operator has confirmed a long async budget. Off by default.
96
+ * The PRIMARY signal is a `globalThis` marker the emitted background function's
97
+ * entry sets at cold start the deployed Lambda name is not guaranteed to end
98
+ * in `-background` on Netlify, so the entry marks its own runtime. A `globalThis`
99
+ * flag (not `process.env`) keeps the no-env-mutation guard satisfied and carries
100
+ * no cross-request state (set once per isolate). The `AWS_LAMBDA_FUNCTION_NAME`
101
+ * suffix and the explicit `AGENT_CHAT_FORCE_BACKGROUND_RUNTIME` env (truthy) are
102
+ * additional signals — the latter an operator escape hatch. Off by default.
99
103
  */
100
104
  export function isInBackgroundFunctionRuntime() {
105
+ // Set by the emitted `-background` function entry at cold start (the primary,
106
+ // most reliable signal — see the emit in deploy/build.ts).
107
+ if (globalThis
108
+ .__AGENT_NATIVE_BACKGROUND_RUNTIME__ === true) {
109
+ return true;
110
+ }
101
111
  const lambdaName = process.env.AWS_LAMBDA_FUNCTION_NAME;
102
112
  if (typeof lambdaName === "string" &&
103
113
  lambdaName.toLowerCase().endsWith("-background")) {
@@ -1 +1 @@
1
- {"version":3,"file":"durable-background.js","sourceRoot":"","sources":["../../src/agent/durable-background.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Durable background agent-chat runs (Netlify background functions).\n *\n * Off by default. When enabled, a long in-app agent-chat turn is dispatched\n * into a Netlify *background* function (15-min budget) instead of completing\n * synchronously under the ~40s soft-timeout. The foreground POST claims the\n * run slot, inserts the run row, fires an HMAC-signed self-dispatch to\n * `AGENT_CHAT_PROCESS_RUN_PATH`, and returns the existing SSE subscription so\n * the client streams the same events (via the cross-isolate SQL-poll path)\n * with no client change.\n *\n * This module owns ONLY the gating decision + shared constants so both the\n * HTTP handler (`production-agent.ts`) and the processor route\n * (`agent-chat-plugin.ts`) agree on when the path is active without a circular\n * import. The actual run machinery is reused verbatim from run-manager /\n * run-store / self-dispatch / internal-token.\n *\n * GUARDRAIL: when `isAgentChatDurableBackgroundEnabled()` returns false, the\n * agent-chat handler must behave byte-for-byte like the current synchronous\n * path. The gate is true only when ALL of these hold:\n * 1. `AGENT_CHAT_DURABLE_BACKGROUND` env is not explicitly disabled. It is\n * DEFAULT-ON: unset/empty/unknown counts as enabled; set it to a falsy\n * value (`false`/`0`/`no`/`off`) to opt a specific app back out.\n * 2. The runtime is hosted/serverless (local dev keeps the inline path so SSE\n * stays a single live stream and no second function is needed).\n * 3. `A2A_SECRET` is configured (the HMAC handoff is required to authenticate\n * the background dispatch; without it the dispatch can't be trusted).\n *\n * Default-on is safe because a *dispatch failure degrades to an inline run*: if\n * the self-dispatch self-POST can't be delivered (fast connection error or\n * fast non-2xx), the foreground handler runs the turn synchronously instead of\n * erroring (see `production-agent.ts` — the inline fallback claims the run row\n * atomically so a delayed delivery can never double-execute). So an app where\n * durable dispatch happens to fail still gets a working chat, just without the\n * 15-min budget.\n */\nimport {\n hasConfiguredA2ASecret,\n isA2AProductionRuntime,\n} from \"../a2a/auth-policy.js\";\nimport {\n extractBearerToken,\n verifyInternalToken,\n} from \"../integrations/internal-token.js\";\n\n/**\n * Framework route the background function actually runs — sibling to\n * `AGENT_TEAM_PROCESS_RUN_PATH`. Reached *through* the Netlify background\n * function, so it inherits the 15-min budget.\n */\nexport const AGENT_CHAT_PROCESS_RUN_PATH =\n \"/_agent-native/agent-chat/_process-run\";\n\n/**\n * Env flag for durable background runs. DEFAULT-ON: unset means enabled; an app\n * opts OUT with an explicit falsy value (`false`/`0`/`no`/`off`).\n */\nexport const AGENT_CHAT_DURABLE_BACKGROUND_ENV =\n \"AGENT_CHAT_DURABLE_BACKGROUND\";\n\n/**\n * Body field the foreground handler injects when self-dispatching to the\n * background processor. Its presence is how the re-entered handler knows it is\n * the background worker (run inline with the background soft-timeout; do NOT\n * re-claim the slot or re-dispatch). Untrusted on its own — the route also\n * verifies the HMAC token before invoking the handler.\n */\nexport const AGENT_CHAT_BACKGROUND_RUN_FIELD = \"__backgroundRun\";\n\n/**\n * Mirror of run-manager's private `isHostedRuntime`. Kept in sync deliberately:\n * the durable-background gate must agree with the soft-timeout regime about\n * what \"hosted\" means.\n */\nexport function isHostedRuntimeForDurableBackground(): boolean {\n if (\n process.env.NETLIFY &&\n process.env.NETLIFY !== \"false\" &&\n process.env.NETLIFY_LOCAL !== \"true\"\n ) {\n return true;\n }\n if (\n process.env.AWS_LAMBDA_FUNCTION_NAME &&\n process.env.NETLIFY_LOCAL !== \"true\"\n ) {\n return true;\n }\n return Boolean(\n process.env.CF_PAGES ||\n process.env.VERCEL ||\n process.env.VERCEL_ENV ||\n process.env.RENDER ||\n process.env.FLY_APP_NAME ||\n process.env.K_SERVICE,\n );\n}\n\n/**\n * True when THIS process is actually executing inside a Netlify *background*\n * function (the long, 15-min-budget async function whose deployed name ends in\n * `-background`). Netlify runs functions on AWS Lambda and sets\n * `AWS_LAMBDA_FUNCTION_NAME` to the function's name, so a `-background` suffix is\n * the runtime proof that the ~60s synchronous wall does NOT apply here.\n *\n * This is the SAFETY GUARD for the soft-timeout regime. The `_process-run`\n * self-dispatch worker (`isBackgroundWorker`) is NOT enough on its own: if the\n * `-background` function was never emitted (deploy gate off, or Netlify routed\n * the path to the synchronous function), the self-POST lands on the regular\n * ~60s `server` function. A worker there MUST use the 40s soft-timeout and\n * checkpoint before the 60s wall — using the ~13min budget would overshoot the\n * hard wall and get killed at 60s, then re-dispatch/resume in a wasteful loop.\n * So the 13-min budget is taken ONLY when this returns true.\n *\n * Falls back to the explicit `AGENT_CHAT_FORCE_BACKGROUND_RUNTIME` env (truthy)\n * for hosts that don't expose a `-background`-suffixed function name but where\n * the operator has confirmed a long async budget. Off by default.\n */\nexport function isInBackgroundFunctionRuntime(): boolean {\n const lambdaName = process.env.AWS_LAMBDA_FUNCTION_NAME;\n if (\n typeof lambdaName === \"string\" &&\n lambdaName.toLowerCase().endsWith(\"-background\")\n ) {\n return true;\n }\n const forced = process.env.AGENT_CHAT_FORCE_BACKGROUND_RUNTIME;\n if (forced != null) {\n const v = forced.trim().toLowerCase();\n return v === \"1\" || v === \"true\" || v === \"yes\" || v === \"on\";\n }\n return false;\n}\n\nfunction isFlagEnabled(): boolean {\n // Read the literal key (not `process.env[CONST]`) so guard:no-env-credentials\n // can statically verify it against the allowlisted `AGENT_*` prefix. Keep this\n // in sync with AGENT_CHAT_DURABLE_BACKGROUND_ENV.\n //\n // DEFAULT-ON: durable background runs are the desired behavior for every\n // hosted app. So an unset/empty/unknown flag means ON; an app opts OUT only\n // with an explicit falsy value. This still composes with the hosted +\n // A2A_SECRET gates below, so non-hosted / unconfigured apps stay synchronous.\n // Safety net: a failed dispatch degrades to a synchronous inline run (see\n // production-agent.ts), so default-on cannot break chat even if the\n // self-dispatch can't be delivered on a given app.\n const raw = process.env.AGENT_CHAT_DURABLE_BACKGROUND;\n if (raw == null) return true;\n const normalized = raw.trim().toLowerCase();\n return !(\n normalized === \"0\" ||\n normalized === \"false\" ||\n normalized === \"no\" ||\n normalized === \"off\"\n );\n}\n\n/**\n * The single gate. True when the flag is not explicitly disabled (default-on)\n * AND the runtime is hosted AND A2A_SECRET is configured. False otherwise — and\n * false means the current synchronous behavior is used, unchanged. So a local /\n * non-hosted / unconfigured app stays synchronous even with the flag defaulting\n * on; durable only engages where the runtime actually supports it.\n */\nexport function isAgentChatDurableBackgroundEnabled(): boolean {\n return (\n isFlagEnabled() &&\n isHostedRuntimeForDurableBackground() &&\n hasConfiguredA2ASecret()\n );\n}\n\n/** Decision returned by `prepareProcessRunRequest`. */\nexport type ProcessRunPreparation =\n | {\n ok: true;\n /** The pre-claimed run id the background worker must reuse. */\n runId: string;\n /** Body to stash for the re-entered handler (marker guaranteed present). */\n body: Record<string, unknown>;\n }\n | {\n ok: false;\n /** HTTP status the route should return. */\n status: number;\n /** Error payload. */\n error: string;\n };\n\n/**\n * Pure, transport-agnostic core of the `_process-run` route: validate the body,\n * authenticate the HMAC self-dispatch, and produce the body the re-entered\n * agent-chat handler should run as the background worker.\n *\n * Auth policy mirrors the agent-teams processor exactly:\n * - `A2A_SECRET` set → require a valid `verifyInternalToken(runId, token)`.\n * - no secret but a production runtime → refuse (503) — never run unsigned in\n * prod.\n * - no secret + non-prod (local dev) → allow unsigned; the SQL atomic claim\n * in the worker still prevents double-processing.\n *\n * Extracted from the route handler so the auth + marker-prep decision is unit\n * testable without booting the whole Nitro plugin. The route only adds body\n * reading and the final handler invocation around this.\n */\nexport function prepareProcessRunRequest(\n body: unknown,\n authHeader: string | undefined,\n): ProcessRunPreparation {\n if (!body || typeof body !== \"object\") {\n return { ok: false, status: 400, error: \"Invalid request body\" };\n }\n const record = body as Record<string, unknown>;\n const marker = record[AGENT_CHAT_BACKGROUND_RUN_FIELD] as\n | { runId?: unknown }\n | undefined;\n const runId =\n marker && typeof marker.runId === \"string\"\n ? marker.runId\n : typeof record.taskId === \"string\"\n ? (record.taskId as string)\n : \"\";\n if (!runId) {\n return { ok: false, status: 400, error: \"runId required\" };\n }\n\n if (hasConfiguredA2ASecret()) {\n const token = extractBearerToken(authHeader);\n if (!verifyInternalToken(runId, token ?? \"\")) {\n return {\n ok: false,\n status: 401,\n error: \"Invalid or expired processor token\",\n };\n }\n } else if (isA2AProductionRuntime()) {\n return {\n ok: false,\n status: 503,\n error:\n \"Agent chat background processor not configured — set A2A_SECRET on this deployment.\",\n };\n }\n\n // Ensure the marker is present so the re-entered handler runs as the\n // background worker (reuses runId/turnId, no re-claim, no re-dispatch).\n if (!marker || typeof marker.runId !== \"string\") {\n record[AGENT_CHAT_BACKGROUND_RUN_FIELD] = { runId };\n }\n return { ok: true, runId, body: record };\n}\n"]}
1
+ {"version":3,"file":"durable-background.js","sourceRoot":"","sources":["../../src/agent/durable-background.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Durable background agent-chat runs (Netlify background functions).\n *\n * Off by default. When enabled, a long in-app agent-chat turn is dispatched\n * into a Netlify *background* function (15-min budget) instead of completing\n * synchronously under the ~40s soft-timeout. The foreground POST claims the\n * run slot, inserts the run row, fires an HMAC-signed self-dispatch to\n * `AGENT_CHAT_PROCESS_RUN_PATH`, and returns the existing SSE subscription so\n * the client streams the same events (via the cross-isolate SQL-poll path)\n * with no client change.\n *\n * This module owns ONLY the gating decision + shared constants so both the\n * HTTP handler (`production-agent.ts`) and the processor route\n * (`agent-chat-plugin.ts`) agree on when the path is active without a circular\n * import. The actual run machinery is reused verbatim from run-manager /\n * run-store / self-dispatch / internal-token.\n *\n * GUARDRAIL: when `isAgentChatDurableBackgroundEnabled()` returns false, the\n * agent-chat handler must behave byte-for-byte like the current synchronous\n * path. The gate is true only when ALL of these hold:\n * 1. `AGENT_CHAT_DURABLE_BACKGROUND` env is not explicitly disabled. It is\n * DEFAULT-ON: unset/empty/unknown counts as enabled; set it to a falsy\n * value (`false`/`0`/`no`/`off`) to opt a specific app back out.\n * 2. The runtime is hosted/serverless (local dev keeps the inline path so SSE\n * stays a single live stream and no second function is needed).\n * 3. `A2A_SECRET` is configured (the HMAC handoff is required to authenticate\n * the background dispatch; without it the dispatch can't be trusted).\n *\n * Default-on is safe because a *dispatch failure degrades to an inline run*: if\n * the self-dispatch self-POST can't be delivered (fast connection error or\n * fast non-2xx), the foreground handler runs the turn synchronously instead of\n * erroring (see `production-agent.ts` — the inline fallback claims the run row\n * atomically so a delayed delivery can never double-execute). So an app where\n * durable dispatch happens to fail still gets a working chat, just without the\n * 15-min budget.\n */\nimport {\n hasConfiguredA2ASecret,\n isA2AProductionRuntime,\n} from \"../a2a/auth-policy.js\";\nimport {\n extractBearerToken,\n verifyInternalToken,\n} from \"../integrations/internal-token.js\";\n\n/**\n * Framework route the background function actually runs — sibling to\n * `AGENT_TEAM_PROCESS_RUN_PATH`. Reached *through* the Netlify background\n * function, so it inherits the 15-min budget.\n */\nexport const AGENT_CHAT_PROCESS_RUN_PATH =\n \"/_agent-native/agent-chat/_process-run\";\n\n/**\n * Env flag for durable background runs. DEFAULT-ON: unset means enabled; an app\n * opts OUT with an explicit falsy value (`false`/`0`/`no`/`off`).\n */\nexport const AGENT_CHAT_DURABLE_BACKGROUND_ENV =\n \"AGENT_CHAT_DURABLE_BACKGROUND\";\n\n/**\n * Body field the foreground handler injects when self-dispatching to the\n * background processor. Its presence is how the re-entered handler knows it is\n * the background worker (run inline with the background soft-timeout; do NOT\n * re-claim the slot or re-dispatch). Untrusted on its own — the route also\n * verifies the HMAC token before invoking the handler.\n */\nexport const AGENT_CHAT_BACKGROUND_RUN_FIELD = \"__backgroundRun\";\n\n/**\n * Mirror of run-manager's private `isHostedRuntime`. Kept in sync deliberately:\n * the durable-background gate must agree with the soft-timeout regime about\n * what \"hosted\" means.\n */\nexport function isHostedRuntimeForDurableBackground(): boolean {\n if (\n process.env.NETLIFY &&\n process.env.NETLIFY !== \"false\" &&\n process.env.NETLIFY_LOCAL !== \"true\"\n ) {\n return true;\n }\n if (\n process.env.AWS_LAMBDA_FUNCTION_NAME &&\n process.env.NETLIFY_LOCAL !== \"true\"\n ) {\n return true;\n }\n return Boolean(\n process.env.CF_PAGES ||\n process.env.VERCEL ||\n process.env.VERCEL_ENV ||\n process.env.RENDER ||\n process.env.FLY_APP_NAME ||\n process.env.K_SERVICE,\n );\n}\n\n/**\n * True when THIS process is actually executing inside a Netlify *background*\n * function (the long, 15-min-budget async function whose deployed name ends in\n * `-background`). Netlify runs functions on AWS Lambda and sets\n * `AWS_LAMBDA_FUNCTION_NAME` to the function's name, so a `-background` suffix is\n * the runtime proof that the ~60s synchronous wall does NOT apply here.\n *\n * This is the SAFETY GUARD for the soft-timeout regime. The `_process-run`\n * self-dispatch worker (`isBackgroundWorker`) is NOT enough on its own: if the\n * `-background` function was never emitted (deploy gate off, or Netlify routed\n * the path to the synchronous function), the self-POST lands on the regular\n * ~60s `server` function. A worker there MUST use the 40s soft-timeout and\n * checkpoint before the 60s wall — using the ~13min budget would overshoot the\n * hard wall and get killed at 60s, then re-dispatch/resume in a wasteful loop.\n * So the 13-min budget is taken ONLY when this returns true.\n *\n * The PRIMARY signal is a `globalThis` marker the emitted background function's\n * entry sets at cold start — the deployed Lambda name is not guaranteed to end\n * in `-background` on Netlify, so the entry marks its own runtime. A `globalThis`\n * flag (not `process.env`) keeps the no-env-mutation guard satisfied and carries\n * no cross-request state (set once per isolate). The `AWS_LAMBDA_FUNCTION_NAME`\n * suffix and the explicit `AGENT_CHAT_FORCE_BACKGROUND_RUNTIME` env (truthy) are\n * additional signals — the latter an operator escape hatch. Off by default.\n */\nexport function isInBackgroundFunctionRuntime(): boolean {\n // Set by the emitted `-background` function entry at cold start (the primary,\n // most reliable signal — see the emit in deploy/build.ts).\n if (\n (globalThis as Record<string, unknown>)\n .__AGENT_NATIVE_BACKGROUND_RUNTIME__ === true\n ) {\n return true;\n }\n const lambdaName = process.env.AWS_LAMBDA_FUNCTION_NAME;\n if (\n typeof lambdaName === \"string\" &&\n lambdaName.toLowerCase().endsWith(\"-background\")\n ) {\n return true;\n }\n const forced = process.env.AGENT_CHAT_FORCE_BACKGROUND_RUNTIME;\n if (forced != null) {\n const v = forced.trim().toLowerCase();\n return v === \"1\" || v === \"true\" || v === \"yes\" || v === \"on\";\n }\n return false;\n}\n\nfunction isFlagEnabled(): boolean {\n // Read the literal key (not `process.env[CONST]`) so guard:no-env-credentials\n // can statically verify it against the allowlisted `AGENT_*` prefix. Keep this\n // in sync with AGENT_CHAT_DURABLE_BACKGROUND_ENV.\n //\n // DEFAULT-ON: durable background runs are the desired behavior for every\n // hosted app. So an unset/empty/unknown flag means ON; an app opts OUT only\n // with an explicit falsy value. This still composes with the hosted +\n // A2A_SECRET gates below, so non-hosted / unconfigured apps stay synchronous.\n // Safety net: a failed dispatch degrades to a synchronous inline run (see\n // production-agent.ts), so default-on cannot break chat even if the\n // self-dispatch can't be delivered on a given app.\n const raw = process.env.AGENT_CHAT_DURABLE_BACKGROUND;\n if (raw == null) return true;\n const normalized = raw.trim().toLowerCase();\n return !(\n normalized === \"0\" ||\n normalized === \"false\" ||\n normalized === \"no\" ||\n normalized === \"off\"\n );\n}\n\n/**\n * The single gate. True when the flag is not explicitly disabled (default-on)\n * AND the runtime is hosted AND A2A_SECRET is configured. False otherwise — and\n * false means the current synchronous behavior is used, unchanged. So a local /\n * non-hosted / unconfigured app stays synchronous even with the flag defaulting\n * on; durable only engages where the runtime actually supports it.\n */\nexport function isAgentChatDurableBackgroundEnabled(): boolean {\n return (\n isFlagEnabled() &&\n isHostedRuntimeForDurableBackground() &&\n hasConfiguredA2ASecret()\n );\n}\n\n/** Decision returned by `prepareProcessRunRequest`. */\nexport type ProcessRunPreparation =\n | {\n ok: true;\n /** The pre-claimed run id the background worker must reuse. */\n runId: string;\n /** Body to stash for the re-entered handler (marker guaranteed present). */\n body: Record<string, unknown>;\n }\n | {\n ok: false;\n /** HTTP status the route should return. */\n status: number;\n /** Error payload. */\n error: string;\n };\n\n/**\n * Pure, transport-agnostic core of the `_process-run` route: validate the body,\n * authenticate the HMAC self-dispatch, and produce the body the re-entered\n * agent-chat handler should run as the background worker.\n *\n * Auth policy mirrors the agent-teams processor exactly:\n * - `A2A_SECRET` set → require a valid `verifyInternalToken(runId, token)`.\n * - no secret but a production runtime → refuse (503) — never run unsigned in\n * prod.\n * - no secret + non-prod (local dev) → allow unsigned; the SQL atomic claim\n * in the worker still prevents double-processing.\n *\n * Extracted from the route handler so the auth + marker-prep decision is unit\n * testable without booting the whole Nitro plugin. The route only adds body\n * reading and the final handler invocation around this.\n */\nexport function prepareProcessRunRequest(\n body: unknown,\n authHeader: string | undefined,\n): ProcessRunPreparation {\n if (!body || typeof body !== \"object\") {\n return { ok: false, status: 400, error: \"Invalid request body\" };\n }\n const record = body as Record<string, unknown>;\n const marker = record[AGENT_CHAT_BACKGROUND_RUN_FIELD] as\n | { runId?: unknown }\n | undefined;\n const runId =\n marker && typeof marker.runId === \"string\"\n ? marker.runId\n : typeof record.taskId === \"string\"\n ? (record.taskId as string)\n : \"\";\n if (!runId) {\n return { ok: false, status: 400, error: \"runId required\" };\n }\n\n if (hasConfiguredA2ASecret()) {\n const token = extractBearerToken(authHeader);\n if (!verifyInternalToken(runId, token ?? \"\")) {\n return {\n ok: false,\n status: 401,\n error: \"Invalid or expired processor token\",\n };\n }\n } else if (isA2AProductionRuntime()) {\n return {\n ok: false,\n status: 503,\n error:\n \"Agent chat background processor not configured — set A2A_SECRET on this deployment.\",\n };\n }\n\n // Ensure the marker is present so the re-entered handler runs as the\n // background worker (reuses runId/turnId, no re-claim, no re-dispatch).\n if (!marker || typeof marker.runId !== \"string\") {\n record[AGENT_CHAT_BACKGROUND_RUN_FIELD] = { runId };\n }\n return { ok: true, runId, body: record };\n}\n"]}