@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.
- package/CHANGELOG.md +150 -0
- package/dist/agents/cumulative-stats.d.ts +26 -0
- package/dist/agents/cumulative-stats.js +56 -0
- package/dist/agents/observer.d.ts +11 -7
- package/dist/agents/observer.js +2 -4
- package/dist/agents/subagent.d.ts +4 -0
- package/dist/agents/subagent.js +1 -1
- package/dist/agents/verify.d.ts +27 -3
- package/dist/agents/verify.js +7 -2
- package/dist/core/governance-codes.js +14 -0
- package/dist/core/hooks.js +1 -1
- package/dist/core/lsp-diagnostics.d.ts +19 -17
- package/dist/core/lsp-diagnostics.js +11 -5
- package/dist/core/mcp.d.ts +214 -5
- package/dist/core/mcp.js +315 -30
- package/dist/core/memory-engine/consolidation.d.ts +378 -0
- package/dist/core/memory-engine/consolidation.js +342 -0
- package/dist/core/memory-engine/dual-root.js +3 -0
- package/dist/core/memory-engine/engine.d.ts +237 -4
- package/dist/core/memory-engine/engine.js +1111 -4
- package/dist/core/memory-engine/export-bundle.js +9 -0
- package/dist/core/memory-engine/file-backend.js +27 -1
- package/dist/core/memory-engine/frontmatter.d.ts +20 -1
- package/dist/core/memory-engine/frontmatter.js +111 -0
- package/dist/core/memory-engine/index.d.ts +4 -2
- package/dist/core/memory-engine/index.js +3 -1
- package/dist/core/memory-engine/memory-backend-contract.js +131 -0
- package/dist/core/memory-engine/sync-client.js +26 -0
- package/dist/core/memory-engine/tools.d.ts +9 -0
- package/dist/core/memory-engine/tools.js +57 -13
- package/dist/core/memory-engine/types.d.ts +99 -0
- package/dist/core/memory-recall.js +4 -3
- package/dist/core/memory.d.ts +33 -3
- package/dist/core/memory.js +6 -4
- package/dist/core/permission-rule-model.d.ts +140 -21
- package/dist/core/permission-rule-model.js +76 -17
- package/dist/core/permission-rule-org.d.ts +4 -3
- package/dist/core/permission-rule-org.js +12 -3
- package/dist/core/permission-rules.d.ts +22 -0
- package/dist/core/permission-rules.js +60 -6
- package/dist/core/reminder-disclosure.d.ts +29 -4
- package/dist/core/reminder-disclosure.js +60 -12
- package/dist/core/runner/prepare-memory.js +7 -2
- package/dist/core/runner/prepare-task.d.ts +31 -1
- package/dist/core/runner/prepare-task.js +31 -14
- package/dist/core/runner/runtask.d.ts +8 -1
- package/dist/core/runner/runtask.js +12 -10
- package/dist/core/runner/session-rule-policy.js +5 -3
- package/dist/core/runner/synthetic-tools.js +4 -2
- package/dist/core/runner/turn-attachments.d.ts +16 -6
- package/dist/core/runner/turn-attachments.js +34 -20
- package/dist/core/tool-policy.d.ts +18 -0
- package/dist/core/tool-policy.js +19 -8
- package/dist/core/types.d.ts +89 -6
- package/dist/core/untrusted-egress.js +12 -2
- package/dist/core/untrusted-text.d.ts +189 -3
- package/dist/core/untrusted-text.js +416 -6
- package/dist/engine/loop/types.d.ts +7 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/orchestration/workflow-types.d.ts +16 -6
- package/dist/orchestration/workflow-types.js +10 -4
- package/dist/orchestration/workflow.js +32 -6
- package/dist/prompts/default.d.ts +12 -2
- package/dist/stores/file/background-agent-store.js +1 -0
- package/dist/stores/file/checkpoint-store.d.ts +6 -2
- package/dist/stores/file/checkpoint-store.js +1 -0
- package/dist/stores/file/fs-atomic.d.ts +151 -10
- package/dist/stores/file/fs-atomic.js +208 -32
- package/dist/stores/file/index.d.ts +26 -3
- package/dist/stores/file/index.js +25 -2
- package/dist/stores/file/shared-ledger.d.ts +40 -5
- package/dist/stores/file/shared-ledger.js +24 -8
- package/dist/stores/file/workflow-run-store.d.ts +8 -1
- package/dist/stores/file/workflow-run-store.js +1 -0
- package/dist/tools/fs/bash-readonly-classifier.d.ts +71 -0
- package/dist/tools/fs/bash-readonly-classifier.js +58 -47
- package/dist/tools/fs/index.d.ts +3 -1
- package/package.json +1 -1
- 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 —
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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
|
|
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). */
|
package/dist/agents/observer.js
CHANGED
|
@@ -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
|
|
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("&", "&")
|
|
@@ -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?
|
package/dist/agents/subagent.js
CHANGED
|
@@ -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) {
|
package/dist/agents/verify.d.ts
CHANGED
|
@@ -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.
|
|
176
|
-
*
|
|
177
|
-
* (the
|
|
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,
|
package/dist/agents/verify.js
CHANGED
|
@@ -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",
|
package/dist/core/hooks.js
CHANGED
|
@@ -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
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
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");
|