@agent-native/core 0.168.13 → 0.169.0

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.
@@ -148,6 +148,29 @@ export declare const UNCLAIMED_BACKGROUND_RUN_REDISPATCH_BOUND_MS: number;
148
148
  export declare const UNCLAIMED_BACKGROUND_RUN_FAST_SWEEP_MS = 20000;
149
149
  /** Test seam — the probe cache is module state, so suites must clear it. */
150
150
  export declare function __resetNoRunningRunsProbeForTests(): void;
151
+ /**
152
+ * Ceiling on run ROWS for one logical turn — the number the continuation-chain
153
+ * guard and stale-run recovery must agree on.
154
+ *
155
+ * This was a literal `25` here plus `MAX_BACKGROUND_RUN_CONTINUATIONS + 5` in
156
+ * production-agent.ts, kept in step by a comment asking the next editor to
157
+ * remember, because importing back from this file would have been circular.
158
+ * It no longer needs to be: the base value is configuration, and `app-config`
159
+ * imports no agent code, so both sites can read the same resolver.
160
+ */
161
+ export declare function resolveTurnRunLedgerBudget(): number;
162
+ /**
163
+ * True when a turn holding `turnRunCount` run rows must not be given another.
164
+ *
165
+ * A predicate rather than a number the callers compare themselves, because both
166
+ * call sites had `turnRunCount > budget` and both were off by one: the current
167
+ * run's row is already inserted when they check, and the successor's row is
168
+ * inserted after — so at equality they permitted a row past the documented
169
+ * ceiling. Two sites, one comparison, no way for them to disagree about the
170
+ * boundary again. That is the third time in this area that one number had two
171
+ * spellings.
172
+ */
173
+ export declare function turnRunLedgerExhausted(turnRunCount: number): boolean;
151
174
  /**
152
175
  * Maximum time the stale reapers (`reapIfStale`, `reapAllStaleRuns`,
153
176
  * `cleanupOldRuns`'s heartbeat-stale pass) will suspend reaping a "running"
@@ -449,6 +472,14 @@ export declare const RUN_DIAG_STAGE: {
449
472
  * the per-turn budget is exhausted). See `attemptStaleRunRecovery`.
450
473
  */
451
474
  readonly staleRunRecoveryAttempted: "stale_run_recovery_attempted";
475
+ /**
476
+ * The run manager reached a server-owned chunk boundary (`no_progress` or
477
+ * `run_timeout`). Detail carries the reason, whether it was recovered in the
478
+ * same invocation or terminated the turn, how long the run had been silent,
479
+ * and the last event type seen — the segment that went quiet, which is what
480
+ * no boundary previously recorded anywhere.
481
+ */
482
+ readonly runBoundaryReached: "run_boundary_reached";
452
483
  };
453
484
  export type RunDiagStage = (typeof RUN_DIAG_STAGE)[keyof typeof RUN_DIAG_STAGE];
454
485
  /**
@@ -1,3 +1,9 @@
1
+ /**
2
+ * SQL persistence for agent runs and events.
3
+ * Enables cross-isolate access on Cloudflare Workers and
4
+ * reliable reconnection after page refreshes.
5
+ */
6
+ import { MAX_BACKGROUND_RUN_CONTINUATIONS, TURN_RUN_LEDGER_SLACK, } from "../app-config/run-lifecycle-invariants.js";
1
7
  import { getDbExec, intType, isPostgres } from "../db/client.js";
2
8
  import { ensureColumnExists, ensureTableExists } from "../db/ddl-guard.js";
3
9
  import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
@@ -190,15 +196,32 @@ async function hasRunningRuns() {
190
196
  return false;
191
197
  }
