@sema-agent/core 5.54.0 → 5.56.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 (85) hide show
  1. package/CHANGELOG.md +160 -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/send-message-tool.js +48 -2
  7. package/dist/agents/subagent.js +250 -89
  8. package/dist/agents/verify.d.ts +27 -3
  9. package/dist/agents/verify.js +7 -2
  10. package/dist/core/auto-compaction.d.ts +17 -4
  11. package/dist/core/auto-compaction.js +3 -0
  12. package/dist/core/context-edit.d.ts +55 -6
  13. package/dist/core/context-edit.js +12 -1
  14. package/dist/core/governance-codes.js +14 -0
  15. package/dist/core/hooks.d.ts +293 -11
  16. package/dist/core/hooks.js +159 -12
  17. package/dist/core/human-input-projection.d.ts +20 -2
  18. package/dist/core/human-input-projection.js +9 -0
  19. package/dist/core/lsp-diagnostics.d.ts +19 -17
  20. package/dist/core/lsp-diagnostics.js +11 -5
  21. package/dist/core/mcp.d.ts +46 -0
  22. package/dist/core/mcp.js +132 -6
  23. package/dist/core/memory-engine/consolidation.d.ts +378 -0
  24. package/dist/core/memory-engine/consolidation.js +342 -0
  25. package/dist/core/memory-engine/dual-root.js +3 -0
  26. package/dist/core/memory-engine/engine.d.ts +237 -4
  27. package/dist/core/memory-engine/engine.js +1111 -4
  28. package/dist/core/memory-engine/export-bundle.js +9 -0
  29. package/dist/core/memory-engine/file-backend.js +27 -1
  30. package/dist/core/memory-engine/frontmatter.d.ts +20 -1
  31. package/dist/core/memory-engine/frontmatter.js +111 -0
  32. package/dist/core/memory-engine/index.d.ts +4 -2
  33. package/dist/core/memory-engine/index.js +3 -1
  34. package/dist/core/memory-engine/memory-backend-contract.js +131 -0
  35. package/dist/core/memory-engine/sync-client.js +26 -0
  36. package/dist/core/memory-engine/tools.d.ts +9 -0
  37. package/dist/core/memory-engine/tools.js +57 -13
  38. package/dist/core/memory-engine/types.d.ts +99 -0
  39. package/dist/core/memory-recall.js +4 -3
  40. package/dist/core/memory.d.ts +33 -3
  41. package/dist/core/memory.js +6 -4
  42. package/dist/core/permission-rules.d.ts +30 -0
  43. package/dist/core/permission-rules.js +71 -8
  44. package/dist/core/reminder-disclosure.d.ts +29 -4
  45. package/dist/core/reminder-disclosure.js +60 -12
  46. package/dist/core/runner/prepare-memory.js +7 -2
  47. package/dist/core/runner/prepare-task.d.ts +39 -1
  48. package/dist/core/runner/prepare-task.js +63 -35
  49. package/dist/core/runner/runtask.d.ts +8 -1
  50. package/dist/core/runner/runtask.js +170 -31
  51. package/dist/core/runner/session-rule-policy.js +5 -3
  52. package/dist/core/runner/synthetic-tools.js +4 -2
  53. package/dist/core/runner/turn-attachments.d.ts +16 -6
  54. package/dist/core/runner/turn-attachments.js +34 -20
  55. package/dist/core/session-reconcile.d.ts +32 -0
  56. package/dist/core/session-reconcile.js +15 -0
  57. package/dist/core/task-notification.d.ts +34 -7
  58. package/dist/core/task-notification.js +11 -1
  59. package/dist/core/task-registry-agent.d.ts +20 -3
  60. package/dist/core/task-registry-agent.js +31 -2
  61. package/dist/core/tool-policy.d.ts +23 -0
  62. package/dist/core/tool-policy.js +29 -13
  63. package/dist/core/types.d.ts +126 -17
  64. package/dist/core/untrusted-egress.js +12 -2
  65. package/dist/core/untrusted-text.d.ts +189 -3
  66. package/dist/core/untrusted-text.js +424 -6
  67. package/dist/engine/compaction/compaction.d.ts +77 -7
  68. package/dist/engine/compaction/compaction.js +98 -9
  69. package/dist/engine/compaction/utils.d.ts +4 -0
  70. package/dist/engine/compaction/utils.js +6 -0
  71. package/dist/engine/harness/agent-harness.d.ts +84 -0
  72. package/dist/engine/harness/agent-harness.js +88 -12
  73. package/dist/engine/harness/messages.d.ts +4 -2
  74. package/dist/engine/harness/messages.js +7 -2
  75. package/dist/engine/harness/types.d.ts +11 -5
  76. package/dist/engine/loop/types.d.ts +14 -0
  77. package/dist/engine/session/import-validate.js +10 -0
  78. package/dist/engine/session/session.js +2 -2
  79. package/dist/index.d.ts +1 -1
  80. package/dist/index.js +1 -1
  81. package/dist/orchestration/run-spec.js +8 -1
  82. package/dist/prompts/default.d.ts +22 -6
  83. package/dist/tools/fs/index.d.ts +3 -1
  84. package/package.json +1 -1
  85. package/test/export-surface.snapshot.json +28 -1
