@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
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { getAppConfig } from "../app-config/index.js";
|
|
2
|
+
import { BACKGROUND_AUTOMATION_SOFT_TIMEOUT_HEADROOM_MS, BACKGROUND_SOFT_TIMEOUT_CEILING_MS, RUN_NO_PROGRESS_HARD_TIMEOUT_MS, } from "../app-config/run-lifecycle-invariants.js";
|
|
2
3
|
import { captureError } from "../server/capture-error.js";
|
|
3
4
|
import { isLlmCredentialError, LLM_MISSING_CREDENTIALS_ERROR_CODE, LLM_MISSING_CREDENTIALS_MESSAGE, } from "./engine/credential-errors.js";
|
|
4
5
|
import { classifyTerminalErrorCode, describeErrorWithCauses, isProviderConnectionError, } from "./engine/error-detail.js";
|
|
5
6
|
import { EngineError } from "./engine/types.js";
|
|
6
|
-
import { insertRun, insertRunEvent, updateRunStatusIfRunning, markRunAborted, getRunAbortState, getRunStatus, getRunEventsSince, getRunById, getRunByThread, getRunTurnRef, markTurnAborted, cleanupOldRuns, updateRunHeartbeat, bumpRunProgress, setRunInFlightMarker, reapIfStale, reapUnclaimedBackgroundRun, shouldRedispatchUnclaimedBackgroundRun, reconcileTerminalRunFromEvents, ensureTerminalRunEvent, getLastTerminalRunEvent, resolveErroredRunTerminalEvent, setRunError, setRunTerminalReason, persistRunCheckpointEvent, terminalEventForAbortReason, } from "./run-store.js";
|
|
7
|
+
import { insertRun, insertRunEvent, updateRunStatusIfRunning, markRunAborted, getRunAbortState, getRunStatus, getRunEventsSince, getRunById, getRunByThread, getRunTurnRef, markTurnAborted, cleanupOldRuns, updateRunHeartbeat, bumpRunProgress, setRunInFlightMarker, reapIfStale, reapUnclaimedBackgroundRun, shouldRedispatchUnclaimedBackgroundRun, reconcileTerminalRunFromEvents, ensureTerminalRunEvent, getLastTerminalRunEvent, resolveErroredRunTerminalEvent, setRunError, setRunTerminalReason, persistRunCheckpointEvent, recordRunDiagnostic, RUN_DIAG_STAGE, terminalEventForAbortReason, } from "./run-store.js";
|
|
7
8
|
import { isContinuationTerminalReason } from "./types.js";
|
|
8
9
|
const activeRuns = new Map();
|
|
9
10
|
const threadToRun = new Map();
|
|
@@ -43,68 +44,7 @@ export const DEFAULT_HOSTED_RUN_SOFT_TIMEOUT_MS = 40_000;
|
|
|
43
44
|
* function (no ~60s wall, 15-min budget) and therefore can safely outlast 40s.
|
|
44
45
|
*/
|
|
45
46
|
export const HOSTED_SOFT_TIMEOUT_CEILING_MS = 40_000;
|
|
46
|
-
|
|
47
|
-
* Hard ceiling for the soft timeout when a run executes inside a Netlify
|
|
48
|
-
* background function (any deployed function whose name ends in `-background`).
|
|
49
|
-
* Background functions return 202 immediately and run detached for up to 15
|
|
50
|
-
* minutes, so the ~60s synchronous function wall that 40s defends against does
|
|
51
|
-
* NOT apply. 13 minutes leaves ~2 min of headroom under Netlify's 15-min hard
|
|
52
|
-
* kill to abort, persist the partial turn, write the terminal event, and (for
|
|
53
|
-
* the rare >13-min turn) self-fire another background continuation.
|
|
54
|
-
*
|
|
55
|
-
* This ceiling is used ONLY when a caller explicitly opts in with
|
|
56
|
-
* `backgroundFunction: true`. It does not change the foreground/interactive
|
|
57
|
-
* ceiling and does not fire unless the durable-background path dispatched the
|
|
58
|
-
* run into a background function. Per the design doc Guardrail, the 40s
|
|
59
|
-
* interactive clamp stays correct for every non-background run.
|
|
60
|
-
*/
|
|
61
|
-
export const BACKGROUND_SOFT_TIMEOUT_CEILING_MS = 13 * 60_000; // 780_000
|
|
62
|
-
/**
|
|
63
|
-
* Default soft-timeout budget for a background-function run when the caller
|
|
64
|
-
* does not pass an explicit `softTimeoutMs`. Same value as the ceiling — we
|
|
65
|
-
* want a background turn to use nearly its whole 15-min budget before handing
|
|
66
|
-
* off to a chained background continuation.
|
|
67
|
-
*/
|
|
68
|
-
export const DEFAULT_BACKGROUND_RUN_SOFT_TIMEOUT_MS = BACKGROUND_SOFT_TIMEOUT_CEILING_MS;
|
|
69
|
-
/**
|
|
70
|
-
* AUTHORITATIVE no-progress backstop for a run, enforced by the run manager
|
|
71
|
-
* itself (timer-driven, independent of any layer below).
|
|
72
|
-
*
|
|
73
|
-
* The finer-grained watchdogs inside the agent loop (model-stream and
|
|
74
|
-
* action-preparation no-progress, both 90s) only guard the model event stream
|
|
75
|
-
* — a stall in any segment OUTSIDE that guarded loop (engine-call
|
|
76
|
-
* establishment, worker setup between continuation chunks, a wedged transport
|
|
77
|
-
* that emits keepalives while the loop never runs) previously hung forever
|
|
78
|
-
* with the client watching keepalives. This backstop covers every segment by
|
|
79
|
-
* construction: if no REAL progress event (see `shouldBumpProgressForEvent`;
|
|
80
|
-
* keepalives and zero-byte prep activity don't count) lands for this long —
|
|
81
|
-
* and no unit of work is in flight (see `inFlightWorkDelta`: tool calls,
|
|
82
|
-
* cross-app calls, and the model stream all legitimately emit nothing for
|
|
83
|
-
* minutes and each carry a bound of their own) — the run manager emits
|
|
84
|
-
* `auto_continue { reason: "no_progress" }` and aborts the chunk, exactly
|
|
85
|
-
* like the soft timeout, so the normal continuation machinery recovers it.
|
|
86
|
-
*
|
|
87
|
-
* Being numerically larger than the in-loop watchdogs is NOT what keeps this
|
|
88
|
-
* from killing a healthy run, and treating it that way is what made it do so:
|
|
89
|
-
* this clock and the loop's `lastModelStreamProgressAt` measure DIFFERENT
|
|
90
|
-
* events. An extended-thinking phase bumps the inner clock on every engine
|
|
91
|
-
* frame while forwarding nothing, so the inner watchdog correctly stayed quiet
|
|
92
|
-
* and this one saw pure silence — runs whose worst gap crossed 150s died while
|
|
93
|
-
* still streaming, some by a single second. Ordering between two clocks only
|
|
94
|
-
* means something when they watch the same events; suspending on in-flight
|
|
95
|
-
* work is what actually makes the two agree.
|
|
96
|
-
*
|
|
97
|
-
* This is now only the CEILING, not the value: `resolveRunNoProgressTimeoutMs`
|
|
98
|
-
* clamps the foreground backstop to a fraction of the chunk's soft timeout
|
|
99
|
-
* (~30s at a 40s chunk), which is BELOW the 90s in-loop watchdogs rather than
|
|
100
|
-
* above them. That ordering is deliberate — the in-loop watchdogs could never
|
|
101
|
-
* fire inside a hosted foreground chunk anyway, since the serverless wall
|
|
102
|
-
* (~57-59s) arrives first. Proven durable-background chunks keep the full
|
|
103
|
-
* `DEFAULT_BACKGROUND_NO_PROGRESS_TIMEOUT_MS` so large outputs can use the
|
|
104
|
-
* background budget. Only armed when a soft-timeout regime is active (hosted
|
|
105
|
-
* runs); local dev stays unbounded.
|
|
106
|
-
*/
|
|
107
|
-
export const RUN_NO_PROGRESS_HARD_TIMEOUT_MS = 150_000;
|
|
47
|
+
// 780_000
|
|
108
48
|
/**
|
|
109
49
|
* Default no-progress window for a run executing inside a proven durable
|
|
110
50
|
* background function. A background worker that is heartbeating but has no
|
|
@@ -157,17 +97,38 @@ export function resolveRunNoProgressTimeoutMs(params) {
|
|
|
157
97
|
const explicit = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
158
98
|
? value
|
|
159
99
|
: undefined;
|
|
100
|
+
// Per-call override wins, then configuration, then the shipped default —
|
|
101
|
+
// the same ladder `resolveRunSoftTimeoutMs` implements.
|
|
102
|
+
const configured = getAppConfig().agent;
|
|
103
|
+
// Largest window that can still fire inside the chunk it is guarding. A
|
|
104
|
+
// backstop at or above the chunk budget is not a loose backstop, it is an
|
|
105
|
+
// absent one.
|
|
106
|
+
const budgetCeilingMs = Math.floor(softTimeoutMs * FOREGROUND_NO_PROGRESS_SOFT_TIMEOUT_FRACTION);
|
|
160
107
|
if (backgroundFunction === true) {
|
|
108
|
+
// The background override exists to RAISE this window, so it is honoured
|
|
109
|
+
// as given — a caller asking for a longer one is making an explicit choice
|
|
110
|
+
// and stays bounded by its own hard abort.
|
|
161
111
|
const override = explicit(params.backgroundOverrideMs) ?? explicit(params.overrideMs);
|
|
162
|
-
if (
|
|
163
|
-
return override;
|
|
164
|
-
|
|
112
|
+
if (!(softTimeoutMs > 0))
|
|
113
|
+
return override ?? 0;
|
|
114
|
+
// Honoured as given, because this override exists to RAISE the window —
|
|
115
|
+
// but still bounded by the chunk it guards. A backstop at or above the
|
|
116
|
+
// chunk budget is not a longer backstop, it is an absent one, so a caller
|
|
117
|
+
// asking for one was disabling recovery without meaning to.
|
|
118
|
+
if (override !== undefined) {
|
|
119
|
+
return override === 0 ? 0 : Math.min(override, budgetCeilingMs);
|
|
120
|
+
}
|
|
121
|
+
// Clamped: returned flat, a deployment that lowered the GLOBAL
|
|
122
|
+
// `runSoftTimeoutMs` shrank the chunk without shrinking the backstop, and
|
|
123
|
+
// the backstop silently stopped being reachable. Nothing changes at the
|
|
124
|
+
// shipped values — min(150s, 0.75 x 13min) is still 150s.
|
|
125
|
+
return Math.min(configured.backgroundNoProgressTimeoutMs, budgetCeilingMs);
|
|
165
126
|
}
|
|
166
127
|
const override = explicit(params.overrideMs);
|
|
167
128
|
// Local dev keeps runs unbounded unless a caller explicitly asks otherwise.
|
|
168
129
|
if (!(softTimeoutMs > 0))
|
|
169
130
|
return override ?? 0;
|
|
170
|
-
const ceiling = Math.min(RUN_NO_PROGRESS_HARD_TIMEOUT_MS,
|
|
131
|
+
const ceiling = Math.min(RUN_NO_PROGRESS_HARD_TIMEOUT_MS, budgetCeilingMs);
|
|
171
132
|
if (override === undefined)
|
|
172
133
|
return ceiling;
|
|
173
134
|
return override === 0 ? 0 : Math.min(override, ceiling);
|
|
@@ -442,7 +403,7 @@ export function resolveRunSoftTimeoutMs(overrideMs, options) {
|
|
|
442
403
|
// A background-function run uses the full background budget by default; the
|
|
443
404
|
// foreground default (40s) is unchanged.
|
|
444
405
|
if (background) {
|
|
445
|
-
return hosted ?
|
|
406
|
+
return hosted ? BACKGROUND_SOFT_TIMEOUT_CEILING_MS : 0;
|
|
446
407
|
}
|
|
447
408
|
return options?.useHostedDefault && hosted
|
|
448
409
|
? DEFAULT_HOSTED_RUN_SOFT_TIMEOUT_MS
|
|
@@ -456,6 +417,37 @@ export function resolveErroredRunRetentionMs() {
|
|
|
456
417
|
return (getAppConfig().agent.erroredRunRetentionMs ??
|
|
457
418
|
DEFAULT_ERRORED_RUN_RETENTION_MS);
|
|
458
419
|
}
|
|
420
|
+
/**
|
|
421
|
+
* Hard abort for one in-process background automation run.
|
|
422
|
+
*
|
|
423
|
+
* This is the host's real function budget for scheduled work, which is exactly
|
|
424
|
+
* the kind of number that differs between deployments — so it is configuration,
|
|
425
|
+
* not a module constant nobody outside this package can see.
|
|
426
|
+
*/
|
|
427
|
+
export function resolveBackgroundRunHardTimeoutMs() {
|
|
428
|
+
return getAppConfig().agent.backgroundRunHardTimeoutMs;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Chunk budget for a background automation, derived from the runner's OWN hard
|
|
432
|
+
* abort rather than from the durable-chat background ceiling.
|
|
433
|
+
*
|
|
434
|
+
* The shipped build took the 13-minute chat ceiling for a path whose process is
|
|
435
|
+
* killed at 10 minutes, which made the recoverable soft-timeout boundary dead
|
|
436
|
+
* code and left the terminal no-progress backstop as the only boundary an
|
|
437
|
+
* automation could ever reach. Deriving from the hard abort keeps
|
|
438
|
+
* `soft timeout < hard abort` true by construction; the invariant check asserts
|
|
439
|
+
* the headroom still fits.
|
|
440
|
+
*/
|
|
441
|
+
export function resolveBackgroundAutomationSoftTimeoutMs(overrideMs) {
|
|
442
|
+
const budget = Math.max(1_000, resolveBackgroundRunHardTimeoutMs() -
|
|
443
|
+
BACKGROUND_AUTOMATION_SOFT_TIMEOUT_HEADROOM_MS);
|
|
444
|
+
const resolved = resolveRunSoftTimeoutMs(overrideMs, {
|
|
445
|
+
useHostedDefault: true,
|
|
446
|
+
backgroundFunction: true,
|
|
447
|
+
});
|
|
448
|
+
// `0` means "no soft-timeout regime" (local dev) and is never clamped up.
|
|
449
|
+
return resolved > 0 ? Math.min(resolved, budget) : 0;
|
|
450
|
+
}
|
|
459
451
|
function isTerminalRunEvent(event) {
|
|
460
452
|
return (event.type === "done" ||
|
|
461
453
|
event.type === "error" ||
|
|
@@ -519,6 +511,49 @@ function terminalReasonForRun(finalStatus, terminalEvent, abortReason, completio
|
|
|
519
511
|
return "done";
|
|
520
512
|
}
|
|
521
513
|
const MAX_RUN_ERROR_DETAIL_LENGTH = 500;
|
|
514
|
+
/**
|
|
515
|
+
* One counter per chunk boundary, dimensioned by reason and by whether the
|
|
516
|
+
* turn continued past it.
|
|
517
|
+
*
|
|
518
|
+
* Boundaries are normal; boundaries that TERMINATE a run are not, and before
|
|
519
|
+
* this the two were indistinguishable from outside — which is how a 37%
|
|
520
|
+
* automation failure rate stayed invisible. The ratio between `recovered:true`
|
|
521
|
+
* and `recovered:false` is the number that belongs on a dashboard.
|
|
522
|
+
*
|
|
523
|
+
* Same swallow-everything mechanism as `emitRunTerminalTrackingEvent`: a
|
|
524
|
+
* missing or broken tracking provider can never affect the run.
|
|
525
|
+
*/
|
|
526
|
+
function emitRunBoundaryTrackingEvent(args) {
|
|
527
|
+
const properties = {
|
|
528
|
+
source: "agent_run_manager",
|
|
529
|
+
run_id: args.runId,
|
|
530
|
+
thread_id: args.threadId,
|
|
531
|
+
reason: args.reason,
|
|
532
|
+
recovered: args.recovered,
|
|
533
|
+
boundary_index: args.boundaryIndex,
|
|
534
|
+
dispatch_mode: args.dispatchMode,
|
|
535
|
+
model: args.model,
|
|
536
|
+
engine: args.engineName,
|
|
537
|
+
};
|
|
538
|
+
for (const key of Object.keys(properties)) {
|
|
539
|
+
if (properties[key] === undefined)
|
|
540
|
+
delete properties[key];
|
|
541
|
+
}
|
|
542
|
+
try {
|
|
543
|
+
void Promise.all([
|
|
544
|
+
import("../tracking/registry.js"),
|
|
545
|
+
import("../observability/tracking-identity.js"),
|
|
546
|
+
])
|
|
547
|
+
.then(([{ track }, { trackingIdentityProperties }]) => {
|
|
548
|
+
track("agent_run_boundary", { ...properties, ...trackingIdentityProperties() }, { userId: args.userId });
|
|
549
|
+
})
|
|
550
|
+
.catch(() => { });
|
|
551
|
+
// coercion-ok: a boundary counter must never affect the run it counts.
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
// Tracking must never affect the agent run or its persisted status.
|
|
555
|
+
}
|
|
556
|
+
}
|
|
522
557
|
/**
|
|
523
558
|
* Emit one analytics event per terminal run — the seam that makes cutoffs
|
|
524
559
|
* (`run_budget_exhausted`, `loop_limit`, aborts, `truncated` continuation
|
|
@@ -547,7 +582,7 @@ function emitRunTerminalTrackingEvent(args) {
|
|
|
547
582
|
? `${args.errorDetail.slice(0, MAX_RUN_ERROR_DETAIL_LENGTH)}…`
|
|
548
583
|
: args.errorDetail
|
|
549
584
|
: undefined,
|
|
550
|
-
dispatch_mode: args.dispatchMode
|
|
585
|
+
dispatch_mode: args.dispatchMode,
|
|
551
586
|
abort_reason: args.abortReason,
|
|
552
587
|
duration_ms: args.durationMs,
|
|
553
588
|
model: args.model,
|
|
@@ -618,6 +653,66 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
618
653
|
abortRun(existingRunId);
|
|
619
654
|
}
|
|
620
655
|
const abort = new AbortController();
|
|
656
|
+
// Chunk-scoped controller, only for a runFn that recovers boundaries in this
|
|
657
|
+
// invocation. `abort` stays the turn: a Stop, the cross-isolate abort check,
|
|
658
|
+
// and a caller's hard timeout all still end the run through it, and it always
|
|
659
|
+
// ends whichever chunk is executing under it.
|
|
660
|
+
const recoverChunkBoundaries = options?.recoverChunkBoundaries === true;
|
|
661
|
+
let chunkAbort = recoverChunkBoundaries
|
|
662
|
+
? new AbortController()
|
|
663
|
+
: null;
|
|
664
|
+
let chunkBoundaryReason = null;
|
|
665
|
+
let recoveredChunkBoundaries = 0;
|
|
666
|
+
/** A boundary that has been reached but not yet proven recovered. */
|
|
667
|
+
let pendingBoundary = null;
|
|
668
|
+
/**
|
|
669
|
+
* Resolve the outstanding boundary once its fate is known: `true` when the
|
|
670
|
+
* caller actually opened another round, `false` when the run ended first.
|
|
671
|
+
*/
|
|
672
|
+
const settleBoundary = (recovered) => {
|
|
673
|
+
const boundary = pendingBoundary;
|
|
674
|
+
if (!boundary)
|
|
675
|
+
return;
|
|
676
|
+
pendingBoundary = null;
|
|
677
|
+
recordRunBoundaryDiagnostic(boundary.reason, boundary.diagnostic, recovered ? "recovered" : "terminal");
|
|
678
|
+
emitRunBoundaryTrackingEvent({
|
|
679
|
+
runId,
|
|
680
|
+
threadId,
|
|
681
|
+
reason: boundary.reason,
|
|
682
|
+
recovered,
|
|
683
|
+
boundaryIndex: boundary.index,
|
|
684
|
+
dispatchMode: options?.dispatchMode,
|
|
685
|
+
model: options?.model,
|
|
686
|
+
engineName: options?.engineName,
|
|
687
|
+
userId: options?.userId,
|
|
688
|
+
});
|
|
689
|
+
};
|
|
690
|
+
if (chunkAbort) {
|
|
691
|
+
abort.signal.addEventListener("abort", () => {
|
|
692
|
+
chunkAbort?.abort(abort.signal.reason);
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
const runControl = {
|
|
696
|
+
get turnSignal() {
|
|
697
|
+
return abort.signal;
|
|
698
|
+
},
|
|
699
|
+
get chunkSignal() {
|
|
700
|
+
return chunkAbort?.signal ?? abort.signal;
|
|
701
|
+
},
|
|
702
|
+
chunkBoundaryReason: () => chunkBoundaryReason,
|
|
703
|
+
beginChunk: () => {
|
|
704
|
+
if (abort.signal.aborted || !chunkAbort)
|
|
705
|
+
return abort.signal;
|
|
706
|
+
settleBoundary(true);
|
|
707
|
+
chunkBoundaryReason = null;
|
|
708
|
+
chunkAbort = new AbortController();
|
|
709
|
+
// The boundary is behind us; the silence clock restarts with the chunk,
|
|
710
|
+
// or the backstop fires again on the elapsed time of the chunk it just
|
|
711
|
+
// ended and every recovery round dies instantly.
|
|
712
|
+
lastRealProgressAt = Date.now();
|
|
713
|
+
return chunkAbort.signal;
|
|
714
|
+
},
|
|
715
|
+
};
|
|
621
716
|
let softTimedOut = false;
|
|
622
717
|
let resolveFinalized = () => { };
|
|
623
718
|
let rejectFinalized = () => { };
|
|
@@ -952,29 +1047,114 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
952
1047
|
checkpointAbortInFlight = false;
|
|
953
1048
|
}
|
|
954
1049
|
};
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1050
|
+
/**
|
|
1051
|
+
* Localise the stall. `RUN_DIAG_STAGE`/`recordRunDiagnostic` existed for
|
|
1052
|
+
* exactly this and were wired only into the `_process-run` HTTP path, which
|
|
1053
|
+
* is why a 37%-failure-rate backstop could not name the segment it killed.
|
|
1054
|
+
*/
|
|
1055
|
+
const recordRunBoundaryDiagnostic = (reason, diagnostic, disposition) => {
|
|
1056
|
+
void recordRunDiagnostic(runId, RUN_DIAG_STAGE.runBoundaryReached, JSON.stringify({
|
|
1057
|
+
reason,
|
|
1058
|
+
disposition,
|
|
1059
|
+
silentForMs: diagnostic.silentForMs,
|
|
1060
|
+
lastEventType: diagnostic.lastEventType,
|
|
1061
|
+
inFlightWorkCount,
|
|
1062
|
+
eventCount: run.events.length,
|
|
1063
|
+
elapsedMs: Date.now() - run.startedAt,
|
|
1064
|
+
})).catch(() => { });
|
|
1065
|
+
};
|
|
1066
|
+
/**
|
|
1067
|
+
* Reach a server-owned chunk boundary.
|
|
1068
|
+
*
|
|
1069
|
+
* Two outcomes, and the difference is the whole point: a runFn that recovers
|
|
1070
|
+
* boundaries in this invocation gets its CHUNK aborted and keeps the turn;
|
|
1071
|
+
* every other caller gets the turn ended so a fresh invocation can continue
|
|
1072
|
+
* it. The recoverable case deliberately does NOT emit `auto_continue` or
|
|
1073
|
+
* write a checkpoint terminal event — both describe a turn that stopped here,
|
|
1074
|
+
* and this one has not: the checkpoint row is written at a reserved seq that
|
|
1075
|
+
* outranks the real `done` this run is still going to emit, so persisting it
|
|
1076
|
+
* would relabel a recovered run as truncated.
|
|
1077
|
+
*/
|
|
1078
|
+
const reachRunBoundary = (reason, diagnostic = {}) => {
|
|
958
1079
|
if (run.status !== "running" || abort.signal.aborted)
|
|
959
1080
|
return;
|
|
960
|
-
if (
|
|
961
|
-
|
|
962
|
-
|
|
1081
|
+
if (chunkAbort) {
|
|
1082
|
+
if (chunkAbort.signal.aborted)
|
|
1083
|
+
return;
|
|
1084
|
+
recoveredChunkBoundaries += 1;
|
|
1085
|
+
console.warn(`[run-manager] chunk boundary (${reason}) — recovering in-invocation`, runId, diagnostic);
|
|
1086
|
+
// NOT counted as recovered yet. `recovered` is the whole point of this
|
|
1087
|
+
// counter — it answers "is the recovery working?" — so it has to mean a
|
|
1088
|
+
// round actually started, not that one was invited to. The caller can
|
|
1089
|
+
// still exhaust its budget or fail to build continuation context, and
|
|
1090
|
+
// counting the invitation would over-report recovery, which is the
|
|
1091
|
+
// direction that hides the failure.
|
|
1092
|
+
pendingBoundary = { reason, diagnostic, index: recoveredChunkBoundaries };
|
|
1093
|
+
chunkBoundaryReason = reason;
|
|
1094
|
+
chunkAbort.abort(reason);
|
|
963
1095
|
return;
|
|
964
|
-
|
|
965
|
-
`or model stream in flight — ` +
|
|
966
|
-
`checkpointing run for continuation`, runId);
|
|
1096
|
+
}
|
|
967
1097
|
// Mirror the soft-timeout semantics exactly: the chunk completes (not
|
|
968
1098
|
// aborts) at an auto_continue boundary, so the continuation machinery —
|
|
969
1099
|
// server-chained for background workers, client-driven for foreground —
|
|
970
1100
|
// recovers the turn.
|
|
971
1101
|
softTimedOut = true;
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1102
|
+
recordRunBoundaryDiagnostic(reason, diagnostic, "terminal");
|
|
1103
|
+
emitRunBoundaryTrackingEvent({
|
|
1104
|
+
runId,
|
|
1105
|
+
threadId,
|
|
1106
|
+
reason,
|
|
1107
|
+
recovered: false,
|
|
1108
|
+
boundaryIndex: recoveredChunkBoundaries + 1,
|
|
1109
|
+
dispatchMode: options?.dispatchMode,
|
|
1110
|
+
model: options?.model,
|
|
1111
|
+
engineName: options?.engineName,
|
|
1112
|
+
userId: options?.userId,
|
|
1113
|
+
});
|
|
1114
|
+
const event = { type: "auto_continue", reason };
|
|
976
1115
|
send(event);
|
|
977
|
-
void checkpointRunBoundary(event,
|
|
1116
|
+
void checkpointRunBoundary(event, reason);
|
|
1117
|
+
};
|
|
1118
|
+
const checkNoProgressBackstop = () => {
|
|
1119
|
+
if (noProgressTimeoutMs <= 0)
|
|
1120
|
+
return;
|
|
1121
|
+
if (run.status !== "running" || abort.signal.aborted)
|
|
1122
|
+
return;
|
|
1123
|
+
if (inFlightWorkCount > 0)
|
|
1124
|
+
return;
|
|
1125
|
+
const silentForMs = Date.now() - lastRealProgressAt;
|
|
1126
|
+
if (silentForMs < noProgressTimeoutMs)
|
|
1127
|
+
return;
|
|
1128
|
+
const lastEventType = run.events.at(-1)?.event.type;
|
|
1129
|
+
if (!chunkAbort) {
|
|
1130
|
+
// This backstop ends the TURN here; whether a successor invocation picks
|
|
1131
|
+
// it up is decided later and elsewhere, so from this vantage it is a run
|
|
1132
|
+
// that died on silence. It reached production for two releases as one
|
|
1133
|
+
// console line nobody read. A boundary recovered in THIS invocation stays
|
|
1134
|
+
// a log line — that distinction is the point.
|
|
1135
|
+
console.error(`[run-manager] no real progress for ${noProgressTimeoutMs}ms with no tool ` +
|
|
1136
|
+
`or model stream in flight — ` +
|
|
1137
|
+
`checkpointing run for continuation`, runId);
|
|
1138
|
+
captureError(new Error(`Agent run checkpointed after ${silentForMs}ms of silence (no_progress)`), {
|
|
1139
|
+
route: "/_agent-native/agent-chat",
|
|
1140
|
+
aiTraceId: runId,
|
|
1141
|
+
tags: {
|
|
1142
|
+
source: "agent-run-manager",
|
|
1143
|
+
phase: "no-progress-backstop",
|
|
1144
|
+
terminalReason: "no_progress",
|
|
1145
|
+
lastEventType,
|
|
1146
|
+
},
|
|
1147
|
+
extra: {
|
|
1148
|
+
runId,
|
|
1149
|
+
threadId,
|
|
1150
|
+
silentForMs,
|
|
1151
|
+
noProgressTimeoutMs,
|
|
1152
|
+
lastEventType,
|
|
1153
|
+
eventCount: run.events.length,
|
|
1154
|
+
},
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
reachRunBoundary("no_progress", { silentForMs, lastEventType });
|
|
978
1158
|
};
|
|
979
1159
|
// Periodic SQL abort check interval (for cross-isolate abort on Workers).
|
|
980
1160
|
// Also self-aborts when our row is no longer status='running' — catches the
|
|
@@ -1096,17 +1276,17 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
1096
1276
|
overrideMs: options?.noProgressTimeoutMs,
|
|
1097
1277
|
backgroundOverrideMs: options?.backgroundNoProgressTimeoutMs,
|
|
1098
1278
|
});
|
|
1099
|
-
|
|
1279
|
+
// Not armed for a runFn that recovers boundaries in this invocation. That
|
|
1280
|
+
// runFn already races the SAME wall with its own per-round timer, budgeted
|
|
1281
|
+
// against cumulative elapsed time — so this timer fires at the moment the
|
|
1282
|
+
// wrapper has nothing left to continue with, producing a boundary that is
|
|
1283
|
+
// recoverable in name only and burning the tail of the budget on nothing.
|
|
1284
|
+
// One wall, one clock; the caller's hard abort still backstops it.
|
|
1285
|
+
const softTimeoutTimer = softTimeoutMs > 0 && !recoverChunkBoundaries
|
|
1100
1286
|
? setTimeout(() => {
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
const event = {
|
|
1105
|
-
type: "auto_continue",
|
|
1106
|
-
reason: "run_timeout",
|
|
1107
|
-
};
|
|
1108
|
-
send(event);
|
|
1109
|
-
void checkpointRunBoundary(event, "run_timeout");
|
|
1287
|
+
reachRunBoundary("run_timeout", {
|
|
1288
|
+
lastEventType: run.events.at(-1)?.event.type,
|
|
1289
|
+
});
|
|
1110
1290
|
}, softTimeoutMs)
|
|
1111
1291
|
: null;
|
|
1112
1292
|
let pendingTerminalEvent = null;
|
|
@@ -1213,8 +1393,13 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
1213
1393
|
emitRunEvent(runEvent);
|
|
1214
1394
|
};
|
|
1215
1395
|
// Run in background — intentionally detached from any HTTP connection
|
|
1216
|
-
const runPromise = runFn(send,
|
|
1396
|
+
const runPromise = runFn(send, runControl.chunkSignal, runControl)
|
|
1217
1397
|
.then(() => {
|
|
1398
|
+
// Settled inside the existing handlers rather than a `.finally()`: that
|
|
1399
|
+
// would add a microtask tick to a chain whose ordering callers depend on.
|
|
1400
|
+
// The runFn is done and never opened another round, so an outstanding
|
|
1401
|
+
// boundary ended the run rather than being recovered from.
|
|
1402
|
+
settleBoundary(false);
|
|
1218
1403
|
if (abort.signal.aborted) {
|
|
1219
1404
|
run.status = softTimedOut ? "completed" : "aborted";
|
|
1220
1405
|
return;
|
|
@@ -1222,6 +1407,7 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
1222
1407
|
run.status = "completed";
|
|
1223
1408
|
})
|
|
1224
1409
|
.catch((err) => {
|
|
1410
|
+
settleBoundary(false);
|
|
1225
1411
|
// Don't surface abort errors — the run was intentionally stopped
|
|
1226
1412
|
if (abort.signal.aborted) {
|
|
1227
1413
|
run.status = softTimedOut ? "completed" : "aborted";
|
|
@@ -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
|
/**
|
package/dist/agent/run-store.js
CHANGED
|
@@ -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
|
-
*
|
|
194
|
-
* stale-run recovery
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
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
|
-
|
|
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
|
-
* `
|
|
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 (`
|
|
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>;
|
package/dist/app-config/agent.js
CHANGED
|
@@ -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
|
});
|