192
198
  /**
193
- * FIX 3 (durable-background incident) per-turn run-count ceiling for
194
- * stale-run recovery mirrors `chainServerDrivenContinuation`'s own ledger
195
- * guard in production-agent.ts (`MAX_BACKGROUND_RUN_CONTINUATIONS + 5` = 25).
196
- * Duplicated as a literal rather than imported: production-agent.ts already
197
- * imports run-manager.ts, which imports this file, so a runtime import back
198
- * from here would be circular. Keep this numerically in sync if that
199
- * constant ever changes.
199
+ * Ceiling on run ROWS for one logical turn the number the continuation-chain
200
+ * guard and stale-run recovery must agree on.
201
+ *
202
+ * This was a literal `25` here plus `MAX_BACKGROUND_RUN_CONTINUATIONS + 5` in
203
+ * production-agent.ts, kept in step by a comment asking the next editor to
204
+ * remember, because importing back from this file would have been circular.
205
+ * It no longer needs to be: the base value is configuration, and `app-config`
206
+ * imports no agent code, so both sites can read the same resolver.
207
+ */
208
+ export function resolveTurnRunLedgerBudget() {
209
+ return MAX_BACKGROUND_RUN_CONTINUATIONS + TURN_RUN_LEDGER_SLACK;
210
+ }
211
+ /**
212
+ * True when a turn holding `turnRunCount` run rows must not be given another.
213
+ *
214
+ * A predicate rather than a number the callers compare themselves, because both
215
+ * call sites had `turnRunCount > budget` and both were off by one: the current
216
+ * run's row is already inserted when they check, and the successor's row is
217
+ * inserted after — so at equality they permitted a row past the documented
218
+ * ceiling. Two sites, one comparison, no way for them to disagree about the
219
+ * boundary again. That is the third time in this area that one number had two
220
+ * spellings.
200
221
  */
201
- const STALE_RUN_RECOVERY_MAX_TURN_RUNS = 25;
222
+ export function turnRunLedgerExhausted(turnRunCount) {
223
+ return turnRunCount >= resolveTurnRunLedgerBudget();
224
+ }
202
225
  /**
203
226
  * Circuit breaker for a DETERMINISTIC dead-on-arrival loop: some request
204
227
  * shapes make the worker hang almost immediately every single time (e.g. an
@@ -206,7 +229,7 @@ const STALE_RUN_RECOVERY_MAX_TURN_RUNS = 25;
206
229
  * hitting a transient blip. Because `attemptStaleRunRecovery` replays the
207
230
  * SAME captured `dispatch_payload` on every successor (never a fresh
208
231
  * request), such a turn was retrying an unwinnable request up to
209
- * `STALE_RUN_RECOVERY_MAX_TURN_RUNS` (25) times — ~25 * 53s ≈ 22 minutes,
232
+ * `resolveTurnRunLedgerBudget()` (25) times — ~25 * 53s ≈ 22 minutes,
210
233
  * each cycle re-billing the full input context — before finally giving up.
211
234
  * Confirmed live in prod (assets: one turn cycled 24x, each attempt an
212
235
  * identical ~32K-token request that made a token of real progress around
@@ -1271,6 +1294,14 @@ export const RUN_DIAG_STAGE = {
1271
1294
  * the per-turn budget is exhausted). See `attemptStaleRunRecovery`.
1272
1295
  */
1273
1296
  staleRunRecoveryAttempted: "stale_run_recovery_attempted",
1297
+ /**
1298
+ * The run manager reached a server-owned chunk boundary (`no_progress` or
1299
+ * `run_timeout`). Detail carries the reason, whether it was recovered in the
1300
+ * same invocation or terminated the turn, how long the run had been silent,
1301
+ * and the last event type seen — the segment that went quiet, which is what
1302
+ * no boundary previously recorded anywhere.
1303
+ */
1304
+ runBoundaryReached: "run_boundary_reached",
1274
1305
  };
