@sema-agent/core 5.58.0 → 5.60.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 (59) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/dist/brain/anthropic.js +15 -5
  3. package/dist/brain/errors.d.ts +18 -1
  4. package/dist/brain/errors.js +7 -1
  5. package/dist/brain/input-too-long.d.ts +57 -0
  6. package/dist/brain/input-too-long.js +35 -0
  7. package/dist/brain/route-adjudicator.d.ts +8 -1
  8. package/dist/brain/route-adjudicator.js +8 -1
  9. package/dist/brain/stream-engine.js +9 -1
  10. package/dist/core/auto-compaction.js +2 -2
  11. package/dist/core/checkpoint-store.d.ts +116 -19
  12. package/dist/core/checkpoint-store.js +15 -8
  13. package/dist/core/context-edit.d.ts +243 -41
  14. package/dist/core/context-edit.js +247 -32
  15. package/dist/core/governance-codes.d.ts +37 -10
  16. package/dist/core/governance-codes.js +57 -1
  17. package/dist/core/locked-config.d.ts +36 -4
  18. package/dist/core/locked-config.js +34 -1
  19. package/dist/core/mcp.js +10 -6
  20. package/dist/core/memory-engine/consolidation-driver.d.ts +6 -2
  21. package/dist/core/memory-engine/consolidation-driver.js +54 -5
  22. package/dist/core/memory-engine/consolidation.d.ts +73 -1
  23. package/dist/core/memory-engine/consolidation.js +21 -1
  24. package/dist/core/memory-engine/content-origin.d.ts +24 -2
  25. package/dist/core/memory-engine/content-origin.js +6 -1
  26. package/dist/core/memory-engine/engine.d.ts +97 -8
  27. package/dist/core/memory-engine/engine.js +112 -20
  28. package/dist/core/memory-engine/file-backend.d.ts +13 -1
  29. package/dist/core/memory-engine/file-backend.js +3 -0
  30. package/dist/core/memory-engine/index.d.ts +4 -3
  31. package/dist/core/memory-engine/layout.js +20 -6
  32. package/dist/core/memory-engine/types.d.ts +17 -0
  33. package/dist/core/memory.d.ts +10 -0
  34. package/dist/core/park-selfcheck.js +1 -0
  35. package/dist/core/permission-rule-consent.js +9 -5
  36. package/dist/core/permission-rule-model.d.ts +42 -1
  37. package/dist/core/permission-rule-model.js +12 -0
  38. package/dist/core/runner/prepare-config-doors.d.ts +22 -1
  39. package/dist/core/runner/prepare-config-doors.js +36 -0
  40. package/dist/core/runner/prepare-task.d.ts +28 -1
  41. package/dist/core/runner/prepare-task.js +109 -11
  42. package/dist/core/runner/runtask.js +45 -8
  43. package/dist/core/store-contracts/checkpoint-store-contract.js +32 -0
  44. package/dist/core/tool-policy.d.ts +74 -0
  45. package/dist/core/tool-policy.js +80 -1
  46. package/dist/core/tools.js +1 -1
  47. package/dist/core/trace.d.ts +36 -0
  48. package/dist/core/types.d.ts +172 -22
  49. package/dist/core/types.js +4 -3
  50. package/dist/core/untrusted-text.d.ts +11 -0
  51. package/dist/core/untrusted-text.js +1 -0
  52. package/dist/engine/llm/types.d.ts +21 -2
  53. package/dist/engine/loop/agent-loop.js +7 -1
  54. package/dist/engine/loop/types.d.ts +4 -1
  55. package/dist/index.d.ts +3 -3
  56. package/dist/index.js +2 -2
  57. package/dist/tools/fs/fs-bash.js +1 -2
  58. package/package.json +1 -1
  59. package/test/export-surface.snapshot.json +1771 -1
@@ -22,7 +22,7 @@
22
22
  */
23
23
  import type { EngineNotice } from "../types.js";
24
24
  import { type ConsolidationGateRow, type ConsolidationRunStopReason, type MemoryConsolidationOptions } from "./consolidation.js";
25
- import { type ConsolidationPlanSummary } from "./engine.js";
25
+ import { type ConsolidationPlanFoldEvidence, type ConsolidationPlanSummary } from "./engine.js";
26
26
  import { mintLlmConsolidationPlan, type ConsolidationDriveCycleRow, type ConsolidationDriveEngine, type ConsolidationFoldState, type DistillerChatFn, type LlmConsolidationPlan, type LlmConsolidationPlanProduct, type LlmDistillerContract } from "./distiller.js";
27
27
  export declare const CONSOLIDATION_DRIVER_RUNS_FILE = "distiller-runs.json";
28
28
  /** §1.6-4 — the mint archive: what was shown to the model and what it answered, verbatim, per run
@@ -114,10 +114,14 @@ export declare function readConsolidationDriverRun(controlDir: string, scope: st
114
114
  * run row's `planArchive` (written under the attempt guard) names the one that counts. */
115
115
  export declare function distillerPlanArchiveName(runId: string, attempt: number): string;
116
116
  export declare function archiveDistillerPlan(controlDir: string, runId: string, plan: LlmConsolidationPlan, attempt?: number): string;
