@sema-agent/core 5.54.0 → 5.55.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 (58) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/agents/cumulative-stats.d.ts +26 -0
  3. package/dist/agents/cumulative-stats.js +56 -0
  4. package/dist/agents/observer.d.ts +11 -7
  5. package/dist/agents/observer.js +2 -4
  6. package/dist/agents/verify.d.ts +27 -3
  7. package/dist/agents/verify.js +7 -2
  8. package/dist/core/governance-codes.js +14 -0
  9. package/dist/core/hooks.js +1 -1
  10. package/dist/core/lsp-diagnostics.d.ts +19 -17
  11. package/dist/core/lsp-diagnostics.js +11 -5
  12. package/dist/core/mcp.d.ts +46 -0
  13. package/dist/core/mcp.js +132 -6
  14. package/dist/core/memory-engine/consolidation.d.ts +378 -0
  15. package/dist/core/memory-engine/consolidation.js +342 -0
  16. package/dist/core/memory-engine/dual-root.js +3 -0
  17. package/dist/core/memory-engine/engine.d.ts +237 -4
  18. package/dist/core/memory-engine/engine.js +1111 -4
  19. package/dist/core/memory-engine/export-bundle.js +9 -0
  20. package/dist/core/memory-engine/file-backend.js +27 -1
  21. package/dist/core/memory-engine/frontmatter.d.ts +20 -1
  22. package/dist/core/memory-engine/frontmatter.js +111 -0
  23. package/dist/core/memory-engine/index.d.ts +4 -2
  24. package/dist/core/memory-engine/index.js +3 -1
  25. package/dist/core/memory-engine/memory-backend-contract.js +131 -0
  26. package/dist/core/memory-engine/sync-client.js +26 -0
  27. package/dist/core/memory-engine/tools.d.ts +9 -0
  28. package/dist/core/memory-engine/tools.js +57 -13
  29. package/dist/core/memory-engine/types.d.ts +99 -0
  30. package/dist/core/memory-recall.js +4 -3
  31. package/dist/core/memory.d.ts +33 -3
  32. package/dist/core/memory.js +6 -4
  33. package/dist/core/permission-rules.d.ts +22 -0
  34. package/dist/core/permission-rules.js +60 -6
  35. package/dist/core/reminder-disclosure.d.ts +29 -4
  36. package/dist/core/reminder-disclosure.js +60 -12
  37. package/dist/core/runner/prepare-memory.js +7 -2
  38. package/dist/core/runner/prepare-task.d.ts +31 -1
  39. package/dist/core/runner/prepare-task.js +31 -14
  40. package/dist/core/runner/runtask.d.ts +8 -1
  41. package/dist/core/runner/runtask.js +12 -10
  42. package/dist/core/runner/session-rule-policy.js +5 -3
  43. package/dist/core/runner/synthetic-tools.js +4 -2
  44. package/dist/core/runner/turn-attachments.d.ts +16 -6
  45. package/dist/core/runner/turn-attachments.js +34 -20
  46. package/dist/core/tool-policy.d.ts +18 -0
  47. package/dist/core/tool-policy.js +19 -8
  48. package/dist/core/types.d.ts +89 -6
  49. package/dist/core/untrusted-egress.js +12 -2
  50. package/dist/core/untrusted-text.d.ts +189 -3
  51. package/dist/core/untrusted-text.js +416 -6
  52. package/dist/engine/loop/types.d.ts +7 -0
  53. package/dist/index.d.ts +1 -1
  54. package/dist/index.js +1 -1
  55. package/dist/prompts/default.d.ts +12 -2
  56. package/dist/tools/fs/index.d.ts +3 -1
  57. package/package.json +1 -1
  58. package/test/export-surface.snapshot.json +28 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,99 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.55.0 — 2026-08-23
