@agent-native/core 0.74.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 (38) hide show
  1. package/corpus/core/CHANGELOG.md +44 -0
  2. package/corpus/core/package.json +1 -1
  3. package/corpus/core/src/agent/durable-background.ts +48 -0
  4. package/corpus/core/src/agent/production-agent.ts +37 -4
  5. package/corpus/core/src/cli/skills.ts +15 -11
  6. package/corpus/core/src/deploy/build.ts +54 -19
  7. package/corpus/core/src/deploy/workspace-deploy.ts +12 -6
  8. package/corpus/core/src/templates/default/_gitignore +1 -0
  9. package/corpus/core/src/templates/headless/_gitignore +1 -0
  10. package/corpus/core/src/templates/workspace-root/_gitignore +1 -0
  11. package/corpus/templates/clips/actions/list-recordings.ts +21 -0
  12. package/corpus/templates/clips/app/components/library/library-layout.tsx +21 -2
  13. package/corpus/templates/clips/app/hooks/use-library.ts +18 -0
  14. package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +7 -5
  15. package/dist/agent/durable-background.d.ts +25 -0
  16. package/dist/agent/durable-background.d.ts.map +1 -1
  17. package/dist/agent/durable-background.js +43 -0
  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 +36 -5
  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 +15 -11
  25. package/dist/cli/skills.js.map +1 -1
  26. package/dist/deploy/build.d.ts +27 -11
  27. package/dist/deploy/build.d.ts.map +1 -1
  28. package/dist/deploy/build.js +54 -19
  29. package/dist/deploy/build.js.map +1 -1
  30. package/dist/deploy/workspace-deploy.js +12 -6
  31. package/dist/deploy/workspace-deploy.js.map +1 -1
  32. package/dist/templates/default/_gitignore +1 -0
  33. package/dist/templates/headless/_gitignore +1 -0
  34. package/dist/templates/workspace-root/_gitignore +1 -0
  35. package/package.json +1 -1
  36. package/src/templates/default/_gitignore +1 -0
  37. package/src/templates/headless/_gitignore +1 -0
  38. package/src/templates/workspace-root/_gitignore +1 -0
@@ -1,5 +1,49 @@
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
+
19
+ ## 0.75.0
20
+
21
+ ### Minor Changes
22
+
23
+ - 1272755: Durable background agent-chat runs now complete cleanly instead of looping at
24
+ the 60s Netlify wall.
25
+ - The Netlify `-background` function (15-min async budget) is now emitted by
26
+ default. The deploy gates (`isDurableBackgroundDeployEnabled` in
27
+ `deploy/build.ts` and `deploy/workspace-deploy.ts`) are inverted to default-ON
28
+ to match the runtime gate — unset/empty means enabled; opt out with a falsy
29
+ `AGENT_CHAT_DURABLE_BACKGROUND` (`false`/`0`/`no`/`off`). This makes the chat
30
+ `_process-run` self-dispatch land on the real 15-min async function, so the
31
+ worker runs with its full budget (and likely fixes the app-specific dispatch
32
+ fast-fail, since the self-POST now hits an instant-202 async function).
33
+ - The run soft-timeout is now tied to the REAL function budget rather than
34
+ merely "I am the background worker." A new `isInBackgroundFunctionRuntime()`
35
+ guard (the Lambda function name ends in `-background`) gates the ~13-min
36
+ soft-timeout. A worker that lands on the regular ~60s function — or the
37
+ graceful inline fallback running in the foreground ~60s function — keeps the
38
+ ~40s soft-timeout and checkpoints before the hard wall instead of overshooting
39
+ and re-dispatching in a loop.
40
+ - Server-driven continuation stays on for every durable worker so a run survives
41
+ the client disconnecting (closed tab): a worker on the regular ~60s function (a
42
+ Netlify routing miss, or a non-Netlify host with no `-background` function)
43
+ self-chains 40s chunks, while a worker in a real `-background` function chains
44
+ ~13-min chunks. Only the per-chunk budget differs by function type; the
45
+ continuation is always server-driven.
46
+
3
47
  ## 0.74.0
