@sema-agent/core 5.17.0-pre.0 → 5.17.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +107 -1
  2. package/dist/agents/subagent.d.ts +8 -0
  3. package/dist/agents/subagent.js +69 -2
  4. package/dist/core/ask-question.js +6 -1
  5. package/dist/core/canonical-json.js +176 -14
  6. package/dist/core/checkpoint-store.d.ts +14 -0
  7. package/dist/core/checkpoint-store.js +73 -0
  8. package/dist/core/hooks.d.ts +4 -1
  9. package/dist/core/hooks.js +24 -6
  10. package/dist/core/mcp.d.ts +1 -0
  11. package/dist/core/mcp.js +15 -3
  12. package/dist/core/runner/prepare-task.d.ts +1 -0
  13. package/dist/core/runner/prepare-task.js +78 -22
  14. package/dist/core/runner/runtask.js +3 -1
  15. package/dist/core/runner/turn-attachments.d.ts +2 -1
  16. package/dist/core/runner/turn-attachments.js +9 -6
  17. package/dist/core/session-reconcile.js +19 -1
  18. package/dist/core/task-registry-agent.js +1 -0
  19. package/dist/core/tool-policy.d.ts +9 -0
  20. package/dist/core/tool-policy.js +28 -8
  21. package/dist/core/types.d.ts +1 -0
  22. package/dist/core/wiring-manifest.js +2 -2
  23. package/dist/engine/llm/validation.js +121 -5
  24. package/dist/engine/loop/agent-loop.d.ts +2 -0
  25. package/dist/engine/loop/agent-loop.js +17 -4
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +1 -0
  28. package/dist/prompts/supervisor.d.ts +1 -1
  29. package/dist/prompts/supervisor.js +1 -1
  30. package/dist/stores/file/checkpoint-store.d.ts +1 -0
  31. package/dist/stores/file/checkpoint-store.js +1 -0
  32. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
  33. package/dist/tools/fs/bash-readonly-classifier.js +11 -10
  34. package/dist/tools/fs/fs-bash.js +6 -6
  35. package/dist/tools/fs/fs-read.d.ts +1 -1
  36. package/dist/tools/fs/fs-read.js +4 -3
  37. package/dist/tools/fs/fs-shared.d.ts +3 -0
  38. package/dist/tools/fs/fs-shared.js +8 -1
  39. package/dist/tools/fs/fs-write.js +8 -8
  40. package/dist/tools/fs/index.d.ts +1 -0
  41. package/dist/tools/fs/index.js +1 -1
  42. package/dist/tools/fs/safety.d.ts +1 -0
  43. package/dist/tools/fs/safety.js +9 -2
  44. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,6 +1,112 @@
1
1
  # Changelog
2
2
 