4
+
5
+ ### BREAKING
6
+ - **MemoryBackend conformance suite tightens 34 → 40 clauses** (design/339 B-1): c35-c40 cover the
7
+ distilled carrier's immutability seat, the supersession read face, and plan storage. A third-party
8
+ backend green at 34 can be red at 40 — **upgrade the backend BEFORE the engine** (the suite is the
9
+ executable contract; run `memoryBackendContract` against your backend first).
10
+ - **New power face** (design/339 B-5): the engine can now restructure memory — but ONLY through
11
+ two-phase intents under a per-scope seat (all-satisfied settlement; conflict parks; delete is a
12
+ terminal-state judgment; `resolveConsolidationPlan(planId, "retry" | "discard")` is the host valve).
13
+ A deployment that assumed the engine never writes distilled products must review its mounts.
14
+ - **memory_search/get face rev 4 → 5** (design/339 §3.4): SUPERSEDED entries leave the default
15
+ result face and the MEMORY.md mechanical index (data-driven, including pre-existing stores whose
16
+ entries acquire edges); they return under `includeSuperseded` and `memory_get` renders a
17
+ supersession banner. `CleanMemorySearchHit.supersededBy` / handle-shape twin are the new seats.
18
+
19
+ ### Added
20
+ - **design/339 consolidation write protocol**: typed distilled carrier (planId/at/carrierRev
21
+ self-anchor outside the rev tuple; inputs with `superseded: true` edges), pure active-set
22
+ derivation (input-rev + carrier-rev anchors, challenge/latch suspension, chain rule, cross-scope
23
+ edges inert), four verbs (snapshot/freeze/apply/reconcile+resolve), gate-store minter accounting,
24
+ construction-time refusals, multiNode bit. New notice codes: `memory.consolidation_recommended` /
25
+ `_committed` / `_conflict` / `_refused` (+ the `memory.consolidation_*` refusal/incident family).
26
+ - **`tool_end.gatedCallId`** (top-level, optional): a park-contamination frame names the toolCallId
27
+ the committed gate holds, minted engine-side (not forgeable by tools); absent when the park holds
28
+ no call (plan review / resource slice). Interrupt/timeout aborts are byte-unchanged. **Wire
29
+ erratum**: an earlier board announcement placed this under `details` — `tool_end` frames carry no
30
+ `details`; the key is top-level.
31
+ - **Authority-envelope anti-forgery** (#391): all five harness-minted envelopes
32
+ (system-reminder / task-notification / new-diagnostics / user_memory / scope) are census-registered
33
+ (single source) and sink-neutralized by source; verbatim lanes stay byte-frozen with a positional
34
+ judgment bit. New disclosure counters `<outlet>.envelope` / `<outlet>.envelope_throttled` in
35
+ `TaskResult.stats.mechanisms.reminderDisclosures`; the trailer grows an envelope-family sentence.
36
+ - **MCP covering rules + resources-only mount** (#387/#386): `mcpRuleNameCovers` (CC's three
37
+ covering shapes) consulted in all five name-keyed lanes; resources-capable tools-less servers
38
+ mount; an out-of-charset top-level parameter name mounts WITH an operator advisory (connect leg).
39
+ - **openclaw benchmark rig** (design/184 ticket 2): HTTP-boundary comparison harness for the v3
40
+ auto-consolidation acceptance obligation (baseline bracket measured; head-to-head numbers pending
41
+ the openclaw build).
42
+
43
+ ### Fixed
44
+ - **Verification loops report the CUMULATIVE spend** (#406, test P0-KPI): runWithVerification /
45
+ resumeWithVerification returned only the last leg's stats (a $1.70 two-round run disclosed $0.70).
46
+ Every leg now folds through `buildCumulativeStats`; early-exit paths return the
47
+ accumulated-so-far account; the RB-368 unpriced axis is preserved.
48
+ - **The persisted-rule seat is time-bounded** (#415, test P0-KPI): `persistedRules.admits` was
49
+ awaited bare — a hanging provider wedged the approval gate forever. The wait now has the org
50
+ sibling's bound (15s + abort); timeout reads as could-not-answer (ask + unavailable), never a
51
+ clean no-rule.
52
+ - **MCP model-facing schemas keep validation-bearing values byte-faithful** (pre-release rescan,
53
+ fixed before publish): the intake neutralizer introduced in this window NFKC-folded enum/const/
54
+ default/examples/pattern values, so the model was shown spellings the server refuses. The schema
55
+ walk now carries a seat: prose (`description`/`title`/`$comment`) and parameter-name keys are
56
+ neutralized; validation-bearing values, unknown/vendor subtrees, and patternProperties KEYS
57
+ (regexes) transit byte-verbatim; `required`/`$ref`/`$defs`/anchor families move in lockstep with
58
+ rewritten names. **Flipped pin**: an advertised ZWSP-bearing enum member is now shown verbatim
59
+ (wire correctness over card-face literal defusing; prose/name lanes still guard spoofing).
60
+ - **Fence truncation is self-reported** (pre-release rescan): `fenceMcpServerInstructions`
61
+ pre-measured with the narrow tag set while the fence clipped the widened form — a server placing
62
+ instructions in the (cap−N, cap] window got a silent cut with no truncation note, on both the
63
+ stable and delta lanes. `delimitUntrustedWithClip` now reports its own clip; the RB-152 egress
64
+ `…[+N chars]` count stays anchored to the true source omission.
65
+ - **The consolidation scope wall holds on the read side** (pre-release rescan): a carrier in scope A
66
+ could suppress a target in scope B on every read face (edge validity never compared scopes; a
67
+ supported cross-scope MOVE keeps the rev byte-identical). Cross-scope edges are now inert —
68
+ existing stores: any cross-scope-suppressed entry RETURNS to the default face (revival direction).
69
+ - **The rewrite terminal carries provenance/trust**: dropping them minted the whitewash patch every
70
+ conforming backend must refuse (the plan parked forever holding the scope seat); a trust-only
71
+ entry was instead silently whitewashed on stores keying the guard on provenance.kind. Both fields
72
+ now carry forward beside origin/distilled (this also corrects plannedPostRev).
73
+ - **The CONFLICT notice stops overstating** (server/cli pins on the old string will red): it names
74
+ all three park causes (concurrent write / store-declined patch / re-judgment) and scopes the
75
+ no-overwrite claim to REFUSED targets (applied siblings stand; discard rolls nothing back);
76
+ retry-synthesized parks now write their own per-target audit rows.
77
+ - **memory_get pages the displayed string**: budgets/cursors previously addressed the raw body while
78
+ the fence displayed a longer neutralized one; the body is now neutralized once into a canonical
79
+ line array every page and cursor reads. Envelope-tag-bearing bodies page differently (fewer lines
80
+ per page, never over budget); marker-free bodies are byte-identical.
81
+ - **The third memory composer joins the widened tag family** (#391 class sweep):
82
+ `composeSelectiveBody` hard-coded `["user_memory"]` in its three sanitize seats while its two
83
+ siblings were widened to `MEMORY_INJECTION_TAGS`.
84
+ - **Contract texts catch up with minted disclosure keys**: `envelope`/`envelope_throttled`/
85
+ `mark_echo` join the `reminderDisclosures` JSDoc and docs/REMINDER-PROVENANCE.md (ratchet-pinned:
86
+ minted set ⊆ both texts; DISCLOSED tag set must be named in both).
87
+
88
+ ### Notes
89
+ - Conformance fixture rename (`id-legacy-0001` → `id-plain-0001`); 14 `memory.consolidation_*`
90
+ codes registered non-governance (mechanism receipts, not per-principal verdicts); type-hygiene
91
+ memory-engine checked-cast baseline 14 → 23 (each behind a runtime validator added in the same
92
+ commits).
93
+ - Residuals filed, not shipped silently: #414① (refresh-leg advisory drop), #416 (loosenReasons
94
+ covering-awareness — a published-corpus change, scheduled with the names window), patternProperties
95
+ `$ref` pointer form (disclosed at the seat table).
96
+
3
97
  ## 5.54.0 — 2026-08-22
4
98
 
5
99
  ### Fixed
@@ -83,3 +83,29 @@ export interface CumulativeStatsAccumulator {
83
83
  * base: a ratio is not additive, so the last leg's rate is never the operation's rate.
84
84
  */
85
85
  export declare function buildCumulativeStats(base: TaskResult["stats"], acc: CumulativeStatsAccumulator): TaskResult["stats"];
86
+ /** A running {@link CumulativeStatsAccumulator} with the per-leg fold built in. */
87
+ export interface CumulativeStatsTracker {
88
+ /** Fold ONE leg's stats into the running totals. Call once per leg, in dispatch order. */
89
+ add(s: TaskResult["stats"]): void;
90
+ /** Build the cumulative stats over `base` — conventionally the FINAL leg's stats (it carries the
91
+ * non-accumulated fields such as `model`). */
92
+ build(base: TaskResult["stats"]): TaskResult["stats"];
93
+ /** How many legs have been folded in. A composition that ran exactly ONE leg can use this to hand that
94
+ * leg's own stats back untouched: the cumulative account over one leg IS that leg, and re-deriving it
95
+ * would rewrite a caller's object (0 → `undefined` normalization, a re-derived `cacheHitRate`) for no
96
+ * gain. */
97
+ readonly legs: number;
98
+ }
99
+ /**
100
+ * Create a {@link CumulativeStatsTracker}.
101
+ *
102
+ * {@link buildCumulativeStats} made the OUTPUT literal single-source, but the INPUT fold stayed copied:
103
+ * cascade / repair-loop / teacher each hand-maintain the same dozen `+=` lines, so a new stats field still
104
+ * has to be remembered in four places — the same duplication one level down from the hole this module
105
+ * exists to close. New call sites take this factory instead. Migrating the three originals onto it is a
106
+ * separate change (their folds are equivalent to this one today, but rewriting three shipped compositions
107
+ * is not part of a cost-accounting bug fix).
108
+ *
109
+ * OWN and NESTED stay DISJOINT here too: `add` never folds a leg's `nested.*` into the own totals.
110
+ */
111
+ export declare function createCumulativeStatsTracker(): CumulativeStatsTracker;
@@ -27,3 +27,59 @@ export function buildCumulativeStats(base, acc) {
27
27
  : undefined,
28
28
  };
29
29
  }
30
+ export function createCumulativeStatsTracker() {
31
+ let legs = 0;
32
+ const acc = {
33
+ tokens: 0,
34
+ turns: 0,
35
+ promptTokens: 0,
36
+ totalInputTokens: 0,
37
+ cachedTokens: 0,
38
+ outputTokens: 0,
39
+ cacheWriteTokens: 0,
40
+ cacheWriteTokensLong: 0,
41
+ costMicroUsd: 0,
42
+ anyLegUnpriced: false,
43
+ llmRootMicroUsd: 0,
44
+ compactionMicroUsd: 0,
45
+ nested: { tokens: 0, turns: 0, tasks: 0, costMicroUsd: 0 },
46
+ humanReview: { count: 0, totalWaitMs: 0, gates: [] },
47
+ };
48
+ return {
49
+ get legs() {
50
+ return legs;
51
+ },
52
+ add(s) {
53
+ legs += 1;
54
+ acc.tokens += s.tokens;
55
+ acc.turns += s.turns;
56
+ acc.promptTokens += s.promptTokens ?? 0;
57
+ acc.totalInputTokens += s.totalInputTokens ?? 0;
58
+ acc.cachedTokens += s.cachedTokens ?? 0;
59
+ acc.outputTokens += s.outputTokens ?? 0;
60
+ acc.cacheWriteTokens += s.cacheWriteTokens ?? 0;
61
+ acc.cacheWriteTokensLong += s.cacheWriteTokensLong ?? 0;
62
+ acc.costMicroUsd = (acc.costMicroUsd ?? 0) + (s.costMicroUsd ?? 0);
63
+ if (s.costMicroUsd === undefined)
64
+ acc.anyLegUnpriced = true;
65
+ if (s.costBreakdown) {
66
+ acc.llmRootMicroUsd += s.costBreakdown.llmRootMicroUsd;
67
+ acc.compactionMicroUsd += s.costBreakdown.compactionMicroUsd;
68
+ }
69
+ if (s.humanReview) {
70
+ acc.humanReview.count += s.humanReview.count;
71
+ acc.humanReview.totalWaitMs += s.humanReview.totalWaitMs;
72
+ acc.humanReview.gates.push(...s.humanReview.gates);
73
+ }
74
+ if (s.nested) {
75
+ acc.nested.tokens += s.nested.tokens;
76
+ acc.nested.turns += s.nested.turns;
77
+ acc.nested.tasks += s.nested.tasks;
78
+ acc.nested.costMicroUsd += s.nested.costMicroUsd ?? 0;
79
+ }
80
+ },
81
+ build(base) {
82
+ return buildCumulativeStats(base, acc);
83
+ },
84
+ };
85
+ }
@@ -17,6 +17,7 @@
17
17
  import { Type } from "typebox";
18
18
  import type { AgentDefinition, TaskEvent } from "../core/types.js";
19
19
  import { defineTool } from "../core/tools.js";
20
+ import { escapeEnvelopeTag } from "../core/untrusted-text.js";
20
21
  /** The four tagged digest event kinds (CC `Tcg`) — `assistant_text` renders untagged. */
21
22
  export declare const OBSERVER_EVENT_TAGS: readonly ["tool-call", "user-message", "tool-result", "turn-ended"];
22
23
  export type ObserverDigestEvent = {
@@ -39,14 +40,17 @@ export type ObserverDigestEvent = {
39
40
  /** CC `S$r` — anti-injection: a caller-declared envelope tag inside observed content is defused in place. */
40
41
  export declare function escapeObserverTags(text: string): string;
41
42
  /**
42
- * CC `zZe` @8339092 — verbatim: `t.replace(new RegExp(`<(?=/?${e}(?:[>\\s/]|$))`, "gi"), "<\\")`.
43
- * Defuses a caller-declared occurrence of ONE specific tag (opening AND closing) inside content the second
44
- * escape layer CC applies to the DYNAMIC envelope tag (`<{slug}-activity>`) after joining, which the
45
- * static four-tag `escapeObserverTags` (Scg layer) cannot cover. Deviation from CC (hardening): the
46
- * tag is regex-escaped before entering the RegExp CC interpolates raw, safe only because its slugs
47
- * are `[a-zA-Z0-9_-]`; ours are too, but we don't rely on the caller for that invariant.
43
+ * CC `zZe` @8339092 — the named-envelope tag escape. It is the second escape layer CC applies to the
44
+ * DYNAMIC envelope tag (`<{slug}-activity>`) after joining, which the static four-tag
45
+ * {@link escapeObserverTags} (Scg layer) cannot cover.
46
+ *
47
+ * RE-HOMED to `core/untrusted-text.ts` beside the ENGINE_ENVELOPES registry (whose guard column
48
+ * points at it): "escape one named envelope tag" is a containment primitive, not an observer
49
+ * concern, and having it live under `agents/` is what let two later envelopes (`skills`, `scope`)
50
+ * ship with no guard — nothing in the containment module named it. Re-exported here so this
51
+ * module's own call sites and `send-message-tool.ts` are unchanged.
48
52
  */
49
- export declare function escapeEnvelopeTag(tag: string, text: string): string;
53
+ export { escapeEnvelopeTag };
50
54
  /** CC `Yp` @8338930 — attribute-value escape: `oc` entities (& < >) plus quote/apostrophe. */
51
55
  export declare function escapeAttributeValue(value: string): string;
52
56
  /** CC `Eqi` = 2000 — the per-payload digest truncation bound (tool inputs/results, user messages). */
@@ -1,15 +1,13 @@
1
1
  import { Type } from "typebox";
2
2
  import { defineTool } from "../core/tools.js";
3
3
  import { createSafeNotifier } from "../core/safe-notify.js";
4
+ import { escapeEnvelopeTag } from "../core/untrusted-text.js";
4
5
  export const OBSERVER_EVENT_TAGS = ["tool-call", "user-message", "tool-result", "turn-ended"];
5
6
  const TAG_ESCAPE_RE = new RegExp(`<(?=/?(?:${OBSERVER_EVENT_TAGS.join("|")})(?:[>\\s/]|$))`, "gi");
6
7
  export function escapeObserverTags(text) {
7
8
  return text.replace(TAG_ESCAPE_RE, "<\\");
8
9
  }
9
- export function escapeEnvelopeTag(tag, text) {
10
- const escapedTag = tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11
- return text.replace(new RegExp(`<(?=/?${escapedTag}(?:[>\\s/]|$))`, "gi"), "<\\");
12
- }
10
+ export { escapeEnvelopeTag };
13
11
  export function escapeAttributeValue(value) {
14
12
  return value
15
13
  .replaceAll("&", "&amp;")
@@ -172,9 +172,20 @@ export interface VerificationOutcome {
172
172
  * Total cost (micro-USD) of the VERIFIER run(s) across all rounds — the verification OVERHEAD, separate from
173
173
  * the implementation's own cost (which is the returned `TaskResult.stats`, as the verifier runs in its own
174
174
  * session). Mirrors `runWithTeacher`'s `teacherStats` work-vs-overhead split. Σ of each verifier run's
175
- * cost+nested. `result.stats.costMicroUsd + verification.verifierCost` is the EXACT operation total for the
176
- * common single-pass case; in the rarer multi-round fix case `result.stats` is the FINAL impl attempt's cost
177
- * (the returned `...current`) so an earlier failed attempt's impl cost is not separately surfaced. Omitted
175
+ * cost+nested.
176
+ *
177
+ * THE OPERATION TOTAL, exactly (note the middle term it is easy to miss and this doc used to omit it):
178
+ * `stats.costMicroUsd + (stats.nested?.costMicroUsd ?? 0) + verification.verifierCost`. The axes are
179
+ * DISJOINT, so nothing is counted twice — but they are not symmetric, which is why the nested term is
180
+ * spelled out: THIS field already folds the verifier's own delegated spend into itself, while the
181
+ * implementation side keeps own and nested apart (the family convention — a consumer adds `stats.X +
182
+ * stats.nested.X` for the true total, and nested cost is never folded into `costMicroUsd`).
183
+ *
184
+ * That total is now exact **in the multi-round fix case too**, since the returned `stats` accumulates
185
+ * every impl-side leg (see {@link VerificationResult}). It held only for the single-pass case before: the
186
+ * returned stats were the FINAL impl attempt's alone, so once a fix round ran, the ORIGINAL attempt's
187
+ * cost was absent from the entire return value and any such sum silently undercounted the operation by
188
+ * exactly that much. Omitted
178
189
  * (undefined) when no verifier ran (e.g. an impl that suspended/was opted out before verification).
179
190
  */
180
191
  verifierCost?: number;
@@ -201,6 +212,19 @@ export interface VerificationOutcome {
201
212
  export interface VerificationResult extends TaskResult {
202
213
  /** The verification outcome. The task `result`/`status` is the implementation's; consult `verdict` for quality. */
203
214
  verification: VerificationOutcome;
215
+ /**
216
+ * ACCOUNTING NOTE for the inherited `stats` (it is the ONE inherited field the gate does not simply pass
217
+ * through). The gate can run the implementation session SEVERAL times — the impl leg, then a continuation
218
+ * per fix round — and each leg's own `stats` cover only that leg. So `stats` here is the CUMULATIVE
219
+ * impl-side account (every impl + fix leg summed, own and nested kept disjoint as everywhere else), not
220
+ * the final leg's; the non-accumulated fields such as `model` come from the final leg. When exactly one
221
+ * leg contributed (the common no-fix path, and every early hand-back before a fix round), it is that
222
+ * leg's own object, untouched.
223
+ *
224
+ * The VERIFIER's spend is NOT in here — it is the separate overhead axis on
225
+ * {@link VerificationOutcome.verifierCost}, which documents the sum that gives the operation total.
226
+ */
227
+ stats: TaskResult["stats"];
204
228
  }
205
229
  /**
206
230
  * Verify an already-**completed** implementation `result` behind the independent falsification-style verifier,
@@ -4,6 +4,7 @@ import { releaseSession } from "./session-util.js";
4
4
  import { mapNestedSuspend, isDurablePause } from "./suspend-guard.js";
5
5
  import { delimitUntrusted, sanitizeUntrustedText } from "../core/untrusted-text.js";
6
6
  import { createSafeNotifier } from "../core/safe-notify.js";
7
+ import { createCumulativeStatsTracker } from "./cumulative-stats.js";
7
8
  export const VERIFICATION_PROMPT = `You are a verification specialist. Your job is NOT to confirm the implementation works — it is to try to BREAK it.
8
9
 
9
10
  You have two documented failure patterns. First, verification avoidance: faced with a check, you find reasons not to run it — you read code, narrate what you would test, declare "PASS," and move on. Second, being seduced by the first 80%: a polished result or a passing test suite makes you inclined to pass it, not noticing the edge that crashes, the state that vanishes, the bad input that is unhandled. The first 80% is the easy part. Your entire value is in finding the last 20%.
@@ -172,6 +173,9 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
172
173
  }
173
174
  };
174
175
  let current = result;
176
+ const implAccount = createCumulativeStatsTracker();
177
+ implAccount.add(result.stats);
178
+ const withImplAccount = (r) => implAccount.legs > 1 ? { ...r, stats: implAccount.build(r.stats) } : r;
175
179
  let outcome = { verdict: "unverified", rounds: 0, findings: [] };
176
180
  const startedAt = Date.now();
177
181
  let spend = 0;
@@ -240,13 +244,14 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
240
244
  ...(foldedReadDeny.length > 0 ? { readDenyPatterns: [...foldedReadDeny] } : {}),
241
245
  }, internals);
242
246
  spend += (current.stats.costMicroUsd ?? 0) + (current.stats.nested?.costMicroUsd ?? 0);
247
+ implAccount.add(current.stats);
243
248
  if (isDurablePause(current.status)) {
244
- return { ...mapNestedSuspend(carryFrozenPosture(current)), verification: outcome };
249
+ return { ...mapNestedSuspend(carryFrozenPosture(withImplAccount(current))), verification: outcome };
245
250
  }
246
251
  if (current.status !== "completed")
247
252
  break;
248
253
  }
249
- return { ...carryFrozenPosture(current), verification: outcome };
254
+ return { ...carryFrozenPosture(withImplAccount(current)), verification: outcome };
250
255
  }
251
256
  export async function runWithVerification(runner, implSpec, config = {}, internals) {
252
257
  refuseUnhonorableInternals(internals, "door");
@@ -20,6 +20,20 @@ export const NON_GOVERNANCE_MEMORY_CODES = new Set([
20
20
  "memory.challenge_sweep_failed",
21
21
  "memory.lineage_settle_failed",
22
22
  "memory.challenge_ledger_corrupt",
23
+ "memory.consolidation_recommended",
24
+ "memory.consolidation_committed",
25
+ "memory.consolidation_conflict",
26
+ "memory.consolidation_refused",
27
+ "memory.consolidation_disabled",
28
+ "memory.consolidation_gated",
29
+ "memory.consolidation_busy",
30
+ "memory.consolidation_stale_snapshot",
31
+ "memory.consolidation_gate_corrupt",
32
+ "memory.consolidation_governance_unreadable",
33
+ "memory.consolidation_lease_unavailable",
34
+ "memory.consolidation_backend_failed",
35
+ "memory.consolidation_gate_failed",
36
+ "memory.active_set_collapse",
23
37
  "memory.challenge_ledger_oversize",
24
38
  "memory.control_plane_rebuilt",
25
39
  "memory.control_plane_not_corrupt",
@@ -548,7 +548,7 @@ export async function runToolGate(input) {
548
548
  catch {
549
549
  return { unreadable: true };
550
550
  }
551
- return await pendingHit.then(normalizePersistedRuleHit).catch(() => ({ unreadable: true }));
551
+ return await settleOrgVerdictWithin(pendingHit.then(normalizePersistedRuleHit).catch(() => ({ unreadable: true })), { unreadable: true }, { ...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}), timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
552
552
  })();
553
553
  const hitEntry = answer.hit;
554
554
  const hit = hitEntry?.rule;
@@ -1,19 +1,3 @@
1
- /**
2
- * design/121 — LSP diagnostics registry + model-facing formatting (CC 2.1.198 parity).
3
- *
4
- * CC's two diagnostics sources (IDE MCP baseline/diff + passive LSP publishDiagnostics) collapse to
5
- * ONE in sema: the passive registry. The "only NEW diagnostics" semantics CC gets from per-file
6
- * baselines falls out of the delivered-set here — a diagnostic is injected at most once PER RUN, and a
7
- * run editing a file clears its delivered set so a persisting problem can resurface (CC `Fjn` same
8
- * behavior). The registry object itself is deployment-scoped, so "per run" is a keyed fact, not an
9
- * object lifetime; see {@link LspDiagnosticsRegistry}.
10
- *
11
- * Volumes and wire format are CC-exact (198:320661 `Njn=10, nqa=30`; 198:320480-320505 summary +
12
- * 4000-char cap; severity symbols ✖/⚠/ℹ/★).
13
- *
14
- * NOT durable: diagnostics regenerate from the language server on the next edit; a suspend/resume
15
- * simply starts empty (recorded in design/121 §2).
16
- */
17
1
  /** One LSP diagnostic, the subset the model/shell needs (LSP `Diagnostic` narrowed). */
18
2
  export interface LspDiagnostic {
19
3
  message: string;
@@ -102,7 +86,25 @@ export declare class LspDiagnosticsRegistry {
102
86
  * {symbol} [Line {line+1}:{col+1}] {message}[ [{code}]][ ({source})]
103
87
  * ```
104
88
  * capped at 4000 chars with an honest `…[truncated]` tail.
89
+ *
90
+ * ENVELOPE CONTAINMENT (untrusted-text.ts `ENGINE_ENVELOPES`, the `new-diagnostics` row): the
91
+ * per-diagnostic strings (`message`, `code`, `source`) and the file basename come from a LANGUAGE
92
+ * SERVER — a process the engine launches over repository content, i.e. the same trust class as the
93
+ * repository. They land inside the `<new-diagnostics>` envelope, and (since this block is now
94
+ * delivered inside a marked reminder shell) inside an authority-marked reminder too, so a diagnostic
95
+ * message carrying `</new-diagnostics>` closed the envelope and placed server text at the top level
96
+ * of engine-authored authority. CC 223 does not neutralize here either — a deliberate, disclosed
97
+ * hardening divergence: CC ships the same block inside the same reminder shell, so the hole exists on
98
+ * both sides and only the fix is ours. Neutralization runs BEFORE the join, so the 4000-char cap
99
+ * still bounds the FINAL text and can never cut inside a neutralized marker's own bytes.
105
100
  */
106
101
  export declare function formatDiagnosticsSummary(files: LspFileDiagnostics[]): string;
107
- /** CC 198 model-facing injection block (198:320500-320503), verbatim framing. */
102
+ /** CC 198 model-facing injection block (198:320500-320503), verbatim framing.
103
+ *
104
+ * DELIVERY (design/319 sibling work): the runner shells this block in a MARKED `<system-reminder>`
105
+ * before steering it — CC 223 `case "diagnostics"` does the same (`ih([Vr({content: …})])` → the
106
+ * reminder wrap), and until that was matched this was the engine's only authority text reaching the
107
+ * model as a bare block with no provenance mark at all. The block's own bytes are unchanged; the
108
+ * shell is applied at the delivery site (runtask), not baked in here, so this exported renderer keeps
109
+ * serving callers that place the block themselves. */
108
110
  export declare function formatDiagnosticsBlock(files: LspFileDiagnostics[]): string;
@@ -1,3 +1,4 @@
1
+ import { sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "./untrusted-text.js";
1
2
  const MAX_PER_FILE = 10;
2
3
  const MAX_TOTAL = 30;
3
4
  const MAX_SUMMARY_CHARS = 4000;
@@ -79,14 +80,19 @@ function basenameOfUri(uri) {
79
80
  return idx >= 0 ? path.slice(idx + 1) : path;
80
81
  }
81
82
  export function formatDiagnosticsSummary(files) {
83
+ const safe = (t) => sanitizeUntrustedText(t, SHELLED_BODY_ENVELOPE_TAGS);
82
84
  const lines = [];
83
85
  for (const f of files) {
84
- lines.push(`${basenameOfUri(f.uri)}:`);
86
+ lines.push(`${safe(basenameOfUri(f.uri))}:`);
85
87
  for (const d of f.diagnostics) {
86
- const pos = d.range ? `[Line ${d.range.start.line + 1}:${d.range.start.character + 1}] ` : "";
87
- const code = d.code !== undefined ? ` [${d.code}]` : "";
88
- const source = d.source ? ` (${d.source})` : "";
89
- lines.push(` ${severitySymbol(d.severity)} ${pos}${d.message}${code}${source}`);
88
+ const startLine = d.range?.start?.line;
89
+ const startChar = d.range?.start?.character;
90
+ const pos = typeof startLine === "number" && Number.isFinite(startLine) && typeof startChar === "number" && Number.isFinite(startChar)
91
+ ? `[Line ${Math.trunc(startLine) + 1}:${Math.trunc(startChar) + 1}] `
92
+ : "";
93
+ const code = d.code !== undefined ? ` [${safe(String(d.code))}]` : "";
94
+ const source = d.source ? ` (${safe(String(d.source))})` : "";
95
+ lines.push(` ${severitySymbol(d.severity)} ${pos}${safe(String(d.message))}${code}${source}`);
90
96
  }
91
97
  }
92
98
  const summary = lines.join("\n");
@@ -488,6 +488,30 @@ interface McpContentItem {
488
488
  };
489
489
  }
490
490
  export declare function mapContent(content: Array<McpContentItem>, serverName?: string, imageResizer?: McpImageResizer): Promise<Array<TextContent | ImageContent>>;
491
+ /**
492
+ * Connect to each MCP server, list its tools, and wrap them as AgentTools.
493
+ * Tools are namespaced `mcp__<server>__<tool>` (CC parity) to avoid collisions. The model sees each
494
+ * tool's real JSON-Schema (`inputSchema`), which the agent loop validates natively.
495
+ *
496
+ * Fail-open (design/29): a server that fails to connect or list tools is SKIPPED — its error is
497
+ * returned in `warnings`, the healthy servers still materialize. A single misconfigured server
498
+ * (bad stdio command, unreachable URL) never bricks the whole task.
499
+ *
500
+ * Call `dispose()` when the task finishes — connections are task-scoped, never persisted.
501
+ */
502
+ /**
503
+ * Invisible / format / private-use / unassigned characters stripped out of every model-facing string a
504
+ * server advertises — CC `MZg` :144247. CC spells this as the property class BELOW followed by five
505
+ * explicit ranges (zero-width U+200B–U+200F, bidi embedding U+202A–U+202E, bidi isolates U+2066–U+2069,
506
+ * the BOM, and the BMP private-use area); on this engine's Unicode data every one of those code points
507
+ * is already `Cf` or `Co`, so the class alone is byte-equivalent to CC's expression and the source stays
508
+ * free of literal invisible characters (a file carrying them is a grep blind spot, and this repo has
509
+ * paid for that once already). The equivalence is asserted by a pin, not assumed — a Unicode-data change
510
+ * that moved any of those ranges out of the class would open exactly the hole this strips.
511
+ *
512
+ * `\p{Cc}` is deliberately NOT here: newlines and tabs are legitimate description formatting.
513
+ */
514
+ export declare const MCP_INVISIBLE_TEXT_RE: RegExp;
491
515
  /** One tool's schema-normalization outcome (CC 220 `zyo` @336616-336654's return shape). */
492
516
  export type McpSchemaNormalizeResult = {
493
517
  outcome: "unchanged";
@@ -537,6 +561,28 @@ export declare function normalizeMcpToolSchema(schema: unknown): McpSchemaNormal
537
561
  * to catch, and this gate alone does not catch it (no type key ⇒ passes the root-type check; the
538
562
  * combinator's own structure is legal JSON Schema ⇒ passes validateJsonSchemaShape too).
539
563
  */
564
+ /**
565
+ * A schema defect the operator must HEAR about but that must not cost the tool its mount — returns a
566
+ * core-authored sentence, or `undefined` when there is nothing to say.
567
+ *
568
+ * Today that is exactly one thing: a TOP-LEVEL parameter name outside `[a-zA-Z0-9_.-]{1,64}`. A name
569
+ * like `"user name"` is rejected at the Anthropic wire, and because the tool table rides EVERY request
570
+ * it takes the whole request down with it — while the operator previously got nothing at all pointing
571
+ * at the cause (no warning, no drop record, just a task that stopped working). CC checks the identical
572
+ * names against the identical expression (`EX_`/`_xo` :325793/:325824, reported as `check:"propertyKey"`).
573
+ *
574
+ * WHY AN ADVISORY AND NOT A DROP, stated because the sibling structural gate above does drop: CC's own
575
+ * disposition here is keep-and-warn — its drop arm sits behind a rollout flag that ships OFF (`Xyd` /
576
+ * `Yyd` :354641/:354622, an empty remote-config list ⇒ false), so CC's shipped behavior is to keep the
577
+ * tool and warn "requests that include it may fail". The difference from the structural half is real
578
+ * and not a technicality: a schema with no object root is unusable everywhere, whereas this charset is
579
+ * ONE provider's rule, and this engine is bring-your-own-model. Dropping here would delete a working
580
+ * tool from a deployment whose provider accepts the name — a Chinese- or Japanese-named parameter is
581
+ * the ordinary case, not a hostile one. Detection was the gap; removal was never the mandate.
582
+ *
583
+ * TOP-LEVEL only, like CC: a nested property name is not what the provider validates.
584
+ */
585
+ export declare function mcpToolSchemaAdvisory(schema: unknown): string | undefined;
540
586
  export declare function mcpToolSchemaProblem(schema: unknown): string | undefined;
541
587
  export declare function materializeMcpTools(specs: McpServerSpec[], principal?: string, onElicit?: OnElicit, imageResizer?: McpImageResizer, // design/116 CONFIRM-1 seam: deployment-injected; default = auto-detected sharp
542
588
  reminderDisclosure?: {