@@ -0,0 +1,342 @@
1
+ import { existsSync, readdirSync, renameSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { atomicWriteFileSync, lockedStrictUpdate, readStrictSidecar, ControlPlaneCorruptError, ensureDirExists } from "./layout.js";
4
+ export function deriveSupersededSet(headers, opts = {}) {
5
+ const byId = new Map();
6
+ for (const h of headers)
7
+ byId.set(h.id, h);
8
+ const out = new Map();
9
+ for (const h of headers) {
10
+ const d = h.distilled;
11
+ if (d === undefined || d.supersedes.length === 0)
12
+ continue;
13
+ if (opts.excluded?.(h.id) === true)
14
+ continue;
15
+ if (h.rev !== d.carrierRev)
16
+ continue;
17
+ for (const edge of d.supersedes) {
18
+ const target = byId.get(edge.id);
19
+ if (target === undefined)
20
+ continue;
21
+ if (target.scope !== h.scope)
22
+ continue;
23
+ if (target.rev !== edge.rev)
24
+ continue;
25
+ if (!out.has(edge.id))
26
+ out.set(edge.id, h.id);
27
+ }
28
+ }
29
+ return out;
30
+ }
31
+ export function memorySupersededNote(carrierId) {
32
+ return `note: a newer consolidated entry supersedes this one — memory entry ${carrierId} carries the current form; this entry remains readable as evidence.`;
33
+ }
34
+ export const MEMORY_SEARCH_SUPERSEDED_TAG = "[superseded]";
35
+ export const CONSOLIDATION_DEFAULTS = {
36
+ supersedeRatioCap: 0.25,
37
+ supersedeAbsoluteFloor: 4,
38
+ maxProductsPerRun: 64,
39
+ maxInputsPerProduct: 32,
40
+ maxDirectedPatchesPerPlan: 32,
41
+ minRunIntervalMs: 24 * 60 * 60 * 1000,
42
+ minSessionsBetweenRuns: 5,
43
+ };
44
+ export const CONSOLIDATION_FORCE_THROTTLE_FLOOR_MS = 60_000;
45
+ export const CONSOLIDATION_SESSION_RING_MAX = 64;
46
+ export const CONSOLIDATION_SEAT_FREEZE_GRACE_MS = 10 * 60_000;
47
+ function coded(message, code) {
48
+ const e = new Error(message);
49
+ e.code = code;
50
+ return e;
51
+ }
52
+ export function screenConsolidationOptions(raw, provenance) {
53
+ if (provenance === "off") {
54
+ throw coded(`MemoryEngineOptions.consolidation cannot be enabled under provenance: "off" — the consolidation fold law must be able to mint origin markers (a marked input's product would otherwise commit unmarked). Enable provenance "carry" or remove the consolidation options.`, "config.memory_consolidation_provenance_off");
55
+ }
56
+ const num = (name, v, check, want) => {
57
+ if (v === undefined)
58
+ return CONSOLIDATION_DEFAULTS[name];
59
+ if (typeof v !== "number" || !Number.isFinite(v) || !check(v)) {
60
+ const got = typeof v === "number" ? String(v) : v === null ? "null" : typeof v;
61
+ throw coded(`MemoryEngineOptions.consolidation.${name} must be ${want} when present (got ${got}) — an unevaluable knob is refused loudly, never folded to the default.`, "config.memory_consolidation");
62
+ }
63
+ return v;
64
+ };
65
+ const screened = {
66
+ supersedeRatioCap: num("supersedeRatioCap", raw.supersedeRatioCap, (n) => n > 0 && n <= 1, "a number in (0, 1]"),
67
+ supersedeAbsoluteFloor: num("supersedeAbsoluteFloor", raw.supersedeAbsoluteFloor, (n) => Number.isInteger(n) && n >= 0, "a non-negative integer"),
68
+ maxProductsPerRun: num("maxProductsPerRun", raw.maxProductsPerRun, (n) => Number.isInteger(n) && n >= 1, "a positive integer"),
69
+ maxInputsPerProduct: num("maxInputsPerProduct", raw.maxInputsPerProduct, (n) => Number.isInteger(n) && n >= 1, "a positive integer"),
70
+ maxDirectedPatchesPerPlan: num("maxDirectedPatchesPerPlan", raw.maxDirectedPatchesPerPlan, (n) => Number.isInteger(n) && n >= 1, "a positive integer"),
71
+ minRunIntervalMs: num("minRunIntervalMs", raw.minRunIntervalMs, (n) => n > 0, "a positive number of milliseconds"),
72
+ minSessionsBetweenRuns: num("minSessionsBetweenRuns", raw.minSessionsBetweenRuns, (n) => Number.isInteger(n) && n >= 0, "a non-negative integer"),
73
+ multiNode: raw.multiNode === true,
74
+ ...(raw.lease !== undefined ? { lease: raw.lease } : {}),
75
+ };
76
+ if (raw.multiNode !== undefined && raw.multiNode !== true) {
77
+ throw coded(`MemoryEngineOptions.consolidation.multiNode must be exactly true when present (a posture declaration, not a boolean knob).`, "config.memory_consolidation");
78
+ }
79
+ if (screened.multiNode && screened.lease === undefined) {
80
+ 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
+ }
82
+ if (raw.lease !== undefined && typeof raw.lease.acquire !== "function") {
83
+ throw coded(`MemoryEngineOptions.consolidation.lease.acquire must be a function.`, "config.memory_consolidation");
84
+ }
85
+ return screened;
86
+ }
87
+ export function supersessionFuseCeiling(activeSetSize, opts) {
88
+ if (activeSetSize <= 1)
89
+ return 0;
90
+ return Math.min(activeSetSize - 1, Math.max(opts.supersedeAbsoluteFloor, Math.floor(opts.supersedeRatioCap * activeSetSize)));
91
+ }
92
+ export function consolidationTypeEligible(type) {
93
+ return type !== "episodic" && type !== "procedural";
94
+ }
95
+ export const CONSOLIDATION_GATE_FILE = "consolidation-gate.json";
96
+ export const CONSOLIDATION_PLANS_DIR = "consolidation-plans";
97
+ export const CONSOLIDATION_INTENTS_FILE = "consolidation-intents.json";
98
+ function coerceGateFile(raw) {
99
+ if (raw === undefined)
100
+ return { v: 1, scopes: {} };
101
+ if (typeof raw !== "object" || raw === null || raw.v !== 1 || typeof raw.scopes !== "object" || raw.scopes === null) {
102
+ throw new ControlPlaneCorruptError("memory consolidation gate store has an unrecognized shape (fail-closed sidecar)");
103
+ }
104
+ const scopes = raw.scopes;
105
+ const out = { v: 1, scopes: {} };
106
+ for (const [scope, rowRaw] of Object.entries(scopes)) {
107
+ if (typeof rowRaw !== "object" || rowRaw === null)
108
+ throw new ControlPlaneCorruptError(`memory consolidation gate store row for scope ${JSON.stringify(scope)} is malformed (fail-closed sidecar)`);
109
+ const r = rowRaw;
110
+ const sessions = r["sessions"];
111
+ if (typeof sessions !== "object" ||
112
+ sessions === null ||
113
+ typeof sessions.count !== "number" ||
114
+ !Array.isArray(sessions.ring ?? []) ||
115
+ (sessions.ring !== undefined && !sessions.ring.every((s) => typeof s === "string")) ||
116
+ typeof r["epoch"] !== "number" ||
117
+ !Number.isInteger(r["epoch"])) {
118
+ throw new ControlPlaneCorruptError(`memory consolidation gate store row for scope ${JSON.stringify(scope)} is malformed (fail-closed sidecar)`);
119
+ }
120
+ out.scopes[scope] = rowRaw;
121
+ }
122
+ return out;
123
+ }
124
+ function freshRow() {
125
+ return { sessions: { count: 0, ring: [] }, epoch: 0 };
126
+ }
127
+ export function updateConsolidationGate(controlDir, fn) {
128
+ return lockedStrictUpdate(controlDir, CONSOLIDATION_GATE_FILE, "memory consolidation gate store", coerceGateFile, fn);
129
+ }
130
+ export function readConsolidationGateFile(controlDir) {
131
+ return coerceGateFile(readStrictSidecar(controlDir, CONSOLIDATION_GATE_FILE, "memory consolidation gate store"));
132
+ }
133
+ export function consolidationGateFileExists(controlDir) {
134
+ return existsSync(join(controlDir, CONSOLIDATION_GATE_FILE));
135
+ }
136
+ export function recordConsolidationSession(controlDir, scope, sessionId, opts, now) {
137
+ return updateConsolidationGate(controlDir, (file) => {
138
+ const row = file.scopes[scope] ?? freshRow();
139
+ if (row.sessions.ring.includes(sessionId)) {
140
+ return { result: { recommended: false, sessionsSince: row.sessions.count } };
141
+ }
142
+ row.sessions.count += 1;
143
+ row.sessions.ring.push(sessionId);
144
+ if (row.sessions.ring.length > CONSOLIDATION_SESSION_RING_MAX)
145
+ row.sessions.ring.splice(0, row.sessions.ring.length - CONSOLIDATION_SESSION_RING_MAX);
146
+ const at = now();
147
+ const timeOpen = row.lastRunAt === undefined || at - row.lastRunAt >= opts.minRunIntervalMs;
148
+ const sessionsOpen = row.sessions.count >= opts.minSessionsBetweenRuns;
149
+ const edgeArmed = row.lastRecommendedAt === undefined || (row.lastRunAt !== undefined && row.lastRecommendedAt <= row.lastRunAt);
150
+ let recommended = false;
151
+ if (timeOpen && sessionsOpen && edgeArmed) {
152
+ recommended = true;
153
+ row.lastRecommendedAt = at;
154
+ }
155
+ file.scopes[scope] = row;
156
+ return { next: file, result: { recommended, sessionsSince: row.sessions.count } };
157
+ });
158
+ }
159
+ export function recordActiveSetBaseline(controlDir, scope, size, ratioCap, _now) {
160
+ return updateConsolidationGate(controlDir, (file) => {
161
+ const row = file.scopes[scope] ?? freshRow();
162
+ const prev = row.activeSetBaseline;
163
+ row.activeSetBaseline = size;
164
+ file.scopes[scope] = row;
165
+ const collapsed = prev !== undefined && prev > 0 && size < prev && (prev - size) / prev > ratioCap ? { from: prev, to: size } : undefined;
166
+ return { next: file, result: collapsed !== undefined ? { collapsed } : {} };
167
+ });
168
+ }
169
+ export function isValidConsolidationPlanId(value) {
170
+ return /^[A-Za-z0-9][A-Za-z0-9_-]{7,63}$/.test(value);
171
+ }
172
+ function planPath(controlDir, planId) {
173
+ if (!isValidConsolidationPlanId(planId)) {
174
+ throw new ControlPlaneCorruptError(`consolidation plan id ${JSON.stringify(planId.slice(0, 80))} fails the id grammar — a plan id doubles as a control-plane file name and must never be joined as a path`);
175
+ }
176
+ return join(controlDir, CONSOLIDATION_PLANS_DIR, `${planId}.json`);
177
+ }
178
+ export function writeConsolidationPlan(controlDir, plan) {
179
+ ensureDirExists(join(controlDir, CONSOLIDATION_PLANS_DIR));
180
+ atomicWriteFileSync(planPath(controlDir, plan.planId), `${JSON.stringify(plan, null, 2)}\n`);
181
+ }
182
+ const CONSOLIDATION_PLAN_STATES = new Set(["open", "applying", "completed", "conflict", "discarded"]);
183
+ const CONSOLIDATION_DIRECTED_STATES = new Set(["pending", "applied", "satisfied", "conflict"]);
184
+ const CONSOLIDATION_INTENT_STATES = new Set(["pending", "settled", "abandoned"]);
185
+ function planShapeInvalid(raw, planId) {
186
+ if (typeof raw !== "object" || raw === null)
187
+ return "not an object";
188
+ const p = raw;
189
+ if (p["v"] !== 1)
190
+ return "unrecognized version";
191
+ if (p["planId"] !== planId)
192
+ return "planId mismatch";
193
+ if (typeof p["scope"] !== "string" || p["scope"].length === 0)
194
+ return "missing scope";
195
+ if (typeof p["requestId"] !== "string")
196
+ return "missing requestId";
197
+ if (typeof p["createdAt"] !== "number")
198
+ return "missing createdAt";
199
+ if (typeof p["visibleMarked"] !== "boolean")
200
+ return "missing visibleMarked";
201
+ if (typeof p["state"] !== "string" || !CONSOLIDATION_PLAN_STATES.has(p["state"]))
202
+ return "unrecognized state";
203
+ if (!Array.isArray(p["products"]) || !p["products"].every((e) => typeof e === "object" && e !== null && typeof e.id === "string"))
204
+ return "malformed products";
205
+ const productIds = p["products"].map((e) => e.id);
206
+ const states = p["productStates"];
207
+ if (typeof states !== "object" || states === null)
208
+ return "malformed productStates";
209
+ const stateKeys = Object.keys(states);
210
+ if (stateKeys.length !== productIds.length || !productIds.every((id) => states[id] === "pending" || states[id] === "applied" || states[id] === "conflict")) {
211
+ return "productStates do not cover the product set one-to-one";
212
+ }
213
+ if (!Array.isArray(p["directed"]) ||
214
+ !p["directed"].every((d0) => {
215
+ if (typeof d0 !== "object" || d0 === null)
216
+ return false;
217
+ const d = d0;
218
+ if (typeof d["id"] !== "string" || typeof d["intentRequestId"] !== "string" || typeof d["baseRev"] !== "string")
219
+ return false;
220
+ if (d["op"] !== "update" && d["op"] !== "delete")
221
+ return false;
222
+ if (typeof d["state"] !== "string" || !CONSOLIDATION_DIRECTED_STATES.has(d["state"]))
223
+ return false;
224
+ if (d["op"] === "update") {
225
+ if (typeof d["plannedPostRev"] !== "string")
226
+ return false;
227
+ if (d["state"] === "pending" && (typeof d["entry"] !== "object" || d["entry"] === null))
228
+ return false;
229
+ }
230
+ else if (d["entry"] !== undefined)
231
+ return false;
232
+ return true;
233
+ })) {
234
+ return "malformed directed set";
235
+ }
236
+ const intentIds = new Set();
237
+ if (!Array.isArray(p["intents"]) ||
238
+ !p["intents"].every((i0) => {
239
+ if (typeof i0 !== "object" || i0 === null)
240
+ return false;
241
+ const i = i0;
242
+ if (typeof i["requestId"] !== "string")
243
+ return false;
244
+ intentIds.add(i["requestId"]);
245
+ return typeof i["state"] === "string" && CONSOLIDATION_INTENT_STATES.has(i["state"]);
246
+ })) {
247
+ return "malformed intents";
248
+ }
249
+ if (!p["directed"].every((d) => intentIds.has(d.intentRequestId)))
250
+ return "directed patch references an undeclared intent";
251
+ if (!Array.isArray(p["audit"]))
252
+ return "malformed audit trail";
253
+ return undefined;
254
+ }
255
+ export function readConsolidationPlan(controlDir, planId) {
256
+ if (!isValidConsolidationPlanId(planId))
257
+ return { state: "absent" };
258
+ const path = planPath(controlDir, planId);
259
+ if (!existsSync(path))
260
+ return { state: "absent" };
261
+ try {
262
+ const raw = readStrictSidecar(join(controlDir, CONSOLIDATION_PLANS_DIR), `${planId}.json`, "memory consolidation plan");
263
+ const bad = planShapeInvalid(raw, planId);
264
+ if (bad !== undefined)
265
+ return { state: "corrupt", detail: bad };
266
+ return { state: "ok", plan: raw };
267
+ }
268
+ catch (err) {
269
+ return { state: "corrupt", detail: err instanceof Error ? err.message : String(err) };
270
+ }
271
+ }
272
+ export function listConsolidationPlanIds(controlDir) {
273
+ const dir = join(controlDir, CONSOLIDATION_PLANS_DIR);
274
+ if (!existsSync(dir))
275
+ return [];
276
+ try {
277
+ return readdirSync(dir)
278
+ .filter((f) => f.endsWith(".json") && !f.endsWith(".corrupt.json"))
279
+ .map((f) => f.slice(0, -".json".length))
280
+ .filter((id) => isValidConsolidationPlanId(id));
281
+ }
282
+ catch {
283
+ return [];
284
+ }
285
+ }
286
+ export function quarantineCorruptPlan(controlDir, planId) {
287
+ if (!isValidConsolidationPlanId(planId))
288
+ return undefined;
289
+ const from = planPath(controlDir, planId);
290
+ if (!existsSync(from))
291
+ return undefined;
292
+ const name = `${planId}.corrupt.json`;
293
+ try {
294
+ renameSync(from, join(controlDir, CONSOLIDATION_PLANS_DIR, name));
295
+ return name;
296
+ }
297
+ catch {
298
+ return undefined;
299
+ }
300
+ }
301
+ function coerceIntentFile(raw) {
302
+ if (raw === undefined)
303
+ return { v: 1, rows: {} };
304
+ if (typeof raw !== "object" || raw === null || raw.v !== 1 || typeof raw.rows !== "object" || raw.rows === null) {
305
+ throw new ControlPlaneCorruptError("memory consolidation intent-credential account has an unrecognized shape (fail-closed sidecar)");
306
+ }
307
+ return raw;
308
+ }
309
+ export function recordIntentCredentials(controlDir, rows) {
310
+ if (rows.length === 0)
311
+ return [];
312
+ return lockedStrictUpdate(controlDir, CONSOLIDATION_INTENTS_FILE, "memory consolidation intent-credential account", coerceIntentFile, (file) => {
313
+ const written = [];
314
+ for (const row of rows) {
315
+ if (file.rows[row.requestId] !== undefined)
316
+ continue;
317
+ file.rows[row.requestId] = row;
318
+ written.push(row.requestId);
319
+ }
320
+ return written.length > 0 ? { next: file, result: written } : { result: written };
321
+ });
322
+ }
323
+ export function readIntentCredentials(controlDir) {
324
+ const raw = readStrictSidecar(controlDir, CONSOLIDATION_INTENTS_FILE, "memory consolidation intent-credential account");
325
+ return coerceIntentFile(raw).rows;
326
+ }
327
+ export function deriveProductSlug(name, fallback) {
328
+ const base = (name ?? "").toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[-.]+|[-.]+$/g, "");
329
+ return base.length > 0 ? base : fallback;
330
+ }
331
+ export class ConsolidationRefusedError extends Error {
332
+ code;
333
+ reasons;
334
+ constructor(code, message, reasons = []) {
335
+ super(reasons.length > 0 ? `${message}\n${reasons.map((r) => ` - ${r}`).join("\n")}` : message);
336
+ this.code = code;
337
+ this.reasons = reasons;
338
+ }
339
+ }
340
+ export function productAddPatch(product) {
341
+ return { op: "add", id: product.id, entry: product, guard: "absent" };
342
+ }
@@ -75,6 +75,9 @@ export function mergeHarvestReports(a, b) {
75
75
  const qf = [...(a.quarantineFailures ?? []), ...(b.quarantineFailures ?? [])];
76
76
  if (qf.length > 0)
77
77
  merged.quarantineFailures = qf;
78
+ const rec = a.consolidationRecommended ?? b.consolidationRecommended;
79
+ if (rec !== undefined)
80
+ merged.consolidationRecommended = rec;
78
81
  const ac = a.containment;
79
82
  const bc = b.containment;
80
83
  if (ac !== undefined || bc !== undefined) {
@@ -1,10 +1,11 @@
1
1
  import type { EngineNotice } from "../types.js";
2
+ import { type ConsolidationGateRow, type ConsolidationPlanFile, type ConsolidationProposal, type MemoryConsolidationOptions } from "./consolidation.js";
2
3
  import { foreignDanglingSessionAccounts, type EffectiveSettlement, type HoldRow } from "./delegation-settlement.js";
3
4
  import { type OriginClearanceRow } from "./origin-clearance.js";
4
5
  import { type CommittedBinding, type EraseMemoryEntriesInput, type MemoryErasureAttestation, type TransferEvidence } from "./file-backend.js";
5
6
  import { type MemoryExportBundle, type MemoryImportReport } from "./export-bundle.js";
6
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";
7
- import type { HarvestReport, MemoryAnnouncement, MemoryBackend, MemoryEntryOrigin, MemorySessionHandle, ScanFinding } from "./types.js";
8
+ import type { HarvestReport, MemoryAnnouncement, MemoryBackend, MemoryEntry, MemoryEntryOrigin, MemorySessionHandle, ScanFinding } from "./types.js";
8
9
  /**
9
10
  * The CC `# Memory` instruction section — VERBATIM from the live capture
10
11
  * (bench/cc-parity-toolsuite/reminders/live-capture-main-session-2026-07-08.md §12), with the
@@ -100,10 +101,12 @@ export declare const MEMORY_ANNOUNCEMENT_READONLY_CODA = "The memory store itsel
100
101
  * false` — where the runner's write gate also refuses the file channel, keeping the engine-refusal
101
102
  * sentence true), and for a write-less roster the persistence inference cannot vouch for. A merely
102
103
  * Write-less roster does NOT qualify — other tools can still write the root. Name-free by
103
- * construction (#181 class — it names no tool), and NOT part of the CC-verbatim capture: CC has no
104
- * read-only memory layering, so there is nothing to capture; the section is sema-authored.
104
+ * construction (#181 class — it names no tool). The closing sentence is CC-verbatim (2.1.223
105
+ * read-only memory arm the same sentence CC uses for both its whole-memory and team-memory
106
+ * read-only states); the state sentence above it is sema-authored context (CC's surrounding prose
107
+ * describes its own file layout, which does not transfer here).
105
108
  */
106
- export declare const MEMORY_READONLY_NOTICE = "# Memory\n\nYou have READ-ONLY access to persistent memory in this session: stored notes are available below, but this session has no memory write channel \u2014 the engine will not accept writes into the memory store. If the user asks you to remember something for later, say plainly that you cannot save it in this session \u2014 never claim to have noted or remembered it.";
109
+ export declare const MEMORY_READONLY_NOTICE = "# Memory\n\nYou have READ-ONLY access to persistent memory in this session: stored notes are available below, but this session has no memory write channel \u2014 the engine will not accept writes into the memory store. If the user asks you to remember something, explain that memory is read-only in this session.";
107
110
  /** CC index-injection parameters: MEMORY.md's first 200 lines / 25KB enter the prompt. */
108
111
  export declare const MEMORY_INDEX_MAX_LINES = 200;
109
112
  export declare const MEMORY_INDEX_MAX_BYTES: number;
@@ -201,6 +204,20 @@ export interface MemoryEngineOptions {
201
204
  * Absent ⇒ advisory-silent; neither a mount nor a harvest ever fails over this seat.
202
205
  */
203
206
  onIncident?: MemoryPartitionIncidentSink;
207
+ /**
208
+ * design/339 §6.1 — the v3 consolidation write protocol's master switch. ABSENT = OFF (the
209
+ * shipped default: continuous automatic consolidation degrading useful memory is the研究-backed
210
+ * failure mode, so the protocol only arms on an explicit host decision): the four consolidation
211
+ * verbs refuse loudly (`memory.consolidation_disabled`), the gate store writes nothing (no file
212
+ * is ever created), the recommendation seat never mints, and the control plane stays
213
+ * byte-identical to a pre-339 deployment (G1). PRESENT = enabled; every knob screens at
214
+ * construction (#123 bad-value loudness — see {@link import("./consolidation.js").screenConsolidationOptions}),
215
+ * `provenance: "off"` beside it refuses at construction (D-12a: the fold law must be able to
216
+ * mint), and `multiNode: true` without an injected lease refuses (D-13a). Note the READ-side
217
+ * active-set treatment (index rows / search filter / get disclosure) is NOT behind this switch —
218
+ * supersession edges are store-level data facts and take effect wherever they exist (D-4a).
219
+ */
220
+ consolidation?: MemoryConsolidationOptions;
204
221
  }
205
222
  /** What {@link MemoryEngine.inject} produces: the trusted CC-verbatim instruction section + the
206
223
  * FENCED (untrusted-downgraded) derived index, and the combined block for the memory prompt slot. */
@@ -426,6 +443,103 @@ export declare function memoryHarvestQuarantinedNotice(input: {
426
443
  * identifiers, neutralized and length-capped before they ride the message or the detail.
427
444
  */
428
445
  export declare function memoryHoldNotices(report: HarvestReport, sessionId?: string): EngineNotice[];
446
+ /** §6.3 B-4 — verb ②'s receipt. */
447
+ export interface ConsolidationCommitReceipt {
448
+ planId: string;
449
+ state: "completed" | "conflict";
450
+ products: {
451
+ frozen: number;
452
+ applied: number;
453
+ conflicts: number;
454
+ refused: Array<{
455
+ index: number;
456
+ reason: string;
457
+ }>;
458
+ };
459
+ directed: {
460
+ frozen: number;
461
+ applied: number;
462
+ satisfied: number;
463
+ conflicts: number;
464
+ };
465
+ intents: {
466
+ settled: string[];
467
+ pending: string[];
468
+ };
469
+ foldedOrigin: boolean;
470
+ notices: EngineNotice[];
471
+ }
472
+ export interface ConsolidationReconcileReport {
473
+ replayed?: {
474
+ planId: string;
475
+ state: "completed" | "conflict";
476
+ };
477
+ releasedOrphanSeat?: string;
478
+ /** codex r2 — a seat whose plan file is absent but whose claim is YOUNGER than the freeze grace:
479
+ * a live commit is (very likely) mid-freeze; reconcile leaves it alone and reports it here. */
480
+ seatInFlight?: string;
481
+ corruptPlan?: {
482
+ planId: string;
483
+ detail: string;
484
+ };
485
+ propagatedChallenges: string[];
486
+ sweepError?: string;
487
+ }
488
+ export interface ConsolidationResolveReceipt {
489
+ planId: string;
490
+ action: "discard" | "retry";
491
+ state: "discarded" | "completed" | "conflict";
492
+ custody?: string;
493
+ abandonedIntents?: string[];
494
+ retryPlanId?: string;
495
+ retryReceipt?: ConsolidationCommitReceipt;
496
+ }
497
+ export interface ConsolidationPlanSummary {
498
+ planId: string;
499
+ corrupt?: true;
500
+ scope?: string;
501
+ state?: ConsolidationPlanFile["state"];
502
+ createdAt?: number;
503
+ products?: number;
504
+ directed?: number;
505
+ intents?: number;
506
+ }
507
+ /** §2.2 — the recommendation notice, minted off the harvest report's advisory member (the runner's
508
+ * hold-notice consumption point is its natural home). Undefined ⇔ this harvest crossed no edge. */
509
+ export declare function memoryConsolidationRecommendedNotice(report: HarvestReport, sessionId?: string): EngineNotice | undefined;
510
+ export declare function memoryConsolidationCommittedNotice(input: {
511
+ planId: string;
512
+ scope: string;
513
+ products: number;
514
+ superseded: number;
515
+ intents: number;
516
+ }): EngineNotice;
517
+ /** The CONFLICT parking notice. Two things it must not overstate:
518
+ * - the CAUSE. A park is reached three ways: a concurrent write takes a target to a third state
519
+ * (the CAS arm), the store declines a patch outright (an immutability guard, a malformed-patch
520
+ * refusal), or the engine's own re-judgment parks a row before any patch is offered at all (a
521
+ * retry whose target moved scope, vanished, or has no replayable terminal). A fixed label naming
522
+ * only the first would report a race that never happened, and naming only the first two would
523
+ * claim a store answer that was never asked for. The per-target reason is verbatim in the plan
524
+ * file's audit rows for ALL THREE — the re-judgment writes its own rows precisely so this
525
+ * sentence stays true.
526
+ * - the EXTENT. A park is plan-wide but application is per-row: a batch answers applied and
527
+ * conflicted siblings together, and the product batch runs before the directed one, so a parked
528
+ * plan may well have committed some of its patches. "Nothing was overwritten" is true only of
529
+ * the REFUSED targets, and `discard` rolls nothing back — an operator reading otherwise would
530
+ * abandon a plan believing it had no effects. The counts live on the receipt (products/directed
531
+ * applied vs conflicts); this notice stays ids-only and points at them. */
532
+ export declare function memoryConsolidationConflictNotice(input: {
533
+ planId: string;
534
+ scope: string;
535
+ }): EngineNotice;
536
+ /** The refusal's notice dialect (the verb itself throws the coded {@link ConsolidationRefusedError};
537
+ * a host converting the throw into its notice stream uses this one spelling). */
538
+ export declare function memoryConsolidationRefusedNotice(input: {
539
+ code: string;
540
+ scope?: string;
541
+ reason?: string;
542
+ }): EngineNotice;
429
543
  export declare class MemoryEngine {
430
544
  private readonly backend;
431
545
  private readonly memoryDir;
@@ -446,6 +560,8 @@ export declare class MemoryEngine {
446
560
  /** A backend-pinned directory (FileBackend zero-copy detection). */
447
561
  private readonly backendPinnedRoot?;
448
562
  constructor(opts: MemoryEngineOptions);
563
+ /** {@link MemoryEngineOptions.consolidation}, screened — undefined ⇔ the protocol is OFF. */
564
+ private readonly consolidation?;
449
565
  /**
450
566
  * The ONE delivery point for a failed announcement-queue operation (`memory.announce_failed`, ruled
451
567
  * 2026-08-03). Every enqueue/drain call site stays fail-OPEN — a broken queue must never fail a
@@ -763,6 +879,123 @@ export declare class MemoryEngine {
763
879
  /** Read face: the dangling foreign session-account rows a given session would classify residue
764
880
  * against (host audit for the §3.6 valve). */
765
881
  readDanglingSessionAccounts(selfSessionId: string): ReturnType<typeof foreignDanglingSessionAccounts>;
882
+ /** The coded off-switch refusal every verb answers first (G1). */
883
+ private requireConsolidation;
884
+ /** Gate-store read with the strict fail-closed translation (F5: corrupt ⇒ verbs refuse coded;
885
+ * read faces and harvests are untouched — they never come through here). */
886
+ private readConsolidationGateOrRefuse;
887
+ /** §1.7 — the lease check run before EVERY apply batch (fencing-token re-validation; the token
888
+ * narrows a stale holder's overlap to a single batch — §1.2-4 names the in-batch residue).
889
+ * Absent seat ⇒ single-machine deployment, the plan-seat CAS is the mutex. */
890
+ /** §1.7 — best-effort lease RELEASE at every verb exit (an outside pass's finding, adopted:
891
+ * acquire without a paired release starved every later worker on explicit-release lease
892
+ * implementations until expiry). Releasing beside an UNFINISHED durable plan is safe — the
893
+ * plan replays under whichever holder next acquires, and the fencing re-check before each
894
+ * batch keeps a stale holder to one batch. Failures die here (the lease is cost suppression,
895
+ * never safety — a failed release converges at the lease's own expiry). */
896
+ private releaseConsolidationLease;
897
+ private consolidationLeaseCurrent;
898
+ /** §3.1 — the candidate-eligibility context, read once per snapshot/freeze pass off the
899
+ * committed faces and governance accounts (write and read sides share this predicate). */
900
+ private consolidationEligibility;
901
+ /**
902
+ * §1.2-1 — verb ①: snapshot the candidate set (committed, side-effect-free read face), gate-
903
+ * checked (§2.2 案 c: mutex and the same-scope throttle floor are HARD; the time and session
904
+ * gates yield to `force` with a requestId audit). Increments are CHANGE-DRIVEN: the candidate
905
+ * set is the eligible entries that are new to — or moved rev against — the last completed run's
906
+ * fingerprint; a lost/absent fingerprint answers the full set (conservative). `full: true`
907
+ * requests the full set explicitly (a host's re-distillation choice).
908
+ */
909
+ snapshotConsolidationCandidates(scope: string, opts?: {
910
+ force?: {
911
+ requestId: string;
912
+ };
913
+ full?: boolean;
914
+ }): Promise<{
915
+ cycleToken: string;
916
+ watermark: {
917
+ epoch: number;
918
+ at: number;
919
+ };
920
+ candidates: Array<{
921
+ entry: MemoryEntry;
922
+ marked: boolean;
923
+ }>;
924
+ }>;
925
+ /**
926
+ * §1.2-2/§1.2-3 — verb ②: mechanical acceptance + plan FREEZE + apply-to-terminal. Structural
927
+ * violations (scan findings, out-of-snapshot attribution, caps, the instruction hard gate, the
928
+ * supersession fuse) refuse the WHOLE plan — zero partial freeze; per-product eligibility drift
929
+ * (an input's rev moved / an input got challenged between snapshot and freeze) refuses THAT
930
+ * product with a disclosed reason while the rest freeze (§1.2-2 冻结时资格复验).
931
+ */
932
+ commitConsolidationPlan(cycleToken: string, proposal: ConsolidationProposal, opts: {
933
+ requestId: string;
934
+ }): Promise<ConsolidationCommitReceipt>;
935
+ /** §1.2-2/§1.2-3 — the freeze: mechanical acceptance over the committed NOW + assembly of the
936
+ * immutable plan workbook (engine-authored frontmatter: minted ids, folded origin, distilled
937
+ * blocks with the carrierRev anchor; per-product rev pre-computed). */
938
+ private freezeConsolidationPlan;
939
+ /**
940
+ * §1.3/§1.5-2 — the apply-to-terminal leg (first run AND every replay: the plan is the ONLY
941
+ * input — no distillation ever re-runs). Idempotent by construction: product adds ride
942
+ * `guard: "absent"` over plan-frozen ids (an identical replay is a no-op), directed patches
943
+ * judge conflicts TERMINAL-STATE-wise (update: committed rev == plannedPostRev ⇒ satisfied;
944
+ * delete: absence ⇒ satisfied — this arm has no re-record leg, so acknowledging absence can
945
+ * revive nothing; attribution degrades to "unattributed" across the crash micro-window while
946
+ * the judgment never degrades, r2-4/G25).
947
+ */
948
+ private applyConsolidationPlan;
949
+ /** Terminal settlement: per-item terminals → the plan terminal, the credential write (§1.5-3:
950
+ * ALL-satisfied only), the gate watermark/fingerprint/seat update and the announcements. */
951
+ private settleConsolidationPlan;
952
+ private consolidationReceipt;
953
+ /**
954
+ * §1.7/§1.8 — verb ③: idempotent replay/convergence. Replays the seated plan to terminal (a
955
+ * crash at ANY point converges — the plan is the only input), releases an orphan seat (a crash
956
+ * between seat and freeze left nothing to replay: the plan never returned to its caller), and
957
+ * runs the RETROACTIVE challenge propagation sweep (D-6a: committed products whose
958
+ * `distilled.inputs` name a currently-challenged id are challenged mechanically — the lineage
959
+ * travels with the entry, so products that arrived by sync/bundle propagate too, r1-4/G19).
960
+ */
961
+ reconcileConsolidation(scope: string): Promise<ConsolidationReconcileReport>;
962
+ /** The stale-claim orphan-seat release (extracted for the in-flight split above): crash between
963
+ * seat and plan-file write — nothing frozen, nothing to replay — release the seat with an audit
964
+ * line (the commit call never returned a planId to its caller). */
965
+ private releaseOrphanConsolidationSeat;
966
+ /** ---- D-6a retroactive propagation sweep (reconcile's second half) ---- */
967
+ private runConsolidationPropagationSweep;
968
+ /** The gate half of a terminal plan's settlement (idempotent, seat-guarded — reconcile's repair
969
+ * arm for a crash between the plan-state write and the gate update). */
970
+ private settleGateForTerminalPlan;
971
+ /** The settlement fingerprint's FRESH eligibility predicate: (id, rev) stays in the map only
972
+ * while it is committed at that rev, unchallenged and unsuperseded RIGHT NOW. Undefined ⇔ the
973
+ * read failed — the caller drops the fingerprint (full candidates next round). */
974
+ private settleFingerprintFilter;
975
+ /**
976
+ * §1.5-3 — verb ④: the host valve over a parked/corrupt plan. `discard` = the intents settle
977
+ * ABANDONED explicitly (audited; never a silent drop — unfulfilled and given-up stay two
978
+ * distinguishable account facts, G24) and the seat releases; `retry` = the plan's unfulfilled
979
+ * intents re-judge NOW into a NEW plan (fresh freeze, fresh baseRevs — the ONE authorized
980
+ * re-judgment point) which applies immediately. Both audited by requestId.
981
+ */
982
+ resolveConsolidationPlan(planId: string, action: "discard" | "retry", opts: {
983
+ requestId: string;
984
+ }): Promise<ConsolidationResolveReceipt>;
985
+ /** Rebuild the directed patches for a retry: the ORIGINAL terminal intent (recovered from the
986
+ * conflicted plan's frozen patch) re-judged against the committed now.
987
+ *
988
+ * Some of those judgments settle a row as CONFLICT here, without ever offering a patch to the
989
+ * store — the target moved out of the plan's scope, it vanished, or the frozen entry is not
990
+ * replayable. Those rows get no backend answer, so they would otherwise reach the parked plan
991
+ * with no per-target reason anywhere: `reasons` carries them out to the retry plan's audit, which
992
+ * is where the conflict notice tells a host to look. */
993
+ private reconstructIntentsForRetry;
994
+ /** §6.2 — host read face: every plan's summary (audit enumeration, zero model surface). */
995
+ listConsolidationPlans(): ConsolidationPlanSummary[];
996
+ /** §6.2 — host read face: one scope's gate row (undefined when the store has no row — reading
997
+ * never creates the file, so the OFF deployment stays byte-identical, G1). */
998
+ readConsolidationGate(scope: string): ConsolidationGateRow | undefined;
766
999
  /** The committed, side-effect-free audit read: the adoption-restricted committed view when the
767
1000
  * backend offers one (zero-copy File — ledger+shadow, no disk adoption), else the non-adopting
768
1001
  * retrieval view (copy-out File), else the backend itself (Pg/TiDB shapes — naturally committed