@agent-native/core 0.168.13 → 0.169.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/README.md +1 -1
- package/corpus/templates/analytics/actions/bigquery-table-info.ts +3 -2
- package/corpus/templates/analytics/actions/search-bigquery-schema.ts +130 -4
- package/corpus/templates/analytics/agent-native.config.ts +1 -0
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +36 -23
- package/corpus/templates/analytics/app/lib/dashboard-visibility.ts +41 -0
- package/corpus/templates/analytics/app/pages/DashboardOverview.tsx +34 -9
- package/corpus/templates/analytics/changelog/2026-08-21-dashboard-filters-keep-other-users-dashboards-out-of-mine.md +6 -0
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +14 -3
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +33 -3
- package/corpus/templates/brain/server/lib/brain.ts +27 -2
- package/corpus/templates/clips/actions/stop-meeting-recording.ts +87 -6
- package/corpus/templates/clips/app/components/library/search-bar.tsx +1 -1
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +3 -4
- package/corpus/templates/design/app/components/design/EditPanel.tsx +15 -47
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +3 -3
- package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +0 -7
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +2 -4
- package/corpus/templates/design/app/i18n-data.ts +10 -10
- package/corpus/templates/design/app/pages/DesignEditor.tsx +22 -22
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +39 -8
- package/corpus/templates/design/app/pages/DesignSystems.tsx +1 -1
- package/corpus/templates/design/app/pages/Index.tsx +1 -1
- package/corpus/templates/design/app/pages/design-editor/preview-navigation.ts +25 -0
- package/corpus/templates/mail/app/hooks/use-emails.ts +13 -3
- package/dist/agent/engine/credential-errors.d.ts +1 -1
- package/dist/agent/engine/credential-errors.js +2 -2
- package/dist/agent/engine/first-event-timeout.d.ts +8 -0
- package/dist/agent/engine/first-event-timeout.js +8 -0
- package/dist/agent/production-agent.d.ts +0 -30
- package/dist/agent/production-agent.js +17 -38
- package/dist/agent/run-loop-with-resume.d.ts +38 -25
- package/dist/agent/run-loop-with-resume.js +140 -55
- package/dist/agent/run-manager.d.ts +83 -68
- package/dist/agent/run-manager.js +280 -94
- package/dist/agent/run-store.d.ts +31 -0
- package/dist/agent/run-store.js +42 -12
- package/dist/app-config/agent.d.ts +2 -0
- package/dist/app-config/agent.js +33 -0
- package/dist/app-config/run-lifecycle-invariants.d.ts +248 -0
- package/dist/app-config/run-lifecycle-invariants.js +342 -0
- package/dist/app-config/schema.d.ts +2 -0
- package/dist/app-config/store.js +9 -1
- package/dist/client/AssistantChat.js +47 -15
- package/dist/client/ConnectBuilderCard.d.ts +3 -1
- package/dist/client/ConnectBuilderCard.js +6 -3
- package/dist/client/agent-chat-adapter.d.ts +0 -2
- package/dist/client/agent-chat-adapter.js +7 -23
- package/dist/client/chat/message-components.d.ts +7 -0
- package/dist/client/chat/message-components.js +1 -1
- package/dist/client/chat/run-recovery.js +5 -2
- package/dist/client/chat/tool-call-display.js +9 -3
- package/dist/client/error-format.js +12 -0
- package/dist/client/navigation/index.d.ts +1 -0
- package/dist/client/navigation/index.js +1 -0
- package/dist/client/route-state.js +6 -0
- package/dist/client/workspace-app-navigation.d.ts +10 -0
- package/dist/client/workspace-app-navigation.js +38 -0
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.d.ts +13 -0
- package/dist/deploy/build.js +51 -0
- package/dist/jobs/background-automation-runner.d.ts +25 -0
- package/dist/jobs/background-automation-runner.js +104 -21
- package/dist/jobs/run-history.d.ts +7 -1
- package/dist/jobs/run-history.js +57 -14
- package/dist/localization/core-messages/ar-SA.js +1 -1
- package/dist/localization/core-messages/de-DE.js +1 -1
- package/dist/localization/core-messages/en-US.d.ts +1 -1
- package/dist/localization/core-messages/en-US.js +1 -1
- package/dist/localization/core-messages/es-ES.js +1 -1
- package/dist/localization/core-messages/fr-FR.js +1 -1
- package/dist/localization/core-messages/hi-IN.js +1 -1
- package/dist/localization/core-messages/ja-JP.js +1 -1
- package/dist/localization/core-messages/ko-KR.js +1 -1
- package/dist/localization/core-messages/pt-BR.js +1 -1
- package/dist/localization/core-messages/zh-CN.js +1 -1
- package/dist/localization/core-messages/zh-TW.js +1 -1
- package/dist/localization/core-messages.d.ts +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/observability/traces.d.ts +13 -0
- package/dist/observability/traces.js +369 -317
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
- package/dist/provider-api/actions/provider-api.d.ts +13 -13
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/db/check-scoping.js +9 -6
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/agent-chat-plugin.js +2 -4
- package/dist/server/builder-browser.d.ts +4 -0
- package/dist/server/builder-browser.js +23 -1
- package/dist/server/core-routes-plugin.js +13 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +3 -3
- /package/corpus/templates/dispatch/app/routes/{apps.$appId.tsx → apps.$appId.$.tsx} +0 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import type { AppConfig } from "./schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* Wall-clock reserved between a background automation's round budget and its
|
|
4
|
+
* own hard abort.
|
|
5
|
+
*
|
|
6
|
+
* It covers wind-down only — emit the terminal event, persist the turn, let
|
|
7
|
+
* `finalized` settle — because the recoverable boundary on this path belongs to
|
|
8
|
+
* the agent-loop wrapper's own per-round timer, not to a second timer in the
|
|
9
|
+
* run manager.
|
|
10
|
+
*
|
|
11
|
+
* This is the constant that makes `automation soft timeout < automation hard
|
|
12
|
+
* abort` true by construction rather than by hoping two independently chosen
|
|
13
|
+
* numbers happen to be ordered — they were not: the shipped build gave the
|
|
14
|
+
* automation path a 13-minute soft timeout under a 10-minute hard abort, so
|
|
15
|
+
* the recoverable boundary was dead code and the only boundary an automation
|
|
16
|
+
* could reach was the terminal one.
|
|
17
|
+
*/
|
|
18
|
+
export declare const BACKGROUND_AUTOMATION_SOFT_TIMEOUT_HEADROOM_MS = 20000;
|
|
19
|
+
/**
|
|
20
|
+
* The host's hard kill for a background function (Netlify: 15 minutes).
|
|
21
|
+
*
|
|
22
|
+
* Not configuration — a deployment does not choose it, the platform does. It is
|
|
23
|
+
* here because `backgroundSoftTimeoutCeilingMs` IS the clamp that
|
|
24
|
+
* `resolveRunSoftTimeoutMs` reduces every background soft timeout to, so once
|
|
25
|
+
* that ceiling became configurable nothing was left bounding it: a deployment
|
|
26
|
+
* could set 60 minutes and push its own chunk boundary past the wall the
|
|
27
|
+
* ceiling exists to stay inside, turning every long background turn back into
|
|
28
|
+
* the silent platform kill it was introduced to prevent. Configurable must not
|
|
29
|
+
* mean unclamped.
|
|
30
|
+
*/
|
|
31
|
+
export declare const BACKGROUND_FUNCTION_WALL_MS: number;
|
|
32
|
+
/**
|
|
33
|
+
* Wall-clock a background chunk must leave itself to abort, persist the partial
|
|
34
|
+
* turn, write the terminal event, and chain a successor before the host kills
|
|
35
|
+
* the invocation. The shipped 13-minute ceiling under a 15-minute wall is
|
|
36
|
+
* exactly this margin.
|
|
37
|
+
*/
|
|
38
|
+
export declare const BACKGROUND_FUNCTION_WALL_HEADROOM_MS: number;
|
|
39
|
+
/**
|
|
40
|
+
* Slack between the CHAIN bound (`agent.maxBackgroundRunContinuations`) and the
|
|
41
|
+
* per-turn LEDGER bound below.
|
|
42
|
+
*
|
|
43
|
+
* They count different things. The chain bound counts handoffs a chunk decided
|
|
44
|
+
* to make; the ledger counts every run ROW the turn produced, which also
|
|
45
|
+
* includes sweep redispatches and stale-run recoveries no chunk ever decided.
|
|
46
|
+
* Without slack the ledger would refuse a turn before the chain bound it is
|
|
47
|
+
* meant to sit above, so a turn recovered once would die holding unused chain
|
|
48
|
+
* budget.
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* Shipped run-lifecycle bounds.
|
|
52
|
+
*
|
|
53
|
+
* They live beside the relationships that constrain them so a change to one is
|
|
54
|
+
* checked against the others in the same file. `run-manager.ts` and
|
|
55
|
+
* `production-agent.ts` re-export them under their historical names; this
|
|
56
|
+
* module imports no agent code, so nothing here can become circular.
|
|
57
|
+
*/
|
|
58
|
+
/**
|
|
59
|
+
* Hard ceiling for the soft timeout when a run executes inside a Netlify
|
|
60
|
+
* background function (any deployed function whose name ends in `-background`).
|
|
61
|
+
* Background functions return 202 immediately and run detached for up to 15
|
|
62
|
+
* minutes, so the ~60s synchronous function wall that 40s defends against does
|
|
63
|
+
* NOT apply. 13 minutes leaves ~2 min of headroom under Netlify's 15-min hard
|
|
64
|
+
* kill to abort, persist the partial turn, write the terminal event, and (for
|
|
65
|
+
* the rare >13-min turn) self-fire another background continuation.
|
|
66
|
+
*
|
|
67
|
+
* This ceiling is used ONLY when a caller explicitly opts in with
|
|
68
|
+
* `backgroundFunction: true`. It does not change the foreground/interactive
|
|
69
|
+
* ceiling and does not fire unless the durable-background path dispatched the
|
|
70
|
+
* run into a background function. Per the design doc Guardrail, the 40s
|
|
71
|
+
* interactive clamp stays correct for every non-background run.
|
|
72
|
+
*/
|
|
73
|
+
export declare const BACKGROUND_SOFT_TIMEOUT_CEILING_MS: number;
|
|
74
|
+
/**
|
|
75
|
+
* AUTHORITATIVE no-progress backstop for a run, enforced by the run manager
|
|
76
|
+
* itself (timer-driven, independent of any layer below).
|
|
77
|
+
*
|
|
78
|
+
* The finer-grained watchdogs inside the agent loop (model-stream and
|
|
79
|
+
* action-preparation no-progress, both 90s) only guard the model event stream
|
|
80
|
+
* — a stall in any segment OUTSIDE that guarded loop (engine-call
|
|
81
|
+
* establishment, worker setup between continuation chunks, a wedged transport
|
|
82
|
+
* that emits keepalives while the loop never runs) previously hung forever
|
|
83
|
+
* with the client watching keepalives. This backstop covers every segment by
|
|
84
|
+
* construction: if no REAL progress event (see `shouldBumpProgressForEvent`;
|
|
85
|
+
* keepalives and zero-byte prep activity don't count) lands for this long —
|
|
86
|
+
* and no unit of work is in flight (see `inFlightWorkDelta`: tool calls,
|
|
87
|
+
* cross-app calls, and the model stream all legitimately emit nothing for
|
|
88
|
+
* minutes and each carry a bound of their own) — the run manager emits
|
|
89
|
+
* `auto_continue { reason: "no_progress" }` and aborts the chunk, exactly
|
|
90
|
+
* like the soft timeout, so the normal continuation machinery recovers it.
|
|
91
|
+
*
|
|
92
|
+
* Being numerically larger than the in-loop watchdogs is NOT what keeps this
|
|
93
|
+
* from killing a healthy run, and treating it that way is what made it do so:
|
|
94
|
+
* this clock and the loop's `lastModelStreamProgressAt` measure DIFFERENT
|
|
95
|
+
* events. An extended-thinking phase bumps the inner clock on every engine
|
|
96
|
+
* frame while forwarding nothing, so the inner watchdog correctly stayed quiet
|
|
97
|
+
* and this one saw pure silence — runs whose worst gap crossed 150s died while
|
|
98
|
+
* still streaming, some by a single second. Ordering between two clocks only
|
|
99
|
+
* means something when they watch the same events; suspending on in-flight
|
|
100
|
+
* work is what actually makes the two agree.
|
|
101
|
+
*
|
|
102
|
+
* This is now only the CEILING, not the value: `resolveRunNoProgressTimeoutMs`
|
|
103
|
+
* clamps the foreground backstop to a fraction of the chunk's soft timeout
|
|
104
|
+
* (~30s at a 40s chunk), which is BELOW the 90s in-loop watchdogs rather than
|
|
105
|
+
* above them. That ordering is deliberate — the in-loop watchdogs could never
|
|
106
|
+
* fire inside a hosted foreground chunk anyway, since the serverless wall
|
|
107
|
+
* (~57-59s) arrives first. Proven durable-background chunks keep the full
|
|
108
|
+
* `DEFAULT_BACKGROUND_NO_PROGRESS_TIMEOUT_MS` so large outputs can use the
|
|
109
|
+
* background budget. Only armed when a soft-timeout regime is active (hosted
|
|
110
|
+
* runs); local dev stays unbounded.
|
|
111
|
+
*/
|
|
112
|
+
export declare const RUN_NO_PROGRESS_HARD_TIMEOUT_MS = 150000;
|
|
113
|
+
/**
|
|
114
|
+
* Default in-loop watchdog for silence while an action's arguments stream in.
|
|
115
|
+
* Read through `resolveActionPreparationNoProgressTimeoutMs`, never directly:
|
|
116
|
+
* a host diagnosing a timeout has to be able to see and change this number.
|
|
117
|
+
*/
|
|
118
|
+
export declare const ACTION_PREPARATION_NO_PROGRESS_TIMEOUT_MS = 90000;
|
|
119
|
+
/**
|
|
120
|
+
* Default in-loop watchdog for silence between engine stream frames. Read
|
|
121
|
+
* through `resolveModelStreamNoProgressTimeoutMs`, never directly.
|
|
122
|
+
*/
|
|
123
|
+
export declare const MODEL_STREAM_NO_PROGRESS_TIMEOUT_MS = 90000;
|
|
124
|
+
/**
|
|
125
|
+
* Consecutive chunks allowed to end on the SAME terminal error code having
|
|
126
|
+
* produced nothing before the chain stops.
|
|
127
|
+
*
|
|
128
|
+
* Two, because two independent recovery layers multiply here and neither can
|
|
129
|
+
* see the other: the engine already retried this identical request 3x with
|
|
130
|
+
* backoff before the error was ever emitted, and a recoverable error is also a
|
|
131
|
+
* continuation boundary, so every chunk that fails costs 4 gateway attempts
|
|
132
|
+
* and dispatches a fresh one. A production turn spent 27 background runs and
|
|
133
|
+
* 15 minutes on one message this way. The first repeat is the retry this path
|
|
134
|
+
* exists for; a second identical failure that moved nothing is evidence the
|
|
135
|
+
* retrying itself is what is broken, not the request.
|
|
136
|
+
*/
|
|
137
|
+
export declare const MAX_CONSECUTIVE_NO_PROGRESS_CONTINUATIONS = 2;
|
|
138
|
+
/**
|
|
139
|
+
* Wall-clock ceiling on a single logical turn. The run-count ledger alone is
|
|
140
|
+
* not a time bound: in durable mode each of the ~25 permitted chunks may burn
|
|
141
|
+
* ~780s, so the ledger's real worst case is over five hours (production has an
|
|
142
|
+
* observed 2h34m turn). Nobody is waiting that long, and every minute past
|
|
143
|
+
* this point is spend on a request the user has abandoned.
|
|
144
|
+
*/
|
|
145
|
+
export declare const MAX_TURN_WALL_CLOCK_MS: number;
|
|
146
|
+
/**
|
|
147
|
+
* Cap on continuation iterations inside a single
|
|
148
|
+
* `runAgentLoopDirectWithSoftTimeout` invocation. The host's hard function
|
|
149
|
+
* timeout usually bounds this naturally — but a defensive cap prevents an
|
|
150
|
+
* instant-error spiral from looping forever inside hosting environments with a
|
|
151
|
+
* generous budget.
|
|
152
|
+
*
|
|
153
|
+
* 6 leaves room for: 1 normal completion + a few resume rounds for design
|
|
154
|
+
* generation (prompt + 3 variants ≈ 4 LLM calls), with a small safety margin.
|
|
155
|
+
*/
|
|
156
|
+
export declare const MAX_RUN_LOOP_CONTINUATIONS = 6;
|
|
157
|
+
/**
|
|
158
|
+
* A delegated turn that is proven to be running inside a durable background
|
|
159
|
+
* function has the same 15-minute host budget as main chat, but this wrapper
|
|
160
|
+
* historically kept the foreground-sized six-continuation cap. A healthy
|
|
161
|
+
* child A2A call can consume several minutes and the receiving model may then
|
|
162
|
+
* need more than six recovery/model-stream boundaries to finish its own tool
|
|
163
|
+
* work. Keep a hard cap, but give the proven background path the same bounded
|
|
164
|
+
* continuation allowance as the durable main-chat runner. The cumulative
|
|
165
|
+
* soft-timeout below still prevents these rounds from exceeding the one real
|
|
166
|
+
* background-function wall-clock budget.
|
|
167
|
+
*/
|
|
168
|
+
export declare const MAX_BACKGROUND_RUN_LOOP_CONTINUATIONS = 20;
|
|
169
|
+
export declare const TURN_RUN_LEDGER_SLACK = 5;
|
|
170
|
+
/**
|
|
171
|
+
* Hard cap on server-driven background→background continuation chunks for a
|
|
172
|
+
* single logical turn. A `backgroundFunction` run gets a ~13-min soft timeout,
|
|
173
|
+
* so reaching this boundary at all is the rare exception (most turns finish in
|
|
174
|
+
* one chunk). The cap bounds a pathological turn that would otherwise chain
|
|
175
|
+
* background invocations forever, mirroring `MAX_AGENT_TEAM_CONTINUATIONS`.
|
|
176
|
+
*/
|
|
177
|
+
export declare const MAX_BACKGROUND_RUN_CONTINUATIONS = 20;
|
|
178
|
+
/**
|
|
179
|
+
* Per-TURN follow budgets the browser applies while reading a background turn.
|
|
180
|
+
*
|
|
181
|
+
* They live here, not in `client/agent-chat-adapter.ts`, because they are one
|
|
182
|
+
* half of an ordering relationship whose other half is server configuration —
|
|
183
|
+
* and a relationship checked in only one of its two homes is the failure this
|
|
184
|
+
* module exists to prevent. This file has no runtime imports (the `AppConfig`
|
|
185
|
+
* import is type-only and erased), so the browser bundle pays nothing to read
|
|
186
|
+
* them from here.
|
|
187
|
+
*
|
|
188
|
+
* CLIENT-ABOVE-SERVER: these MUST stay above the server's own ceilings. The
|
|
189
|
+
* client fires on a clock and cannot tell looping from working; the server can,
|
|
190
|
+
* so the server must always terminate a turn first and write a truthful
|
|
191
|
+
* terminal reason. They shipped at 10 min / 6 runs while ONE legal background
|
|
192
|
+
* chunk may run 13 minutes — so the client killed healthy turns the server was
|
|
193
|
+
* still streaming, measured in production as aborts at 11-25 minutes with
|
|
194
|
+
* progress recorded right up to the abort. That was the top non-auth cause of
|
|
195
|
+
* "the chat just stopped".
|
|
196
|
+
*
|
|
197
|
+
* Do NOT tighten these to catch a stuck turn. A turn that is not progressing is
|
|
198
|
+
* already caught twice by mechanisms that read progress rather than a clock:
|
|
199
|
+
* `BACKGROUND_FOLLOW_IDLE_TIMEOUT_MS` and the repeated-terminal-reason
|
|
200
|
+
* detector.
|
|
201
|
+
*/
|
|
202
|
+
export declare const MAX_FOLLOWED_BACKGROUND_RUNS = 30;
|
|
203
|
+
export declare const MAX_BACKGROUND_FOLLOW_WALL_TIME_MS: number;
|
|
204
|
+
/**
|
|
205
|
+
* Ordering relationships between the run-lifecycle bounds.
|
|
206
|
+
*
|
|
207
|
+
* Every one of these was already argued for in a source comment somewhere and
|
|
208
|
+
* enforced by nothing, which is how the framework shipped a violated pair. The
|
|
209
|
+
* check runs on resolved configuration — including the all-defaults case — so
|
|
210
|
+
* a relationship broken by a new default fails the same way a relationship
|
|
211
|
+
* broken by a deployment does.
|
|
212
|
+
*
|
|
213
|
+
* DECLARED EXCEPTION, deliberately not asserted:
|
|
214
|
+
* `backgroundSoftTimeoutCeilingMs` (13 min) sits ABOVE
|
|
215
|
+
* `backgroundRunHardTimeoutMs` (10 min). Those two bound different paths — the
|
|
216
|
+
* ceiling belongs to a durable background CHAT chunk, whose wall is the host's
|
|
217
|
+
* 15-minute background-function budget, while the hard abort belongs to the
|
|
218
|
+
* in-process automation runner. The automation path does not inherit the
|
|
219
|
+
* ceiling: it derives its budget from its own hard abort minus
|
|
220
|
+
* `BACKGROUND_AUTOMATION_SOFT_TIMEOUT_HEADROOM_MS` (see
|
|
221
|
+
* `resolveBackgroundAutomationSoftTimeoutMs`), which is what invariant 4 below
|
|
222
|
+
* checks is possible at all.
|
|
223
|
+
*/
|
|
224
|
+
interface Invariant {
|
|
225
|
+
name: string;
|
|
226
|
+
smaller: {
|
|
227
|
+
key: string;
|
|
228
|
+
value: number;
|
|
229
|
+
};
|
|
230
|
+
larger: {
|
|
231
|
+
key: string;
|
|
232
|
+
value: number;
|
|
233
|
+
};
|
|
234
|
+
relation: "<" | "<=";
|
|
235
|
+
why: string;
|
|
236
|
+
}
|
|
237
|
+
export declare class RunLifecycleInvariantError extends Error {
|
|
238
|
+
constructor(violations: readonly Invariant[]);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Throws when the resolved run-lifecycle bounds cannot all do their job.
|
|
242
|
+
*
|
|
243
|
+
* Called from configuration resolution, so it fails at startup naming both
|
|
244
|
+
* constants and the relationship rather than at 3am when a run dies inside the
|
|
245
|
+
* window a mis-ordered pair opened.
|
|
246
|
+
*/
|
|
247
|
+
export declare function assertRunLifecycleInvariants(agent: AppConfig["agent"]): void;
|
|
248
|
+
export {};
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wall-clock reserved between a background automation's round budget and its
|
|
3
|
+
* own hard abort.
|
|
4
|
+
*
|
|
5
|
+
* It covers wind-down only — emit the terminal event, persist the turn, let
|
|
6
|
+
* `finalized` settle — because the recoverable boundary on this path belongs to
|
|
7
|
+
* the agent-loop wrapper's own per-round timer, not to a second timer in the
|
|
8
|
+
* run manager.
|
|
9
|
+
*
|
|
10
|
+
* This is the constant that makes `automation soft timeout < automation hard
|
|
11
|
+
* abort` true by construction rather than by hoping two independently chosen
|
|
12
|
+
* numbers happen to be ordered — they were not: the shipped build gave the
|
|
13
|
+
* automation path a 13-minute soft timeout under a 10-minute hard abort, so
|
|
14
|
+
* the recoverable boundary was dead code and the only boundary an automation
|
|
15
|
+
* could reach was the terminal one.
|
|
16
|
+
*/
|
|
17
|
+
export const BACKGROUND_AUTOMATION_SOFT_TIMEOUT_HEADROOM_MS = 20_000;
|
|
18
|
+
/**
|
|
19
|
+
* The host's hard kill for a background function (Netlify: 15 minutes).
|
|
20
|
+
*
|
|
21
|
+
* Not configuration — a deployment does not choose it, the platform does. It is
|
|
22
|
+
* here because `backgroundSoftTimeoutCeilingMs` IS the clamp that
|
|
23
|
+
* `resolveRunSoftTimeoutMs` reduces every background soft timeout to, so once
|
|
24
|
+
* that ceiling became configurable nothing was left bounding it: a deployment
|
|
25
|
+
* could set 60 minutes and push its own chunk boundary past the wall the
|
|
26
|
+
* ceiling exists to stay inside, turning every long background turn back into
|
|
27
|
+
* the silent platform kill it was introduced to prevent. Configurable must not
|
|
28
|
+
* mean unclamped.
|
|
29
|
+
*/
|
|
30
|
+
export const BACKGROUND_FUNCTION_WALL_MS = 15 * 60_000;
|
|
31
|
+
/**
|
|
32
|
+
* Wall-clock a background chunk must leave itself to abort, persist the partial
|
|
33
|
+
* turn, write the terminal event, and chain a successor before the host kills
|
|
34
|
+
* the invocation. The shipped 13-minute ceiling under a 15-minute wall is
|
|
35
|
+
* exactly this margin.
|
|
36
|
+
*/
|
|
37
|
+
export const BACKGROUND_FUNCTION_WALL_HEADROOM_MS = 2 * 60_000;
|
|
38
|
+
/**
|
|
39
|
+
* Slack between the CHAIN bound (`agent.maxBackgroundRunContinuations`) and the
|
|
40
|
+
* per-turn LEDGER bound below.
|
|
41
|
+
*
|
|
42
|
+
* They count different things. The chain bound counts handoffs a chunk decided
|
|
43
|
+
* to make; the ledger counts every run ROW the turn produced, which also
|
|
44
|
+
* includes sweep redispatches and stale-run recoveries no chunk ever decided.
|
|
45
|
+
* Without slack the ledger would refuse a turn before the chain bound it is
|
|
46
|
+
* meant to sit above, so a turn recovered once would die holding unused chain
|
|
47
|
+
* budget.
|
|
48
|
+
*/
|
|
49
|
+
/**
|
|
50
|
+
* Shipped run-lifecycle bounds.
|
|
51
|
+
*
|
|
52
|
+
* They live beside the relationships that constrain them so a change to one is
|
|
53
|
+
* checked against the others in the same file. `run-manager.ts` and
|
|
54
|
+
* `production-agent.ts` re-export them under their historical names; this
|
|
55
|
+
* module imports no agent code, so nothing here can become circular.
|
|
56
|
+
*/
|
|
57
|
+
/**
|
|
58
|
+
* Hard ceiling for the soft timeout when a run executes inside a Netlify
|
|
59
|
+
* background function (any deployed function whose name ends in `-background`).
|
|
60
|
+
* Background functions return 202 immediately and run detached for up to 15
|
|
61
|
+
* minutes, so the ~60s synchronous function wall that 40s defends against does
|
|
62
|
+
* NOT apply. 13 minutes leaves ~2 min of headroom under Netlify's 15-min hard
|
|
63
|
+
* kill to abort, persist the partial turn, write the terminal event, and (for
|
|
64
|
+
* the rare >13-min turn) self-fire another background continuation.
|
|
65
|
+
*
|
|
66
|
+
* This ceiling is used ONLY when a caller explicitly opts in with
|
|
67
|
+
* `backgroundFunction: true`. It does not change the foreground/interactive
|
|
68
|
+
* ceiling and does not fire unless the durable-background path dispatched the
|
|
69
|
+
* run into a background function. Per the design doc Guardrail, the 40s
|
|
70
|
+
* interactive clamp stays correct for every non-background run.
|
|
71
|
+
*/
|
|
72
|
+
export const BACKGROUND_SOFT_TIMEOUT_CEILING_MS = 13 * 60_000;
|
|
73
|
+
/**
|
|
74
|
+
* AUTHORITATIVE no-progress backstop for a run, enforced by the run manager
|
|
75
|
+
* itself (timer-driven, independent of any layer below).
|
|
76
|
+
*
|
|
77
|
+
* The finer-grained watchdogs inside the agent loop (model-stream and
|
|
78
|
+
* action-preparation no-progress, both 90s) only guard the model event stream
|
|
79
|
+
* — a stall in any segment OUTSIDE that guarded loop (engine-call
|
|
80
|
+
* establishment, worker setup between continuation chunks, a wedged transport
|
|
81
|
+
* that emits keepalives while the loop never runs) previously hung forever
|
|
82
|
+
* with the client watching keepalives. This backstop covers every segment by
|
|
83
|
+
* construction: if no REAL progress event (see `shouldBumpProgressForEvent`;
|
|
84
|
+
* keepalives and zero-byte prep activity don't count) lands for this long —
|
|
85
|
+
* and no unit of work is in flight (see `inFlightWorkDelta`: tool calls,
|
|
86
|
+
* cross-app calls, and the model stream all legitimately emit nothing for
|
|
87
|
+
* minutes and each carry a bound of their own) — the run manager emits
|
|
88
|
+
* `auto_continue { reason: "no_progress" }` and aborts the chunk, exactly
|
|
89
|
+
* like the soft timeout, so the normal continuation machinery recovers it.
|
|
90
|
+
*
|
|
91
|
+
* Being numerically larger than the in-loop watchdogs is NOT what keeps this
|
|
92
|
+
* from killing a healthy run, and treating it that way is what made it do so:
|
|
93
|
+
* this clock and the loop's `lastModelStreamProgressAt` measure DIFFERENT
|
|
94
|
+
* events. An extended-thinking phase bumps the inner clock on every engine
|
|
95
|
+
* frame while forwarding nothing, so the inner watchdog correctly stayed quiet
|
|
96
|
+
* and this one saw pure silence — runs whose worst gap crossed 150s died while
|
|
97
|
+
* still streaming, some by a single second. Ordering between two clocks only
|
|
98
|
+
* means something when they watch the same events; suspending on in-flight
|
|
99
|
+
* work is what actually makes the two agree.
|
|
100
|
+
*
|
|
101
|
+
* This is now only the CEILING, not the value: `resolveRunNoProgressTimeoutMs`
|
|
102
|
+
* clamps the foreground backstop to a fraction of the chunk's soft timeout
|
|
103
|
+
* (~30s at a 40s chunk), which is BELOW the 90s in-loop watchdogs rather than
|
|
104
|
+
* above them. That ordering is deliberate — the in-loop watchdogs could never
|
|
105
|
+
* fire inside a hosted foreground chunk anyway, since the serverless wall
|
|
106
|
+
* (~57-59s) arrives first. Proven durable-background chunks keep the full
|
|
107
|
+
* `DEFAULT_BACKGROUND_NO_PROGRESS_TIMEOUT_MS` so large outputs can use the
|
|
108
|
+
* background budget. Only armed when a soft-timeout regime is active (hosted
|
|
109
|
+
* runs); local dev stays unbounded.
|
|
110
|
+
*/
|
|
111
|
+
export const RUN_NO_PROGRESS_HARD_TIMEOUT_MS = 150_000;
|
|
112
|
+
/**
|
|
113
|
+
* Default in-loop watchdog for silence while an action's arguments stream in.
|
|
114
|
+
* Read through `resolveActionPreparationNoProgressTimeoutMs`, never directly:
|
|
115
|
+
* a host diagnosing a timeout has to be able to see and change this number.
|
|
116
|
+
*/
|
|
117
|
+
export const ACTION_PREPARATION_NO_PROGRESS_TIMEOUT_MS = 90_000;
|
|
118
|
+
/**
|
|
119
|
+
* Default in-loop watchdog for silence between engine stream frames. Read
|
|
120
|
+
* through `resolveModelStreamNoProgressTimeoutMs`, never directly.
|
|
121
|
+
*/
|
|
122
|
+
export const MODEL_STREAM_NO_PROGRESS_TIMEOUT_MS = 90_000;
|
|
123
|
+
/**
|
|
124
|
+
* Consecutive chunks allowed to end on the SAME terminal error code having
|
|
125
|
+
* produced nothing before the chain stops.
|
|
126
|
+
*
|
|
127
|
+
* Two, because two independent recovery layers multiply here and neither can
|
|
128
|
+
* see the other: the engine already retried this identical request 3x with
|
|
129
|
+
* backoff before the error was ever emitted, and a recoverable error is also a
|
|
130
|
+
* continuation boundary, so every chunk that fails costs 4 gateway attempts
|
|
131
|
+
* and dispatches a fresh one. A production turn spent 27 background runs and
|
|
132
|
+
* 15 minutes on one message this way. The first repeat is the retry this path
|
|
133
|
+
* exists for; a second identical failure that moved nothing is evidence the
|
|
134
|
+
* retrying itself is what is broken, not the request.
|
|
135
|
+
*/
|
|
136
|
+
export const MAX_CONSECUTIVE_NO_PROGRESS_CONTINUATIONS = 2;
|
|
137
|
+
/**
|
|
138
|
+
* Wall-clock ceiling on a single logical turn. The run-count ledger alone is
|
|
139
|
+
* not a time bound: in durable mode each of the ~25 permitted chunks may burn
|
|
140
|
+
* ~780s, so the ledger's real worst case is over five hours (production has an
|
|
141
|
+
* observed 2h34m turn). Nobody is waiting that long, and every minute past
|
|
142
|
+
* this point is spend on a request the user has abandoned.
|
|
143
|
+
*/
|
|
144
|
+
export const MAX_TURN_WALL_CLOCK_MS = 90 * 60_000;
|
|
145
|
+
/**
|
|
146
|
+
* Cap on continuation iterations inside a single
|
|
147
|
+
* `runAgentLoopDirectWithSoftTimeout` invocation. The host's hard function
|
|
148
|
+
* timeout usually bounds this naturally — but a defensive cap prevents an
|
|
149
|
+
* instant-error spiral from looping forever inside hosting environments with a
|
|
150
|
+
* generous budget.
|
|
151
|
+
*
|
|
152
|
+
* 6 leaves room for: 1 normal completion + a few resume rounds for design
|
|
153
|
+
* generation (prompt + 3 variants ≈ 4 LLM calls), with a small safety margin.
|
|
154
|
+
*/
|
|
155
|
+
export const MAX_RUN_LOOP_CONTINUATIONS = 6;
|
|
156
|
+
/**
|
|
157
|
+
* A delegated turn that is proven to be running inside a durable background
|
|
158
|
+
* function has the same 15-minute host budget as main chat, but this wrapper
|
|
159
|
+
* historically kept the foreground-sized six-continuation cap. A healthy
|
|
160
|
+
* child A2A call can consume several minutes and the receiving model may then
|
|
161
|
+
* need more than six recovery/model-stream boundaries to finish its own tool
|
|
162
|
+
* work. Keep a hard cap, but give the proven background path the same bounded
|
|
163
|
+
* continuation allowance as the durable main-chat runner. The cumulative
|
|
164
|
+
* soft-timeout below still prevents these rounds from exceeding the one real
|
|
165
|
+
* background-function wall-clock budget.
|
|
166
|
+
*/
|
|
167
|
+
export const MAX_BACKGROUND_RUN_LOOP_CONTINUATIONS = 20;
|
|
168
|
+
export const TURN_RUN_LEDGER_SLACK = 5;
|
|
169
|
+
/**
|
|
170
|
+
* Hard cap on server-driven background→background continuation chunks for a
|
|
171
|
+
* single logical turn. A `backgroundFunction` run gets a ~13-min soft timeout,
|
|
172
|
+
* so reaching this boundary at all is the rare exception (most turns finish in
|
|
173
|
+
* one chunk). The cap bounds a pathological turn that would otherwise chain
|
|
174
|
+
* background invocations forever, mirroring `MAX_AGENT_TEAM_CONTINUATIONS`.
|
|
175
|
+
*/
|
|
176
|
+
export const MAX_BACKGROUND_RUN_CONTINUATIONS = 20;
|
|
177
|
+
/**
|
|
178
|
+
* Per-TURN follow budgets the browser applies while reading a background turn.
|
|
179
|
+
*
|
|
180
|
+
* They live here, not in `client/agent-chat-adapter.ts`, because they are one
|
|
181
|
+
* half of an ordering relationship whose other half is server configuration —
|
|
182
|
+
* and a relationship checked in only one of its two homes is the failure this
|
|
183
|
+
* module exists to prevent. This file has no runtime imports (the `AppConfig`
|
|
184
|
+
* import is type-only and erased), so the browser bundle pays nothing to read
|
|
185
|
+
* them from here.
|
|
186
|
+
*
|
|
187
|
+
* CLIENT-ABOVE-SERVER: these MUST stay above the server's own ceilings. The
|
|
188
|
+
* client fires on a clock and cannot tell looping from working; the server can,
|
|
189
|
+
* so the server must always terminate a turn first and write a truthful
|
|
190
|
+
* terminal reason. They shipped at 10 min / 6 runs while ONE legal background
|
|
191
|
+
* chunk may run 13 minutes — so the client killed healthy turns the server was
|
|
192
|
+
* still streaming, measured in production as aborts at 11-25 minutes with
|
|
193
|
+
* progress recorded right up to the abort. That was the top non-auth cause of
|
|
194
|
+
* "the chat just stopped".
|
|
195
|
+
*
|
|
196
|
+
* Do NOT tighten these to catch a stuck turn. A turn that is not progressing is
|
|
197
|
+
* already caught twice by mechanisms that read progress rather than a clock:
|
|
198
|
+
* `BACKGROUND_FOLLOW_IDLE_TIMEOUT_MS` and the repeated-terminal-reason
|
|
199
|
+
* detector.
|
|
200
|
+
*/
|
|
201
|
+
export const MAX_FOLLOWED_BACKGROUND_RUNS = 30;
|
|
202
|
+
export const MAX_BACKGROUND_FOLLOW_WALL_TIME_MS = 110 * 60_000;
|
|
203
|
+
export class RunLifecycleInvariantError extends Error {
|
|
204
|
+
constructor(violations) {
|
|
205
|
+
super(`Agent run-lifecycle configuration is inconsistent:\n${violations
|
|
206
|
+
.map((v) => ` - ${v.name}: ${v.smaller.key} (${v.smaller.value}) must be ` +
|
|
207
|
+
`${v.relation === "<" ? "less than" : "at most"} ` +
|
|
208
|
+
`${v.larger.key} (${v.larger.value}) — ${v.why}`)
|
|
209
|
+
.join("\n")}`);
|
|
210
|
+
this.name = "RunLifecycleInvariantError";
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Throws when the resolved run-lifecycle bounds cannot all do their job.
|
|
215
|
+
*
|
|
216
|
+
* Called from configuration resolution, so it fails at startup naming both
|
|
217
|
+
* constants and the relationship rather than at 3am when a run dies inside the
|
|
218
|
+
* window a mis-ordered pair opened.
|
|
219
|
+
*/
|
|
220
|
+
export function assertRunLifecycleInvariants(agent) {
|
|
221
|
+
// Only two of these are configuration. The rest are the shipped constants,
|
|
222
|
+
// read here rather than duplicated as config defaults — a number with two
|
|
223
|
+
// homes needs a test to keep them in step, and that test is the tell that it
|
|
224
|
+
// should have had one home to begin with. A deployment that wants to move a
|
|
225
|
+
// bound it cannot currently reach should get a field added deliberately, with
|
|
226
|
+
// the relationship below extended to cover it.
|
|
227
|
+
const { backgroundNoProgressTimeoutMs, backgroundRunHardTimeoutMs } = agent;
|
|
228
|
+
const backgroundSoftTimeoutCeilingMs = BACKGROUND_SOFT_TIMEOUT_CEILING_MS;
|
|
229
|
+
const modelStreamNoProgressTimeoutMs = MODEL_STREAM_NO_PROGRESS_TIMEOUT_MS;
|
|
230
|
+
const actionPreparationNoProgressTimeoutMs = ACTION_PREPARATION_NO_PROGRESS_TIMEOUT_MS;
|
|
231
|
+
const maxBackgroundRunContinuations = MAX_BACKGROUND_RUN_CONTINUATIONS;
|
|
232
|
+
const maxConsecutiveNoProgressContinuations = MAX_CONSECUTIVE_NO_PROGRESS_CONTINUATIONS;
|
|
233
|
+
const maxTurnWallClockMs = MAX_TURN_WALL_CLOCK_MS;
|
|
234
|
+
const violations = [];
|
|
235
|
+
const require = (name, smaller, larger, why) => {
|
|
236
|
+
if (smaller.value < larger.value)
|
|
237
|
+
return;
|
|
238
|
+
violations.push({ name, smaller, larger, relation: "<", why });
|
|
239
|
+
};
|
|
240
|
+
const requireAtMost = (name, smaller, larger, why) => {
|
|
241
|
+
if (smaller.value <= larger.value)
|
|
242
|
+
return;
|
|
243
|
+
violations.push({ name, smaller, larger, relation: "<=", why });
|
|
244
|
+
};
|
|
245
|
+
// A disabled backstop (0) has no ordering to satisfy — it never fires.
|
|
246
|
+
if (backgroundNoProgressTimeoutMs > 0) {
|
|
247
|
+
require("in-loop watchdog before the run-manager backstop", {
|
|
248
|
+
key: "agent.modelStreamNoProgressTimeoutMs",
|
|
249
|
+
value: modelStreamNoProgressTimeoutMs,
|
|
250
|
+
}, {
|
|
251
|
+
key: "agent.backgroundNoProgressTimeoutMs",
|
|
252
|
+
value: backgroundNoProgressTimeoutMs,
|
|
253
|
+
}, "the in-loop watchdog emits a boundary the agent loop itself recovers; the run-manager backstop is the coarser one above it");
|
|
254
|
+
require("action-preparation watchdog before the run-manager backstop", {
|
|
255
|
+
key: "agent.actionPreparationNoProgressTimeoutMs",
|
|
256
|
+
value: actionPreparationNoProgressTimeoutMs,
|
|
257
|
+
}, {
|
|
258
|
+
key: "agent.backgroundNoProgressTimeoutMs",
|
|
259
|
+
value: backgroundNoProgressTimeoutMs,
|
|
260
|
+
}, "a stalled argument stream must be caught by the watchdog that knows which tool stalled");
|
|
261
|
+
require("background backstop inside the background chunk budget", {
|
|
262
|
+
key: "agent.backgroundNoProgressTimeoutMs",
|
|
263
|
+
value: backgroundNoProgressTimeoutMs,
|
|
264
|
+
}, {
|
|
265
|
+
key: "agent.backgroundSoftTimeoutCeilingMs",
|
|
266
|
+
value: backgroundSoftTimeoutCeilingMs,
|
|
267
|
+
}, "a backstop at or above the chunk budget can never fire — the chunk boundary always arrives first");
|
|
268
|
+
require("background backstop inside the automation's own budget", {
|
|
269
|
+
key: "agent.backgroundNoProgressTimeoutMs",
|
|
270
|
+
value: backgroundNoProgressTimeoutMs,
|
|
271
|
+
}, {
|
|
272
|
+
key: "agent.backgroundRunHardTimeoutMs - BACKGROUND_AUTOMATION_SOFT_TIMEOUT_HEADROOM_MS",
|
|
273
|
+
value: backgroundRunHardTimeoutMs -
|
|
274
|
+
BACKGROUND_AUTOMATION_SOFT_TIMEOUT_HEADROOM_MS,
|
|
275
|
+
}, "an automation whose backstop outlives its own chunk budget dies at the hard abort instead of checkpointing");
|
|
276
|
+
}
|
|
277
|
+
requireAtMost("background chunk budget inside the host's background-function wall", {
|
|
278
|
+
key: "agent.backgroundSoftTimeoutCeilingMs",
|
|
279
|
+
value: backgroundSoftTimeoutCeilingMs,
|
|
280
|
+
}, {
|
|
281
|
+
key: "BACKGROUND_FUNCTION_WALL_MS - BACKGROUND_FUNCTION_WALL_HEADROOM_MS",
|
|
282
|
+
value: BACKGROUND_FUNCTION_WALL_MS - BACKGROUND_FUNCTION_WALL_HEADROOM_MS,
|
|
283
|
+
}, "this ceiling is the clamp every background soft timeout is reduced to, so raising it past the host wall makes the chunk boundary unreachable and the run dies as a silent platform kill instead");
|
|
284
|
+
require("graceful boundary fits before the hard abort", {
|
|
285
|
+
key: "BACKGROUND_AUTOMATION_SOFT_TIMEOUT_HEADROOM_MS",
|
|
286
|
+
value: BACKGROUND_AUTOMATION_SOFT_TIMEOUT_HEADROOM_MS,
|
|
287
|
+
}, {
|
|
288
|
+
key: "agent.backgroundRunHardTimeoutMs",
|
|
289
|
+
value: backgroundRunHardTimeoutMs,
|
|
290
|
+
}, "without room for the headroom there is no chunk budget left to hand a boundary to");
|
|
291
|
+
requireAtMost("no-progress streak bound inside the chain bound", {
|
|
292
|
+
key: "agent.maxConsecutiveNoProgressContinuations",
|
|
293
|
+
value: maxConsecutiveNoProgressContinuations,
|
|
294
|
+
}, {
|
|
295
|
+
key: "agent.maxBackgroundRunContinuations",
|
|
296
|
+
value: maxBackgroundRunContinuations,
|
|
297
|
+
}, "a streak bound above the chain bound can never trip, so a repeating failure runs to the chain limit instead");
|
|
298
|
+
// ── Client-above-server ────────────────────────────────────────────────
|
|
299
|
+
//
|
|
300
|
+
// Until this branch these were pinned in `agent-chat-adapter.spec.ts` against
|
|
301
|
+
// the server's module CONSTANTS. Making those constants configurable moved
|
|
302
|
+
// the real values out from under that test without moving the test: a
|
|
303
|
+
// deployment could raise any of them past what the shipped client can follow
|
|
304
|
+
// and every check still passed. Asserting against the RESOLVED config is what
|
|
305
|
+
// closes that.
|
|
306
|
+
require("server chunk budget leaves the client room for more than one chunk", {
|
|
307
|
+
key: "agent.backgroundSoftTimeoutCeilingMs * 2",
|
|
308
|
+
value: backgroundSoftTimeoutCeilingMs * 2,
|
|
309
|
+
}, {
|
|
310
|
+
key: "MAX_BACKGROUND_FOLLOW_WALL_TIME_MS",
|
|
311
|
+
value: MAX_BACKGROUND_FOLLOW_WALL_TIME_MS,
|
|
312
|
+
}, "a whole-turn client budget below two full-length chunks kills a healthy turn mid-stream — the exact inversion that shipped");
|
|
313
|
+
require("server turn ceiling below the client's follow budget", {
|
|
314
|
+
// EFFECTIVE, not nominal: the ceiling is checked at chunk boundaries, so a
|
|
315
|
+
// turn passing the check one chunk short of it still gets a whole further
|
|
316
|
+
// chunk. Comparing the configured number alone hid a real inversion in the
|
|
317
|
+
// shipped values — 90min + a 13min chunk against a client that stopped
|
|
318
|
+
// following at 95min.
|
|
319
|
+
key: "agent.maxTurnWallClockMs + agent.backgroundSoftTimeoutCeilingMs",
|
|
320
|
+
value: maxTurnWallClockMs + backgroundSoftTimeoutCeilingMs,
|
|
321
|
+
}, {
|
|
322
|
+
key: "MAX_BACKGROUND_FOLLOW_WALL_TIME_MS",
|
|
323
|
+
value: MAX_BACKGROUND_FOLLOW_WALL_TIME_MS,
|
|
324
|
+
}, "the server must end the turn first, because it is the side that can tell progress from a loop and write a truthful terminal reason");
|
|
325
|
+
require("server chain bound below the client's follow-run budget", {
|
|
326
|
+
// EFFECTIVE, not nominal: the durable ledger allows the chain bound PLUS
|
|
327
|
+
// the recovery slack in run ROWS, and the client counts rows. 20 + 5 = 25
|
|
328
|
+
// against a client that stopped at 24 — the same inversion, hidden the
|
|
329
|
+
// same way.
|
|
330
|
+
key: "agent.maxBackgroundRunContinuations + TURN_RUN_LEDGER_SLACK",
|
|
331
|
+
value: maxBackgroundRunContinuations + TURN_RUN_LEDGER_SLACK,
|
|
332
|
+
}, {
|
|
333
|
+
key: "MAX_FOLLOWED_BACKGROUND_RUNS",
|
|
334
|
+
value: MAX_FOLLOWED_BACKGROUND_RUNS,
|
|
335
|
+
}, "a client that stops following before the server stops chaining leaves the user watching a spinner over a live run");
|
|
336
|
+
requireAtMost("turn ceiling above one chunk budget", {
|
|
337
|
+
key: "agent.backgroundSoftTimeoutCeilingMs",
|
|
338
|
+
value: backgroundSoftTimeoutCeilingMs,
|
|
339
|
+
}, { key: "agent.maxTurnWallClockMs", value: maxTurnWallClockMs }, "a turn ceiling below a single chunk budget kills every turn at its first chunk boundary");
|
|
340
|
+
if (violations.length > 0)
|
|
341
|
+
throw new RunLifecycleInvariantError(violations);
|
|
342
|
+
}
|
|
@@ -23,6 +23,8 @@ export declare const appConfigSchema: z.ZodObject<{
|
|
|
23
23
|
runSoftTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
24
24
|
completedRunRetentionMs: z.ZodOptional<z.ZodNumber>;
|
|
25
25
|
erroredRunRetentionMs: z.ZodOptional<z.ZodNumber>;
|
|
26
|
+
backgroundNoProgressTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
27
|
+
backgroundRunHardTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
26
28
|
}, z.core.$strip>>;
|
|
27
29
|
app: z.ZodPrefault<z.ZodObject<{
|
|
28
30
|
id: z.ZodOptional<z.ZodString>;
|
package/dist/app-config/store.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readEnvConfigLayer } from "./env-layer.js";
|
|
2
|
+
import { assertRunLifecycleInvariants } from "./run-lifecycle-invariants.js";
|
|
2
3
|
import { appConfigSchema, } from "./schema.js";
|
|
3
4
|
/**
|
|
4
5
|
* Resolution order for app configuration, lowest opinion first.
|
|
@@ -46,7 +47,14 @@ function resolve(envLayer) {
|
|
|
46
47
|
if (value)
|
|
47
48
|
merged = mergeLayers(merged, value);
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
+
const parsed = appConfigSchema.parse(merged);
|
|
51
|
+
// Checked on the MERGED result, not per layer: a deployment may legitimately
|
|
52
|
+
// set one half of a relationship in the environment and the other in a
|
|
53
|
+
// plugin, and a per-layer check would reject that pairing before it exists.
|
|
54
|
+
// Defaults go through here too — the pair that shipped violated was a pair of
|
|
55
|
+
// defaults.
|
|
56
|
+
assertRunLifecycleInvariants(parsed.agent);
|
|
57
|
+
return parsed;
|
|
50
58
|
}
|
|
51
59
|
/**
|
|
52
60
|
* Nitro embeds build-only deployment markers into direct env reads. Netlify's
|