117
- /** The engine face this verb consumes — `MemoryEngine` satisfies it structurally. */
117
+ /** The engine face this verb consumes — `MemoryEngine` satisfies it structurally.
118
+ * (`readConsolidationPlanFoldEvidence` joined in design/376 slice ③, #436: the conservation
119
+ * wall's terminal witness is member-level, so the face is REQUIRED — an engine without it could
120
+ * only offer the store-delta counts an interleaved fold can numerically offset.) */
118
121
  export interface ConsolidationDriverEngine extends ConsolidationDriveEngine {
119
122
  readConsolidationGate(scope: string): ConsolidationGateRow | undefined;
120
123
  listConsolidationPlans(): ConsolidationPlanSummary[];
124
+ readConsolidationPlanFoldEvidence(planId: string): ConsolidationPlanFoldEvidence;
121
125
  readConsolidationFoldState(scope: string): Promise<ConsolidationFoldState>;
122
126
  readonly controlPlaneRoot: string;
123
127
  }
@@ -182,6 +182,7 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
182
182
  return settle("parked", undefined, parkedSeat);
183
183
  }
184
184
  let staleResidue = [];
185
+ let staleResidueMemberIds = new Set();
185
186
  const distill = async (candidates, snapshot, s) => {
186
187
  if (run.planCache === undefined) {
187
188
  let minted;
@@ -233,11 +234,12 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
233
234
  writeDriverRun(controlDir, run);
234
235
  archiveDistillerPlan(controlDir, run.runId, plan, run.attempt);
235
236
  }
237
+ const offered = new Map();
238
+ for (const c of candidates)
239
+ if (typeof c?.entry?.id === "string")
240
+ offered.set(c.entry.id, c.entry.rev);
241
+ staleResidueMemberIds = new Set();
236
242
  staleResidue = (run.planCache?.products ?? []).flatMap((p) => {
237
- const offered = new Map();
238
- for (const c of candidates)
239
- if (typeof c?.entry?.id === "string")
240
- offered.set(c.entry.id, c.entry.rev);
241
243
  const ids = p.inputIds ?? [];
242
244
  const live = ids.filter((id) => offered.has(id));
243
245
  if (live.length === 0)
@@ -245,7 +247,11 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
245
247
  const stale = p.inputRevs !== undefined && live.some((id) => p.inputRevs[id] !== undefined && offered.get(id) !== p.inputRevs[id]);
246
248
  const vanished = live.length < ids.length;
247
249
  const unanchored = p.inputRevs === undefined;
248
- return stale || vanished || unanchored ? [{ name: p.name, targets: live.length }] : [];
250
+ if (!(stale || vanished || unanchored))
251
+ return [];
252
+ for (const id of live)
253
+ staleResidueMemberIds.add(id);
254
+ return [{ name: p.name, targets: live.length }];
249
255
  });
250
256
  const replayView = run.planCache !== undefined && run.planCache.products.some((p) => p.inputRevs === undefined)
251
257
  ? { ...run.planCache, products: run.planCache.products.filter((p) => p.inputRevs !== undefined) }
@@ -329,6 +335,7 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
329
335
  const writeResidue = run.writeFailures.map((w) => ({ name: w.key, targets: w.members }));
330
336
  const driverResidue = [...staleResidue, ...writeResidue];
331
337
  if (drive.converged) {
338
+ const plannedIds = [...new Set((run.planCache?.products ?? []).flatMap((p) => p.inputIds ?? []))];
332
339
  const plannedMembers = (run.planCache?.products ?? []).reduce((a, p) => a + (p.inputIds?.length ?? 0), 0);
333
340
  const rowAccount = run.entriesSuperseded ?? 0;
334
341
  const storeAccount = run.foldBaseline !== undefined ? Math.max(0, drive.foldState.after.superseded - run.foldBaseline.superseded) : rowAccount;
@@ -339,6 +346,24 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
339
346
  run.residue = driverResidue;
340
347
  return settle("driver_failed", drive, `the drive reached a fixpoint but the account does not balance: ${plannedMembers} planned member(s), ${supersededAttested} superseded attested for this run (row account ${rowAccount}, store delta ${run.foldBaseline !== undefined ? storeAccount : "unavailable — the row predates the store anchor"}), ${residueTargets} in named residue — ${plannedMembers - accounted} member(s) left the candidate set without a fold the store attests (deleted or excluded mid-run${run.foldBaseline !== undefined ? "" : ", or folded in a window whose account write did not survive"}); their entries stand (or revive with their exclusion), and a fresh run re-mints over the current library`);
341
348
  }
349
+ if (plannedIds.length > 0) {
350
+ const evidence = collectRunFoldEvidence(engine, scope, run.requestId);
351
+ if (evidence.skipped.length > 0) {
352
+ const named = evidence.skipped.slice(0, 5).map((s) => `${s.planId} (${s.state})`).join(", ");
353
+ announce([
354
+ `memory consolidation conservation wall for scope ${inlineUntrusted(scope, 80)}: ${evidence.skipped.length} same-scope plan file(s) answered unreadable and were SKIPPED as foreign to run ${run.runId} — they attest nothing for this run and need out-of-band repair: ${named}${evidence.skipped.length > 5 ? ", …" : ""}`,
355
+ ]);
356
+ }
357
+ if (evidence.unreadable !== undefined) {
358
+ run.residue = driverResidue;
359
+ return settle("driver_failed", drive, `the drive reached a fixpoint but the member-level fold evidence is incomplete: plan ${evidence.unreadable} — one of THIS run's own cycles — answered ${evidence.unreadableState} (control-plane damage on run-owned evidence, not a transient race); a converged label needs attestable members; the library stands as committed and a fresh run re-mints over the current bytes`);
360
+ }
361
+ const missing = plannedIds.filter((id) => !evidence.targets.has(id) && !staleResidueMemberIds.has(id));
362
+ if (missing.length > 0) {
363
+ run.residue = driverResidue;
364
+ return settle("driver_failed", drive, `the drive reached a fixpoint but ${missing.length} of ${plannedIds.length} planned member(s) left the candidate set without a fold this run's plans attest (deleted, excluded, or superseded by another writer mid-run; e.g. ${missing.slice(0, 5).join(", ")}) — a store-delta count can be numerically offset by an unrelated interleaved fold, so member identity is the final judge; the entries stand (or revive with their exclusion) and a fresh run re-mints over the current library`);
365
+ }
366
+ }
342
367
  if (driverResidue.length > 0) {
343
368
  run.residue = driverResidue;
344
369
  return settle("driver_failed", drive, `the drive reached the surviving plan's fixpoint, but the driver's own half left ${driverResidue.length} group(s) unfolded (${staleResidue.length} stale against edited inputs, ${writeResidue.length} writing-pass failure(s)) — their members stay as they were; a fresh run re-mints over the current bytes`);
@@ -356,6 +381,30 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
356
381
  return settle("parked", drive, stop.reason);
357
382
  return settle("refused", drive, stop?.reason ?? "the drive stopped without a stated reason");
358
383
  }
384
+ function collectRunFoldEvidence(engine, scope, requestId) {
385
+ const cycleId = new RegExp(`^${requestId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}-c\\d+$`);
386
+ const targets = new Set();
387
+ const skipped = [];
388
+ for (const summary of engine.listConsolidationPlans()) {
389
+ if (summary.corrupt === true)
390
+ continue;
391
+ if (summary.scope !== scope)
392
+ continue;
393
+ const ev = engine.readConsolidationPlanFoldEvidence(summary.planId);
394
+ if (ev.state !== "ok") {
395
+ if (ev.state === "corrupt" && ev.requestId !== undefined && cycleId.test(ev.requestId)) {
396
+ return { targets, unreadable: summary.planId, unreadableState: ev.state, skipped };
397
+ }
398
+ skipped.push({ planId: summary.planId, state: ev.state });
399
+ continue;
400
+ }
401
+ if (ev.scope !== scope || !cycleId.test(ev.requestId))
402
+ continue;
403
+ for (const t of ev.supersededTargets)
404
+ targets.add(t);
405
+ }
406
+ return { targets, skipped };
407
+ }
359
408
  async function recoverSeat(engine, scope, { sleep, windowMs }) {
360
409
  await engine.reconcileConsolidation(scope);
361
410
  let seat = engine.readConsolidationGate(scope)?.openPlanId;
@@ -91,6 +91,25 @@ export interface MemoryConsolidationOptions {
91
91
  maxDirectedPatchesPerPlan?: number;
92
92
  minRunIntervalMs?: number;
93
93
  minSessionsBetweenRuns?: number;
94
+ /** design/376 C-2② — the scan-refusal fuse's ratio leg, in (0, 1]: scan findings refuse the
95
+ * affected PRODUCT (disclosed beside eligibility drift), and the whole plan refuses only when
96
+ * more than max(1, ⌊ratio × submitted products⌋) of them fail the scan (the floor keeps the
97
+ * per-product tolerance alive on 1-4 product plans — the drive lane's common size) — see
98
+ * {@link scanRefusalCeiling}. Default 0.25. */
99
+ scanRefusalRatioCap?: number;
100
+ /** design/376 C-2② — the fuse's absolute leg (positive integer): the large-batch upper bound on
101
+ * tolerated scan refusals per plan, so the ratio cannot scale the tolerance without limit.
102
+ * Default 8 (the point where the 0.25 ratio hands over at 32 submitted products — the family's
103
+ * "小 N" quantification; below that the ratio leg alone governs). */
104
+ scanRefusalMaxPerPlan?: number;
105
+ /** design/376 C-3 — the EXPLICIT escape hatch for the unconditional instruction-form gate:
106
+ * instruction-form consolidation products (`type: "feedback"` / v2 hint carriers) are refused
107
+ * by default REGARDLESS of the visible set's markers — a clean-set instruction product is the
108
+ * model-reachable privilege route (N ordinary notes in, one instruction entry out). `true` is
109
+ * the only accepted value (a posture declaration, like {@link multiNode}); it opens the
110
+ * CLEAN-set arm only — a marked visible set refuses instruction forms under the laundering
111
+ * hard gate no matter what (privilege and hard gate enter and exit through one door). */
112
+ allowInstructionProducts?: true;
94
113
  /** D-13a — the multi-node POSTURE declaration: present ⇒ `lease` must be injected (constructor
95
114
  * refusal otherwise). Multi-node without declaring is OUT OF CONTRACT (structurally
96
115
  * undetectable — the declaration turns a silent omission into an explicit choice). */
@@ -113,6 +132,8 @@ export declare const CONSOLIDATION_DEFAULTS: {
113
132
  readonly maxDirectedPatchesPerPlan: 32;
114
133
  readonly minRunIntervalMs: number;
115
134
  readonly minSessionsBetweenRuns: 5;
135
+ readonly scanRefusalRatioCap: 0.25;
136
+ readonly scanRefusalMaxPerPlan: 8;
116
137
  };
117
138
  /**
118
139
  * §2.2 — the HARD throttle floor: even a `force` run cannot start a second same-scope run within
@@ -142,6 +163,9 @@ export interface ScreenedConsolidationOptions {
142
163
  maxDirectedPatchesPerPlan: number;
143
164
  minRunIntervalMs: number;
144
165
  minSessionsBetweenRuns: number;
166
+ scanRefusalRatioCap: number;
167
+ scanRefusalMaxPerPlan: number;
168
+ allowInstructionProducts: boolean;
145
169
  multiNode: boolean;
146
170
  lease?: ConsolidationLeaseSeat;
147
171
  }
@@ -159,6 +183,21 @@ export declare function screenConsolidationOptions(raw: MemoryConsolidationOptio
159
183
  /** §1.2-2 — the supersession fuse ceiling: min(N-1, max(floor, ⌊ratio×N⌋)); N ≤ 1 ⇒ 0 (one plan
160
184
  * may NEVER empty the active set — the N-1 ceiling is unconditional, G21). */
161
185
  export declare function supersessionFuseCeiling(activeSetSize: number, opts: Pick<ScreenedConsolidationOptions, "supersedeRatioCap" | "supersedeAbsoluteFloor">): number;
186
+ /**
187
+ * design/376 C-2② — the scan-refusal fuse ceiling: how many scan-refused products ONE plan may
188
+ * carry before the whole plan refuses instead of committing around them.
189
+ * `min(max(1, ⌊ratio × submitted⌋), absolute max)`; the ratio leg governs small plans, the
190
+ * absolute leg is the large-batch upper bound, and the FLOOR of one keeps C-2①'s per-product
191
+ * tolerance alive at the drive lane's common plan sizes — under the default 0.25 ratio a bare
192
+ * floor answers 0 for every ≤3-product plan, so one stray refused product re-killed the whole
193
+ * plan there, exactly the shape C-2① was adopted to remove (an adopted review finding). The
194
+ * floor is NOT a hole for the fully-refused tiny plan: a plan whose every product is refused
195
+ * (and which carries no directed work) still dies at the freeze-emptied C-2③ backstop, never a
196
+ * quiet mostly-no-op — what the floor tolerates is exactly one refused product beside at least
197
+ * one survivor. Submitted count = the proposal's product count (per THIS plan, never a
198
+ * cross-run tally).
199
+ */
200
+ export declare function scanRefusalCeiling(submittedProducts: number, opts: Pick<ScreenedConsolidationOptions, "scanRefusalRatioCap" | "scanRefusalMaxPerPlan">): number;
162
201
  /** §3.1 — the type axis of the eligibility predicate: episodic entries are read-only evidence
163
202
  * (never candidates, never supersession/directed targets, A-7); procedural promotion is a v3
164
203
  * follow-on ticket. Unknown/absent types are ordinary semantic notes — eligible. */
@@ -231,6 +270,24 @@ export declare function updateConsolidationGate<T>(controlDir: string, fn: (file
231
270
  export declare function readConsolidationGateFile(controlDir: string): ConsolidationGateFile;
232
271
  /** True ⇔ the gate store file exists at all (G1's zero-file pin reads this negatively). */
233
272
  export declare function consolidationGateFileExists(controlDir: string): boolean;
273
+ /**
274
+ * design/376 slice ③ (#437②) — the gate read face's DISCRIMINATED answer
275
+ * ({@link import("./engine.js").MemoryEngine.readConsolidationGateState}): `present` carries the
276
+ * row; `absent` means the store is readable and simply has no row for the scope (a never-run
277
+ * scope, or no gate file at all); `corrupt` means the store cannot be trusted and carries the
278
+ * refusal detail. The undiscriminated `readConsolidationGate` face folds corrupt into `undefined`
279
+ * — an operator (or a driver's scheduling read) could not tell a healthy never-run scope from a
280
+ * store every verb will refuse fail-closed.
281
+ */
282
+ export type ConsolidationGateRead = {
283
+ state: "present";
284
+ row: ConsolidationGateRow;
285
+ } | {
286
+ state: "absent";
287
+ } | {
288
+ state: "corrupt";
289
+ detail: string;
290
+ };
234
291
  /**
235
292
  * §2.2 — the engine-minted session count (harvest-tail bookkeeping; ≤1 row write per terminal
236
293
  * harvest; the OFF mode never calls this — D-9a zero-write). Returns the recommendation edge:
@@ -304,7 +361,15 @@ export interface ConsolidationDirectedPatch {
304
361
  }
305
362
  export type ConsolidationPlanState = "open" | "applying" | "completed" | "conflict" | "discarded";
306
363
  export interface ConsolidationPlanFile {
307
- v: 1;
364
+ /** Plan schema version. `2` ⇔ the plan carries {@link freezeRefusedInputIds} — that member is a
365
+ * LOAD-BEARING settle input (C-2④ keeps the named ids out of the completion fingerprint), and
366
+ * a reader that predates it would accept the file as v1, ignore the member, and settle a
367
+ * fingerprint that stamps the refused members (silently blinding the incremental face — the
368
+ * exact hole the field closes). Version-gating the member makes such a reader answer its
369
+ * "unrecognized version" corrupt arm instead: the plan parks loudly for the valve, never
370
+ * settles wrong (mixed-fleet crash window between the durable plan write and gate settlement).
371
+ * Plans without the member stay v1 — zero noise on the common path. */
372
+ v: 1 | 2;
308
373
  planId: string;
309
374
  scope: string;
310
375
  requestId: string;
@@ -318,6 +383,13 @@ export interface ConsolidationPlanFile {
318
383
  /** Frozen products — FULL entry bytes (id/frontmatter incl. distilled + origin/body/rev). */
319
384
  products: MemoryEntry[];
320
385
  productStates: Record<string, "pending" | "applied" | "conflict">;
386
+ /** design/376 C-2④ — the declared input ids of every product the FREEZE refused (scan findings
387
+ * and eligibility drift alike). Settlement keeps these ids OUT of the completion fingerprint:
388
+ * a scan-refused input's rev never moved, so stamping it would make every later incremental
389
+ * snapshot read it as unchanged and never offer it again — the refused member must stay a
390
+ * candidate. Absent on plans frozen before the field existed, and on plans with nothing
391
+ * refused (either way the settle filter simply has nothing extra to exclude). */
392
+ freezeRefusedInputIds?: string[];
321
393
  directed: ConsolidationDirectedPatch[];
322
394
  intents: Array<{
323
395
  requestId: string;
@@ -40,6 +40,8 @@ export const CONSOLIDATION_DEFAULTS = {
40
40
  maxDirectedPatchesPerPlan: 32,
41
41
  minRunIntervalMs: 24 * 60 * 60 * 1000,
42
42
  minSessionsBetweenRuns: 5,
43
+ scanRefusalRatioCap: 0.25,
44
+ scanRefusalMaxPerPlan: 8,
43
45
  };
44
46
  export const CONSOLIDATION_FORCE_THROTTLE_FLOOR_MS = 60_000;
45
47
  export const CONSOLIDATION_SESSION_RING_MAX = 64;
@@ -70,12 +72,18 @@ export function screenConsolidationOptions(raw, provenance) {
70
72
  maxDirectedPatchesPerPlan: num("maxDirectedPatchesPerPlan", raw.maxDirectedPatchesPerPlan, (n) => Number.isInteger(n) && n >= 1, "a positive integer"),
71
73
  minRunIntervalMs: num("minRunIntervalMs", raw.minRunIntervalMs, (n) => n > 0, "a positive number of milliseconds"),
72
74
  minSessionsBetweenRuns: num("minSessionsBetweenRuns", raw.minSessionsBetweenRuns, (n) => Number.isInteger(n) && n >= 0, "a non-negative integer"),
75
+ scanRefusalRatioCap: num("scanRefusalRatioCap", raw.scanRefusalRatioCap, (n) => n > 0 && n <= 1, "a number in (0, 1]"),
76
+ scanRefusalMaxPerPlan: num("scanRefusalMaxPerPlan", raw.scanRefusalMaxPerPlan, (n) => Number.isInteger(n) && n >= 1, "a positive integer"),
77
+ allowInstructionProducts: raw.allowInstructionProducts === true,
73
78
  multiNode: raw.multiNode === true,
74
79
  ...(raw.lease !== undefined ? { lease: raw.lease } : {}),
75
80
  };
76
81
  if (raw.multiNode !== undefined && raw.multiNode !== true) {
77
82
  throw coded(`MemoryEngineOptions.consolidation.multiNode must be exactly true when present (a posture declaration, not a boolean knob).`, "config.memory_consolidation");
78
83
  }
84
+ if (raw.allowInstructionProducts !== undefined && raw.allowInstructionProducts !== true) {
85
+ throw coded(`MemoryEngineOptions.consolidation.allowInstructionProducts must be exactly true when present (a posture declaration, not a boolean knob — absent means the instruction-form gate stays closed).`, "config.memory_consolidation");
86
+ }
79
87
  if (screened.multiNode && screened.lease === undefined) {
80
88
  throw coded(`MemoryEngineOptions.consolidation.multiNode declared without a lease seat — a multi-node deployment MUST inject the host's global lease (the local plan-seat CAS does not reach across nodes). Inject consolidation.lease or remove the multiNode declaration.`, "config.memory_consolidation_lease_required");
81
89
  }
@@ -89,6 +97,11 @@ export function supersessionFuseCeiling(activeSetSize, opts) {
89
97
  return 0;
90
98
  return Math.min(activeSetSize - 1, Math.max(opts.supersedeAbsoluteFloor, Math.floor(opts.supersedeRatioCap * activeSetSize)));
91
99
  }
100
+ export function scanRefusalCeiling(submittedProducts, opts) {
101
+ if (submittedProducts <= 0)
102
+ return 0;
103
+ return Math.min(Math.max(1, Math.floor(opts.scanRefusalRatioCap * submittedProducts)), opts.scanRefusalMaxPerPlan);
104
+ }
92
105
  export function consolidationTypeEligible(type) {
93
106
  return type !== "episodic" && type !== "procedural";
94
107
  }
@@ -186,7 +199,7 @@ function planShapeInvalid(raw, planId) {
186
199
  if (typeof raw !== "object" || raw === null)
187
200
  return "not an object";
188
201
  const p = raw;
189
- if (p["v"] !== 1)
202
+ if (p["v"] !== 1 && p["v"] !== 2)
190
203
  return "unrecognized version";
191
204
  if (p["planId"] !== planId)
192
205
  return "planId mismatch";
@@ -248,6 +261,13 @@ function planShapeInvalid(raw, planId) {
248
261
  }
249
262
  if (!p["directed"].every((d) => intentIds.has(d.intentRequestId)))
250
263
  return "directed patch references an undeclared intent";
264
+ const freezeRefused = p["freezeRefusedInputIds"];
265
+ if (freezeRefused !== undefined && (!Array.isArray(freezeRefused) || !freezeRefused.every((x) => typeof x === "string"))) {
266
+ return "malformed freezeRefusedInputIds";
267
+ }
268
+ if ((p["v"] === 2) !== (freezeRefused !== undefined)) {
269
+ return p["v"] === 2 ? "v2 plan without its freezeRefusedInputIds member" : "freezeRefusedInputIds on a v1 plan";
270
+ }
251
271
  if (!Array.isArray(p["audit"]))
252
272
  return "malformed audit trail";
253
273
  return undefined;
@@ -22,13 +22,22 @@
22
22
  * does not, the default is EXTERNAL (fail-closed: unknown = external), overridable only through the
23
23
  * explicit `TaskSpec.memory.trustedTools` allowlist. A core-mounted built-in without a declaration
24
24
  * defaults local: core owns those definitions, and the fail-closed default exists for tools core
25
- * CANNOT vouch for, not for its own.
25
+ * CANNOT vouch for, not for its own. A declaration is honored only when it SPELLS a member of the
26
+ * closed vocabulary — an unreadable one is not a declaration.
27
+ *
28
+ * The three tier names are BOUNDARY-RELATIVE, not statements about topology: `"local"` means "does
29
+ * not bring content from outside the deployment's trust boundary", which a unix socket to another
30
+ * process the deployment itself runs satisfies and a loopback HTTP call to somebody else's service
31
+ * does not. Distance is not the axis; whose content it is, is.
26
32
  *
27
33
  * There is deliberately no "gate inactive" state: every mounted tool gets a class.
28
34
  */
29
35
  import type { ToolContentOrigin } from "../types.js";
30
36
  export interface ClassifyToolContentOriginInput {
31
- /** The tool's own `contentOrigin` declaration (ToolSpec/AgentTool carry) — wins when present. */
37
+ /** The tool's own `contentOrigin` declaration (ToolSpec/AgentTool carry) — wins when present AND
38
+ * a member of the closed vocabulary ({@link isToolContentOrigin}); a present non-member is the
39
+ * unreadable-declaration case and classifies EXTERNAL. The annotation is intent, not a guarantee:
40
+ * the property rides tool objects untyped. */
32
41
  declared?: ToolContentOrigin;
33
42
  /** True ⇔ the name belongs to a protocol namespace (mcp__/a2a__ …) — an external channel. */
34
43
  isProtocolTool: boolean;
@@ -37,6 +46,10 @@ export interface ClassifyToolContentOriginInput {
37
46
  /** True ⇔ `TaskSpec.memory.trustedTools` names this tool (explicit host exemption). */
38
47
  trusted: boolean;
39
48
  }
49
+ /** The vocabulary in wire/message order — the one spelling list a refusal quotes back. */
50
+ export declare const TOOL_CONTENT_ORIGINS: readonly ToolContentOrigin[];
51
+ /** Runtime membership in the closed vocabulary. */
52
+ export declare function isToolContentOrigin(value: unknown): value is ToolContentOrigin;
40
53
  export declare function classifyToolContentOrigin(input: ClassifyToolContentOriginInput): ToolContentOrigin;
41
54
  /**
42
55
  * Declare `origin` on a tool object built OUTSIDE `defineTool` — a raw AgentTool literal, which is
@@ -60,6 +73,15 @@ export interface AgentToolFace {
60
73
  export interface AgentPoolTool {
61
74
  name: string;
62
75
  aliases?: readonly string[];
76
+ /** The pool row's own declaration — a STATIC surface the deployment writes when it composes the
77
+ * delegation tool, and the only class information this judgment has about the child's roster.
78
+ *
79
+ * A per-entry MCP declaration (design/378, `McpServerSpec.contentOrigin`) does NOT reach here: the
80
+ * pool is a declaration, not a live roster, and this judgment never sees the server entries. A
81
+ * deployment that hands a declared server's `mcp__` tools to children MIRRORS the value onto the
82
+ * matching pool rows; not mirroring it leaves them on the protocol arm, so the delegation reads
83
+ * external and the parent over-marks — the safe direction, and the reason this is a documented
84
+ * duty rather than a refusal. */
63
85
  contentOrigin?: ToolContentOrigin;
64
86
  }
65
87
  /**
@@ -1,6 +1,11 @@
1
+ const TOOL_CONTENT_ORIGIN_SET = { external: true, execution: true, local: true };
2
+ export const TOOL_CONTENT_ORIGINS = Object.keys(TOOL_CONTENT_ORIGIN_SET);
3
+ export function isToolContentOrigin(value) {
4
+ return typeof value === "string" && Object.prototype.hasOwnProperty.call(TOOL_CONTENT_ORIGIN_SET, value);
5
+ }
1
6
  export function classifyToolContentOrigin(input) {
2
7
  if (input.declared !== undefined)
3
- return input.declared;
8
+ return isToolContentOrigin(input.declared) ? input.declared : "external";
4
9
  if (input.trusted)
5
10
  return "local";
6
11
  if (input.isProtocolTool)
@@ -1,11 +1,11 @@
1
1
  import type { EngineNotice } from "../types.js";
2
- import { type ConsolidationGateRow, type ConsolidationPlanFile, type ConsolidationProposal, type ConsolidationRunStopReason, type MemoryConsolidationOptions } from "./consolidation.js";
2
+ import { type ConsolidationGateRead, type ConsolidationGateRow, type ConsolidationPlanFile, type ConsolidationProposal, type ConsolidationRunStopReason, type MemoryConsolidationOptions } from "./consolidation.js";
3
3
  import { foreignDanglingSessionAccounts, type EffectiveSettlement, type HoldRow } from "./delegation-settlement.js";
4
4
  import { type OriginClearanceRow } from "./origin-clearance.js";
5
5
  import { type CommittedBinding, type EraseMemoryEntriesInput, type MemoryErasureAttestation, type TransferEvidence } from "./file-backend.js";
6
6
  import { type MemoryExportBundle, type MemoryImportReport } from "./export-bundle.js";
7
7
  import { type ChallengeAssignment, type ChallengeEvent, type ControlPlaneRebuildReceipt, type StrictControlPlaneLedger, type ChallengedHistoryRow, type LineagePendingTxn, type LineagePromotion, type MemoryPartitionIncidentSink, type RetrievedAccountRow, type SessionPollutionMarkOutcome, type SessionPollutionRecord } from "./layout.js";
8
- import type { HarvestReport, MemoryAnnouncement, MemoryBackend, MemoryEntry, MemoryEntryOrigin, MemorySessionHandle, ScanFinding } from "./types.js";
8
+ import type { HarvestReport, MemoryAnnouncement, MemoryBackend, MemoryEntry, MemoryEntryOrigin, MemoryScopeEnumeration, MemorySessionHandle, ScanFinding } from "./types.js";
9
9
  /**
10
10
  * The CC `# Memory` instruction section — VERBATIM from the live capture
11
11
  * (bench/cc-parity-toolsuite/reminders/live-capture-main-session-2026-07-08.md §12), with the
@@ -514,6 +514,36 @@ export interface ConsolidationPlanSummary {
514
514
  directed?: number;
515
515
  intents?: number;
516
516
  }
517
+ /**
518
+ * design/376 slice ③ (#436) — one plan's MEMBER-LEVEL fold evidence
519
+ * ({@link MemoryEngine.readConsolidationPlanFoldEvidence}'s answer): which entry ids this plan's
520
+ * APPLIED products attest as superseded. Discriminated so an unreadable plan can never be told
521
+ * apart from an empty one by accident.
522
+ */
523
+ export type ConsolidationPlanFoldEvidence = {
524
+ state: "ok";
525
+ scope: string;
526
+ /** The commit call's requestId, frozen on the plan — the durable run-attribution anchor
527
+ * (the driver's per-cycle requestIds share the run's prefix, so a cycle whose receipt was
528
+ * lost to a crash is still attributable to its run through the plan file). */
529
+ requestId: string;
530
+ planState: ConsolidationPlanFile["state"];
531
+ /** Every supersession-edge target of an APPLIED product, deduplicated and sorted. Products
532
+ * still pending/conflicted attest nothing — the store never saw their edges land. */
533
+ supersededTargets: string[];
534
+ } | {
535
+ state: "absent";
536
+ } | {
537
+ state: "corrupt";
538
+ detail: string;
539
+ /** Present when the plan FILE parsed and only its product frontmatter is damaged (the deep
540
+ * per-product walk's corrupt arm): the frozen attribution anchors survive the damage, so a
541
+ * caller can still attribute the damaged plan to a run — the conservation wall uses this
542
+ * to fail closed ONLY on a run-owned damaged plan while a foreign/historical one is
543
+ * skipped with disclosure. Absent when the file itself is unreadable/unparseable. */
544
+ scope?: string;
545
+ requestId?: string;
546
+ };
517
547
  /** §2.2 — the recommendation notice, minted off the harvest report's advisory member (the runner's
518
548
  * hold-notice consumption point is its natural home). Undefined ⇔ this harvest crossed no edge. */
519
549
  export declare function memoryConsolidationRecommendedNotice(report: HarvestReport, sessionId?: string): EngineNotice | undefined;
@@ -956,10 +986,18 @@ export declare class MemoryEngine {
956
986
  }>;
957
987
  /**
958
988
  * §1.2-2/§1.2-3 — verb ②: mechanical acceptance + plan FREEZE + apply-to-terminal. Structural
959
- * violations (scan findings, out-of-snapshot attribution, caps, the instruction hard gate, the
960
- * supersession fuse) refuse the WHOLE plan — zero partial freeze; per-product eligibility drift
961
- * (an input's rev moved / an input got challenged between snapshot and freeze) refuses THAT
962
- * product with a disclosed reason while the rest freeze (§1.2-2 冻结时资格复验).
989
+ * violations (out-of-snapshot attribution, caps, the instruction hard gate, the supersession
990
+ * fuse, an empty plan) refuse the WHOLE plan — zero partial freeze. Per-product refusals, each
991
+ * disclosed on the receipt (`products.refused`): eligibility drift (an input's rev moved / an
992
+ * input got challenged between snapshot and freeze, §1.2-2 冻结时资格复验) AND scan findings
993
+ * (design/376 C-2① — the explicit revision of 339 §1.2-2's whole-plan clause: one product's
994
+ * refused bytes must not permanently deny service to every other fold in the plan). The scan
995
+ * seat keeps two whole-plan backstops: the scan-refusal fuse (C-2②, {@link scanRefusalCeiling})
996
+ * refuses a plan saturated with scan-refused products, and a plan the freeze emptied entirely
997
+ * refuses rather than settling as a vacuous completed run (C-2③ — completion writes `lastRunAt`
998
+ * and stamps the full eligible set into the fingerprint, blinding every later incremental
999
+ * snapshot). Intent (directed) scan findings stay whole-plan: intents are host-authored, not
1000
+ * driver products.
963
1001
  */
964
1002
  commitConsolidationPlan(cycleToken: string, proposal: ConsolidationProposal, opts: {
965
1003
  requestId: string;
@@ -1009,7 +1047,12 @@ export declare class MemoryEngine {
1009
1047
  * ABANDONED explicitly (audited; never a silent drop — unfulfilled and given-up stay two
1010
1048
  * distinguishable account facts, G24) and the seat releases; `retry` = the plan's unfulfilled
1011
1049
  * intents re-judge NOW into a NEW plan (fresh freeze, fresh baseRevs — the ONE authorized
1012
- * re-judgment point) which applies immediately. Both audited by requestId.
1050
+ * re-judgment point) which applies immediately. A retry with NOTHING to re-judge (a
1051
+ * products-only conflict plan, or every unfulfilled intent already credentialed) refuses
1052
+ * instead of settling a vacuous completed run — C-2③'s empty-plan law at this corridor's door
1053
+ * (the completion would burn the time gate and stamp the original cycle's eligible map into
1054
+ * the fingerprint); `discard` is the exit that releases the seat without wearing the
1055
+ * completed-run label. Both audited by requestId.
1013
1056
  */
1014
1057
  resolveConsolidationPlan(planId: string, action: "discard" | "retry", opts: {
1015
1058
  requestId: string;
@@ -1025,9 +1068,44 @@ export declare class MemoryEngine {
1025
1068
  private reconstructIntentsForRetry;
1026
1069
  /** §6.2 — host read face: every plan's summary (audit enumeration, zero model surface). */
1027
1070
  listConsolidationPlans(): ConsolidationPlanSummary[];
1071
+ /**
1072
+ * design/376 slice ③ (#436) — host read face: one plan's MEMBER-LEVEL fold evidence (which ids
1073
+ * its APPLIED products supersede). This is the conservation wall's terminal witness: the
1074
+ * driver's other two witnesses (per-cycle row delta, since-open store delta) are both
1075
+ * store-COUNT readings, and an unrelated fold interleaved into the run's scope can numerically
1076
+ * offset a planned member that was deleted mid-run — masking the shortfall behind a balanced
1077
+ * count. Member identity cannot be offset: a planned member either appears among some
1078
+ * run-attributed plan's applied supersession targets, sits in named residue, or the run must
1079
+ * not wear the converged label. Committed plan-file read, side-effect-free; the answer carries
1080
+ * the plan's frozen `requestId` so a caller can attribute plans to a run even across the
1081
+ * commit→receipt crash window (the plan file is durable before any receipt is).
1082
+ *
1083
+ * SCOPE of the attestation, disclosed (an outside review probed it; recorded as the designed
1084
+ * limit rather than patched): this is the committed HISTORICAL fact "this plan's edge landed" —
1085
+ * it does not re-derive whether the edge still stands NOW. A target edit or a carrier challenge
1086
+ * later SUSPENDS the edge (`deriveSupersededSet`'s revival law), and a competing writer may
1087
+ * re-fold the revived member before the run's next snapshot; the attestation still exempts it
1088
+ * and the run keeps its converged label. That label stays materially true in every reachable
1089
+ * arm: the member's content is preserved in THIS run's committed carrier either way, the store
1090
+ * is at a fixpoint (re-covered by a standing edge, or explicitly tombstoned), and the next full
1091
+ * mint re-judges the live world. A LIVE-standing re-derivation here would instead false-red the
1092
+ * honestly converged re-covered case, and the ruled evidence form is the per-plan applied edge.
1093
+ */
1094
+ readConsolidationPlanFoldEvidence(planId: string): ConsolidationPlanFoldEvidence;
1028
1095
  /** §6.2 — host read face: one scope's gate row (undefined when the store has no row — reading
1029
- * never creates the file, so the OFF deployment stays byte-identical, G1). */
1096
+ * never creates the file, so the OFF deployment stays byte-identical, G1). A corrupt store
1097
+ * ALSO answers undefined here — this face cannot distinguish the two; a consumer that needs
1098
+ * the difference reads {@link readConsolidationGateState} (#437②). */
1030
1099
  readConsolidationGate(scope: string): ConsolidationGateRow | undefined;
1100
+ /**
1101
+ * design/376 slice ③ (#437②) — host read face: one scope's gate row as a DISCRIMINATED answer.
1102
+ * The undiscriminated face above folds a corrupt store into `undefined`, so a healthy never-run
1103
+ * scope and a store every verb will refuse fail-closed were indistinguishable to an operator or
1104
+ * a scheduling read. `present` carries the row; `absent` = readable store, no row (reading never
1105
+ * creates the file — the OFF deployment stays byte-identical, G1); `corrupt` carries the
1106
+ * refusal detail. Additive; the undiscriminated face is unchanged.
1107
+ */
1108
+ readConsolidationGateState(scope: string): ConsolidationGateRead;
1031
1109
  /**
1032
1110
  * design/376 片② — host read face: one scope's committed FOLD STATE, in the freeze's own
1033
1111
  * accounting (`active` counts entries neither superseded nor excluded — byte-for-byte the
@@ -1050,6 +1128,17 @@ export declare class MemoryEngine {
1050
1128
  * (design/376 §1.6-4); path knowledge is not authority — nothing here bypasses a verb.
1051
1129
  */
1052
1130
  get controlPlaneRoot(): string;
1131
+ /**
1132
+ * design/376 slice ③ (#437③) — host read face: the store's scope enumeration, capability-probed
1133
+ * on the backend (`listScopes`, the audit-face probe family). A backend WITHOUT the face
1134
+ * answers `supported: false` — never an empty array: "cannot enumerate" and "hosts no scopes"
1135
+ * must stay two different answers, or every audit sweep over a non-enumerating backend would
1136
+ * silently claim a clean store. The File backend derives the answer from the store root's
1137
+ * durable scope registry (store-level fact — never a session roster); the SQL twins gain the
1138
+ * face through the store-contracts extension (separate ticket). A supporting backend's corrupt
1139
+ * registry THROWS fail-closed — the same reason an unsupported one never reads as empty.
1140
+ */
1141
+ listMemoryScopes(): Promise<MemoryScopeEnumeration>;
1053
1142
  /** The committed, side-effect-free audit read: the adoption-restricted committed view when the
1054
1143
  * backend offers one (zero-copy File — ledger+shadow, no disk adoption), else the non-adopting
1055
1144
  * retrieval view (copy-out File), else the backend itself (Pg/TiDB shapes — naturally committed