@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.
Files changed (96) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/actions/bigquery-table-info.ts +3 -2
  3. package/corpus/templates/analytics/actions/search-bigquery-schema.ts +130 -4
  4. package/corpus/templates/analytics/agent-native.config.ts +1 -0
  5. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +36 -23
  6. package/corpus/templates/analytics/app/lib/dashboard-visibility.ts +41 -0
  7. package/corpus/templates/analytics/app/pages/DashboardOverview.tsx +34 -9
  8. package/corpus/templates/analytics/changelog/2026-08-21-dashboard-filters-keep-other-users-dashboards-out-of-mine.md +6 -0
  9. package/corpus/templates/analytics/server/lib/real-data-actions.ts +14 -3
  10. package/corpus/templates/analytics/server/plugins/agent-chat.ts +33 -3
  11. package/corpus/templates/brain/server/lib/brain.ts +27 -2
  12. package/corpus/templates/clips/actions/stop-meeting-recording.ts +87 -6
  13. package/corpus/templates/clips/app/components/library/search-bar.tsx +1 -1
  14. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +3 -4
  15. package/corpus/templates/design/app/components/design/EditPanel.tsx +15 -47
  16. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +3 -3
  17. package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +0 -7
  18. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +2 -4
  19. package/corpus/templates/design/app/i18n-data.ts +10 -10
  20. package/corpus/templates/design/app/pages/DesignEditor.tsx +22 -22
  21. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +39 -8
  22. package/corpus/templates/design/app/pages/DesignSystems.tsx +1 -1
  23. package/corpus/templates/design/app/pages/Index.tsx +1 -1
  24. package/corpus/templates/design/app/pages/design-editor/preview-navigation.ts +25 -0
  25. package/corpus/templates/mail/app/hooks/use-emails.ts +13 -3
  26. package/dist/agent/engine/credential-errors.d.ts +1 -1
  27. package/dist/agent/engine/credential-errors.js +2 -2
  28. package/dist/agent/engine/first-event-timeout.d.ts +8 -0
  29. package/dist/agent/engine/first-event-timeout.js +8 -0
  30. package/dist/agent/production-agent.d.ts +0 -30
  31. package/dist/agent/production-agent.js +17 -38
  32. package/dist/agent/run-loop-with-resume.d.ts +38 -25
  33. package/dist/agent/run-loop-with-resume.js +140 -55
  34. package/dist/agent/run-manager.d.ts +83 -68
  35. package/dist/agent/run-manager.js +280 -94
  36. package/dist/agent/run-store.d.ts +31 -0
  37. package/dist/agent/run-store.js +42 -12
  38. package/dist/app-config/agent.d.ts +2 -0
  39. package/dist/app-config/agent.js +33 -0
  40. package/dist/app-config/run-lifecycle-invariants.d.ts +248 -0
  41. package/dist/app-config/run-lifecycle-invariants.js +342 -0
  42. package/dist/app-config/schema.d.ts +2 -0
  43. package/dist/app-config/store.js +9 -1
  44. package/dist/client/AssistantChat.js +47 -15
  45. package/dist/client/ConnectBuilderCard.d.ts +3 -1
  46. package/dist/client/ConnectBuilderCard.js +6 -3
  47. package/dist/client/agent-chat-adapter.d.ts +0 -2
  48. package/dist/client/agent-chat-adapter.js +7 -23
  49. package/dist/client/chat/message-components.d.ts +7 -0
  50. package/dist/client/chat/message-components.js +1 -1
  51. package/dist/client/chat/run-recovery.js +5 -2
  52. package/dist/client/chat/tool-call-display.js +9 -3
  53. package/dist/client/error-format.js +12 -0
  54. package/dist/client/navigation/index.d.ts +1 -0
  55. package/dist/client/navigation/index.js +1 -0
  56. package/dist/client/route-state.js +6 -0
  57. package/dist/client/workspace-app-navigation.d.ts +10 -0
  58. package/dist/client/workspace-app-navigation.js +38 -0
  59. package/dist/collab/struct-routes.d.ts +1 -1
  60. package/dist/deploy/build.d.ts +13 -0
  61. package/dist/deploy/build.js +51 -0
  62. package/dist/jobs/background-automation-runner.d.ts +25 -0
  63. package/dist/jobs/background-automation-runner.js +104 -21
  64. package/dist/jobs/run-history.d.ts +7 -1
  65. package/dist/jobs/run-history.js +57 -14
  66. package/dist/localization/core-messages/ar-SA.js +1 -1
  67. package/dist/localization/core-messages/de-DE.js +1 -1
  68. package/dist/localization/core-messages/en-US.d.ts +1 -1
  69. package/dist/localization/core-messages/en-US.js +1 -1
  70. package/dist/localization/core-messages/es-ES.js +1 -1
  71. package/dist/localization/core-messages/fr-FR.js +1 -1
  72. package/dist/localization/core-messages/hi-IN.js +1 -1
  73. package/dist/localization/core-messages/ja-JP.js +1 -1
  74. package/dist/localization/core-messages/ko-KR.js +1 -1
  75. package/dist/localization/core-messages/pt-BR.js +1 -1
  76. package/dist/localization/core-messages/zh-CN.js +1 -1
  77. package/dist/localization/core-messages/zh-TW.js +1 -1
  78. package/dist/localization/core-messages.d.ts +1 -1
  79. package/dist/notifications/routes.d.ts +1 -1
  80. package/dist/observability/routes.d.ts +3 -3
  81. package/dist/observability/traces.d.ts +13 -0
  82. package/dist/observability/traces.js +369 -317
  83. package/dist/progress/routes.d.ts +1 -1
  84. package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
  85. package/dist/provider-api/actions/provider-api.d.ts +13 -13
  86. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  87. package/dist/resources/handlers.d.ts +1 -1
  88. package/dist/scripts/db/check-scoping.js +9 -6
  89. package/dist/secrets/routes.d.ts +6 -6
  90. package/dist/server/agent-chat-plugin.js +2 -4
  91. package/dist/server/builder-browser.d.ts +4 -0
  92. package/dist/server/builder-browser.js +23 -1
  93. package/dist/server/core-routes-plugin.js +13 -1
  94. package/dist/server/transcribe-voice.d.ts +1 -1
  95. package/package.json +3 -3
  96. /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>;
@@ -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
- return appConfigSchema.parse(merged);
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