4
48
 
5
49
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.74.0",
3
+ "version": "0.75.1",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -96,6 +96,54 @@ export function isHostedRuntimeForDurableBackground(): boolean {
96
96
  );
97
97
  }
98
98
 
99
+ /**
100
+ * True when THIS process is actually executing inside a Netlify *background*
101
+ * function (the long, 15-min-budget async function whose deployed name ends in
102
+ * `-background`). Netlify runs functions on AWS Lambda and sets
103
+ * `AWS_LAMBDA_FUNCTION_NAME` to the function's name, so a `-background` suffix is
104
+ * the runtime proof that the ~60s synchronous wall does NOT apply here.
105
+ *
106
+ * This is the SAFETY GUARD for the soft-timeout regime. The `_process-run`
107
+ * self-dispatch worker (`isBackgroundWorker`) is NOT enough on its own: if the
108
+ * `-background` function was never emitted (deploy gate off, or Netlify routed
109
+ * the path to the synchronous function), the self-POST lands on the regular
110
+ * ~60s `server` function. A worker there MUST use the 40s soft-timeout and
111
+ * checkpoint before the 60s wall — using the ~13min budget would overshoot the
112
+ * hard wall and get killed at 60s, then re-dispatch/resume in a wasteful loop.
113
+ * So the 13-min budget is taken ONLY when this returns true.
114
+ *
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.
122
+ */
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
+ }
132
+ const lambdaName = process.env.AWS_LAMBDA_FUNCTION_NAME;
133
+ if (
134
+ typeof lambdaName === "string" &&
135
+ lambdaName.toLowerCase().endsWith("-background")
136
+ ) {
137
+ return true;
138
+ }
139
+ const forced = process.env.AGENT_CHAT_FORCE_BACKGROUND_RUNTIME;
140
+ if (forced != null) {
141
+ const v = forced.trim().toLowerCase();
142
+ return v === "1" || v === "true" || v === "yes" || v === "on";
143
+ }
144
+ return false;
145
+ }
146
+
99
147
  function isFlagEnabled(): boolean {
100
148
  // Read the literal key (not `process.env[CONST]`) so guard:no-env-credentials
101
149
  // can statically verify it against the allowlisted `AGENT_*` prefix. Keep this
@@ -68,6 +68,7 @@ import {
68
68
  AGENT_CHAT_PROCESS_RUN_PATH,
69
69
  AGENT_CHAT_BACKGROUND_RUN_FIELD,
70
70
  isAgentChatDurableBackgroundEnabled,
71
+ isInBackgroundFunctionRuntime,
71
72
  } from "./durable-background.js";
72
73
  import { fireInternalDispatch } from "../server/self-dispatch.js";
73
74
  import { readBody } from "../server/h3-helpers.js";
@@ -3735,6 +3736,13 @@ export function createProductionAgentHandler(
3735
3736
  ? body[AGENT_CHAT_BACKGROUND_RUN_FIELD]!
3736
3737
  : null;
3737
3738
  const isBackgroundWorker = backgroundRunMarker !== null;
3739
+ // Whether this worker is REALLY executing inside a 15-min Netlify
3740
+ // `-background` function (proven by the runtime function name), not merely a
3741
+ // `_process-run` re-entry that may have landed on the ~60s synchronous
3742
+ // function. Only a true value unlocks the ~13-min soft-timeout budget; a
3743
+ // worker on the 60s function keeps the 40s clamp and checkpoints cleanly.
3744
+ const runsInBackgroundFunction =
3745
+ isBackgroundWorker && isInBackgroundFunctionRuntime();
3738
3746
  // How many server-driven background continuations have already chained into
3739
3747
  // this logical turn (0 on the first chunk). Used to bound the chain.
3740
3748
  const backgroundContinuationCount =
@@ -4597,6 +4605,23 @@ export function createProductionAgentHandler(
4597
4605
  // user-stopped runs do NOT chain.
4598
4606
  if (
4599
4607
  shouldChainBackgroundContinuation({
4608
+ // Self-chain server-side for EVERY durable worker, not only the
4609
+ // ones inside a `-background` function. Server-driven
4610
+ // continuation is the whole point of durable background: the run
4611
+ // must survive the client disconnecting (closed tab), so it
4612
+ // cannot depend on the browser re-POSTing `auto_continue`. A
4613
+ // worker on the regular ~60s function — a Netlify routing miss,
4614
+ // or a non-Netlify host (Vercel/Cloudflare/Render/Fly) that
4615
+ // never emits a `-background` function — checkpoints at the 40s
4616
+ // soft-timeout and self-dispatches the next 40s chunk; a worker
4617
+ // in a real `-background` function chains ~13-min chunks. Only
4618
+ // the per-chunk BUDGET differs by function type (gated by
4619
+ // `runsInBackgroundFunction` at the startRun call below); the
4620
+ // continuation itself must stay server-driven on both. (The
4621
+ // self-chain is only reachable when the initial dispatch already
4622
+ // succeeded — a dispatch fast-fail degrades to the inline
4623
+ // foreground fallback, which is not a worker and rides the
4624
+ // connected client's auto_continue instead.)
4600
4625
  isBackgroundWorker,
4601
4626
  run,
4602
4627
  continuationCount: backgroundContinuationCount,
@@ -5055,10 +5080,18 @@ export function createProductionAgentHandler(
5055
5080
  {
5056
5081
  softTimeoutMs: options.runSoftTimeoutMs,
5057
5082
  useHostedSoftTimeoutDefault: true,
5058
- // Inside the Netlify background function there is no ~60s wall, so lift
5059
- // the soft-timeout clamp to ~13min for THIS run only. Foreground runs
5060
- // never set this, so their 40s clamp is unchanged.
5061
- backgroundFunction: isBackgroundWorker,
5083
+ // Lift the soft-timeout clamp to ~13min ONLY when this run is actually
5084
+ // executing inside a real Netlify `-background` function (15-min budget,
5085
+ // no ~60s wall). Being the `_process-run` worker (`isBackgroundWorker`)
5086
+ // is NOT sufficient: if the `-background` function wasn't emitted, or
5087
+ // Netlify routed the self-POST to the synchronous function, the worker
5088
+ // landed on the regular ~60s `server` function — there it MUST keep the
5089
+ // 40s clamp and checkpoint before the wall, or it overshoots the 60s
5090
+ // hard kill and re-dispatches in a loop. `runsInBackgroundFunction`
5091
+ // gates the 13-min budget on the proven runtime, not merely on "I'm the
5092
+ // worker." Foreground runs never set this, so their 40s clamp is
5093
+ // unchanged.
5094
+ backgroundFunction: runsInBackgroundFunction,
5062
5095
  // Fold continuation runs of one logical turn onto a single durable
5063
5096
  // assistant message. Falls back to the runId (turn == run) when the
5064
5097
  // client doesn't supply a turnId.
@@ -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.
@@ -1480,14 +1480,25 @@ export function findInstalledResvgPackages(
1480
1480
  /**
1481
1481
  * Deploy-time gate for emitting the second `-background` Netlify function.
1482
1482
  * Reads the same env flag the runtime gate uses
1483
- * (`AGENT_CHAT_DURABLE_BACKGROUND`). Off by default — when off, the deploy
1484
- * emits exactly one function (today's behavior, byte-for-byte).
1483
+ * (`AGENT_CHAT_DURABLE_BACKGROUND`).
1484
+ *
1485
+ * DEFAULT-ON, matching the runtime gate (`isFlagEnabled` in
1486
+ * durable-background.ts): unset/empty/unknown means enabled; an app opts OUT
1487
+ * only with an explicit falsy value (`false`/`0`/`no`/`off`). This is what
1488
+ * actually emits the 15-min `-background` function so the `_process-run`
1489
+ * dispatch lands on it (async 202 → the worker runs with the real 15-min budget
1490
+ * → its ~13-min soft-timeout fits). The deploy gate and runtime gate MUST agree:
1491
+ * if the deploy emitted no `-background` function but the runtime still routed
1492
+ * the worker into the ~13-min timeout regime, the worker would overshoot the
1493
+ * ~60s synchronous wall and re-dispatch in a loop. (The runtime now also guards
1494
+ * the ~13-min budget on the real function name via `isInBackgroundFunctionRuntime`,
1495
+ * so a missing emit degrades to clean 40s-chunked runs rather than the loop.)
1485
1496
  */
1486
1497
  export function isDurableBackgroundDeployEnabled(): boolean {
1487
1498
  const raw = process.env.AGENT_CHAT_DURABLE_BACKGROUND;
1488
- if (raw == null) return false;
1499
+ if (raw == null) return true;
1489
1500
  const v = raw.trim().toLowerCase();
1490
- return v === "1" || v === "true" || v === "yes" || v === "on";
1501
+ return !(v === "0" || v === "false" || v === "no" || v === "off");
1491
1502
  }
1492
1503
 
1493
1504
  /**
@@ -1501,15 +1512,20 @@ export function isDurableBackgroundDeployEnabled(): boolean {
1501
1512
  * that directory to a sibling `<...>-background` function and write an entry
1502
1513
  * with a `config.path` of the process-run route.
1503
1514
  *
1504
- * ⚠️ REAL-DEPLOY VERIFICATION REQUIRED. Whether Netlify routes
1505
- * `/_agent-native/agent-chat/_process-run` to this `-background` function (and
1506
- * invokes it asynchronously with the 15-min budget) vs the synchronous Nitro
1507
- * function cannot be verified in this environment Nitro owns the primary
1508
- * function's routing manifest, and the precedence between the two functions for
1509
- * that path is a Netlify runtime behavior. If routing resolves to the
1510
- * synchronous function, the run still completes via the existing 40s
1511
- * soft-timeout path (no durable win, no regression). See
1512
- * 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).
1513
1529
  */
1514
1530
  export function emitSingleTemplateNetlifyBackgroundFunction(
1515
1531
  projectCwd: string,
@@ -1532,7 +1548,16 @@ export function emitSingleTemplateNetlifyBackgroundFunction(
1532
1548
  // Drop the original Nitro entry so our background entry is the entrypoint.
1533
1549
  fs.rmSync(path.join(dest, "server.mjs"), { force: true });
1534
1550
 
1535
- 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;
1536
1561
 
1537
1562
  export default async function handler(...args) {
1538
1563
  cachedHandler ??= (await import("./main.mjs")).default;
@@ -1542,6 +1567,15 @@ export default async function handler(...args) {
1542
1567
  export const config = {
1543
1568
  name: "agent background handler",
1544
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,
1545
1579
  path: ${JSON.stringify([AGENT_CHAT_PROCESS_RUN_PATH])},
1546
1580
  nodeBundler: "none",
1547
1581
  includedFiles: ["**"],
@@ -2018,11 +2052,12 @@ export default bundle;
2018
2052
  copyInstalledFfmpegStaticPackage(nitro.options.output.serverDir);
2019
2053
  }
2020
2054
 
2021
- // Durable background agent runs (off by default). Additive ONLY: emits a
2022
- // SECOND Netlify function whose name ends in `-background` re-exporting the
2023
- // same handler bundle, so the chat `_process-run` POST lands on Netlify's
2024
- // async (15-min) function. When the flag is off this is a no-op and the
2025
- // single-function deploy is byte-for-byte unchanged.
2055
+ // Durable background agent runs (default-ON; opt out with a falsy
2056
+ // AGENT_CHAT_DURABLE_BACKGROUND). Additive ONLY: emits a SECOND Netlify
2057
+ // function whose name ends in `-background` re-exporting the same handler
2058
+ // bundle, so the chat `_process-run` POST lands on Netlify's async (15-min)
2059
+ // function. When explicitly opted out this is a no-op and the single-function
2060
+ // deploy is byte-for-byte unchanged.
2026
2061
  if (preset === "netlify" && isDurableBackgroundDeployEnabled()) {
2027
2062
  try {
2028
2063
  emitSingleTemplateNetlifyBackgroundFunction(cwd);
@@ -667,8 +667,9 @@ function copyNetlifyFunctionIntoWorkspace(
667
667
  copyDir(src, dest);
668
668
  patchNetlifyFunctionEntry(dest, app, workspaceApps, staticDir);
669
669
 
670
- // Durable background agent runs (off by default). Additive ONLY: when the
671
- // flag is off this emits nothing and the single-function deploy is unchanged.
670
+ // Durable background agent runs (default-ON; opt out with a falsy
671
+ // AGENT_CHAT_DURABLE_BACKGROUND). Additive ONLY: when explicitly opted out
672
+ // this emits nothing and the single-function deploy is unchanged.
672
673
  if (isDurableBackgroundDeployEnabled()) {
673
674
  emitNetlifyBackgroundFunction(workspaceRoot, app, src, workspaceApps);
674
675
  }
@@ -677,14 +678,19 @@ function copyNetlifyFunctionIntoWorkspace(
677
678
  /**
678
679
  * Deploy-time gate for emitting the second `-background` Netlify function. Reads
679
680
  * the same env flag the runtime gate uses (`AGENT_CHAT_DURABLE_BACKGROUND`).
680
- * Off by default — when off, the deploy emits exactly one function per app
681
- * (today's behavior, byte-for-byte).
681
+ *
682
+ * DEFAULT-ON, matching the runtime gate (`isFlagEnabled` in
683
+ * durable-background.ts) and the single-template gate
684
+ * (`isDurableBackgroundDeployEnabled` in deploy/build.ts): unset/empty/unknown
685
+ * means enabled; an app opts OUT only with an explicit falsy value
686
+ * (`false`/`0`/`no`/`off`). This emits the per-app 15-min `-background` function
687
+ * so the chat `_process-run` dispatch lands on it with the real long budget.
682
688
  */
683
689
  function isDurableBackgroundDeployEnabled(): boolean {
684
690
  const raw = process.env.AGENT_CHAT_DURABLE_BACKGROUND;
685
- if (raw == null) return false;
691
+ if (raw == null) return true;
686
692
  const v = raw.trim().toLowerCase();
687
- return v === "1" || v === "true" || v === "yes" || v === "on";
693
+ return !(v === "0" || v === "false" || v === "no" || v === "off");
688
694
  }
689
695
 
690
696
  /**
@@ -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.
@@ -23,6 +23,31 @@ export declare const AGENT_CHAT_BACKGROUND_RUN_FIELD = "__backgroundRun";
23
23
  * what "hosted" means.
24
24
  */
25
25
  export declare function isHostedRuntimeForDurableBackground(): boolean;
26
+ /**
27
+ * True when THIS process is actually executing inside a Netlify *background*
28
+ * function (the long, 15-min-budget async function whose deployed name ends in
29
+ * `-background`). Netlify runs functions on AWS Lambda and sets
30
+ * `AWS_LAMBDA_FUNCTION_NAME` to the function's name, so a `-background` suffix is
31
+ * the runtime proof that the ~60s synchronous wall does NOT apply here.
32
+ *
33
+ * This is the SAFETY GUARD for the soft-timeout regime. The `_process-run`
34
+ * self-dispatch worker (`isBackgroundWorker`) is NOT enough on its own: if the
35
+ * `-background` function was never emitted (deploy gate off, or Netlify routed
36
+ * the path to the synchronous function), the self-POST lands on the regular
37
+ * ~60s `server` function. A worker there MUST use the 40s soft-timeout and
38
+ * checkpoint before the 60s wall — using the ~13min budget would overshoot the
39
+ * hard wall and get killed at 60s, then re-dispatch/resume in a wasteful loop.
40
+ * So the 13-min budget is taken ONLY when this returns true.
41
+ *
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.
49
+ */
50
+ export declare function isInBackgroundFunctionRuntime(): boolean;
26
51
  /**
27
52
  * The single gate. True when the flag is not explicitly disabled (default-on)
28
53
  * AND the runtime is hosted AND A2A_SECRET is configured. False otherwise — and
@@ -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;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"}