3
- ## 5.17.0 — 2026-08-08
3
+ ## 5.17.0 — 2026-08-09
4
+
5
+ > One release, three campaigns: design/176 peer guard + design/177 shared memory stores (below, from
6
+ > the first RC), the error-surface re-anchor batch, and the durable-approval honesty tail
7
+ > (capture/fidelity/seat-criterion). The re-verified RC supersedes `c48e66d`.
8
+
9
+ ### BREAKING — model-visible error text re-anchored
10
+
11
+ - **Validation failure is a synthesized sentence, not a dump.** A tool-argument validation error now
12
+ reads as classified one-line findings (missing / unexpected / type-mismatched parameters, other
13
+ issues listed with a 2,000-char cap) — and **the full argument echo is gone** (the failed call sits
14
+ immediately above its result in the transcript; the echo was a verbatim second copy, paid once per
15
+ retry). The schema re-teach section stays, capped at 4,000 chars (an intentional divergence: a BYOM
16
+ model benefits from re-teaching). Tests pinning the old `Received arguments:` form must re-pin.
17
+ - **Every loop-minted error text is bounded.** `createErrorToolResult` runs the same 10k head+tail
18
+ truncation the tool-throw path already had — the tool-not-found, abort, hook/policy-text,
19
+ harness-throw and after-tool-note arms are no longer unbounded.
20
+ - **The tool-not-found roster lists ≤25 names** (`… and N more`, with a ToolSearch pointer when
21
+ mounted) instead of the whole roster.
22
+ - **Repeated interruption stubs collapse**: within one reconcile batch, the 2nd..nth orphaned call of
23
+ the same class gets a one-line back-reference instead of the full boilerplate.
24
+ - An MCP error's `structuredContent` is no longer appended when its JSON already appears in the text
25
+ parts; an ambiguous-Edit refusal echoes at most 200 chars of the needle; four repeated guidance
26
+ sentences are single-sourced (model-visible bytes unchanged).
27
+ - **The per-read content-safety reminder is gated** (`readCyberReminder` on the hands band / toolkit
28
+ options): default ON (injected after every text read, as before), explicit `false` for deployments
29
+ whose model carries this mitigation natively.
30
+
31
+ ### BREAKING — durable approval rows filed at backend width
32
+
33
+ - The park mint files arguments at the width its checkpoint backend can hold. `CheckpointStore` gains
34
+ the third honest-declaration axis `fidelity` (`"structured-clone" | "json"`; ABSENT reads
35
+ fail-closed as `"json"`); the row's args, preview, risk descriptor and opaque `boundInputHash` are
36
+ minted from the backend's projection, so what an approver sees, what is on disk and what a resume
37
+ executes are one value. Arguments with no JSON encoding (BigInt, a cycle) are refused at the mint,
38
+ naming the backend. A projection that moves the value is re-adjudicated against the deployment's
39
+ policy; a deny — or a rewrite naming anything else — refuses the park and falls back to the
40
+ synchronous gate. `StoreFidelity` is exported.
41
+ **Upgrade note:** rows minted by earlier versions keep their pre-projection hash and already-degraded
42
+ arguments; drain pending approvals (approve/deny them) before upgrading, or accept the old semantics
43
+ on those rows.
44
+
45
+ ### Changed
46
+
47
+ - **Model-visible text (four faces) now states what this engine actually does.** Each is a string the
48
+ model reads, so a deployment pinning these bytes must re-pin:
49
+ 1. `tool_search_usage_reminder` no longer ends with "Calling a tool before its schema is loaded will
50
+ fail." With the RB-403 direct-call lane mounted — the DEFAULT (`TaskSpec.deferSelfResolve` not
51
+ disabled) — a call whose arguments match the real schema executes, so the absolute claim was false
52
+ for that posture. The default posture now reads "You do not have these tools' parameters, so
53
+ activate one rather than guessing its arguments."; the absolute wording is retained only under
54
+ `deferSelfResolve: false`, where it is exactly true. Same amendment DD-5 already made to the
55
+ sibling ToolSearch description.
56
+ 2. `tools_delta`'s static `added` arm attributes schemas to "the tool result that activated them"
57
+ instead of "the ToolSearch result". Under `toolMaterializeStrategy: "static"` the direct-call lane
58
+ is always mounted, so an announced tool may have been activated with no ToolSearch result in
59
+ existence at all.
60
+ 3. `AskUserQuestion`'s description is posture-conditional. Under `interactionPosture: "interactive"`
61
+ without `interactiveQuestionFallback`, both no-answer arms are coded failures, so the description
62
+ states that instead of promising "you will be told to proceed with your best judgment".
63
+ 4. The deferred-orchestration prompt block no longer promises "the full contract arrives with it" —
64
+ under the static face only the parameter schema materializes, never the tool's description.
65
+
66
+ ### Fixed
67
+
68
+ - **A durable park that cannot capture its arguments now says so, instead of leaving the call to report
69
+ a missing approver.** Arguments a `structuredClone` cannot copy — a hook/policy rewrite handing over a
70
+ function, a symbol, a live handle; model arguments are JSON, so the deployment is the only source —
71
+ used to reach the checkpoint store and throw a `DataCloneError` from inside `put`. That failure went to
72
+ `onError` alone, the park reported "not suspended", and the fallback chain's headless auto-deny told
73
+ the model and the `permissionDenied` observer that no approver was wired: true, and about a different
74
+ subject than the one that stopped the call. The park leg now checks capturability before any side
75
+ effect (with the same helper the synchronous approval boundary uses), refuses with the runtime's own
76
+ sentence, and never enters the store. A park attempt that fails for any reason carries its cause to
77
+ the gate, which appends it to whatever deny the fallback produces — the fallback itself, and the
78
+ compensation contract behind it, are unchanged. Three narrowings ride along: arguments carrying shared
79
+ memory (`SharedArrayBuffer`) are refused at the mint rather than filed as a row another holder can
80
+ still mutate; the row stores the checked snapshot, so its arguments, preview and binding hash describe
81
+ one reading rather than one per consumer; and a park that already made its one real attempt is not
82
+ asked again when the approver then reports unavailable (a suspend and a checkpoint write are not
83
+ idempotent — the invariant the content-ask lane already states, now held on the permission lane too).
84
+ Refusal text quoting a thrown value is neutralized and bounded on its way to the model, since a clone
85
+ error renders the offending value's own source into its message; the exception itself still reaches
86
+ `onError` with its type and stack intact.
87
+ - **BREAKING (narrow) — the child-chain durable-approval mandate uses the same seat criterion as the
88
+ task's own gate, single-sourced.** A blanket `onAsk: "allow"`/`"deny"` is a policy setting, not a
89
+ live approver — the parent's gate already said so; the inherited-constraint leg still read a string
90
+ seat as occupied. One predicate now serves both sites. The reachable defect was in the storeless
91
+ rows: with `durableApproval` configured but no `CheckpointStore`, a child under a blanket `"allow"`
92
+ chain entry EXECUTED side-effecting calls the parent's own gate would not have resolved through that
93
+ blanket. Such a child now parks with its own facility or denies fail-closed. (The store remains the
94
+ park lane's arming condition — it is just no longer misread as part of the seat.)
95
+ - **`wiring-manifest` no longer reports an auto verdict for runs whose asks park.** `deriveAskEffective`
96
+ returns `park_only` for a blanket `"allow"`/`"deny"` seat on an armed park lane. Operator/governance
97
+ projection only; the interaction-posture door is unaffected (it tests `human_reachable` alone).
98
+ - **A revived background agent no longer inherits the previous cycle's stop attribution.**
99
+ `reviveBackgroundAgentLane` clears `stoppedBy` in lockstep with the durable row's cleared-field set:
100
+ cycle 2's running and completed polls were reporting cycle 1's stopper while the durable-row lane
101
+ reported none, and cycle 2's real stopper was discarded in favour of cycle 1's.
102
+ - **Three CI discipline gates that had been red on every push are green.** `gate:field-liveness` (the
103
+ managed-retention contract never got allowlist rows), `gate:message-branching` and
104
+ `gate:domain-lexicon` (one false positive each, both narrowed rather than allowlisted). The step
105
+ aborts on first failure, so the six gate test files after it had not been running either.
106
+ - **The `eval/` and `bench/memory-write-channel` harnesses run again.** Both failed at task start on
107
+ config keys retired in 5.8.0 (`limits.timeoutSec`) and 2.0.0 (`memory: { scope }`) and reported it
108
+ only as a score of zero. `eval/` now runs 5/5 offline as its README claims, and joins `tsconfig`'s
109
+ include so the class reds `tsc` instead of rotting silently (the build config is unaffected).
4
110
 
5
111
  ### Added
6
112
 
@@ -9,6 +9,14 @@ import { SubagentRetainLedger } from "./retain-ledger.js";
9
9
  import { type PeerSelfRef } from "./peer-admission.js";
10
10
  export type { SubagentStep, SubagentEditedFile } from "./subagent-steps.js";
11
11
  export declare function notifyResultField(result: string | undefined): string | undefined;
12
+ interface ReviewSample {
13
+ text: string;
14
+ readChars: number;
15
+ totalChars: number;
16
+ sampled: boolean;
17
+ gaps: number;
18
+ }
19
+ export declare function layeredReviewSample(text: string, budget?: number, windows?: number): ReviewSample;
12
20
  export declare function inheritedManifestScopeFor(snapshot: readonly unknown[] | undefined): RunInternals["inheritedManifestScope"];
13
21
  export declare const DEFAULT_SUBAGENT_TOOL_NAME = "Agent";
14
22
  export declare const EXTRA_TOOLS_MAX_FACTORY_CALLS_PER_TREE = 64;
@@ -55,6 +55,66 @@ export function notifyResultField(result) {
55
55
  ? `${result.slice(0, BG_AGENT_NOTIFY_RESULT_MAX)}\n[result truncated: ${result.length} chars total — call TaskOutput for the full text]`
56
56
  : result;
57
57
  }
58
+ const HANDBACK_REVIEW_SAMPLE_BUDGET = 12_000;
59
+ const HANDBACK_REVIEW_SAMPLE_WINDOWS = 5;
60
+ const HANDBACK_REVIEW_SAMPLE_MAX_WINDOWS = 16;
61
+ const HANDBACK_REVIEW_MARKER_COST = 32;
62
+ const REVIEW_MARKER_SCAR = String.fromCharCode(0xff3b);
63
+ function defuseGapMarkers(slice) {
64
+ return slice.replace(/\[…/g, `${REVIEW_MARKER_SCAR}…`);
65
+ }
66
+ function surrogateSafeSlice(text, from, width, anchorEnd = false) {
67
+ const isLow = (i) => text.charCodeAt(i) >= 0xdc00 && text.charCodeAt(i) <= 0xdfff;
68
+ const isHigh = (i) => text.charCodeAt(i) >= 0xd800 && text.charCodeAt(i) <= 0xdbff;
69
+ let start = from;
70
+ if (start > 0 && start < text.length && isLow(start) && isHigh(start - 1))
71
+ start--;
72
+ if (anchorEnd)
73
+ return { text: text.slice(start), start, end: text.length };
74
+ let end = Math.min(start + width, text.length);
75
+ if (end > start && end < text.length && isHigh(end - 1) && isLow(end))
76
+ end--;
77
+ return { text: text.slice(start, end), start, end };
78
+ }
79
+ export function layeredReviewSample(text, budget = HANDBACK_REVIEW_SAMPLE_BUDGET, windows = HANDBACK_REVIEW_SAMPLE_WINDOWS) {
80
+ const total = text.length;
81
+ const budgetChars = Number.isFinite(budget) ? Math.max(4, Math.floor(budget)) : HANDBACK_REVIEW_SAMPLE_BUDGET;
82
+ if (total <= budgetChars)
83
+ return { text, readChars: total, totalChars: total, sampled: false, gaps: 0 };
84
+ const requested = Number.isFinite(windows) ? Math.floor(windows) : HANDBACK_REVIEW_SAMPLE_WINDOWS;
85
+ const count = Math.max(2, Math.min(Math.max(2, requested), HANDBACK_REVIEW_SAMPLE_MAX_WINDOWS, Math.floor(budgetChars / HANDBACK_REVIEW_MARKER_COST)));
86
+ const size = Math.floor(budgetChars / count);
87
+ const span = total - size;
88
+ const parts = [];
89
+ let readChars = 0;
90
+ let prevEnd = 0;
91
+ let gaps = 0;
92
+ for (let i = 0; i < count; i++) {
93
+ const start = Math.round((span * i) / (count - 1));
94
+ const slice = surrogateSafeSlice(text, start, size, i === count - 1);
95
+ if (i > 0 && slice.start > prevEnd) {
96
+ parts.push(`\n[… ${slice.start - prevEnd} chars not shown …]\n`);
97
+ gaps++;
98
+ }
99
+ parts.push(defuseGapMarkers(slice.text));
100
+ readChars += slice.text.length;
101
+ prevEnd = slice.end;
102
+ }
103
+ const out = parts.join("");
104
+ if (out.length >= total)
105
+ return { text, readChars: total, totalChars: total, sampled: false, gaps: 0 };
106
+ return { text: out, readChars, totalChars: total, sampled: true, gaps };
107
+ }
108
+ function reviewCoverageNote(fields) {
109
+ const sampled = fields.filter((f) => f.sample.sampled);
110
+ if (sampled.length === 0)
111
+ return undefined;
112
+ return (sampled
113
+ .map((f) => `${f.field}: the classifier was shown ${f.sample.readChars} of ${f.sample.totalChars} characters (UTF-16 code units) ` +
114
+ `(layered head/middle/tail sample; the ${f.sample.gaps} skipped span(s) are marked in place as ` +
115
+ `"[… N chars not shown …]" — any further such marker in the text is the child's own writing, not this sampler's)`)
116
+ .join("; ") + " — the unshown spans were NOT reviewed; treat them as unknown, not as benign.");
117
+ }
58
118
  const HANDBACK_ASK_MESSAGE = "Subagent has finished and is handing back control to the main agent. Review the subagent's work and flag if any action may violate security policy.";
59
119
  async function reviewHandback(opts) {
60
120
  const { review, evidence, signal } = opts;
@@ -64,15 +124,22 @@ async function reviewHandback(opts) {
64
124
  return undefined;
65
125
  if (!evidence.result && !evidence.steps && !evidence.edits && !evidence.partialText)
66
126
  return undefined;
127
+ const resultSample = evidence.result ? layeredReviewSample(evidence.result) : undefined;
128
+ const partialSample = evidence.partialText ? layeredReviewSample(evidence.partialText) : undefined;
129
+ const coverage = reviewCoverageNote([
130
+ ...(resultSample ? [{ field: "result", sample: resultSample }] : []),
131
+ ...(partialSample ? [{ field: "partialFindings", sample: partialSample }] : []),
132
+ ]);
67
133
  const verdict = await review.decider
68
134
  .decide({
69
135
  req: {
70
136
  toolName: opts.toolName,
71
137
  args: {
72
- result: evidence.result?.slice(0, 2_000),
138
+ result: resultSample?.text,
73
139
  toolSteps: evidence.steps,
74
140
  editedFiles: evidence.edits,
75
- ...(evidence.partialText ? { partialFindings: evidence.partialText.slice(0, 2_000) } : {}),
141
+ ...(partialSample ? { partialFindings: partialSample.text } : {}),
142
+ ...(coverage ? { reviewCoverage: coverage } : {}),
76
143
  },
77
144
  toolCallId: opts.toolCallId,
78
145
  },
@@ -174,7 +174,12 @@ export function createAskUserQuestionTool(onQuestion, source, opts) {
174
174
  description: "Ask the user a structured multiple-choice question when you are genuinely stuck or a decision is " +
175
175
  "the user's to make (which approach, which of these). Provide 1-4 questions, each with 2-4 options " +
176
176
  "(label + description). Use sparingly — not for anything you can determine yourself from the code or " +
177
- "the task. If no human is available you will be told to proceed with your best judgment.\n" +
177
+ "the task. " +
178
+ (onQuestion !== undefined && opts?.posture === "interactive" && opts.interactiveFallback !== true
179
+ ? "If no human can be reached this call FAILS with a coded error — do not plan around being told to " +
180
+ "proceed with your best judgment."
181
+ : "If no human is available you will be told to proceed with your best judgment.") +
182
+ "\n" +
178
183
  "\n" +
179
184
  "Usage notes:\n" +
180
185
  '- Users will always be able to select "Other" to provide custom text input\n' +
@@ -39,20 +39,10 @@ function encode(value, depth = 0) {
39
39
  out += encode(el, depth + 1);
40
40
  return `${out}]`;
41
41
  }
42
- const obj = value;
43
- const keys = Object.keys(obj).sort();
44
- let out = `o${keys.length}:{`;
45
- for (const k of keys) {
46
- let encoded;
47
- try {
48
- encoded = encode(obj[k], depth + 1);
49
- }
50
- catch {
51
- encoded = "<unreadable-property>";
52
- }
53
- out += `${Buffer.byteLength(k, "utf8")}:${k}=${encoded}`;
54
- }
55
- return `${out}}`;
42
+ const branded = encodeBranded(value, depth);
43
+ if (branded !== undefined)
44
+ return branded;
45
+ return encodeOwnKeys(value, depth);
56
46
  }
57
47
  catch {
58
48
  return "<unserializable>";
@@ -62,6 +52,178 @@ function encode(value, depth = 0) {
62
52
  return "U";
63
53
  }
64
54
  }
55
+ function encodeOwnKeys(source, depth, skipIndicesBelow = 0) {
56
+ const obj = source;
57
+ const all = Object.keys(obj);
58
+ const keys = (skipIndicesBelow > 0 ? all.filter((k) => !isIndexBelow(k, skipIndicesBelow)) : all).sort();
59
+ let out = `o${keys.length}:{`;
60
+ for (const k of keys) {
61
+ let encoded;
62
+ try {
63
+ encoded = encode(obj[k], depth + 1);
64
+ }
65
+ catch {
66
+ encoded = "<unreadable-property>";
67
+ }
68
+ out += `${Buffer.byteLength(k, "utf8")}:${k}=${encoded}`;
69
+ }
70
+ return `${out}}`;
71
+ }
72
+ function isIndexBelow(key, limit) {
73
+ const n = Number(key);
74
+ return Number.isInteger(n) && n >= 0 && n < limit && String(n) === key;
75
+ }
76
+ function brandOf(value) {
77
+ try {
78
+ const s = Object.prototype.toString.call(value);
79
+ return s.startsWith("[object ") && s.endsWith("]") ? s.slice(8, -1) : "Object";
80
+ }
81
+ catch {
82
+ return "Object";
83
+ }
84
+ }
85
+ function encodeBranded(value, depth) {
86
+ const brand = brandOf(value);
87
+ if (brand === "Object")
88
+ return undefined;
89
+ if (ArrayBuffer.isView(value)) {
90
+ try {
91
+ const { buffer, byteOffset, byteLength, elements, family } = viewRange(value);
92
+ const bytesToken = digestBytes(new Uint8Array(buffer, byteOffset, byteLength));
93
+ return `V:${family}:${brand}:${byteLength}:${bytesToken}${encodeOwnKeys(value, depth, elements)}`;
94
+ }
95
+ catch {
96
+ }
97
+ }
98
+ const own = encodeOwnKeys(value, depth);
99
+ try {
100
+ switch (brand) {
101
+ case "Date": {
102
+ const t = Date.prototype.getTime.call(value);
103
+ return `D:${Number.isNaN(t) ? "Invalid" : encodeNumber(t)}${own}`;
104
+ }
105
+ case "Map": {
106
+ const size = Number(mapSizeGetter.call(value));
107
+ let out = `m${size}:{`;
108
+ let seen = 0;
109
+ for (const [k, v] of mapEntries.call(value)) {
110
+ if (seen++ >= size)
111
+ break;
112
+ out += `${encode(k, depth + 1)}=${encode(v, depth + 1)}`;
113
+ }
114
+ return `${out}}${own}`;
115
+ }
116
+ case "Set": {
117
+ const size = Number(setSizeGetter.call(value));
118
+ let out = `t${size}:[`;
119
+ let seen = 0;
120
+ for (const v of setValues.call(value)) {
121
+ if (seen++ >= size)
122
+ break;
123
+ out += encode(v, depth + 1);
124
+ }
125
+ return `${out}]${own}`;
126
+ }
127
+ case "RegExp": {
128
+ const source = String(regexpSourceGetter.call(value));
129
+ const flags = String(regexpFlagsGetter.call(value));
130
+ return `r${Buffer.byteLength(source, "utf8")}:${source}/${flags}${own}`;
131
+ }
132
+ case "Error": {
133
+ const name = String(value.name);
134
+ const message = String(value.message);
135
+ return `E${Buffer.byteLength(name, "utf8")}:${name}:${Buffer.byteLength(message, "utf8")}:${message}${own}`;
136
+ }
137
+ case "URL": {
138
+ const href = String(urlHrefGetter.call(value));
139
+ return `L${Buffer.byteLength(href, "utf8")}:${href}${own}`;
140
+ }
141
+ case "Boolean":
142
+ case "Number":
143
+ case "String":
144
+ case "BigInt":
145
+ case "Symbol": {
146
+ const unboxed = boxedValueOf[brand].call(value);
147
+ return `B:${encode(unboxed, depth + 1)}${own}`;
148
+ }
149
+ case "ArrayBuffer":
150
+ case "SharedArrayBuffer": {
151
+ const byteLength = Number((brand === "ArrayBuffer" ? arrayBufferByteLengthGetter : sharedArrayBufferByteLengthGetter).call(value));
152
+ return `A:${brand}:${byteLength}:${digestBytes(new Uint8Array(value))}${own}`;
153
+ }
154
+ default:
155
+ break;
156
+ }
157
+ }
158
+ catch {
159
+ }
160
+ return `x${Buffer.byteLength(brand, "utf8")}:${brand}${own}`;
161
+ }
162
+ function primordialGetter(proto, key) {
163
+ const get = Object.getOwnPropertyDescriptor(proto, key)?.get;
164
+ if (!get) {
165
+ return () => {
166
+ throw new Error(`no primordial getter for ${key}`);
167
+ };
168
+ }
169
+ return get;
170
+ }
171
+ const mapEntries = Map.prototype.entries;
172
+ const setValues = Set.prototype.values;
173
+ const mapSizeGetter = primordialGetter(Map.prototype, "size");
174
+ const setSizeGetter = primordialGetter(Set.prototype, "size");
175
+ const regexpSourceGetter = primordialGetter(RegExp.prototype, "source");
176
+ const regexpFlagsGetter = primordialGetter(RegExp.prototype, "flags");
177
+ const urlHrefGetter = primordialGetter(URL.prototype, "href");
178
+ const arrayBufferByteLengthGetter = primordialGetter(ArrayBuffer.prototype, "byteLength");
179
+ const sharedArrayBufferByteLengthGetter = typeof SharedArrayBuffer !== "undefined"
180
+ ? primordialGetter(SharedArrayBuffer.prototype, "byteLength")
181
+ : primordialGetter({}, "byteLength");
182
+ const boxedValueOf = {
183
+ Boolean: Boolean.prototype.valueOf,
184
+ Number: Number.prototype.valueOf,
185
+ String: String.prototype.valueOf,
186
+ BigInt: BigInt.prototype.valueOf,
187
+ Symbol: Symbol.prototype.valueOf,
188
+ };
189
+ const typedArrayProto = Object.getPrototypeOf(Uint8Array.prototype);
190
+ const typedArrayLengthGetter = primordialGetter(typedArrayProto, "length");
191
+ const viewGetters = {
192
+ typed: {
193
+ buffer: primordialGetter(typedArrayProto, "buffer"),
194
+ byteOffset: primordialGetter(typedArrayProto, "byteOffset"),
195
+ byteLength: primordialGetter(typedArrayProto, "byteLength"),
196
+ },
197
+ dataView: {
198
+ buffer: primordialGetter(DataView.prototype, "buffer"),
199
+ byteOffset: primordialGetter(DataView.prototype, "byteOffset"),
200
+ byteLength: primordialGetter(DataView.prototype, "byteLength"),
201
+ },
202
+ };
203
+ function viewRange(view) {
204
+ for (const family of ["typed", "dataView"]) {
205
+ const g = viewGetters[family];
206
+ try {
207
+ const range = {
208
+ buffer: g.buffer.call(view),
209
+ byteOffset: Number(g.byteOffset.call(view)),
210
+ byteLength: Number(g.byteLength.call(view)),
211
+ elements: 0,
212
+ family,
213
+ };
214
+ if (family === "typed")
215
+ range.elements = Number(typedArrayLengthGetter.call(view));
216
+ return range;
217
+ }
218
+ catch {
219
+ continue;
220
+ }
221
+ }
222
+ throw new Error("not a readable ArrayBuffer view");
223
+ }
224
+ function digestBytes(bytes) {
225
+ return createHash("sha256").update(bytes).digest("hex");
226
+ }
65
227
  function encodeNumber(n) {
66
228
  if (Number.isNaN(n))
67
229
  return "NaN";
@@ -283,9 +283,22 @@ export declare class CheckpointError extends Error {
283
283
  } | undefined);
284
284
  }
285
285
  export type StoreDurability = "durable" | "process-local";
286
+ export type StoreFidelity = "structured-clone" | "json";
287
+ export declare function resolveDeclaredFidelity(store: {
288
+ readonly fidelity?: StoreFidelity;
289
+ } | undefined, storeName: string): StoreFidelity;
290
+ export declare function encodeAtFidelity(fidelity: StoreFidelity, value: unknown): {
291
+ ok: true;
292
+ value: unknown;
293
+ } | {
294
+ ok: false;
295
+ cause: unknown;
296
+ };
297
+ export declare function samePlainValue(a: unknown, b: unknown): boolean;
286
298
  export interface CheckpointStore {
287
299
  readonly retention?: import("./retention.js").RetentionDeclaration;
288
300
  readonly durability?: StoreDurability;
301
+ readonly fidelity?: StoreFidelity;
289
302
  put(token: CheckpointToken, cp: Checkpoint): Promise<void>;
290
303
  get(token: CheckpointToken): Promise<Checkpoint | null>;
291
304
  resolve(token: CheckpointToken, scope: string, outcome: ResumeOutcome, expect?: ResolveExpectation): Promise<boolean>;
@@ -315,6 +328,7 @@ export type CheckpointFaultMode = "resolve-after-commit" | "resolve-before-commi
315
328
  export declare class InMemoryCheckpointStore implements CheckpointStore {
316
329
  readonly retention: "none";
317
330
  readonly durability: "process-local";
331
+ readonly fidelity: "structured-clone";
318
332
  private cps;
319
333
  private fault;
320
334
  put(token: CheckpointToken, cp: Checkpoint): Promise<void>;
@@ -218,6 +218,78 @@ export class CheckpointError extends Error {
218
218
  this.name = "CheckpointError";
219
219
  }
220
220
  }
221
+ export function resolveDeclaredFidelity(store, storeName) {
222
+ const declared = store?.fidelity;
223
+ if (declared === undefined)
224
+ return "json";
225
+ if (declared === "structured-clone" || declared === "json")
226
+ return declared;
227
+ const e = new Error(`${storeName}.fidelity declares ${JSON.stringify(declared)} — not a recognized StoreFidelity ` +
228
+ `("structured-clone" | "json"). Fix the declaration; an unparseable fidelity cannot be folded to either arm.`);
229
+ e.code = "config.store_fidelity_invalid";
230
+ throw e;
231
+ }
232
+ export function encodeAtFidelity(fidelity, value) {
233
+ if (fidelity === "structured-clone")
234
+ return { ok: true, value };
235
+ try {
236
+ const encoded = JSON.stringify(value);
237
+ return { ok: true, value: encoded === undefined ? undefined : JSON.parse(encoded) };
238
+ }
239
+ catch (err) {
240
+ return { ok: false, cause: err };
241
+ }
242
+ }
243
+ const MAX_SAME_VALUE_DEPTH = 256;
244
+ export function samePlainValue(a, b) {
245
+ return sameValueAt(a, b, 0, new Map(), new Map());
246
+ }
247
+ function sameValueAt(a, b, depth, aToB, bToA) {
248
+ if (Object.is(a, b))
249
+ return true;
250
+ if (depth >= MAX_SAME_VALUE_DEPTH)
251
+ return false;
252
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object")
253
+ return false;
254
+ try {
255
+ const pairedWithA = aToB.get(a);
256
+ const pairedWithB = bToA.get(b);
257
+ if (pairedWithA !== undefined || pairedWithB !== undefined)
258
+ return pairedWithA === b && pairedWithB === a;
259
+ aToB.set(a, b);
260
+ bToA.set(b, a);
261
+ const proto = Object.getPrototypeOf(a);
262
+ if (proto !== Object.getPrototypeOf(b))
263
+ return false;
264
+ if (proto !== Object.prototype && proto !== Array.prototype)
265
+ return false;
266
+ if (Object.isExtensible(a) !== Object.isExtensible(b))
267
+ return false;
268
+ const ka = Reflect.ownKeys(a);
269
+ const kb = Reflect.ownKeys(b);
270
+ if (ka.length !== kb.length)
271
+ return false;
272
+ for (let i = 0; i < ka.length; i++) {
273
+ const k = ka[i];
274
+ if (k !== kb[i])
275
+ return false;
276
+ const da = Object.getOwnPropertyDescriptor(a, k);
277
+ const db = Object.getOwnPropertyDescriptor(b, k);
278
+ if (da === undefined || db === undefined)
279
+ return false;
280
+ if (!("value" in da) || !("value" in db))
281
+ return false;
282
+ if (da.enumerable !== db.enumerable || da.writable !== db.writable || da.configurable !== db.configurable)
283
+ return false;
284
+ if (!sameValueAt(da.value, db.value, depth + 1, aToB, bToA))
285
+ return false;
286
+ }
287
+ return true;
288
+ }
289
+ catch {
290
+ return false;
291
+ }
292
+ }
221
293
  export function resolveCheckpointStore(spec, deps) {
222
294
  if (spec.checkpointStore === null)
223
295
  return undefined;
@@ -385,6 +457,7 @@ export function checkpointOccMatches(cp, expect) {
385
457
  export class InMemoryCheckpointStore {
386
458
  retention = "none";
387
459
  durability = "process-local";
460
+ fidelity = "structured-clone";
388
461
  cps = new Map();
389
462
  fault = null;
390
463
  async put(token, cp) {
@@ -123,6 +123,9 @@ export type ContentAskOutcome = {
123
123
  code: string;
124
124
  presentedInput?: unknown;
125
125
  };
126
+ export interface ParkAttemptFailed {
127
+ parkFailed: string;
128
+ }
126
129
  export interface ToolGateInput {
127
130
  onNotifyError?: (failure: import("./safe-notify.js").SafeNotifyFailure) => void;
128
131
  event: {
@@ -134,7 +137,7 @@ export interface ToolGateInput {
134
137
  hookEnv?: HookEnvCapabilities;
135
138
  adjudicate?: (req: ToolCallRequest) => Promise<PermissionResult>;
136
139
  resolveAsk: (decision: PermissionResult, req: ToolCallRequest) => Promise<ResolvedAsk>;
137
- suspendAsk?: (req: ToolCallRequest, postHookArgs: unknown, safety?: import("./checkpoint-store.js").SafetyAxis, liveFaceUnavailable?: boolean) => Promise<ToolGateResult["suspend"] | undefined>;
140
+ suspendAsk?: (req: ToolCallRequest, postHookArgs: unknown, safety?: import("./checkpoint-store.js").SafetyAxis, liveFaceUnavailable?: boolean) => Promise<ToolGateResult["suspend"] | ParkAttemptFailed | undefined>;
138
141
  resolveContentAsk?: (req: ToolCallRequest) => Promise<ContentAskOutcome>;
139
142
  egress?: boolean;
140
143
  irreversibility?: "never" | "maybe" | "always";
@@ -78,6 +78,14 @@ export function createHookEnvCapabilities(env) {
78
78
  export function formatHookFeedback(text) {
79
79
  return `<system-reminder>\n${text}\n</system-reminder>`;
80
80
  }
81
+ const PARK_FAILURE_CAUSE_MAX = 600;
82
+ function withParkFailureCause(reason, parkFailed) {
83
+ if (parkFailed === undefined)
84
+ return reason;
85
+ return (`${reason} — note: a durable approval park was attempted for this call FIRST and could not be minted ` +
86
+ `(${inlineUntrusted(parkFailed, PARK_FAILURE_CAUSE_MAX)}), so the refusal above is what the fallback had ` +
87
+ `left to say, not the reason the call stopped.`);
88
+ }
81
89
  function preToolUseCrashReason(subject, err) {
82
90
  const raw = err instanceof Error ? err.message.trim() || err.name : String(err);
83
91
  const cause = inlineUntrusted(raw, 200);
@@ -126,6 +134,7 @@ export async function runToolGate(input) {
126
134
  let currentInput = event.input;
127
135
  const preToolContext = [];
128
136
  let hookAsk;
137
+ let parkFailed;
129
138
  const notifier = createSafeNotifier(input.onNotifyError !== undefined ? { onError: input.onNotifyError } : undefined);
130
139
  if (preToolUse) {
131
140
  let r;
@@ -241,7 +250,10 @@ export async function runToolGate(input) {
241
250
  if (suspendAsk && decision.action === "ask") {
242
251
  const suspended = await suspendAsk(req, currentInput, safety);
243
252
  if (suspended) {
244
- return { suspend: suspended, preToolContext };
253
+ if ("parkFailed" in suspended)
254
+ parkFailed = suspended.parkFailed;
255
+ else
256
+ return { suspend: suspended, preToolContext };
245
257
  }
246
258
  }
247
259
  if (decision.action === "ask" && req.toolName === ASK_USER_QUESTION_TOOL_NAME) {
@@ -254,10 +266,13 @@ export async function runToolGate(input) {
254
266
  currentInput = outcome.presentedInput;
255
267
  req.args = outcome.presentedInput;
256
268
  }
257
- if (suspendAsk && outcome.parkDeclined) {
269
+ if (suspendAsk && outcome.parkDeclined && parkFailed === undefined) {
258
270
  const suspended = await suspendAsk(req, currentInput, safety, true);
259
271
  if (suspended) {
260
- return { suspend: suspended, preToolContext };
272
+ if ("parkFailed" in suspended)
273
+ parkFailed = suspended.parkFailed;
274
+ else
275
+ return { suspend: suspended, preToolContext };
261
276
  }
262
277
  }
263
278
  decision = {
@@ -278,10 +293,13 @@ export async function runToolGate(input) {
278
293
  if (decision.action === "ask") {
279
294
  const resolved = await resolveAsk(decision, req);
280
295
  decision = resolved;
281
- if (resolved.action === "deny" && resolved.approverUnavailable === true && suspendAsk) {
296
+ if (resolved.action === "deny" && resolved.approverUnavailable === true && suspendAsk && parkFailed === undefined) {
282
297
  const suspended = await suspendAsk(req, currentInput, safety, true);
283
298
  if (suspended) {
284
- return { suspend: suspended, preToolContext };
299
+ if ("parkFailed" in suspended)
300
+ parkFailed = suspended.parkFailed;
301
+ else
302
+ return { suspend: suspended, preToolContext };
285
303
  }
286
304
  }
287
305
  if (decision.action === "allow" && decision.updatedInput !== undefined) {
@@ -369,7 +387,7 @@ export async function runToolGate(input) {
369
387
  }
370
388
  }
371
389
  if (decision.action === "deny") {
372
- const denyReason = decisionText(decision) ?? `tool "${toolName}" denied by policy`;
390
+ const denyReason = withParkFailureCause(decisionText(decision) ?? `tool "${toolName}" denied by policy`, parkFailed);
373
391
  if (decision.updatedInput !== undefined) {
374
392
  currentInput = decision.updatedInput;
375
393
  }
@@ -60,6 +60,7 @@ export interface McpServerStatus {
60
60
  export declare const MCP_PREFIX: "mcp__";
61
61
  export declare function resolveMcpDeclaredResultSize(meta: Record<string, unknown> | undefined): number | undefined;
62
62
  export declare function gateMcpOutput(content: Array<TextContent | ImageContent>, limitTokens?: number): Array<TextContent | ImageContent>;
63
+ export declare function structuredContentErrorLine(structuredContent: unknown, collectedText: string): string | undefined;
63
64
  export declare function truncateMcpErrorText(s: string): string;
64
65
  export declare const MCP_TOOL_TIMEOUT_DEFAULT_MS = 100000000;
65
66
  export declare function mcpToolTimeoutMs(): number;