@sema-agent/core 5.19.0 → 5.20.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.
- package/CHANGELOG.md +93 -4
- package/dist/agents/roster-store.js +3 -0
- package/dist/brain/circuit-breaker.js +14 -3
- package/dist/brain/timeout.d.ts +1 -0
- package/dist/brain/timeout.js +11 -0
- package/dist/core/background-agent-store.d.ts +1 -0
- package/dist/core/background-agent-store.js +5 -0
- package/dist/core/hooks.d.ts +1 -0
- package/dist/core/mailbox-store.js +2 -0
- package/dist/core/mcp.d.ts +4 -0
- package/dist/core/mcp.js +58 -11
- package/dist/core/retention-policy.d.ts +7 -0
- package/dist/core/retention-policy.js +21 -0
- package/dist/core/runner/prepare-task.js +39 -3
- package/dist/core/runner/runtask.js +17 -2
- package/dist/core/task-registry-agent.js +2 -0
- package/dist/core/tool-policy.js +3 -0
- package/dist/core/workflow-run-store.js +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/orchestration/run-spec.js +4 -0
- package/dist/orchestration/workflow.js +13 -2
- package/dist/stores/file/background-agent-store.js +2 -1
- package/dist/stores/file/mailbox-store.js +2 -0
- package/dist/stores/file/workflow-run-store.js +2 -0
- package/dist/tools/web.js +32 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,89 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.20.0 — 2026-08-08
|
|
4
|
+
|
|
5
|
+
### BREAKING
|
|
6
|
+
|
|
7
|
+
Configuration hygiene sweep. A malformed numeric knob used to be accepted and then quietly do
|
|
8
|
+
something — usually the OPPOSITE of what it was set to. Every seam below now either refuses the
|
|
9
|
+
value at its resolution point or clamps it and says so. Well-formed values in the documented range
|
|
10
|
+
are untouched everywhere; the break is that values which used to be accepted-then-misbehave are
|
|
11
|
+
now rejected, so a deployment carrying one gets a loud failure at wiring time instead of a
|
|
12
|
+
mysterious runtime.
|
|
13
|
+
|
|
14
|
+
- **MCP millisecond env knobs** (`MCP_TOOL_TIMEOUT`, `MCP_TOOL_TIMEOUT_TOTAL`,
|
|
15
|
+
`MCP_IDLE_TIMEOUT_STDIO`, `MCP_IDLE_TIMEOUT_HTTP`, `MCP_TIMEOUT`) share one parser, which was
|
|
16
|
+
`parseInt`-based: `1e9` meant 1ms, `30s` meant 30ms, and `100_000_000` — the way this repo writes
|
|
17
|
+
the default constant it invites you to copy — meant 100ms. Past 2^31-1 the host timer truncated
|
|
18
|
+
the delay and fired at once, so widening a watchdog to "basically off" made it abort every call,
|
|
19
|
+
while the failure text quoted the requested number back. The parser now reads plain digits,
|
|
20
|
+
scientific notation and digit grouping, refuses anything else, and clamps into
|
|
21
|
+
**[1000, 2147483647] ms**, warning once on stderr with the knob name, the raw text and the value
|
|
22
|
+
actually in force. Timeout frames quote the resolved value. `MAX_MCP_OUTPUT_TOKENS` moves onto the
|
|
23
|
+
same grammar (its `1e5` used to mean a 4-char budget), keeping upstream's unbounded range.
|
|
24
|
+
Upgrade note (the widening side of this fix): on 5.19.0 and earlier, `1e9` / `1_800_000` — both
|
|
25
|
+
legal upstream spellings — silently took effect as **1ms**. After upgrading they take effect at
|
|
26
|
+
face value, so a deployment that wrote one of these will see the knob jump from 1ms to the
|
|
27
|
+
number it always said.
|
|
28
|
+
- **Millisecond knobs that arm a host timer** are refused above 2147483647ms (~24.8 days), where
|
|
29
|
+
the timer truncates and fires immediately: the stall watchdogs (`connectTimeoutMs`,
|
|
30
|
+
`firstTokenTimeoutMs`, `idleTimeoutMs`), `brainCallGuardrailMs`, `runWorkflow`'s `totalTimeoutMs`
|
|
31
|
+
/ `stallMs` / `throttleBackoffMs`, and `createApprovalPolicy`'s `approvalTimeoutMs` — the last of
|
|
32
|
+
which would otherwise have denied every request instantly, fail-closed and silent. Each already
|
|
33
|
+
documents `0` / `false` / omission as "off". `limits.maxWalltimeMs` is the exception and keeps
|
|
34
|
+
its promise of no ceiling: the hard-abort timer now waits in representable chunks.
|
|
35
|
+
- **Retention policies** (`BackgroundAgentStore.reap`, `MailboxStore.reap`, `WorkflowRunStore.reap`,
|
|
36
|
+
`reapDurableAgents`, both roster stores' constructors) refuse a non-finite or negative bound with
|
|
37
|
+
`config.retention_policy_invalid`. `keep` is consumed as `slice(Math.max(0, keep))`, where NaN
|
|
38
|
+
collapses to `slice(0)` and deleted EVERY terminal row in the scope; the roster's `maxAgeMs` made
|
|
39
|
+
every durable address read as expired. Absent fields keep their "this arm is not applied" meaning.
|
|
40
|
+
Operational note: this is a refusal, not a sanitize — a deployment with a malformed retention knob
|
|
41
|
+
will see every reap throw (rows accumulate) until the knob is corrected. Fix the knob before
|
|
42
|
+
upgrading; nothing is auto-repaired on your behalf.
|
|
43
|
+
- **Web and circuit-breaker knobs**: `timeoutMs` on WebFetch / WebSearch / the Searxng backend fed a
|
|
44
|
+
bare `setTimeout`, where NaN and Infinity both become 1ms — a budget written as "no limit" aborted
|
|
45
|
+
every request on arrival and reported "timed out after NaNms". `maxResults` fed `slice(0, max)`,
|
|
46
|
+
so a NaN or 0 cap emptied every search from a backend that answered. The circuit breaker's
|
|
47
|
+
`failureThreshold` / `cooldownMs` / `halfOpenProbes` had no validation at all, and `failures >= NaN`
|
|
48
|
+
is false forever — an unevaluable threshold left the breaker permanently open-loop. All are now
|
|
49
|
+
resolved at assembly time (`config.web_timeout_invalid`,
|
|
50
|
+
`config.web_search_max_results_invalid`, `config.circuit_breaker_invalid`). `probeSearchBackend`
|
|
51
|
+
keeps its documented never-throws contract: a malformed budget comes back as `{ ok: false, error }`.
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- **`Hooks.preToolUseObservational`** — a deployment declares that its PreToolUse face is a pure
|
|
56
|
+
OBSERVER (a tracer, an audit sink) and never judges. 5.19.0's delegation fold keys on PRESENCE,
|
|
57
|
+
which cannot tell a tracer from a screener: a deployment that flipped on a default-OFF diagnostic
|
|
58
|
+
hook thereby added an opaque constraint to every delegated child, and a child that durably parked
|
|
59
|
+
recorded that count on its row — where an approval redeemed by a leg that cannot re-supply the
|
|
60
|
+
live screening closure has no recovery path. With the declaration the face contributes **no
|
|
61
|
+
chain entry**, so `parentConstraintCount` counts only constraints that actually judge; without it,
|
|
62
|
+
5.19.0 behavior is unchanged. The callback still runs in the installing task's own gate, on every
|
|
63
|
+
call, with the same crash posture (a throw is still the fail-closed deny) — only the inherited
|
|
64
|
+
constraint goes away. A declared-observational face that returns something anyway has that return
|
|
65
|
+
**refused, not obeyed**: the call proceeds as if the face had no opinion (this covers
|
|
66
|
+
`additionalContext` too) and `onError` is told (`phase:"hook"`, classification
|
|
67
|
+
`observational-hook-verdict-ignored`), so the declaration cannot become a quiet way to keep a
|
|
68
|
+
screening face's verdicts while shedding the delegation constraint they belong to. A declared face
|
|
69
|
+
is also handed a **detached copy of the arguments** (best-effort, same helper and same limits as the
|
|
70
|
+
other observe-only payloads): the gate reads a rewrite off reference identity, so an in-place edit
|
|
71
|
+
would be a second channel outliving the dropped return. Two further consequences of "it does not
|
|
72
|
+
judge": a declared face no longer collapses an ancestor's screening entry that names the same
|
|
73
|
+
callback (the descendant no longer honors it, so the ancestor's constraint is folded instead), and it
|
|
74
|
+
no longer counts as an effect-aware gate for the loud ungated-write-surface warning.
|
|
75
|
+
- **`createPreToolUseConstraintPolicy` is now a public export**, and a durable park under an inherited
|
|
76
|
+
screening constraint says so at PARK time. A checkpoint records only how many opaque constraints its
|
|
77
|
+
leg ran under; the resume must hand the same chain back. Every entry kind except one is a policy the
|
|
78
|
+
deployment authored — the screening entry 5.19.0 added is engine-minted, so a hook-wired tree's
|
|
79
|
+
parked children were redeemable only in the process that spawned them (same-Runner resumes are
|
|
80
|
+
auto-re-supplied from the in-memory registry and were never affected). The mint is now exported, so a
|
|
81
|
+
fresh Runner can rebuild the entry — same callback, same installed env — and hand the full chain to
|
|
82
|
+
`resumeStream(..., internals)`; the recorded-count check is unchanged, so a short or mismatched chain
|
|
83
|
+
is still refused pre-CAS. And the park itself now announces the requirement once per task on
|
|
84
|
+
`onError` (`phase:"degraded"`, classification `screening-constraint-in-durable-chain`), naming the
|
|
85
|
+
recorded count and the ways out, instead of leaving it to be discovered at a redemption that fails.
|
|
86
|
+
|
|
3
87
|
## 5.19.0 — 2026-08-10
|
|
4
88
|
|
|
5
89
|
> Three bundles, each pre-verified two-way by the evaluation line against the 5.18.1 artifact
|
|
@@ -14,10 +98,15 @@
|
|
|
14
98
|
four-axis dedup so a deps-level hook is consulted once per call at any depth). A screening
|
|
15
99
|
`ask` resolves at the installing task's frozen approver; a hook-wired parent with no approver
|
|
16
100
|
denies its children's screened calls fail-closed.
|
|
17
|
-
**Operational**: `parentConstraintCount` gains a
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
101
|
+
**Operational (corrected per downstream measurement)**: `parentConstraintCount` gains a
|
|
102
|
+
screening entry for hook-wired trees. Pre-5.19.0 rows (including hook-wired parents) recorded
|
|
103
|
+
the old count and rebuild legs that supply it keep matching — upgrade and rollback are clean
|
|
104
|
+
for EXISTING pending checkpoints. The unrecoverable case is a checkpoint **minted on 5.19.0**
|
|
105
|
+
by a hook-wired parent and redeemed by a rebuild leg that cannot re-supply the live screening
|
|
106
|
+
entry (cross-replica redemption): it fails pre-CAS with `resume.parent_constraint_mismatch`
|
|
107
|
+
and stays pending, with no recovery path (the count is written at park time; disabling the
|
|
108
|
+
hook afterwards does not change it). Same-replica resumes via `resumeStream(..., internals)`
|
|
109
|
+
are unaffected. A delegated child of a hook-wired parent also sets
|
|
21
110
|
`requiresParentConstraint`, so its durable resume must re-supply the chain via
|
|
22
111
|
`resumeStream(..., internals)`.
|
|
23
112
|
- **Write guards judge the call's live cwd** (P1, host-lane proven bypass). A resident shell's
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { assertRetentionPolicy } from "../core/retention-policy.js";
|
|
2
3
|
import { atomicWriteFile } from "../stores/file/fs-atomic.js";
|
|
3
4
|
import { dirname } from "node:path";
|
|
4
5
|
import { normalizeAgentName } from "../core/task-registry.js";
|
|
@@ -50,6 +51,7 @@ export class MemoryRosterStore {
|
|
|
50
51
|
maxAgeMs;
|
|
51
52
|
gc;
|
|
52
53
|
constructor(opts) {
|
|
54
|
+
assertRetentionPolicy("RosterStore", opts);
|
|
53
55
|
this.maxAgeMs = opts?.maxAgeMs;
|
|
54
56
|
this.gc = opts;
|
|
55
57
|
}
|
|
@@ -78,6 +80,7 @@ export class FileRosterStore {
|
|
|
78
80
|
gc;
|
|
79
81
|
onCorruptRead;
|
|
80
82
|
constructor(path, opts) {
|
|
83
|
+
assertRetentionPolicy("RosterStore", opts);
|
|
81
84
|
this.path = path;
|
|
82
85
|
this.maxAgeMs = opts?.maxAgeMs;
|
|
83
86
|
this.gc = opts;
|
|
@@ -24,10 +24,21 @@ function errorAssistantMessage(model, code, detail, stopReason = "error") {
|
|
|
24
24
|
timestamp: Date.now(),
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
|
+
function finiteBreakerKnob(value, knob, fallback, wholeAtLeastOne) {
|
|
28
|
+
if (value === undefined)
|
|
29
|
+
return fallback;
|
|
30
|
+
const ok = wholeAtLeastOne ? Number.isInteger(value) && value >= 1 : Number.isFinite(value) && value >= 0;
|
|
31
|
+
if (!ok) {
|
|
32
|
+
const e = new Error(`createCircuitBreakerBrain: ${knob} must be ${wholeAtLeastOne ? "a whole number of 1 or more" : "a finite, non-negative number of milliseconds"} (got ${String(value)}) — an unevaluable knob leaves the breaker permanently open-loop, which is a protection that is silently not there`);
|
|
33
|
+
e.code = "config.circuit_breaker_invalid";
|
|
34
|
+
throw e;
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
27
38
|
export function createCircuitBreakerBrain(inner, opts = {}) {
|
|
28
|
-
const failureThreshold = opts.failureThreshold
|
|
29
|
-
const cooldownMs = opts.cooldownMs
|
|
30
|
-
const halfOpenProbes = opts.halfOpenProbes
|
|
39
|
+
const failureThreshold = finiteBreakerKnob(opts.failureThreshold, "failureThreshold", 5, true);
|
|
40
|
+
const cooldownMs = finiteBreakerKnob(opts.cooldownMs, "cooldownMs", 30_000, false);
|
|
41
|
+
const halfOpenProbes = finiteBreakerKnob(opts.halfOpenProbes, "halfOpenProbes", 1, true);
|
|
31
42
|
const countCodes = new Set(opts.countCodes ?? ["network", "server", "rate_limit"]);
|
|
32
43
|
const keyOf = opts.key ?? ((m) => `${m.provider}:${m.id}`);
|
|
33
44
|
const state = opts.state ?? new InMemoryBreakerState();
|
package/dist/brain/timeout.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { StreamFn } from "../internal/llm.js";
|
|
2
|
+
export declare const MAX_TIMER_DELAY_MS = 2147483647;
|
|
2
3
|
export declare function resolveStallTimeoutMs(value: number | undefined, knob: string): number | undefined;
|
|
3
4
|
export declare const STALL_CONNECT_MS = 30000;
|
|
4
5
|
export declare const STALL_FIRST_TOKEN_MS = 120000;
|
package/dist/brain/timeout.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
1
2
|
export function resolveStallTimeoutMs(value, knob) {
|
|
2
3
|
if (value === undefined)
|
|
3
4
|
return undefined;
|
|
@@ -6,6 +7,11 @@ export function resolveStallTimeoutMs(value, knob) {
|
|
|
6
7
|
e.code = "config.stall_timeout_invalid";
|
|
7
8
|
throw e;
|
|
8
9
|
}
|
|
10
|
+
if (value > MAX_TIMER_DELAY_MS) {
|
|
11
|
+
const e = new Error(`${knob} must not exceed ${MAX_TIMER_DELAY_MS}ms (~24.8 days) — a host timer truncates a larger delay and fires immediately, so this watchdog would abort every call instead of tolerating a long one (got ${String(value)}). Pass 0 to disable it.`);
|
|
12
|
+
e.code = "config.stall_timeout_invalid";
|
|
13
|
+
throw e;
|
|
14
|
+
}
|
|
9
15
|
return value;
|
|
10
16
|
}
|
|
11
17
|
export const STALL_CONNECT_MS = 30_000;
|
|
@@ -66,6 +72,11 @@ export function resolveBrainCallGuardrailMs(knob) {
|
|
|
66
72
|
e.code = "config.brain_call_guardrail_invalid";
|
|
67
73
|
throw e;
|
|
68
74
|
}
|
|
75
|
+
if (knob > MAX_TIMER_DELAY_MS) {
|
|
76
|
+
const e = new Error(`brainCallGuardrailMs must not exceed ${MAX_TIMER_DELAY_MS}ms (~24.8 days) — a host timer truncates a larger delay and fires immediately, so the backstop would abort every model call (got ${String(knob)}). Pass false or 0 to turn the backstop off.`);
|
|
77
|
+
e.code = "config.brain_call_guardrail_invalid";
|
|
78
|
+
throw e;
|
|
79
|
+
}
|
|
69
80
|
return knob;
|
|
70
81
|
}
|
|
71
82
|
function armCallGuardrail(limitMs, outerSignal, ref) {
|
|
@@ -87,6 +87,7 @@ export interface BackgroundAgentReapOptions {
|
|
|
87
87
|
keep?: number;
|
|
88
88
|
staleRunningMaxAgeMs?: number;
|
|
89
89
|
}
|
|
90
|
+
export declare function assertBackgroundAgentReapOptions(opts: BackgroundAgentReapOptions | undefined): void;
|
|
90
91
|
export interface BackgroundAgentStore {
|
|
91
92
|
put(record: BackgroundAgentRecord): Promise<void>;
|
|
92
93
|
get(handle: string, scope: string): Promise<BackgroundAgentRecord | null>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { uuidv7 } from "../internal/harness.js";
|
|
2
|
+
import { assertRetentionPolicy } from "./retention-policy.js";
|
|
2
3
|
export const REVIVED_ROW_CLEARED_FIELDS = [
|
|
3
4
|
"settledAt",
|
|
4
5
|
"stoppedBy",
|
|
@@ -43,6 +44,9 @@ export function canAccessAgentRecord(record, access) {
|
|
|
43
44
|
return false;
|
|
44
45
|
}
|
|
45
46
|
export const STALE_RUNNING_REAP_ATTRIBUTION = "the host process was interrupted while this agent was running — its in-process state was lost. Check its worktree / output file for partial work before assuming the task landed (stale running row reaped).";
|
|
47
|
+
export function assertBackgroundAgentReapOptions(opts) {
|
|
48
|
+
assertRetentionPolicy("BackgroundAgentStore.reap", opts);
|
|
49
|
+
}
|
|
46
50
|
export async function reconcileParkedAgents(stores, scope, now, opts) {
|
|
47
51
|
const out = { failed: 0, rolledBack: 0 };
|
|
48
52
|
const excluded = (row) => (opts?.excludeHandles?.has(row.handle) ?? false) ||
|
|
@@ -256,6 +260,7 @@ export class InMemoryBackgroundAgentStore {
|
|
|
256
260
|
return [...new Set([...this.rows.values()].map((r) => r.scope))].sort();
|
|
257
261
|
}
|
|
258
262
|
async reap(scope, now, opts) {
|
|
263
|
+
assertBackgroundAgentReapOptions(opts);
|
|
259
264
|
if (!opts || (opts.maxAgeMs === undefined && opts.keep === undefined && opts.staleRunningMaxAgeMs === undefined))
|
|
260
265
|
return 0;
|
|
261
266
|
const flippedKeys = new Set();
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ExecutionEnv, FileError, Result, SessionTreeEntry } from "../inter
|
|
|
3
3
|
import type { PermissionResult, ResolvedAsk, ToolCallRequest, ToolPolicy } from "./tool-policy.js";
|
|
4
4
|
export interface Hooks {
|
|
5
5
|
preToolUse?(toolName: string, input: unknown, ctx: HookToolContext): PreToolUseResult | undefined | Promise<PreToolUseResult | undefined>;
|
|
6
|
+
preToolUseObservational?: true;
|
|
6
7
|
postToolUse?(toolName: string, input: unknown, output: HookToolOutput, ctx: HookToolContext): PostToolUseResult | undefined | Promise<PostToolUseResult | undefined>;
|
|
7
8
|
userPromptSubmit?(prompt: string): UserPromptSubmitResult | undefined | Promise<UserPromptSubmitResult | undefined>;
|
|
8
9
|
stop?(ctx: StopHookContext): StopHookResult | undefined | Promise<StopHookResult | undefined>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertRetentionPolicy } from "./retention-policy.js";
|
|
1
2
|
export function newestSentAt(messages) {
|
|
2
3
|
let newest;
|
|
3
4
|
for (const m of messages) {
|
|
@@ -65,6 +66,7 @@ export class InMemoryMailboxStore {
|
|
|
65
66
|
this.boxes.delete(this.key(scope, handle));
|
|
66
67
|
}
|
|
67
68
|
async reap(scope, now, opts) {
|
|
69
|
+
assertRetentionPolicy("MailboxStore.reap", opts);
|
|
68
70
|
if (opts?.maxAgeMs === undefined)
|
|
69
71
|
return 0;
|
|
70
72
|
let dropped = 0;
|
package/dist/core/mcp.d.ts
CHANGED
|
@@ -63,11 +63,15 @@ export declare function gateMcpOutput(content: Array<TextContent | ImageContent>
|
|
|
63
63
|
export declare function structuredContentErrorLine(structuredContent: unknown, collectedText: string): string | undefined;
|
|
64
64
|
export declare function truncateMcpErrorText(s: string): string;
|
|
65
65
|
export declare const MCP_TOOL_TIMEOUT_DEFAULT_MS = 100000000;
|
|
66
|
+
export declare const MCP_ENV_MS_MIN = 1000;
|
|
67
|
+
export declare const MCP_ENV_MS_MAX = 2147483647;
|
|
68
|
+
export declare function __resetMcpEnvAnnouncements(): void;
|
|
66
69
|
export declare function mcpToolTimeoutMs(): number;
|
|
67
70
|
export declare function mcpToolTotalTimeoutMs(perCallMs: number): number;
|
|
68
71
|
export declare const MCP_IDLE_TIMEOUT_STDIO_DEFAULT_MS: number;
|
|
69
72
|
export declare const MCP_IDLE_TIMEOUT_HTTP_DEFAULT_MS: number;
|
|
70
73
|
export declare function mcpIdleTimeoutMs(kind: "stdio" | "http"): number;
|
|
74
|
+
export declare function mcpStartupTimeoutMs(): number | undefined;
|
|
71
75
|
export declare function describeMcpSpecErrorCode(code: unknown): string | undefined;
|
|
72
76
|
export declare function collapseMcpErrorPrefix(message: string): string;
|
|
73
77
|
export declare function networkErrorCode(err: unknown, depth?: number): string | undefined;
|
package/dist/core/mcp.js
CHANGED
|
@@ -24,13 +24,18 @@ export function resolveMcpDeclaredResultSize(meta) {
|
|
|
24
24
|
return Math.min(declared, MCP_META_RESULT_SIZE_CAP);
|
|
25
25
|
}
|
|
26
26
|
function mcpMaxOutputTokens() {
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
const rawEnv = process.env.MAX_MCP_OUTPUT_TOKENS;
|
|
28
|
+
if (rawEnv === undefined)
|
|
29
|
+
return MCP_OUTPUT_TOKENS_DEFAULT;
|
|
30
|
+
const raw = rawEnv.trim();
|
|
31
|
+
if (raw === "")
|
|
32
|
+
return MCP_OUTPUT_TOKENS_DEFAULT;
|
|
33
|
+
const n = parseWholeNumber(raw);
|
|
34
|
+
if (n === undefined || n <= 0) {
|
|
35
|
+
announceMcpEnvKnob(`MAX_MCP_OUTPUT_TOKENS=${rawEnv} was ignored — it is not a positive whole number of tokens. Using ${MCP_OUTPUT_TOKENS_DEFAULT} instead.`);
|
|
36
|
+
return MCP_OUTPUT_TOKENS_DEFAULT;
|
|
32
37
|
}
|
|
33
|
-
return
|
|
38
|
+
return n;
|
|
34
39
|
}
|
|
35
40
|
function mcpTruncationNote(limitTokens) {
|
|
36
41
|
return (`\n\n[OUTPUT TRUNCATED - exceeded ${limitTokens} token limit]\n\n` +
|
|
@@ -100,12 +105,54 @@ export function truncateMcpErrorText(s) {
|
|
|
100
105
|
return out.length < s.length ? out : s;
|
|
101
106
|
}
|
|
102
107
|
export const MCP_TOOL_TIMEOUT_DEFAULT_MS = 100_000_000;
|
|
108
|
+
export const MCP_ENV_MS_MIN = 1_000;
|
|
109
|
+
export const MCP_ENV_MS_MAX = 2_147_483_647;
|
|
110
|
+
const ENV_SCIENTIFIC_RE = /^[+-]?(\d+(\.\d*)?|\.\d+)[eE][+-]?\d+$/;
|
|
111
|
+
const ENV_GROUPED_RE = /^[+-]?\d{1,3}([_,\u00A0\u202F ])\d{3}(?:\1\d{3})*$/;
|
|
112
|
+
const ENV_GROUP_SEPARATORS_RE = /[_,\u00A0\u202F ]/g;
|
|
113
|
+
const ENV_NUMERIC_MAX_LEN = 32;
|
|
114
|
+
function parseWholeNumber(raw) {
|
|
115
|
+
if (/^[+-]?\d+$/.test(raw)) {
|
|
116
|
+
const n = Number(raw);
|
|
117
|
+
return Number.isSafeInteger(n) ? n : undefined;
|
|
118
|
+
}
|
|
119
|
+
if (raw.length > ENV_NUMERIC_MAX_LEN)
|
|
120
|
+
return undefined;
|
|
121
|
+
if (ENV_SCIENTIFIC_RE.test(raw)) {
|
|
122
|
+
const n = Number(raw);
|
|
123
|
+
return Number.isSafeInteger(n) ? n : undefined;
|
|
124
|
+
}
|
|
125
|
+
if (ENV_GROUPED_RE.test(raw))
|
|
126
|
+
return parseInt(raw.replace(ENV_GROUP_SEPARATORS_RE, ""), 10);
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
const announcedMcpEnvKnobs = new Set();
|
|
130
|
+
function announceMcpEnvKnob(line) {
|
|
131
|
+
if (announcedMcpEnvKnobs.has(line))
|
|
132
|
+
return;
|
|
133
|
+
announcedMcpEnvKnobs.add(line);
|
|
134
|
+
console.warn(line);
|
|
135
|
+
}
|
|
136
|
+
export function __resetMcpEnvAnnouncements() {
|
|
137
|
+
announcedMcpEnvKnobs.clear();
|
|
138
|
+
}
|
|
103
139
|
function parseEnvMs(name) {
|
|
104
|
-
const
|
|
105
|
-
if (
|
|
140
|
+
const rawEnv = process.env[name];
|
|
141
|
+
if (rawEnv === undefined)
|
|
142
|
+
return undefined;
|
|
143
|
+
const raw = rawEnv.trim();
|
|
144
|
+
if (raw === "")
|
|
106
145
|
return undefined;
|
|
107
|
-
const n =
|
|
108
|
-
|
|
146
|
+
const n = parseWholeNumber(raw);
|
|
147
|
+
if (n === undefined || n <= 0) {
|
|
148
|
+
announceMcpEnvKnob(`${name}=${rawEnv} was ignored — it is not a positive whole number of milliseconds. Using the built-in default instead.`);
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
const clamped = Math.min(Math.max(n, MCP_ENV_MS_MIN), MCP_ENV_MS_MAX);
|
|
152
|
+
if (clamped !== n) {
|
|
153
|
+
announceMcpEnvKnob(`${name}=${rawEnv} is outside the range this runtime can honor (${MCP_ENV_MS_MIN}..${MCP_ENV_MS_MAX} ms). Using ${clamped}ms instead.`);
|
|
154
|
+
}
|
|
155
|
+
return clamped;
|
|
109
156
|
}
|
|
110
157
|
export function mcpToolTimeoutMs() {
|
|
111
158
|
return parseEnvMs("MCP_TOOL_TIMEOUT") ?? MCP_TOOL_TIMEOUT_DEFAULT_MS;
|
|
@@ -150,7 +197,7 @@ function armMcpIdleWatchdog(health, idleMs, outerSignal) {
|
|
|
150
197
|
},
|
|
151
198
|
};
|
|
152
199
|
}
|
|
153
|
-
function mcpStartupTimeoutMs() {
|
|
200
|
+
export function mcpStartupTimeoutMs() {
|
|
154
201
|
return parseEnvMs("MCP_TIMEOUT");
|
|
155
202
|
}
|
|
156
203
|
const MCP_SPEC_ERROR_CODE_NAMES = new Map([
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
function invalidPolicy(label, knob, value, requirement) {
|
|
2
|
+
const e = new Error(`${label}: ${knob} must be ${requirement} (got ${String(value)}) — a retention bound that cannot be evaluated silently decides what to delete instead of bounding it`);
|
|
3
|
+
e.code = "config.retention_policy_invalid";
|
|
4
|
+
throw e;
|
|
5
|
+
}
|
|
6
|
+
export function assertRetentionPolicy(label, opts) {
|
|
7
|
+
if (opts === undefined)
|
|
8
|
+
return;
|
|
9
|
+
for (const knob of ["maxAgeMs", "staleRunningMaxAgeMs"]) {
|
|
10
|
+
const v = opts[knob];
|
|
11
|
+
if (v !== undefined && (!Number.isFinite(v) || v < 0)) {
|
|
12
|
+
invalidPolicy(label, knob, v, "a finite, non-negative number of milliseconds");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
for (const knob of ["keep", "maxEntries"]) {
|
|
16
|
+
const v = opts[knob];
|
|
17
|
+
if (v !== undefined && (!Number.isInteger(v) || v < 0)) {
|
|
18
|
+
invalidPolicy(label, knob, v, "a whole number of rows, 0 or more");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -1074,7 +1074,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1074
1074
|
const ownCallerPolicy = lockedPreflight.toolPolicy;
|
|
1075
1075
|
const durableMandate = runtimeCaps?.forceDurableGate === true ||
|
|
1076
1076
|
(spec.durableApproval !== undefined && !isLiveApproverSeat(frozenOnAsk));
|
|
1077
|
-
const ownPreToolUse = hooks?.preToolUse;
|
|
1077
|
+
const ownPreToolUse = preToolUseObservational ? undefined : hooks?.preToolUse;
|
|
1078
1078
|
const hookConstraint = ownPreToolUse !== undefined &&
|
|
1079
1079
|
!(inheritedParentConstraints ?? []).some((pc) => pc.preToolUse === ownPreToolUse &&
|
|
1080
1080
|
askApproverIdentity(pc.onAsk) === askApproverIdentity(frozenOnAsk) &&
|
|
@@ -2946,6 +2946,24 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2946
2946
|
const denyNarrowingPolicy = narrowingLayers.length === 0 ? undefined : narrowingLayers.length === 1 ? narrowingLayers[0] : combinePolicies(...narrowingLayers);
|
|
2947
2947
|
const basePolicyForResumeEdit = lockedPreflight.toolPolicy;
|
|
2948
2948
|
const hooks = spec.hooks ?? deps.hooks;
|
|
2949
|
+
const preToolUseObservational = hooks?.preToolUse !== undefined && hooks.preToolUseObservational === true;
|
|
2950
|
+
const ownGatePreToolUse = !preToolUseObservational
|
|
2951
|
+
? hooks?.preToolUse
|
|
2952
|
+
: async (toolName, input, ctx) => {
|
|
2953
|
+
const r = await hooks.preToolUse(toolName, cloneObserverInput(input), ctx);
|
|
2954
|
+
if (r === undefined)
|
|
2955
|
+
return undefined;
|
|
2956
|
+
try {
|
|
2957
|
+
const action = typeof r.action === "string" ? r.action : "(non-string action)";
|
|
2958
|
+
deps.onError?.(new Error(`a PreToolUse hook declared observational (Hooks.preToolUseObservational) returned a decision ("${action}") while screening ` +
|
|
2959
|
+
`"${toolName}" — the decision was NOT adopted and the call proceeded as if the hook had no opinion. ` +
|
|
2960
|
+
`A declared-observational face must return undefined; drop the declaration if its verdicts are meant to count ` +
|
|
2961
|
+
`(they then also travel to delegated children as an inherited screening constraint).`), { phase: "hook", sessionId, classification: "observational-hook-verdict-ignored" });
|
|
2962
|
+
}
|
|
2963
|
+
catch {
|
|
2964
|
+
}
|
|
2965
|
+
return undefined;
|
|
2966
|
+
};
|
|
2949
2967
|
const sameInstanceAncestorCount = policy === undefined ? 0 : (inheritedParentConstraints ?? []).reduce((n, pc) => (pc.policy === policy ? n + 1 : n), 0);
|
|
2950
2968
|
const sharedFirstDecision = new Map();
|
|
2951
2969
|
const SHARED_FIRST_DECISION_CAP = 256;
|
|
@@ -3095,6 +3113,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3095
3113
|
};
|
|
3096
3114
|
const parentConstraintWrappers = (inheritedParentConstraints ?? []).map((pc) => pc.preToolUse !== undefined &&
|
|
3097
3115
|
pc.preToolUse === hooks?.preToolUse &&
|
|
3116
|
+
!preToolUseObservational &&
|
|
3098
3117
|
pc.durableMandate !== true &&
|
|
3099
3118
|
askApproverIdentity(pc.onAsk) === askApproverIdentity(frozenOnAsk) &&
|
|
3100
3119
|
pc.hookEnv === hookEnvSource
|
|
@@ -3310,7 +3329,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3310
3329
|
return eff === "write" || eff === "idempotent";
|
|
3311
3330
|
})
|
|
3312
3331
|
: [];
|
|
3313
|
-
const hasEffectAwareGate = Boolean(policyLayers.length > 0 || hooks?.preToolUse);
|
|
3332
|
+
const hasEffectAwareGate = Boolean(policyLayers.length > 0 || (hooks?.preToolUse !== undefined && !preToolUseObservational));
|
|
3314
3333
|
const destructiveMcpUngated = mcp.tools.some((t) => !irreversibleTools.has(t.name) && !egressTools.has(t.name) && (toolEffects.get(t.name) ?? "write") !== "read");
|
|
3315
3334
|
const firstPartyWriteUngated = (spec.tools ?? [])
|
|
3316
3335
|
.map((t) => t.name)
|
|
@@ -3564,6 +3583,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3564
3583
|
carried.spentMicroUsd = prior + parkedSpendMicroUsd;
|
|
3565
3584
|
return carried;
|
|
3566
3585
|
};
|
|
3586
|
+
const screeningParkDisclosedRef = { done: false };
|
|
3567
3587
|
const serializeCheckpointState = (workspaceHandle, parkedSpendMicroUsd) => ({
|
|
3568
3588
|
activeTools: [...activeTools],
|
|
3569
3589
|
outputRef: { value: outputRef.value, set: outputRef.set },
|
|
@@ -3620,6 +3640,22 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3620
3640
|
await memoryEngineSession.harvest("checkpoint");
|
|
3621
3641
|
try {
|
|
3622
3642
|
await checkpointStore.put(token, cp);
|
|
3643
|
+
const committedCount = cp.state.inheritedGate?.parentConstraintCount;
|
|
3644
|
+
if (cp.state.inheritedGate?.requiresParentConstraint === true &&
|
|
3645
|
+
!screeningParkDisclosedRef.done &&
|
|
3646
|
+
(inheritedParentConstraints ?? []).some((pc) => pc.preToolUse !== undefined)) {
|
|
3647
|
+
screeningParkDisclosedRef.done = true;
|
|
3648
|
+
try {
|
|
3649
|
+
deps.onError?.(new Error(`durable park under an inherited PreToolUse SCREENING constraint: this checkpoint records ` +
|
|
3650
|
+
`requiresParentConstraint with parentConstraintCount=${committedCount ?? "(unrecorded)"}, and the live ` +
|
|
3651
|
+
`closures cannot be persisted. A resume on THIS Runner re-supplies them automatically; a resume on a fresh ` +
|
|
3652
|
+
`Runner (restart / another replica) must hand back the whole chain via resumeStream(..., internals), ` +
|
|
3653
|
+
`rebuilding the screening entry with createPreToolUseConstraintPolicy(hook, env) — otherwise the row stays ` +
|
|
3654
|
+
`pending. If the face only observes, declare Hooks.preToolUseObservational and it stops entering the chain.`), { phase: "degraded", sessionId, classification: "screening-constraint-in-durable-chain" });
|
|
3655
|
+
}
|
|
3656
|
+
catch {
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3623
3659
|
return { ok: true };
|
|
3624
3660
|
}
|
|
3625
3661
|
catch (putErr) {
|
|
@@ -4152,7 +4188,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4152
4188
|
result = await runToolGate({
|
|
4153
4189
|
onNotifyError: (f) => emitTrace(deps.tracer, () => ({ kind: "observer.notify_failed", version: 1, taskId: spec.taskId ?? sessionId, site: f.site, message: f.error.message, ts: Date.now() })),
|
|
4154
4190
|
event: e,
|
|
4155
|
-
preToolUse:
|
|
4191
|
+
preToolUse: ownGatePreToolUse,
|
|
4156
4192
|
...(hookEnvFace !== undefined ? { hookEnv: hookEnvFace } : {}),
|
|
4157
4193
|
adjudicate,
|
|
4158
4194
|
resolveAsk: resolveAskBound,
|
|
@@ -1357,6 +1357,7 @@ export class Runner {
|
|
|
1357
1357
|
const hooks = h
|
|
1358
1358
|
? {
|
|
1359
1359
|
...(typeof h.preToolUse === "function" ? { preToolUse: (t, i, c) => h.preToolUse(t, i, c) } : {}),
|
|
1360
|
+
...(typeof h.preToolUse === "function" && h.preToolUseObservational === true ? { preToolUseObservational: true } : {}),
|
|
1360
1361
|
...(typeof h.postToolUse === "function" ? { postToolUse: (t, i, o, c) => h.postToolUse(t, i, o, c) } : {}),
|
|
1361
1362
|
...(typeof h.userPromptSubmit === "function" ? { userPromptSubmit: (p) => h.userPromptSubmit(p) } : {}),
|
|
1362
1363
|
...(typeof h.stop === "function" ? { stop: (c) => h.stop(c) } : {}),
|
|
@@ -4084,14 +4085,28 @@ function startTimeout(harness, abortController, remainingMs, softSuspendable = f
|
|
|
4084
4085
|
if (remainingMs !== undefined) {
|
|
4085
4086
|
const hardMs = Math.max(0, remainingMs + (softSuspendable ? WALLTIME_SUSPEND_GRACE_SEC * 1000 : 0));
|
|
4086
4087
|
const scheduledAtMs = Date.now() + hardMs;
|
|
4087
|
-
|
|
4088
|
+
let leftMs = hardMs;
|
|
4089
|
+
let timer;
|
|
4090
|
+
const armChunk = () => {
|
|
4091
|
+
const chunk = Math.max(0, Math.min(leftMs, MAX_TIMER_DELAY_MS));
|
|
4092
|
+
timer = setTimeout(() => {
|
|
4093
|
+
leftMs -= chunk;
|
|
4094
|
+
if (leftMs > 0) {
|
|
4095
|
+
armChunk();
|
|
4096
|
+
return;
|
|
4097
|
+
}
|
|
4098
|
+
fireHardAbort();
|
|
4099
|
+
}, chunk);
|
|
4100
|
+
};
|
|
4101
|
+
const fireHardAbort = () => {
|
|
4088
4102
|
if (state.fired)
|
|
4089
4103
|
return;
|
|
4090
4104
|
state.fired = true;
|
|
4091
4105
|
state.latenessMs = Math.max(0, Date.now() - scheduledAtMs);
|
|
4092
4106
|
abortController.abort();
|
|
4093
4107
|
void harness.abort();
|
|
4094
|
-
}
|
|
4108
|
+
};
|
|
4109
|
+
armChunk();
|
|
4095
4110
|
state.clear = () => clearTimeout(timer);
|
|
4096
4111
|
}
|
|
4097
4112
|
return state;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { assertRetentionPolicy } from "./retention-policy.js";
|
|
2
3
|
import { uuidv7 } from "../internal/harness.js";
|
|
3
4
|
import { canAccessAgentRecord, BackgroundAgentStoreError, clearRevivedRowTerminalPayload, REVIVED_ROW_CLEARED_FIELDS, STALE_RUNNING_REAP_ATTRIBUTION, } from "./background-agent-store.js";
|
|
4
5
|
import { shutdownDebug } from "./shutdown-debug.js";
|
|
@@ -163,6 +164,7 @@ export function endDurableClaimLane(core, id) {
|
|
|
163
164
|
core.claimingHandles.delete(id);
|
|
164
165
|
}
|
|
165
166
|
export async function reapDurableAgentsLane(core, scope, deps, policy) {
|
|
167
|
+
assertRetentionPolicy("reapDurableAgents", policy);
|
|
166
168
|
const now = policy.now ?? Date.now();
|
|
167
169
|
if (policy.staleRunningMaxAgeMs !== undefined) {
|
|
168
170
|
await deps.store.reap(scope, now, { staleRunningMaxAgeMs: policy.staleRunningMaxAgeMs });
|
package/dist/core/tool-policy.js
CHANGED
|
@@ -117,6 +117,9 @@ export function createApprovalPolicy(opts) {
|
|
|
117
117
|
if (opts.approvalTimeoutMs !== undefined && !Number.isFinite(opts.approvalTimeoutMs)) {
|
|
118
118
|
throw new Error(`createApprovalPolicy: approvalTimeoutMs must be a finite number of ms (got ${opts.approvalTimeoutMs}) — omit it to wait indefinitely`);
|
|
119
119
|
}
|
|
120
|
+
if (opts.approvalTimeoutMs !== undefined && opts.approvalTimeoutMs > 2_147_483_647) {
|
|
121
|
+
throw new Error(`createApprovalPolicy: approvalTimeoutMs must not exceed 2147483647ms (~24.8 days) (got ${opts.approvalTimeoutMs}) — a host timer truncates a larger delay and fires immediately, denying every request at once; omit it to wait indefinitely`);
|
|
122
|
+
}
|
|
120
123
|
const need = new Set(opts.requireApproval);
|
|
121
124
|
const deny = new Set(opts.deny ?? []);
|
|
122
125
|
const auto = new Set(opts.autoAllow ?? []);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertRetentionPolicy } from "./retention-policy.js";
|
|
1
2
|
export function summarizeWorkflowRun(run) {
|
|
2
3
|
const latestPhase = run.phases.at(-1);
|
|
3
4
|
return {
|
|
@@ -73,6 +74,7 @@ export class InMemoryWorkflowRunStore {
|
|
|
73
74
|
return queryWorkflowRuns(this.runs.values(), scope, opts);
|
|
74
75
|
}
|
|
75
76
|
async reap(scope, now, opts) {
|
|
77
|
+
assertRetentionPolicy("WorkflowRunStore.reap", opts);
|
|
76
78
|
if (opts?.maxAgeMs === undefined && opts?.keep === undefined)
|
|
77
79
|
return 0;
|
|
78
80
|
const terminal = [...this.runs.values()].filter((r) => r.scope === scope && isTerminalWorkflowStatus(r.status));
|
package/dist/index.d.ts
CHANGED
|
@@ -131,7 +131,7 @@ export { parseAllowRuleText, formatAllowRuleText, ruleAdmitsCommand, findAdmitti
|
|
|
131
131
|
export { removePersistedRule, applyTombstones, sameScope, InMemoryPermissionRuleStore, EMPTY_RULE_STORE, type PermissionRuleStore, type PermissionRuleStoreProvider, type StoredAllowRules, type RemoveResult, type PutResult, } from "./core/permission-rule-store.js";
|
|
132
132
|
export { prepareCardApproval, confirmRuleApproval, type ConfirmResult, type ConfirmRefusalReason, redeemRuleTicket, redeemRuleBatch, prepareCcImport, prepareStarterBatch, mintRuleTicket, STARTER_RULES, InMemoryRuleApprovalRecordStore, type RuleTicket, type RuleCandidate, type RuleApprovalKind, type RuleApprovalRecord, type RuleApprovalRecordStore, type RuleConsentDeps, type RedeemResult, type CcImportLayer, type ImportedSettingsLayer, type ImportPreview, type ImportResult, } from "./core/permission-rule-consent.js";
|
|
133
133
|
export { FilePermissionRuleStoreProvider } from "./stores/file/permission-rule-store.js";
|
|
134
|
-
export { formatHookFeedback, runToolGate, createHookEnvCapabilities, type Hooks, type HookToolContext, type HookEnvCapabilities, type HookToolOutput, type PreToolUseResult, type PostToolUseResult, type UserPromptSubmitResult, type HookToolFailure, type PostToolUseFailureResult, type PostToolBatchCall, type PostToolBatchResult, type PreCompactContext, type PreCompactResult, type PostCompactContext, type StopFailureContext, type PermissionDeniedPayload, type PermissionDeniedSource, } from "./core/hooks.js";
|
|
134
|
+
export { formatHookFeedback, runToolGate, createHookEnvCapabilities, createPreToolUseConstraintPolicy, type Hooks, type HookToolContext, type HookEnvCapabilities, type HookToolOutput, type PreToolUseResult, type PostToolUseResult, type UserPromptSubmitResult, type HookToolFailure, type PostToolUseFailureResult, type PostToolBatchCall, type PostToolBatchResult, type PreCompactContext, type PreCompactResult, type PostCompactContext, type StopFailureContext, type PermissionDeniedPayload, type PermissionDeniedSource, } from "./core/hooks.js";
|
|
135
135
|
export { InMemoryMemoryStore, composeMemoryBlock, composeLayeredMemoryBlock, normalizeMemorySpec, type NormalizedMemorySpec, type MemorySpecInput, supportsConsolidation, supportsPeriodicConsolidation, firstSentence, expandLexicalTerms, lexicalSearchMatch, type Embedder, classifyPromotable, enforcePromotableWriteGate, guardedMemoryStore, MemoryGateError, detectSecret, enforceSecretWriteGate, enforceStructuredNoteSecretGate, type UtilityGate, type MemoryStore, type MemoryVectorMode, type ScoredMemory, type MemoryNoteHeader, type MemoryNoteRecord, type MemoryNoteType, type StructuredNoteInput, } from "./core/memory.js";
|
|
136
136
|
export { MemoryEngine, FileMemoryEngineBackend, memoryBackendContract, assertMemoryBackendSearchEquivalence, type MemoryBackendContractHooks, buildMemoryInstruction, truncateIndex, scanEntryFiles, deriveRepoKey, deriveRepoMemoryDir, deriveControlPlaneDir, deriveRepoControlPlaneDir, resolveMemoryEngineRoot, scopeDirName, ControlPlaneCorruptError, parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_INDEX_FILENAME, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, type MemorySearchDetails, type MemorySearchHit, type MemoryGetDetails, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_MAX_ENTRY_DEPTH, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE, renderAnnouncements, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, type MemoryAnnouncement, type ScanFinding, type MemoryEngineOptions, type MemoryInjection, type MemoryBackend, type MemoryEntry, type MemoryEntryFrontmatter, type MemoryEntryHeader, type ScoredMemoryEntry, type NotePatch, type PatchReport, type MaterializedFile, type MemorySessionHandle, type HarvestReport, type HarvestRejection, type HarvestRejectionCode, type ParsedEntryFile, type ScannedEntryFile, SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, type ParsedScopeKey, migrateScope, type MigrateScopeReport, materializeEntriesToFiles, harvestFilesToPatches, projectionsToWriteBack, screenInboundEntries, REMOTE_HARVEST_PER_FILE_BYTES, REMOTE_MASS_DELETION_FUSE_RATIO, type RemoteMemoryFile, type RemoteMemoryBaseline, type RemoteMaterialization, type RemoteHarvestResult, type InboundEntryFinding, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, reconcileMemoryEntries, nextSyncBaseline, mergeRecallHits, type MemorySyncCursor, type MemorySyncConflict, type MemorySyncPlan, type RecallSource, syncMemoryScope, type MemorySyncTransport, type MemorySyncRequestBody, type MemorySyncResponseBody, type MemorySyncClientConflict, type SyncMemoryScopeOptions, type MemorySyncClientResult, } from "./core/memory-engine/index.js";
|
|
137
137
|
export { SHARED_MEMORY_READ_CAP_BYTES, SHARED_MEMORY_LIST_PAGE_SIZE, SharedMemoryStoreError, type SharedMemoryStoreProvider, type SharedMemoryStoreReader, type SharedMemoryStoreInfo, type SharedMemoryDocumentEntry, type SharedMemorySnapshot, type SharedMemoryRequestContext, type MemoryListDetails, type MemoryReadDetails, } from "./core/shared-memory/types.js";
|
package/dist/index.js
CHANGED
|
@@ -116,7 +116,7 @@ export { parseAllowRuleText, formatAllowRuleText, ruleAdmitsCommand, findAdmitti
|
|
|
116
116
|
export { removePersistedRule, applyTombstones, sameScope, InMemoryPermissionRuleStore, EMPTY_RULE_STORE, } from "./core/permission-rule-store.js";
|
|
117
117
|
export { prepareCardApproval, confirmRuleApproval, redeemRuleTicket, redeemRuleBatch, prepareCcImport, prepareStarterBatch, mintRuleTicket, STARTER_RULES, InMemoryRuleApprovalRecordStore, } from "./core/permission-rule-consent.js";
|
|
118
118
|
export { FilePermissionRuleStoreProvider } from "./stores/file/permission-rule-store.js";
|
|
119
|
-
export { formatHookFeedback, runToolGate, createHookEnvCapabilities, } from "./core/hooks.js";
|
|
119
|
+
export { formatHookFeedback, runToolGate, createHookEnvCapabilities, createPreToolUseConstraintPolicy, } from "./core/hooks.js";
|
|
120
120
|
export { InMemoryMemoryStore, composeMemoryBlock, composeLayeredMemoryBlock, normalizeMemorySpec, supportsConsolidation, supportsPeriodicConsolidation, firstSentence, expandLexicalTerms, lexicalSearchMatch, classifyPromotable, enforcePromotableWriteGate, guardedMemoryStore, MemoryGateError, detectSecret, enforceSecretWriteGate, enforceStructuredNoteSecretGate, } from "./core/memory.js";
|
|
121
121
|
export { MemoryEngine, FileMemoryEngineBackend, memoryBackendContract, assertMemoryBackendSearchEquivalence, buildMemoryInstruction, truncateIndex, scanEntryFiles, deriveRepoKey, deriveRepoMemoryDir, deriveControlPlaneDir, deriveRepoControlPlaneDir, resolveMemoryEngineRoot, scopeDirName, ControlPlaneCorruptError, parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_INDEX_FILENAME, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_MAX_ENTRY_DEPTH, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE, renderAnnouncements, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, migrateScope, materializeEntriesToFiles, harvestFilesToPatches, projectionsToWriteBack, screenInboundEntries, REMOTE_HARVEST_PER_FILE_BYTES, REMOTE_MASS_DELETION_FUSE_RATIO, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, reconcileMemoryEntries, nextSyncBaseline, mergeRecallHits, syncMemoryScope, } from "./core/memory-engine/index.js";
|
|
122
122
|
export { SHARED_MEMORY_READ_CAP_BYTES, SHARED_MEMORY_LIST_PAGE_SIZE, SharedMemoryStoreError, } from "./core/shared-memory/types.js";
|
|
@@ -102,6 +102,9 @@ export async function runSpec(runner, contract, opts) {
|
|
|
102
102
|
const userHooks = opts.taskSpec.hooks && deployBaseline?.hooks
|
|
103
103
|
? {
|
|
104
104
|
preToolUse: (opts.taskSpec.hooks.preToolUse ?? deployBaseline.hooks.preToolUse)?.bind(opts.taskSpec.hooks.preToolUse ? opts.taskSpec.hooks : deployBaseline.hooks),
|
|
105
|
+
...((opts.taskSpec.hooks.preToolUse ? opts.taskSpec.hooks : deployBaseline.hooks).preToolUseObservational === true
|
|
106
|
+
? { preToolUseObservational: true }
|
|
107
|
+
: {}),
|
|
105
108
|
postToolUse: (opts.taskSpec.hooks.postToolUse ?? deployBaseline.hooks.postToolUse)?.bind(opts.taskSpec.hooks.postToolUse ? opts.taskSpec.hooks : deployBaseline.hooks),
|
|
106
109
|
userPromptSubmit: (opts.taskSpec.hooks.userPromptSubmit ?? deployBaseline.hooks.userPromptSubmit)?.bind(opts.taskSpec.hooks.userPromptSubmit ? opts.taskSpec.hooks : deployBaseline.hooks),
|
|
107
110
|
stop: (opts.taskSpec.hooks.stop ?? deployBaseline.hooks.stop)?.bind(opts.taskSpec.hooks.stop ? opts.taskSpec.hooks : deployBaseline.hooks),
|
|
@@ -118,6 +121,7 @@ export async function runSpec(runner, contract, opts) {
|
|
|
118
121
|
let gateAbandoned = false;
|
|
119
122
|
const hooks = {
|
|
120
123
|
...(userHooks?.preToolUse && { preToolUse: userHooks.preToolUse.bind(userHooks) }),
|
|
124
|
+
...(userHooks?.preToolUse && userHooks.preToolUseObservational === true ? { preToolUseObservational: true } : {}),
|
|
121
125
|
...(userHooks?.postToolUse && { postToolUse: userHooks.postToolUse.bind(userHooks) }),
|
|
122
126
|
...(userHooks?.userPromptSubmit && { userPromptSubmit: userHooks.userPromptSubmit.bind(userHooks) }),
|
|
123
127
|
...(userHooks?.postToolUseFailure && { postToolUseFailure: userHooks.postToolUseFailure.bind(userHooks) }),
|
|
@@ -245,6 +245,7 @@ function normalizeConcurrency(c) {
|
|
|
245
245
|
}
|
|
246
246
|
return Math.floor(c);
|
|
247
247
|
}
|
|
248
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
248
249
|
function normalizeWorkflowHardCap(name, v) {
|
|
249
250
|
if (v === undefined)
|
|
250
251
|
return undefined;
|
|
@@ -253,12 +254,22 @@ function normalizeWorkflowHardCap(name, v) {
|
|
|
253
254
|
}
|
|
254
255
|
return v;
|
|
255
256
|
}
|
|
257
|
+
function normalizeWorkflowTimerCap(name, v) {
|
|
258
|
+
const normalized = normalizeWorkflowHardCap(name, v);
|
|
259
|
+
if (normalized !== undefined && normalized > MAX_TIMER_DELAY_MS) {
|
|
260
|
+
throw new Error(`runWorkflow: ${name} must not exceed ${MAX_TIMER_DELAY_MS}ms (~24.8 days) (got ${v}) — a host timer truncates a larger delay and fires immediately, cancelling the run at once. Omit it for no deadline.`);
|
|
261
|
+
}
|
|
262
|
+
return normalized;
|
|
263
|
+
}
|
|
256
264
|
function normalizeWorkflowStallMs(v) {
|
|
257
265
|
if (v === undefined)
|
|
258
266
|
return WORKFLOW_AGENT_STALL_MS;
|
|
259
267
|
if (!Number.isFinite(v)) {
|
|
260
268
|
throw new Error(`runWorkflow: stallMs must be a finite number of milliseconds (got ${v}); 0 or a negative value is the documented explicit "off", not NaN/Infinity`);
|
|
261
269
|
}
|
|
270
|
+
if (v > MAX_TIMER_DELAY_MS) {
|
|
271
|
+
throw new Error(`runWorkflow: stallMs must not exceed ${MAX_TIMER_DELAY_MS}ms (~24.8 days) (got ${v}) — a host timer truncates a larger delay and fires immediately, so a watchdog stretched that far would trip on every agent instead of tolerating a long one. Pass 0 to turn it off.`);
|
|
272
|
+
}
|
|
262
273
|
return v;
|
|
263
274
|
}
|
|
264
275
|
function createSemaphore(max) {
|
|
@@ -346,10 +357,10 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
346
357
|
const maxAgents = normalizeWorkflowHardCap("maxAgents", opts.maxAgents);
|
|
347
358
|
const maxLogChars = normalizeWorkflowHardCap("maxLogChars", opts.maxLogChars);
|
|
348
359
|
const maxResultChars = normalizeWorkflowHardCap("maxResultChars", opts.maxResultChars);
|
|
349
|
-
const totalTimeoutMs =
|
|
360
|
+
const totalTimeoutMs = normalizeWorkflowTimerCap("totalTimeoutMs", opts.totalTimeoutMs);
|
|
350
361
|
const stallMs = normalizeWorkflowStallMs(opts.stallMs);
|
|
351
362
|
const agentMaxRetries = normalizeWorkflowHardCap("agentMaxRetries", opts.agentMaxRetries) ?? WORKFLOW_AGENT_MAX_RETRIES;
|
|
352
|
-
const throttleBackoffMs =
|
|
363
|
+
const throttleBackoffMs = normalizeWorkflowTimerCap("throttleBackoffMs", opts.throttleBackoffMs) ?? WORKFLOW_AGENT_THROTTLE_BACKOFF_MS;
|
|
353
364
|
const timers = opts.timers ?? REAL_WORKFLOW_TIMERS;
|
|
354
365
|
const cancelController = new AbortController();
|
|
355
366
|
const timeoutController = totalTimeoutMs !== undefined ? new AbortController() : undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import { BackgroundAgentStoreError, STALE_RUNNING_REAP_ATTRIBUTION, queryBackgroundAgents, } from "../../core/background-agent-store.js";
|
|
2
|
+
import { BackgroundAgentStoreError, STALE_RUNNING_REAP_ATTRIBUTION, assertBackgroundAgentReapOptions, queryBackgroundAgents, } from "../../core/background-agent-store.js";
|
|
3
3
|
import { SharedLedgerTable } from "./shared-ledger.js";
|
|
4
4
|
const agentLedgers = new SharedLedgerTable({
|
|
5
5
|
keyOf: (r) => FileBackgroundAgentStore.key(r.handle, r.scope),
|
|
@@ -113,6 +113,7 @@ export class FileBackgroundAgentStore {
|
|
|
113
113
|
});
|
|
114
114
|
}
|
|
115
115
|
async reap(scope, now, opts) {
|
|
116
|
+
assertBackgroundAgentReapOptions(opts);
|
|
116
117
|
if (!opts || (opts.maxAgeMs === undefined && opts.keep === undefined && opts.staleRunningMaxAgeMs === undefined))
|
|
117
118
|
return 0;
|
|
118
119
|
const changed = new Set();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join, resolve, sep } from "node:path";
|
|
2
|
+
import { assertRetentionPolicy } from "../../core/retention-policy.js";
|
|
2
3
|
import { existsSync, readdirSync, realpathSync } from "node:fs";
|
|
3
4
|
import { newestSentAt, } from "../../core/mailbox-store.js";
|
|
4
5
|
import { AppendLog, atomicWriteFile, canonicalStoreKey, ensureDir, readJsonlRecords, sanitizeScope, sanitizePathComponent } from "./fs-atomic.js";
|
|
@@ -176,6 +177,7 @@ export class FileMailboxStore {
|
|
|
176
177
|
});
|
|
177
178
|
}
|
|
178
179
|
async reap(scope, now, opts) {
|
|
180
|
+
assertRetentionPolicy("MailboxStore.reap", opts);
|
|
179
181
|
if (opts?.maxAgeMs === undefined)
|
|
180
182
|
return 0;
|
|
181
183
|
let dropped = 0;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
import { assertRetentionPolicy } from "../../core/retention-policy.js";
|
|
2
3
|
import { WorkflowRunStoreError, isTerminalWorkflowStatus, nextWorkflowRunOnUpdate, queryWorkflowRuns, } from "../../core/workflow-run-store.js";
|
|
3
4
|
import { SharedLedgerTable } from "./shared-ledger.js";
|
|
4
5
|
const runLedgers = new SharedLedgerTable({
|
|
@@ -64,6 +65,7 @@ export class FileWorkflowRunStore {
|
|
|
64
65
|
return queryWorkflowRuns(this.runs.values(), scope, opts);
|
|
65
66
|
}
|
|
66
67
|
async reap(scope, now, opts) {
|
|
68
|
+
assertRetentionPolicy("WorkflowRunStore.reap", opts);
|
|
67
69
|
if (opts?.maxAgeMs === undefined && opts?.keep === undefined)
|
|
68
70
|
return 0;
|
|
69
71
|
const terminal = [...this.runs.values()].filter((r) => r.scope === scope && isTerminalWorkflowStatus(r.status));
|
package/dist/tools/web.js
CHANGED
|
@@ -25,6 +25,31 @@ function resolveWebMaxBytes(value) {
|
|
|
25
25
|
}
|
|
26
26
|
return value;
|
|
27
27
|
}
|
|
28
|
+
function resolveWebTimeoutMs(value, knob, fallback) {
|
|
29
|
+
if (value === undefined)
|
|
30
|
+
return fallback;
|
|
31
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
32
|
+
const e = new Error(`${knob} must be a positive finite number of milliseconds (got ${String(value)}) — a non-finite or non-positive budget is not "no limit", it aborts every request within a millisecond`);
|
|
33
|
+
e.code = "config.web_timeout_invalid";
|
|
34
|
+
throw e;
|
|
35
|
+
}
|
|
36
|
+
if (value > 2_147_483_647) {
|
|
37
|
+
const e = new Error(`${knob} must not exceed 2147483647ms (~24.8 days) (got ${String(value)}) — a host timer truncates a larger delay and fires immediately, so widening the budget past this point aborts every request at once`);
|
|
38
|
+
e.code = "config.web_timeout_invalid";
|
|
39
|
+
throw e;
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
function resolveSearchMaxResults(value) {
|
|
44
|
+
if (value === undefined)
|
|
45
|
+
return 10;
|
|
46
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
47
|
+
const e = new Error(`WebSearch maxResults must be a whole number of 1 or more (got ${String(value)}) — a zero, negative or unevaluable cap returns an empty result set from a backend that answered`);
|
|
48
|
+
e.code = "config.web_search_max_results_invalid";
|
|
49
|
+
throw e;
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
28
53
|
export const WEBFETCH_GROUNDING_MIN_TEXT_CHARS = 200;
|
|
29
54
|
const GROUNDING_MIN_TEXT_RATIO = 0.01;
|
|
30
55
|
const GROUNDING_RATIO_MAX_TEXT_CHARS = 2_000;
|
|
@@ -230,6 +255,7 @@ async function fetchAllowlisted(doFetch, start, allowHosts, userAgent, signal, o
|
|
|
230
255
|
}
|
|
231
256
|
export function webFetchToolSpec(config = {}) {
|
|
232
257
|
const maxBytes = resolveWebMaxBytes(config.maxBytes);
|
|
258
|
+
const fetchTimeoutMs = resolveWebTimeoutMs(config.timeoutMs, "WebFetch timeoutMs", DEFAULT_FETCH_TIMEOUT_MS);
|
|
233
259
|
const doFetch = config.fetchImpl ?? globalThis.fetch;
|
|
234
260
|
const canSummarize = config.summarize !== undefined;
|
|
235
261
|
return {
|
|
@@ -276,7 +302,7 @@ export function webFetchToolSpec(config = {}) {
|
|
|
276
302
|
}
|
|
277
303
|
if (!doFetch)
|
|
278
304
|
return failure("Error (WebFetch): no fetch implementation available in this environment.");
|
|
279
|
-
const timeoutMs =
|
|
305
|
+
const timeoutMs = fetchTimeoutMs;
|
|
280
306
|
const ac = new AbortController();
|
|
281
307
|
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
282
308
|
const onOuterAbort = () => ac.abort();
|
|
@@ -794,7 +820,8 @@ function webSearchResultAllowed(url, allowed, blocked) {
|
|
|
794
820
|
return true;
|
|
795
821
|
}
|
|
796
822
|
export function createWebSearchTool(config) {
|
|
797
|
-
const max = config.maxResults
|
|
823
|
+
const max = resolveSearchMaxResults(config.maxResults);
|
|
824
|
+
const searchTimeoutMs = resolveWebTimeoutMs(config.timeoutMs, "WebSearch timeoutMs", DEFAULT_SEARCH_TIMEOUT_MS);
|
|
798
825
|
const currentMonthYear = new Date().toLocaleString("en-US", { month: "long", year: "numeric" });
|
|
799
826
|
return {
|
|
800
827
|
name: "WebSearch",
|
|
@@ -831,7 +858,7 @@ export function createWebSearchTool(config) {
|
|
|
831
858
|
if (queryChars > SEARCH_QUERY_MAX_CHARS) {
|
|
832
859
|
return errorResult(`Error (WebSearch): the query is ${queryChars} characters, over the ${SEARCH_QUERY_MAX_CHARS}-character limit. Shorten it to the terms that matter — an identical retry will fail the same way.`, failCard({ retryable: false }));
|
|
833
860
|
}
|
|
834
|
-
const timeoutMs =
|
|
861
|
+
const timeoutMs = searchTimeoutMs;
|
|
835
862
|
const ac = new AbortController();
|
|
836
863
|
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
837
864
|
const onOuterAbort = () => ac.abort();
|
|
@@ -936,7 +963,7 @@ export function createWebSearchTool(config) {
|
|
|
936
963
|
}
|
|
937
964
|
export function createSearxngSearchBackend(baseUrl, options = {}) {
|
|
938
965
|
const doFetch = options.fetchImpl ?? globalThis.fetch;
|
|
939
|
-
const timeoutMs = options.timeoutMs
|
|
966
|
+
const timeoutMs = resolveWebTimeoutMs(options.timeoutMs, "createSearxngSearchBackend timeoutMs", 10_000);
|
|
940
967
|
const parsedBase = new URL(baseUrl);
|
|
941
968
|
parsedBase.hash = "";
|
|
942
969
|
parsedBase.pathname = `${parsedBase.pathname.replace(/\/+$/, "")}/search`;
|
|
@@ -966,9 +993,9 @@ export function createSearxngSearchBackend(baseUrl, options = {}) {
|
|
|
966
993
|
};
|
|
967
994
|
}
|
|
968
995
|
export async function probeSearchBackend(search, options) {
|
|
969
|
-
const budget = options?.timeoutMs ?? 15_000;
|
|
970
996
|
let timer;
|
|
971
997
|
try {
|
|
998
|
+
const budget = resolveWebTimeoutMs(options?.timeoutMs, "probeSearchBackend timeoutMs", 15_000);
|
|
972
999
|
const results = await Promise.race([
|
|
973
1000
|
search("connectivity probe", AbortSignal.timeout(budget)),
|
|
974
1001
|
new Promise((_, reject) => {
|