1275
1306
  /**
1276
1307
  * Record the last reached pipeline stage (+ optional short detail) for a run.
@@ -1491,7 +1522,7 @@ function staleRecoveryDispatchPayload(payload) {
1491
1522
  * caller's own atomic "did I win the reap" gate, this guarantees AT MOST
1492
1523
  * ONE recovery successor per reaped run even under concurrent reapers.
1493
1524
  * - the per-turn run ledger (`countRunsForTurn`'s underlying query) has
1494
- * room (`STALE_RUN_RECOVERY_MAX_TURN_RUNS`) — mirrors
1525
+ * room (`resolveTurnRunLedgerBudget`) — mirrors
1495
1526
  * `chainServerDrivenContinuation`'s own budget guard so a pathological
1496
1527
  * turn can't loop forever through reaper-driven recovery either.
1497
1528
  */
@@ -1535,8 +1566,7 @@ async function attemptStaleRunRecovery(db, runId) {
1535
1566
  args: [threadId, turnId],
1536
1567
  });
1537
1568
  const turnRunCount = Number(countRows?.[0]?.run_count);
1538
- if (Number.isFinite(turnRunCount) &&
1539
- turnRunCount > STALE_RUN_RECOVERY_MAX_TURN_RUNS) {
1569
+ if (Number.isFinite(turnRunCount) && turnRunLedgerExhausted(turnRunCount)) {
1540
1570
  return { outcome: "budget_exhausted" };
1541
1571
  }
1542
1572
  // See `STALE_RUN_RECOVERY_CONSECUTIVE_NO_PROGRESS_LIMIT`: a run whose last
@@ -15,4 +15,6 @@ export declare const agentConfig: z.ZodObject<{
15
15
  runSoftTimeoutMs: z.ZodOptional<z.ZodNumber>;
16
16
  completedRunRetentionMs: z.ZodOptional<z.ZodNumber>;
17
17
  erroredRunRetentionMs: z.ZodOptional<z.ZodNumber>;
18
+ backgroundNoProgressTimeoutMs: z.ZodDefault<z.ZodNumber>;
19
+ backgroundRunHardTimeoutMs: z.ZodDefault<z.ZodNumber>;
18
20
  }, z.core.$strip>;
@@ -66,4 +66,37 @@ export const agentConfig = z.object({
66
66
  env: ["AGENT_ERRORED_RUN_RETENTION_MS"],
67
67
  doc: "How long an errored agent run row is kept, in milliseconds.",
68
68
  }),
69
+ // ── Run-lifecycle bounds ────────────────────────────────────────────────
70
+ //
71
+ // These are the numbers that can TERMINATE a run, or that encode an
72
+ // assumption about the host it runs on. They carry today's shipped values as
73
+ // declared defaults, so a deployment that configures nothing sees no
74
+ // behaviour change; `agent/run-lifecycle.ts` is the only place that reads
75
+ // them, one resolver per field, and `assertRunLifecycleInvariants` checks the
76
+ // ordering between them every time configuration resolves.
77
+ //
78
+ // Derived values (the foreground backstop fraction, tool-timeout headroom)
79
+ // stay internal on purpose: they are relationships, not host facts, and
80
+ // making them settable is how the ordering below stops being checkable.
81
+ //
82
+ // Each default here is the value that shipped as a module constant, and the
83
+ // constant still exists under its historical name where its reasoning is
84
+ // written down. `agent-run-lifecycle-config.spec.ts` pins the two together so
85
+ // editing one alone is a failing test, not a silent divergence.
86
+ backgroundNoProgressTimeoutMs: z
87
+ .number()
88
+ .nonnegative()
89
+ .default(150_000)
90
+ .meta({
91
+ env: ["AGENT_BACKGROUND_NO_PROGRESS_TIMEOUT_MS"],
92
+ doc: "No-progress backstop for a background-function run, in milliseconds. 0 disables it.",
93
+ }),
94
+ backgroundRunHardTimeoutMs: z
95
+ .number()
96
+ .positive()
97
+ .default(10 * 60_000)
98
+ .meta({
99
+ env: ["AGENT_BACKGROUND_RUN_HARD_TIMEOUT_MS"],
100
+ doc: "Hard abort for one in-process background automation run, in milliseconds. This is the host's real function budget for scheduled work.",
101
+ }),
69
102
  });
@@ -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 {};