@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.
- package/corpus/core/CHANGELOG.md +44 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/durable-background.ts +48 -0
- package/corpus/core/src/agent/production-agent.ts +37 -4
- package/corpus/core/src/cli/skills.ts +15 -11
- package/corpus/core/src/deploy/build.ts +54 -19
- package/corpus/core/src/deploy/workspace-deploy.ts +12 -6
- package/corpus/core/src/templates/default/_gitignore +1 -0
- package/corpus/core/src/templates/headless/_gitignore +1 -0
- package/corpus/core/src/templates/workspace-root/_gitignore +1 -0
- package/corpus/templates/clips/actions/list-recordings.ts +21 -0
- package/corpus/templates/clips/app/components/library/library-layout.tsx +21 -2
- package/corpus/templates/clips/app/hooks/use-library.ts +18 -0
- package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +7 -5
- package/dist/agent/durable-background.d.ts +25 -0
- package/dist/agent/durable-background.d.ts.map +1 -1
- package/dist/agent/durable-background.js +43 -0
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +36 -5
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/skills.d.ts +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +15 -11
- package/dist/cli/skills.js.map +1 -1
- package/dist/deploy/build.d.ts +27 -11
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +54 -19
- package/dist/deploy/build.js.map +1 -1
- package/dist/deploy/workspace-deploy.js +12 -6
- package/dist/deploy/workspace-deploy.js.map +1 -1
- package/dist/templates/default/_gitignore +1 -0
- package/dist/templates/headless/_gitignore +1 -0
- package/dist/templates/workspace-root/_gitignore +1 -0
- package/package.json +1 -1
- package/src/templates/default/_gitignore +1 -0
- package/src/templates/headless/_gitignore +1 -0
- package/src/templates/workspace-root/_gitignore +1 -0
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -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
|
package/corpus/core/package.json
CHANGED
|
@@ -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
|
-
//
|
|
5059
|
-
//
|
|
5060
|
-
//
|
|
5061
|
-
|
|
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-
|
|
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.
|
|
402
|
-
\`
|
|
403
|
-
|
|
404
|
-
|
|
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-
|
|
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-
|
|
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.
|
|
1656
|
-
\`
|
|
1657
|
-
|
|
1658
|
-
|
|
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`).
|
|
1484
|
-
*
|
|
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
|
|
1499
|
+
if (raw == null) return true;
|
|
1489
1500
|
const v = raw.trim().toLowerCase();
|
|
1490
|
-
return v === "
|
|
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
|
-
*
|
|
1505
|
-
*
|
|
1506
|
-
*
|
|
1507
|
-
*
|
|
1508
|
-
*
|
|
1509
|
-
*
|
|
1510
|
-
*
|
|
1511
|
-
*
|
|
1512
|
-
*
|
|
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 =
|
|
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 (
|
|
2022
|
-
//
|
|
2023
|
-
//
|
|
2024
|
-
//
|
|
2025
|
-
//
|
|
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 (
|
|
671
|
-
//
|
|
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
|
-
*
|
|
681
|
-
* (
|
|
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
|
|
691
|
+
if (raw == null) return true;
|
|
686
692
|
const v = raw.trim().toLowerCase();
|
|
687
|
-
return v === "
|
|
693
|
+
return !(v === "0" || v === "false" || v === "no" || v === "off");
|
|
688
694
|
}
|
|
689
695
|
|
|
690
696
|
/**
|
|
@@ -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-
|
|
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.
|
|
497
|
-
`
|
|
498
|
-
|
|
499
|
-
|
|
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"}
|