@sema-agent/core 5.53.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 (80) hide show
  1. package/CHANGELOG.md +150 -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/subagent.d.ts +4 -0
  7. package/dist/agents/subagent.js +1 -1
  8. package/dist/agents/verify.d.ts +27 -3
  9. package/dist/agents/verify.js +7 -2
  10. package/dist/core/governance-codes.js +14 -0
  11. package/dist/core/hooks.js +1 -1
  12. package/dist/core/lsp-diagnostics.d.ts +19 -17
  13. package/dist/core/lsp-diagnostics.js +11 -5
  14. package/dist/core/mcp.d.ts +214 -5
  15. package/dist/core/mcp.js +315 -30
  16. package/dist/core/memory-engine/consolidation.d.ts +378 -0
  17. package/dist/core/memory-engine/consolidation.js +342 -0
  18. package/dist/core/memory-engine/dual-root.js +3 -0
  19. package/dist/core/memory-engine/engine.d.ts +237 -4
  20. package/dist/core/memory-engine/engine.js +1111 -4
  21. package/dist/core/memory-engine/export-bundle.js +9 -0
  22. package/dist/core/memory-engine/file-backend.js +27 -1
  23. package/dist/core/memory-engine/frontmatter.d.ts +20 -1
  24. package/dist/core/memory-engine/frontmatter.js +111 -0
  25. package/dist/core/memory-engine/index.d.ts +4 -2
  26. package/dist/core/memory-engine/index.js +3 -1
  27. package/dist/core/memory-engine/memory-backend-contract.js +131 -0
  28. package/dist/core/memory-engine/sync-client.js +26 -0
  29. package/dist/core/memory-engine/tools.d.ts +9 -0
  30. package/dist/core/memory-engine/tools.js +57 -13
  31. package/dist/core/memory-engine/types.d.ts +99 -0
  32. package/dist/core/memory-recall.js +4 -3
  33. package/dist/core/memory.d.ts +33 -3
  34. package/dist/core/memory.js +6 -4
  35. package/dist/core/permission-rule-model.d.ts +140 -21
  36. package/dist/core/permission-rule-model.js +76 -17
  37. package/dist/core/permission-rule-org.d.ts +4 -3
  38. package/dist/core/permission-rule-org.js +12 -3
  39. package/dist/core/permission-rules.d.ts +22 -0
  40. package/dist/core/permission-rules.js +60 -6
  41. package/dist/core/reminder-disclosure.d.ts +29 -4
  42. package/dist/core/reminder-disclosure.js +60 -12
  43. package/dist/core/runner/prepare-memory.js +7 -2
  44. package/dist/core/runner/prepare-task.d.ts +31 -1
  45. package/dist/core/runner/prepare-task.js +31 -14
  46. package/dist/core/runner/runtask.d.ts +8 -1
  47. package/dist/core/runner/runtask.js +12 -10
  48. package/dist/core/runner/session-rule-policy.js +5 -3
  49. package/dist/core/runner/synthetic-tools.js +4 -2
  50. package/dist/core/runner/turn-attachments.d.ts +16 -6
  51. package/dist/core/runner/turn-attachments.js +34 -20
  52. package/dist/core/tool-policy.d.ts +18 -0
  53. package/dist/core/tool-policy.js +19 -8
  54. package/dist/core/types.d.ts +89 -6
  55. package/dist/core/untrusted-egress.js +12 -2
  56. package/dist/core/untrusted-text.d.ts +189 -3
  57. package/dist/core/untrusted-text.js +416 -6
  58. package/dist/engine/loop/types.d.ts +7 -0
  59. package/dist/index.d.ts +2 -2
  60. package/dist/index.js +2 -2
  61. package/dist/orchestration/workflow-types.d.ts +16 -6
  62. package/dist/orchestration/workflow-types.js +10 -4
  63. package/dist/orchestration/workflow.js +32 -6
  64. package/dist/prompts/default.d.ts +12 -2
  65. package/dist/stores/file/background-agent-store.js +1 -0
  66. package/dist/stores/file/checkpoint-store.d.ts +6 -2
  67. package/dist/stores/file/checkpoint-store.js +1 -0
  68. package/dist/stores/file/fs-atomic.d.ts +151 -10
  69. package/dist/stores/file/fs-atomic.js +208 -32
  70. package/dist/stores/file/index.d.ts +26 -3
  71. package/dist/stores/file/index.js +25 -2
  72. package/dist/stores/file/shared-ledger.d.ts +40 -5
  73. package/dist/stores/file/shared-ledger.js +24 -8
  74. package/dist/stores/file/workflow-run-store.d.ts +8 -1
  75. package/dist/stores/file/workflow-run-store.js +1 -0
  76. package/dist/tools/fs/bash-readonly-classifier.d.ts +71 -0
  77. package/dist/tools/fs/bash-readonly-classifier.js +58 -47
  78. package/dist/tools/fs/index.d.ts +3 -1
  79. package/package.json +1 -1
  80. package/test/export-surface.snapshot.json +30 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,155 @@
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
+
97
+ ## 5.54.0 — 2026-08-22
98
+
99
+ ### Fixed
100
+ - **Compound commands are adjudicated segment-wise** (#353 hemostat): the rule lane's whole-string
101
+ floor made every compound spelling invisible to it — an org/user **deny** on `curl` never fired on
102
+ `git status && curl evil.example` (fail-open past the deny into the classifier lane), suggestions
103
+ came back empty, and an exact compound rule could not be minted or matched. Now: any-segment deny
104
+ refuses the whole string before any allow path; whole-string exact allow admits; all-segments-allow
105
+ admits; anything else asks with a per-segment suggestion batch (capped at 5). A path-prefixed
106
+ argv0 (`/usr/bin/curl`) joins the rule lane as text. A build-tool allowlist was considered and
107
+ rejected (not a CC form).
108
+ - **MCP listings walk their pagination** (#381): tools/list and resources/list read only the FIRST
109
+ page — later tools silently never mounted and nothing disclosed it. All list sites now walk pages
110
+ through one bounded chokepoint (20-page cap; cursor-loop guard; a continuation needs a fundable
111
+ remainder — min(250ms, budget/10)); an unfinished walk is disclosed with a typed reason
112
+ (`cursor_invalid` | `page_error` | `cursor_loop` | `page_cap` | `budget_exhausted`) on the model
113
+ face, the operator warning lane (`mcp.listing_incomplete`), and `McpRefreshResult.listingIncomplete`;
114
+ a refresh that cannot prove an entry gone retains it. **Closed-set note**: `McpListingIncomplete.reason`
115
+ gains `budget_exhausted` (+ optional `budgetMs`) — a downstream exhaustive switch needs the case.
116
+ - **Ledger stores enforce single-writer-per-directory at construction** (#382): checkpoint /
117
+ background-agent / workflow-run each documented a boot lock they did not hold — two OS processes
118
+ could each win the SAME once-only approval, and one process's compaction could erase another's
119
+ fsync'd rows. The fence now lives with the authority (`shared-ledger.ts` bootstrap takes
120
+ `<dir>/LOCK`; refcount-joined in-process; crashed-owner locks pruned through a named claim gate);
121
+ a second OS process is refused at construction. New exports: `FileStoreLockError`,
122
+ `FileStoreLockErrorCode` (`store.dir_in_use` | `store.dir_claiming` | `store.lock_unreadable`).
123
+ - **The workflow budget gate re-judges after admission** (#383): the ceiling was checked only at
124
+ dispatch time against a pre-batch snapshot — a 40-item fan-out overshot 26x (serial dispatch
125
+ identically) and the refusal text blamed "a loop without a termination condition". The
126
+ authoritative check now runs after `sem.acquire()` against live spend; refused calls settle as
127
+ failed agent rows carrying `workflow.budget_exceeded`; a fully-cached resume is never gated;
128
+ bad budget values refuse loudly; budget and agent-cap refusals each name their own cause.
129
+ - **The workflow steer marker is unpredictable** (test P0-KPI #9): the workflow lane's steer
130
+ correlation marker was a monotone counter (`steer-1`…) — untrusted content could pre-claim the
131
+ tag and self-declare correlation, the hole the subagent steer seat had already closed
132
+ (design/122 r1-m3). Both seats now single-source the same 12-hex fragment; markers are
133
+ per-delivery distinct, ephemeral, never journaled.
134
+ - **Pre-release rescan dispositions (seven, fixed in-tree before publish)**: the compound-splitter
135
+ extraction was not byte-identical for the readonly classifier (`ls; 2>/dev/null` had flipped from
136
+ ask to auto-allow — restored); a near-exhausted MCP walk budget was spent as a real request and
137
+ its cut-off blamed on the server (now core-attributed, see #381 above); a stuck paginator's
138
+ re-served page was appended twice and the duplicate blamed on a namespacing collision (rewind of
139
+ provably byte-identical re-serves; drop reasons split); a failed `BootLock.release()` left a stale
140
+ holder able to delete its successor's live fence (release is one-shot again); a zero-ref fence
141
+ entry was joinable without consulting the disk (re-validated, foreign owners refused by name);
142
+ `FileWorkflowRunStore.close()`'s doc credited a backend factory that never constructs it;
143
+ `docs/ARCHITECTURE.md`'s lock coverage contradicted `docs/KNOWN-ISSUES.md`.
144
+ - Behavior narrowings (named): org/user denies now reach compound spellings (previously fail-open);
145
+ a second OS process over a ledger directory refuses at construction instead of corrupting;
146
+ `compactNow()` on a closed store throws; workflow budget bad values refuse at construction;
147
+ budget-refused calls appear as failed agent rows; a second `dispose()` after a failed fence
148
+ release removes nothing; an in-process re-open over an unreadable stranded lock refuses
149
+ (`store.lock_unreadable`); a stranded fence taken by another process refuses (`store.dir_in_use`);
150
+ a failed fence release warns (was silent); `ls; 2>/dev/null` asks again (restores the 5.53
151
+ contract).
152
+
3
153
  ## 5.53.0 — 2026-08-21
4
154
 
5
155
  ### 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;")
@@ -356,6 +356,10 @@ export interface SubagentSteerHandle {
356
356
  */
357
357
  resume?: (content: string) => Promise<string>;
358
358
  }
359
+ /** design/122 r1-m3 — UNPREDICTABLE correlation-marker fragment (uuid random tail, 12 hex chars). A marker
360
+ * must not be guessable by untrusted content — the old `steer-a<seq>` global counter let injected text
361
+ * pre-claim "[steer-aN]" and self-declare correlation; the same fix applies to the new resume markers. */
362
+ export declare function markerFragment(): string;
359
363
  /**
360
364
  * design/122 D2 — build the `resume` verb for one delegation (closure over the ledger + delegation runner —
361
365
  * NEVER the parent tool ctx / spec builder, r1-m5). Validation order: TTL sweep (MAJOR-1) → disposed?
@@ -436,7 +436,7 @@ function createBgActivityBeat(parentToolCallId, emitTick, noteActivity) {
436
436
  emitTick(starts);
437
437
  };
438
438
  }
439
- function markerFragment() {
439
+ export function markerFragment() {
440
440
  return uuidv7().replace(/-/g, "").slice(-12);
441
441
  }
442
442
  function createSteerHandle(stream, parentToolCallId, agentName, settled, retain) {
@@ -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");