@sema-agent/core 5.54.0 → 5.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +94 -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/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 +46 -0
- package/dist/core/mcp.js +132 -6
- 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-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 +1 -1
- package/dist/index.js +1 -1
- package/dist/prompts/default.d.ts +12 -2
- package/dist/tools/fs/index.d.ts +3 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +28 -1
|
@@ -6,9 +6,10 @@ import { MAX_MEMORY_BYTES, composeMemoryBlock, firstSentence } from "../memory.j
|
|
|
6
6
|
import { inlineUntrusted } from "../untrusted-text.js";
|
|
7
7
|
import { mintSystemReminder } from "../reminder-mint.js";
|
|
8
8
|
import { formatMemoryAge } from "../memory-recall.js";
|
|
9
|
-
import { committedOriginOf, computeEntryRev, hasOriginFormExtra, isValidEntryId, originEquals, parseEntryFile, serializeEntryFile, stripModelWrittenOrigin } from "./frontmatter.js";
|
|
9
|
+
import { committedDistilledOf, committedOriginOf, computeEntryRev, distilledEquals, hasDistilledFormExtra, hasOriginFormExtra, isValidEntryId, originEquals, parseEntryFile, serializeEntryFile, stripModelWrittenDistilled, stripModelWrittenOrigin } from "./frontmatter.js";
|
|
10
10
|
import { MEMORY_PROVENANCE_RECALL_SENTENCE, memoryExposureIndexRow, parseMemoryExposureIndexRow } from "./provenance-wording.js";
|
|
11
11
|
import { isInstructionEntry } from "./header-hints.js";
|
|
12
|
+
import { ConsolidationRefusedError, CONSOLIDATION_FORCE_THROTTLE_FLOOR_MS, CONSOLIDATION_SEAT_FREEZE_GRACE_MS, consolidationTypeEligible, deriveProductSlug, deriveSupersededSet, isValidConsolidationPlanId, listConsolidationPlanIds, productAddPatch, quarantineCorruptPlan, readConsolidationGateFile, readConsolidationPlan, readIntentCredentials, recordActiveSetBaseline, recordConsolidationSession, recordIntentCredentials, screenConsolidationOptions, supersessionFuseCeiling, updateConsolidationGate, writeConsolidationPlan, } from "./consolidation.js";
|
|
12
13
|
import { classifySessionSettlements, clearHoldResolution, closeSessionAccount, disposeHold, effectiveSettlements, expireOverdueSettlements, foreignDanglingSessionAccounts, markHoldReleased, openInstructionHold, openSessionAccount, readHoldCustody, readHolds, reconcileHolds, replayExternalSettlementEffects, resolveHoldRecord, resolveSessionAccountRecord, resolveSettlementRecord, sessionSettlements, sessionUnattributedSet, } from "./delegation-settlement.js";
|
|
13
14
|
import { noReplaceRestore } from "./delegation-settlement.js";
|
|
14
15
|
import { markOriginClearanceTombstoned, openOriginClearance, readOriginClearances, settleOriginClearance } from "./origin-clearance.js";
|
|
@@ -65,7 +66,7 @@ export const MEMORY_ANNOUNCEMENT_READONLY_PLANE_CODA = "The notices immediately
|
|
|
65
66
|
export const MEMORY_ANNOUNCEMENT_READONLY_CODA = "The memory store itself is not writable this session, so any guidance above to record, update, or tombstone a memory entry cannot be applied here — surface it to the user instead of claiming it done.";
|
|
66
67
|
export const MEMORY_READONLY_NOTICE = `# Memory
|
|
67
68
|
|
|
68
|
-
You have READ-ONLY access to persistent memory in this session: stored notes are available below, but this session has no memory write channel — the engine will not accept writes into the memory store. If the user asks you to remember something
|
|
69
|
+
You have READ-ONLY access to persistent memory in this session: stored notes are available below, but this session has no memory write channel — 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.`;
|
|
69
70
|
export const MEMORY_INDEX_MAX_LINES = 200;
|
|
70
71
|
export const MEMORY_INDEX_MAX_BYTES = 25 * 1024;
|
|
71
72
|
export const STUB_ARCHIVED_LINE = "[body archived — request hydration by listing the slug in memory/.hydrate]";
|
|
@@ -170,6 +171,37 @@ export function memoryHoldNotices(report, sessionId) {
|
|
|
170
171
|
}
|
|
171
172
|
return notices;
|
|
172
173
|
}
|
|
174
|
+
export function memoryConsolidationRecommendedNotice(report, sessionId) {
|
|
175
|
+
const rec = report.consolidationRecommended;
|
|
176
|
+
if (rec === undefined)
|
|
177
|
+
return undefined;
|
|
178
|
+
return {
|
|
179
|
+
code: "memory.consolidation_recommended",
|
|
180
|
+
message: `Memory consolidation is recommended for scope ${inlineUntrusted(rec.scope, 80)}: ${rec.sessionsSince} distinct session(s) harvested since the last run and the time gate is open. The host decides — nothing runs automatically.`,
|
|
181
|
+
detail: { scope: inlineUntrusted(rec.scope, 80), sessionsSince: rec.sessionsSince, ...(sessionId !== undefined ? { sessionId } : {}) },
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
export function memoryConsolidationCommittedNotice(input) {
|
|
185
|
+
return {
|
|
186
|
+
code: "memory.consolidation_committed",
|
|
187
|
+
message: `Memory consolidation plan ${input.planId} completed: ${input.products} product(s) committed, ${input.superseded} entr${input.superseded === 1 ? "y" : "ies"} superseded (retained as evidence), ${input.intents} intent(s) settled.`,
|
|
188
|
+
detail: { planId: input.planId, scope: inlineUntrusted(input.scope, 80), products: input.products, superseded: input.superseded, intents: input.intents },
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
export function memoryConsolidationConflictNotice(input) {
|
|
192
|
+
return {
|
|
193
|
+
code: "memory.consolidation_conflict",
|
|
194
|
+
message: `Memory consolidation plan ${input.planId} parked CONFLICT — at least one target did not reach its planned state (a concurrent write, a patch the store declined, or a target the plan could no longer act on; the plan's audit rows carry the per-target reason). Those targets were NOT overwritten, but patches this plan had already applied stand and are not rolled back by discard (the receipt's product/directed applied-vs-conflict counts say how many). The affected intents stay pending, and resolveConsolidationPlan(planId, "retry" | "discard") is the host valve.`,
|
|
195
|
+
detail: { planId: input.planId, scope: inlineUntrusted(input.scope, 80) },
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
export function memoryConsolidationRefusedNotice(input) {
|
|
199
|
+
return {
|
|
200
|
+
code: "memory.consolidation_refused",
|
|
201
|
+
message: `A memory consolidation verb refused (${input.code})${input.reason !== undefined ? `: ${inlineUntrusted(input.reason, 200)}` : "."}`,
|
|
202
|
+
detail: { refusalCode: input.code, ...(input.scope !== undefined ? { scope: inlineUntrusted(input.scope, 80) } : {}) },
|
|
203
|
+
};
|
|
204
|
+
}
|
|
173
205
|
export class MemoryEngine {
|
|
174
206
|
backend;
|
|
175
207
|
memoryDir;
|
|
@@ -218,7 +250,11 @@ export class MemoryEngine {
|
|
|
218
250
|
}
|
|
219
251
|
this.holdSettleTimeoutMs = opts.holdSettleTimeoutMs ?? DEFAULT_HOLD_SETTLE_TIMEOUT_MS;
|
|
220
252
|
this.onIncident = opts.onIncident;
|
|
253
|
+
if (opts.consolidation !== undefined) {
|
|
254
|
+
this.consolidation = screenConsolidationOptions(opts.consolidation, this.provenance);
|
|
255
|
+
}
|
|
221
256
|
}
|
|
257
|
+
consolidation;
|
|
222
258
|
discloseSessionAccountIncident(what, cause) {
|
|
223
259
|
const sink = this.onIncident;
|
|
224
260
|
if (sink === undefined)
|
|
@@ -1075,6 +1111,37 @@ export class MemoryEngine {
|
|
|
1075
1111
|
const indexText = this.rebuildIndex(handle, headers, { write: indexWrite, ignoreOnDisk: indexGate !== undefined || restricted }, materializeHealWarnings);
|
|
1076
1112
|
handle.indexBaselineLines = countIndexLines(indexText);
|
|
1077
1113
|
handle.indexText = indexText;
|
|
1114
|
+
if (this.consolidation !== undefined) {
|
|
1115
|
+
try {
|
|
1116
|
+
const fuseExclusions = this.readChallengeExclusions();
|
|
1117
|
+
const fuseSuperseded = deriveSupersededSet(headers, { excluded: (id) => fuseExclusions.has(id) });
|
|
1118
|
+
for (const scope of new Set(headers.map((h) => h.scope))) {
|
|
1119
|
+
const size = headers.filter((h) => h.scope === scope && !fuseSuperseded.has(h.id) && !fuseExclusions.has(h.id)).length;
|
|
1120
|
+
const rec = recordActiveSetBaseline(this.controlDir, scope, size, this.consolidation.supersedeRatioCap, this.now);
|
|
1121
|
+
if (rec.collapsed !== undefined) {
|
|
1122
|
+
const detail = `memory active set for scope ${inlineUntrusted(scope, 80)} collapsed ${rec.collapsed.from} → ${rec.collapsed.to} in one window (over the ${this.consolidation.supersedeRatioCap} ratio cap) — superseded entries stay readable as evidence; audit the recent consolidation plans and adoptions`;
|
|
1123
|
+
const sink = this.onIncident;
|
|
1124
|
+
if (sink !== undefined) {
|
|
1125
|
+
try {
|
|
1126
|
+
const e = new Error(detail);
|
|
1127
|
+
e.code = "memory.active_set_collapse";
|
|
1128
|
+
sink(e);
|
|
1129
|
+
}
|
|
1130
|
+
catch {
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
try {
|
|
1134
|
+
enqueueMemoryAnnouncement(this.controlDir, { kind: "gate", at: this.now(), items: [detail] });
|
|
1135
|
+
}
|
|
1136
|
+
catch (err) {
|
|
1137
|
+
this.discloseAnnounceFailure("active-set collapse enqueue", err);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
catch {
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1078
1145
|
if (indexWrite && handle.indexOnDiskUntrusted !== true) {
|
|
1079
1146
|
const dropNotes = materializeHealWarnings.filter((w) => w.includes("opaque external-origin handle"));
|
|
1080
1147
|
if (dropNotes.length > 0) {
|
|
@@ -1668,6 +1735,13 @@ export class MemoryEngine {
|
|
|
1668
1735
|
report.warnings.push(`${rel}: origin-form frontmatter written on the model-visible plane was not adopted — the external-origin marker is engine-authored (the engine's own record of this session decides it)`);
|
|
1669
1736
|
}
|
|
1670
1737
|
}
|
|
1738
|
+
if (stripModelWrittenDistilled(fm)) {
|
|
1739
|
+
const committedDistilledEcho = committedFmHere !== undefined ? committedDistilledOf(committedFmHere) : undefined;
|
|
1740
|
+
const benignDistilledEcho = committedDistilledEcho !== undefined && distilledEquals(parsed.frontmatter.distilled, committedDistilledEcho) && !hasDistilledFormExtra(parsed.frontmatter);
|
|
1741
|
+
if (!benignDistilledEcho) {
|
|
1742
|
+
report.warnings.push(`${rel}: distilled-form frontmatter written on the model-visible plane was not adopted — the consolidation lineage block is engine-authored (minted only by a consolidation plan freeze)`);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1671
1745
|
const instructionForm = carry && (isInstructionEntry(fm) || (committedFmHere !== undefined && isInstructionEntry(committedFmHere)));
|
|
1672
1746
|
if (instructionForm && unattributedSet.has(rel)) {
|
|
1673
1747
|
await containPollutedRecord(f, "this file pre-existed the session under an unresolved foreign session account (crash residue) and is instruction-form — its writer cannot be attributed, so it is withheld for host review");
|
|
@@ -1733,6 +1807,9 @@ export class MemoryEngine {
|
|
|
1733
1807
|
else if (carry && pollutedReason !== undefined) {
|
|
1734
1808
|
fm.origin = mintOrigin();
|
|
1735
1809
|
}
|
|
1810
|
+
const committedDistilled = committedFmHere !== undefined ? committedDistilledOf(committedFmHere) : undefined;
|
|
1811
|
+
if (committedDistilled !== undefined)
|
|
1812
|
+
fm.distilled = committedDistilled;
|
|
1736
1813
|
}
|
|
1737
1814
|
if (fm.name === undefined)
|
|
1738
1815
|
fm.name = f.slug.split("/").pop();
|
|
@@ -2160,6 +2237,36 @@ export class MemoryEngine {
|
|
|
2160
2237
|
report.warnings.push(`memory session account close FAILED and its outcome is UNKNOWN (a failure after the ledger's journal commit still rolls forward, so the account may read closed at the next strict read; a failure before it leaves the ledger's PRIOR state — open, already closed, valve-closed or absent — unchanged, and an open one keeps future crash residue attributed, the over-holding safe side)${deferredSeats.length > 0 ? `; ${deferredSeats.length} file(s) also stay DEFERRED on the plane under standing projection-debt rows: ${seatList}` : ""}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2161
2238
|
}
|
|
2162
2239
|
}
|
|
2240
|
+
if (this.consolidation !== undefined && handle.writeScope !== null && opts?.sessionId !== undefined && report.ok) {
|
|
2241
|
+
try {
|
|
2242
|
+
const rec = recordConsolidationSession(this.controlDir, handle.writeScope, opts.sessionId, this.consolidation, this.now);
|
|
2243
|
+
if (rec.recommended) {
|
|
2244
|
+
report.consolidationRecommended = { scope: handle.writeScope, sessionsSince: rec.sessionsSince };
|
|
2245
|
+
try {
|
|
2246
|
+
enqueueMemoryAnnouncement(this.controlDir, {
|
|
2247
|
+
kind: "gate",
|
|
2248
|
+
at: this.now(),
|
|
2249
|
+
items: [`memory consolidation recommended for scope ${inlineUntrusted(handle.writeScope, 80)}: ${rec.sessionsSince} distinct session(s) since the last run — the host decides (nothing runs automatically)`],
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
catch (err) {
|
|
2253
|
+
this.discloseAnnounceFailure("consolidation recommendation enqueue", err);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
catch (err) {
|
|
2258
|
+
const sink = this.onIncident;
|
|
2259
|
+
if (sink !== undefined) {
|
|
2260
|
+
try {
|
|
2261
|
+
const e = new Error(`memory consolidation gate counting failed (advisory — the verbs refuse loudly over a broken store): ${err instanceof Error ? err.message : String(err)}`);
|
|
2262
|
+
e.code = "memory.consolidation_gate_failed";
|
|
2263
|
+
sink(e);
|
|
2264
|
+
}
|
|
2265
|
+
catch {
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2163
2270
|
return report;
|
|
2164
2271
|
}
|
|
2165
2272
|
async settleDueHolds(handle, report) {
|
|
@@ -2236,8 +2343,14 @@ export class MemoryEngine {
|
|
|
2236
2343
|
if (stripModelWrittenOrigin(fm)) {
|
|
2237
2344
|
report.warnings.push(`${row.relPath}: origin-form frontmatter in the held bytes was not adopted — the external-origin marker is engine-authored`);
|
|
2238
2345
|
}
|
|
2346
|
+
if (stripModelWrittenDistilled(fm)) {
|
|
2347
|
+
report.warnings.push(`${row.relPath}: distilled-form frontmatter in the held bytes was not adopted — the consolidation lineage block is engine-authored`);
|
|
2348
|
+
}
|
|
2239
2349
|
const committedFm = row.entryId !== undefined ? await this.committedFrontmatterFor(row.entryId) : undefined;
|
|
2240
2350
|
const committedOrigin = committedFm !== undefined ? committedOriginOf(committedFm) : undefined;
|
|
2351
|
+
const committedDistilledHold = committedFm !== undefined ? committedDistilledOf(committedFm) : undefined;
|
|
2352
|
+
if (fm.deleted !== true && committedDistilledHold !== undefined)
|
|
2353
|
+
fm.distilled = committedDistilledHold;
|
|
2241
2354
|
if (fm.deleted !== true) {
|
|
2242
2355
|
if (committedOrigin !== undefined)
|
|
2243
2356
|
fm.origin = committedOrigin;
|
|
@@ -2347,6 +2460,988 @@ export class MemoryEngine {
|
|
|
2347
2460
|
readDanglingSessionAccounts(selfSessionId) {
|
|
2348
2461
|
return foreignDanglingSessionAccounts(this.controlDir, selfSessionId);
|
|
2349
2462
|
}
|
|
2463
|
+
requireConsolidation() {
|
|
2464
|
+
if (this.consolidation === undefined) {
|
|
2465
|
+
throw new ConsolidationRefusedError("memory.consolidation_disabled", "memory consolidation is not enabled on this engine (MemoryEngineOptions.consolidation is absent — the v3 default is OFF; the host enables it explicitly).");
|
|
2466
|
+
}
|
|
2467
|
+
return this.consolidation;
|
|
2468
|
+
}
|
|
2469
|
+
readConsolidationGateOrRefuse() {
|
|
2470
|
+
try {
|
|
2471
|
+
return readConsolidationGateFile(this.controlDir);
|
|
2472
|
+
}
|
|
2473
|
+
catch (err) {
|
|
2474
|
+
throw new ConsolidationRefusedError("memory.consolidation_gate_corrupt", `memory consolidation gate store cannot be trusted (fail-closed): ${err instanceof Error ? err.message : String(err)}`);
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
async releaseConsolidationLease(scope) {
|
|
2478
|
+
try {
|
|
2479
|
+
await this.consolidation?.lease?.release?.(scope);
|
|
2480
|
+
}
|
|
2481
|
+
catch {
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
async consolidationLeaseCurrent(scope, frozenToken) {
|
|
2485
|
+
const lease = this.consolidation?.lease;
|
|
2486
|
+
if (lease === undefined)
|
|
2487
|
+
return true;
|
|
2488
|
+
const held = (await (lease.renew?.(scope) ?? lease.acquire(scope))) ?? undefined;
|
|
2489
|
+
if (held === undefined)
|
|
2490
|
+
return false;
|
|
2491
|
+
return frozenToken === undefined || held.token === frozenToken;
|
|
2492
|
+
}
|
|
2493
|
+
consolidationEligibility(headers) {
|
|
2494
|
+
let exclusions;
|
|
2495
|
+
try {
|
|
2496
|
+
exclusions = this.readChallengeExclusions();
|
|
2497
|
+
}
|
|
2498
|
+
catch (err) {
|
|
2499
|
+
throw new ConsolidationRefusedError("memory.consolidation_governance_unreadable", `memory consolidation refused: the challenge/lineage ledger cannot be trusted (fail-closed): ${err instanceof Error ? err.message : String(err)}`);
|
|
2500
|
+
}
|
|
2501
|
+
const superseded = deriveSupersededSet(headers, { excluded: (id) => exclusions.has(id) });
|
|
2502
|
+
const pendingSessions = new Set();
|
|
2503
|
+
try {
|
|
2504
|
+
for (const s of effectiveSettlements(this.controlDir))
|
|
2505
|
+
if (s.effective === "pending")
|
|
2506
|
+
pendingSessions.add(s.row.sessionId);
|
|
2507
|
+
}
|
|
2508
|
+
catch (err) {
|
|
2509
|
+
throw new ConsolidationRefusedError("memory.consolidation_governance_unreadable", `memory consolidation refused: the delegation-settlement account cannot be trusted (fail-closed): ${err instanceof Error ? err.message : String(err)}`);
|
|
2510
|
+
}
|
|
2511
|
+
let lineageByEntry = {};
|
|
2512
|
+
try {
|
|
2513
|
+
lineageByEntry = readLineageRecord(this.controlDir).committed;
|
|
2514
|
+
}
|
|
2515
|
+
catch (err) {
|
|
2516
|
+
throw new ConsolidationRefusedError("memory.consolidation_governance_unreadable", `memory consolidation refused: the lineage ledger cannot be trusted (fail-closed): ${err instanceof Error ? err.message : String(err)}`);
|
|
2517
|
+
}
|
|
2518
|
+
const eligible = new Map();
|
|
2519
|
+
for (const h of headers) {
|
|
2520
|
+
if (exclusions.has(h.id))
|
|
2521
|
+
continue;
|
|
2522
|
+
if (superseded.has(h.id))
|
|
2523
|
+
continue;
|
|
2524
|
+
if (h.distilled !== undefined)
|
|
2525
|
+
continue;
|
|
2526
|
+
if (!consolidationTypeEligible(h.type))
|
|
2527
|
+
continue;
|
|
2528
|
+
if (h.exposure === undefined && pendingSessions.size > 0) {
|
|
2529
|
+
const contributors = Object.keys(lineageByEntry[h.id] ?? {});
|
|
2530
|
+
if (contributors.some((s) => pendingSessions.has(s)))
|
|
2531
|
+
continue;
|
|
2532
|
+
}
|
|
2533
|
+
eligible.set(h.id, h);
|
|
2534
|
+
}
|
|
2535
|
+
return { exclusions, superseded, eligible };
|
|
2536
|
+
}
|
|
2537
|
+
async snapshotConsolidationCandidates(scope, opts = {}) {
|
|
2538
|
+
const screened = this.requireConsolidation();
|
|
2539
|
+
const gate = this.readConsolidationGateOrRefuse();
|
|
2540
|
+
const row = gate.scopes[scope];
|
|
2541
|
+
const at = this.now();
|
|
2542
|
+
if (row?.openPlanId !== undefined) {
|
|
2543
|
+
throw new ConsolidationRefusedError("memory.consolidation_busy", `memory consolidation refused: plan ${row.openPlanId} holds the ${JSON.stringify(scope)} seat (reconcile or resolve it first — the mutex yields to no force).`);
|
|
2544
|
+
}
|
|
2545
|
+
if (row?.lastRunAt !== undefined) {
|
|
2546
|
+
const throttleWindow = Math.min(screened.minRunIntervalMs, CONSOLIDATION_FORCE_THROTTLE_FLOOR_MS);
|
|
2547
|
+
if (at - row.lastRunAt < throttleWindow) {
|
|
2548
|
+
throw new ConsolidationRefusedError("memory.consolidation_gated", `memory consolidation refused (throttle): the last ${JSON.stringify(scope)} run settled ${at - row.lastRunAt}ms ago — the same-scope cost floor (${throttleWindow}ms) yields to no force.`);
|
|
2549
|
+
}
|
|
2550
|
+
if (opts.force === undefined && at - row.lastRunAt < screened.minRunIntervalMs) {
|
|
2551
|
+
throw new ConsolidationRefusedError("memory.consolidation_gated", `memory consolidation refused (time gate): the last ${JSON.stringify(scope)} run settled ${at - row.lastRunAt}ms ago (< minRunIntervalMs ${screened.minRunIntervalMs}ms). Pass force with a requestId for an on-demand run.`);
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
if (opts.force === undefined && (row?.sessions.count ?? 0) < screened.minSessionsBetweenRuns) {
|
|
2555
|
+
throw new ConsolidationRefusedError("memory.consolidation_gated", `memory consolidation refused (session gate): ${row?.sessions.count ?? 0} distinct session(s) since the last run (< minSessionsBetweenRuns ${screened.minSessionsBetweenRuns}). Pass force with a requestId for an on-demand run.`);
|
|
2556
|
+
}
|
|
2557
|
+
const face = this.committedAuditFace();
|
|
2558
|
+
const headers = await face.listHeaders([scope]);
|
|
2559
|
+
const { eligible } = this.consolidationEligibility(headers);
|
|
2560
|
+
const fingerprint = opts.full === true ? undefined : row?.fingerprint;
|
|
2561
|
+
const candidateIds = [];
|
|
2562
|
+
for (const [id, h] of eligible) {
|
|
2563
|
+
const fp = fingerprint?.[id];
|
|
2564
|
+
if (fp === undefined || fp.rev !== h.rev)
|
|
2565
|
+
candidateIds.push(id);
|
|
2566
|
+
}
|
|
2567
|
+
const entries = await face.getByIds(candidateIds);
|
|
2568
|
+
const candidates = entries
|
|
2569
|
+
.filter((e) => e.frontmatter.deleted !== true)
|
|
2570
|
+
.map((e) => ({ entry: e, marked: committedOriginOf(e.frontmatter) !== undefined }));
|
|
2571
|
+
const candidateRevs = {};
|
|
2572
|
+
for (const c of candidates)
|
|
2573
|
+
candidateRevs[c.entry.id] = c.entry.rev;
|
|
2574
|
+
const eligibleRevs = {};
|
|
2575
|
+
for (const [id, h] of eligible)
|
|
2576
|
+
eligibleRevs[id] = h.rev;
|
|
2577
|
+
const marked = candidates.some((c) => c.marked);
|
|
2578
|
+
const epoch = row?.epoch ?? 0;
|
|
2579
|
+
const cycleToken = createHash("sha256").update(JSON.stringify([scope, epoch, at, Object.entries(candidateRevs).sort()]), "utf8").digest("hex").slice(0, 32);
|
|
2580
|
+
updateConsolidationGate(this.controlDir, (file) => {
|
|
2581
|
+
const fresh = file.scopes[scope] ?? { sessions: { count: 0, ring: [] }, epoch: 0 };
|
|
2582
|
+
if (fresh.openPlanId !== undefined) {
|
|
2583
|
+
throw new ConsolidationRefusedError("memory.consolidation_busy", `memory consolidation refused: plan ${fresh.openPlanId} claimed the ${JSON.stringify(scope)} seat during the snapshot read.`);
|
|
2584
|
+
}
|
|
2585
|
+
if (fresh.epoch !== epoch) {
|
|
2586
|
+
throw new ConsolidationRefusedError("memory.consolidation_stale_snapshot", `memory consolidation refused: the ${JSON.stringify(scope)} watermark advanced during the snapshot read — retake the snapshot.`);
|
|
2587
|
+
}
|
|
2588
|
+
fresh.snapshot = { token: cycleToken, at, epoch, candidates: candidateRevs, eligible: eligibleRevs, marked };
|
|
2589
|
+
file.scopes[scope] = fresh;
|
|
2590
|
+
return { next: file, result: undefined };
|
|
2591
|
+
});
|
|
2592
|
+
if (opts.force !== undefined) {
|
|
2593
|
+
try {
|
|
2594
|
+
enqueueMemoryAnnouncement(this.controlDir, { kind: "gate", at, items: [`memory consolidation snapshot FORCED past the time/session gates for scope ${inlineUntrusted(scope, 80)} (requestId ${inlineUntrusted(opts.force.requestId, 80)})`] });
|
|
2595
|
+
}
|
|
2596
|
+
catch (err) {
|
|
2597
|
+
this.discloseAnnounceFailure("consolidation force audit enqueue", err);
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
return { cycleToken, watermark: { epoch, at }, candidates };
|
|
2601
|
+
}
|
|
2602
|
+
async commitConsolidationPlan(cycleToken, proposal, opts) {
|
|
2603
|
+
const screened = this.requireConsolidation();
|
|
2604
|
+
const scope = proposal.scope;
|
|
2605
|
+
if (typeof scope !== "string" || scope.length === 0)
|
|
2606
|
+
throw new ConsolidationRefusedError("memory.consolidation_refused", "memory consolidation plan refused: proposal.scope must be a non-empty string.");
|
|
2607
|
+
const reasons = [];
|
|
2608
|
+
if (proposal.products.length > screened.maxProductsPerRun)
|
|
2609
|
+
reasons.push(`products ${proposal.products.length} > maxProductsPerRun ${screened.maxProductsPerRun}`);
|
|
2610
|
+
const intents = proposal.intents ?? [];
|
|
2611
|
+
if (intents.length > screened.maxDirectedPatchesPerPlan)
|
|
2612
|
+
reasons.push(`directed intents ${intents.length} > maxDirectedPatchesPerPlan ${screened.maxDirectedPatchesPerPlan}`);
|
|
2613
|
+
const intentIds = new Set();
|
|
2614
|
+
for (const intent of intents) {
|
|
2615
|
+
if (typeof intent.requestId !== "string" || intent.requestId.length === 0)
|
|
2616
|
+
reasons.push("an intent carries no requestId");
|
|
2617
|
+
else if (intentIds.has(intent.requestId))
|
|
2618
|
+
reasons.push(`duplicate intent requestId ${JSON.stringify(intent.requestId)}`);
|
|
2619
|
+
else
|
|
2620
|
+
intentIds.add(intent.requestId);
|
|
2621
|
+
}
|
|
2622
|
+
let credentials = {};
|
|
2623
|
+
try {
|
|
2624
|
+
credentials = readIntentCredentials(this.controlDir);
|
|
2625
|
+
}
|
|
2626
|
+
catch (err) {
|
|
2627
|
+
throw new ConsolidationRefusedError("memory.consolidation_gate_corrupt", `memory consolidation refused: the intent-credential account cannot be trusted (fail-closed): ${err instanceof Error ? err.message : String(err)}`);
|
|
2628
|
+
}
|
|
2629
|
+
for (const intent of intents) {
|
|
2630
|
+
const existing = credentials[intent.requestId];
|
|
2631
|
+
if (existing !== undefined)
|
|
2632
|
+
reasons.push(`intent ${JSON.stringify(intent.requestId)} already carries a credential (${existing.state}) — a consumed intent is never re-frozen`);
|
|
2633
|
+
}
|
|
2634
|
+
for (let i = 0; i < proposal.products.length; i++) {
|
|
2635
|
+
const p = proposal.products[i];
|
|
2636
|
+
if (typeof p.body !== "string" || p.body.trim() === "")
|
|
2637
|
+
reasons.push(`product #${i}: empty body`);
|
|
2638
|
+
if (!Array.isArray(p.inputs) || p.inputs.length === 0)
|
|
2639
|
+
reasons.push(`product #${i}: zero declared inputs (a distillation always has material)`);
|
|
2640
|
+
else if (p.inputs.length > screened.maxInputsPerProduct)
|
|
2641
|
+
reasons.push(`product #${i}: ${p.inputs.length} inputs > maxInputsPerProduct ${screened.maxInputsPerProduct}`);
|
|
2642
|
+
}
|
|
2643
|
+
if (reasons.length > 0)
|
|
2644
|
+
throw new ConsolidationRefusedError("memory.consolidation_refused", "memory consolidation plan refused (structural)", reasons);
|
|
2645
|
+
const planId = uuidv7();
|
|
2646
|
+
const snapshot = updateConsolidationGate(this.controlDir, (file) => {
|
|
2647
|
+
const row = file.scopes[scope] ?? { sessions: { count: 0, ring: [] }, epoch: 0 };
|
|
2648
|
+
if (row.openPlanId !== undefined) {
|
|
2649
|
+
throw new ConsolidationRefusedError("memory.consolidation_busy", `memory consolidation refused: plan ${row.openPlanId} already holds the ${JSON.stringify(scope)} seat (one plan at a time; resolve or reconcile it first).`);
|
|
2650
|
+
}
|
|
2651
|
+
if (row.snapshot === undefined || row.snapshot.token !== cycleToken || row.snapshot.epoch !== row.epoch) {
|
|
2652
|
+
throw new ConsolidationRefusedError("memory.consolidation_stale_snapshot", `memory consolidation refused: the cycle token does not match the ${JSON.stringify(scope)} scope's current snapshot (another cycle completed, or a newer snapshot replaced it) — retake the snapshot.`);
|
|
2653
|
+
}
|
|
2654
|
+
row.openPlanId = planId;
|
|
2655
|
+
row.seatClaimedAt = this.now();
|
|
2656
|
+
file.scopes[scope] = row;
|
|
2657
|
+
return { next: file, result: row.snapshot };
|
|
2658
|
+
});
|
|
2659
|
+
let frozenLeaseToken;
|
|
2660
|
+
if (screened.lease !== undefined) {
|
|
2661
|
+
const held = (await screened.lease.acquire(scope)) ?? undefined;
|
|
2662
|
+
if (held === undefined) {
|
|
2663
|
+
updateConsolidationGate(this.controlDir, (file) => {
|
|
2664
|
+
const row = file.scopes[scope];
|
|
2665
|
+
if (row?.openPlanId === planId) {
|
|
2666
|
+
delete row.openPlanId;
|
|
2667
|
+
file.scopes[scope] = row;
|
|
2668
|
+
return { next: file, result: undefined };
|
|
2669
|
+
}
|
|
2670
|
+
return { result: undefined };
|
|
2671
|
+
});
|
|
2672
|
+
throw new ConsolidationRefusedError("memory.consolidation_lease_unavailable", `memory consolidation refused: the global lease for scope ${JSON.stringify(scope)} could not be acquired.`);
|
|
2673
|
+
}
|
|
2674
|
+
frozenLeaseToken = held.token;
|
|
2675
|
+
}
|
|
2676
|
+
let frozen = false;
|
|
2677
|
+
try {
|
|
2678
|
+
const { plan, refusedProducts } = await this.freezeConsolidationPlan({ planId, scope, requestId: opts.requestId, cycleToken, snapshot, proposal, screened });
|
|
2679
|
+
frozen = true;
|
|
2680
|
+
const receipt = await this.applyConsolidationPlan(plan, frozenLeaseToken);
|
|
2681
|
+
receipt.products.refused = refusedProducts;
|
|
2682
|
+
await this.releaseConsolidationLease(scope);
|
|
2683
|
+
return receipt;
|
|
2684
|
+
}
|
|
2685
|
+
catch (err) {
|
|
2686
|
+
await this.releaseConsolidationLease(scope);
|
|
2687
|
+
if (!frozen && err instanceof ConsolidationRefusedError) {
|
|
2688
|
+
updateConsolidationGate(this.controlDir, (file) => {
|
|
2689
|
+
const row = file.scopes[scope];
|
|
2690
|
+
if (row?.openPlanId === planId) {
|
|
2691
|
+
delete row.openPlanId;
|
|
2692
|
+
file.scopes[scope] = row;
|
|
2693
|
+
return { next: file, result: undefined };
|
|
2694
|
+
}
|
|
2695
|
+
return { result: undefined };
|
|
2696
|
+
});
|
|
2697
|
+
if (err.code === "memory.consolidation_refused") {
|
|
2698
|
+
try {
|
|
2699
|
+
enqueueMemoryAnnouncement(this.controlDir, { kind: "gate", at: this.now(), items: [`memory consolidation plan for scope ${inlineUntrusted(scope, 80)} REFUSED: ${inlineUntrusted(err.reasons[0] ?? err.message, 200)}`] });
|
|
2700
|
+
}
|
|
2701
|
+
catch (annErr) {
|
|
2702
|
+
this.discloseAnnounceFailure("consolidation refusal enqueue", annErr);
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
throw err;
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
async freezeConsolidationPlan(input) {
|
|
2710
|
+
const { planId, scope, snapshot, proposal, screened } = input;
|
|
2711
|
+
const at = this.now();
|
|
2712
|
+
const face = this.committedAuditFace();
|
|
2713
|
+
const headers = await face.listHeaders([scope]);
|
|
2714
|
+
const { exclusions, superseded } = this.consolidationEligibility(headers);
|
|
2715
|
+
const headerById = new Map(headers.map((h) => [h.id, h]));
|
|
2716
|
+
const wholeReasons = [];
|
|
2717
|
+
for (let i = 0; i < proposal.products.length; i++) {
|
|
2718
|
+
for (const input2 of proposal.products[i].inputs) {
|
|
2719
|
+
if (snapshot.candidates[input2.id] === undefined)
|
|
2720
|
+
wholeReasons.push(`product #${i}: declared input ${JSON.stringify(input2.id)} is not in the snapshot candidate set`);
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
const activeSetSize = headers.filter((h) => !superseded.has(h.id) && !exclusions.has(h.id)).length;
|
|
2724
|
+
const edgeTargets = new Set();
|
|
2725
|
+
for (const p of proposal.products)
|
|
2726
|
+
for (const i2 of p.inputs)
|
|
2727
|
+
if (i2.supersede === true)
|
|
2728
|
+
edgeTargets.add(i2.id);
|
|
2729
|
+
const ceiling = supersessionFuseCeiling(activeSetSize, screened);
|
|
2730
|
+
if (edgeTargets.size > ceiling) {
|
|
2731
|
+
wholeReasons.push(`supersession fuse: ${edgeTargets.size} distinct targets > ceiling ${ceiling} (active set ${activeSetSize}, ratio ${screened.supersedeRatioCap}, floor ${screened.supersedeAbsoluteFloor}, N-1 unconditional)`);
|
|
2732
|
+
try {
|
|
2733
|
+
enqueueMemoryAnnouncement(this.controlDir, { kind: "gate", at, items: [`memory consolidation plan for scope ${inlineUntrusted(scope, 80)} REFUSED by the supersession fuse (${edgeTargets.size} targets over the ${ceiling} ceiling)`] });
|
|
2734
|
+
}
|
|
2735
|
+
catch (err) {
|
|
2736
|
+
this.discloseAnnounceFailure("consolidation fuse enqueue", err);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
let visibleMarked = snapshot.marked;
|
|
2740
|
+
if (!visibleMarked) {
|
|
2741
|
+
const inputIds = [...new Set(proposal.products.flatMap((p) => p.inputs.map((i2) => i2.id)))];
|
|
2742
|
+
const committedInputs = await face.getByIds(inputIds);
|
|
2743
|
+
visibleMarked = committedInputs.some((e) => committedOriginOf(e.frontmatter) !== undefined);
|
|
2744
|
+
}
|
|
2745
|
+
const foldedOrigin = visibleMarked ? { taint: "external", cause: "derived", at } : undefined;
|
|
2746
|
+
for (let i = 0; i < proposal.products.length; i++) {
|
|
2747
|
+
const p = proposal.products[i];
|
|
2748
|
+
if (visibleMarked && isInstructionEntry({ ...(p.type !== undefined ? { type: p.type } : {}) })) {
|
|
2749
|
+
wholeReasons.push(`product #${i}: instruction-form product (type ${JSON.stringify(p.type)}) refused — the run's visible set contains marked content (the laundering hard gate, G7/G26)`);
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
const assembled = [];
|
|
2753
|
+
const refusedProducts = [];
|
|
2754
|
+
for (let i = 0; i < proposal.products.length; i++) {
|
|
2755
|
+
const p = proposal.products[i];
|
|
2756
|
+
const drift = [];
|
|
2757
|
+
for (const input2 of p.inputs) {
|
|
2758
|
+
if (snapshot.candidates[input2.id] === undefined)
|
|
2759
|
+
continue;
|
|
2760
|
+
const h = headerById.get(input2.id);
|
|
2761
|
+
if (h === undefined)
|
|
2762
|
+
drift.push(`input ${input2.id} is no longer committed`);
|
|
2763
|
+
else if (h.rev !== snapshot.candidates[input2.id])
|
|
2764
|
+
drift.push(`input ${input2.id} moved rev since the snapshot`);
|
|
2765
|
+
else if (exclusions.has(input2.id))
|
|
2766
|
+
drift.push(`input ${input2.id} is challenged/latched`);
|
|
2767
|
+
else if (input2.supersede === true && !consolidationTypeEligible(h.type))
|
|
2768
|
+
drift.push(`supersession target ${input2.id} is ${h.type} (episodic/procedural targets are refused)`);
|
|
2769
|
+
}
|
|
2770
|
+
if (drift.length > 0) {
|
|
2771
|
+
refusedProducts.push({ index: i, reason: drift.join("; ") });
|
|
2772
|
+
continue;
|
|
2773
|
+
}
|
|
2774
|
+
const id = uuidv7();
|
|
2775
|
+
const slug = deriveProductSlug(p.name, `consolidated-${id.slice(0, 8)}`);
|
|
2776
|
+
const entry = {
|
|
2777
|
+
id,
|
|
2778
|
+
slug,
|
|
2779
|
+
scope,
|
|
2780
|
+
frontmatter: {
|
|
2781
|
+
name: p.name ?? slug,
|
|
2782
|
+
...(p.description !== undefined ? { description: p.description } : {}),
|
|
2783
|
+
...(p.type !== undefined ? { type: p.type } : {}),
|
|
2784
|
+
...(foldedOrigin !== undefined ? { origin: { ...foldedOrigin } } : {}),
|
|
2785
|
+
distilled: {
|
|
2786
|
+
planId,
|
|
2787
|
+
at,
|
|
2788
|
+
carrierRev: "",
|
|
2789
|
+
inputs: p.inputs.map((i2) => ({ id: i2.id, rev: snapshot.candidates[i2.id] ?? "", ...(i2.supersede === true ? { superseded: true } : {}) })),
|
|
2790
|
+
},
|
|
2791
|
+
},
|
|
2792
|
+
body: p.body,
|
|
2793
|
+
rev: "",
|
|
2794
|
+
};
|
|
2795
|
+
entry.rev = computeEntryRev(entry);
|
|
2796
|
+
entry.frontmatter.distilled.carrierRev = entry.rev;
|
|
2797
|
+
const nameFinding = scanMemoryFileName(`${slug}.md`);
|
|
2798
|
+
const findings = [...(nameFinding !== undefined ? [nameFinding] : []), ...scanMemoryWrite(serializeEntryFile(entry), { maxBytes: this.perFileBytes })];
|
|
2799
|
+
if (findings.length > 0) {
|
|
2800
|
+
wholeReasons.push(`product #${i}: scan gate (${findings.map((f) => `${f.code}: ${f.reason}`).join("; ")})`);
|
|
2801
|
+
continue;
|
|
2802
|
+
}
|
|
2803
|
+
assembled.push(entry);
|
|
2804
|
+
}
|
|
2805
|
+
const directed = [];
|
|
2806
|
+
const intents = proposal.intents ?? [];
|
|
2807
|
+
const targetIds = intents.map((it) => it.entryId);
|
|
2808
|
+
const targets = new Map((await face.getByIds(targetIds)).map((e) => [e.id, e]));
|
|
2809
|
+
for (const intent of intents) {
|
|
2810
|
+
const committed = targets.get(intent.entryId);
|
|
2811
|
+
if (committed !== undefined && committed.scope !== scope) {
|
|
2812
|
+
wholeReasons.push(`intent ${JSON.stringify(intent.requestId)}: target ${intent.entryId} lives in scope ${JSON.stringify(committed.scope)}, not this plan's ${JSON.stringify(scope)} — one plan, one scope (§3.3)`);
|
|
2813
|
+
continue;
|
|
2814
|
+
}
|
|
2815
|
+
if (committed !== undefined && !consolidationTypeEligible(committed.frontmatter.type)) {
|
|
2816
|
+
wholeReasons.push(`intent ${JSON.stringify(intent.requestId)}: target ${intent.entryId} is ${committed.frontmatter.type} — episodic/procedural entries are never directed-change targets (A-7)`);
|
|
2817
|
+
continue;
|
|
2818
|
+
}
|
|
2819
|
+
if (intent.kind === "forget") {
|
|
2820
|
+
if (committed === undefined || committed.frontmatter.deleted === true) {
|
|
2821
|
+
directed.push({ intentRequestId: intent.requestId, op: "delete", id: intent.entryId, baseRev: "", state: "satisfied", attribution: "unattributed" });
|
|
2822
|
+
}
|
|
2823
|
+
else {
|
|
2824
|
+
directed.push({ intentRequestId: intent.requestId, op: "delete", id: intent.entryId, baseRev: committed.rev, state: "pending" });
|
|
2825
|
+
}
|
|
2826
|
+
continue;
|
|
2827
|
+
}
|
|
2828
|
+
if (committed === undefined || committed.frontmatter.deleted === true) {
|
|
2829
|
+
wholeReasons.push(`intent ${JSON.stringify(intent.requestId)}: rewrite target ${intent.entryId} is not committed (a rewrite needs a standing entry; record a new one instead)`);
|
|
2830
|
+
continue;
|
|
2831
|
+
}
|
|
2832
|
+
const terminal = {
|
|
2833
|
+
id: committed.id,
|
|
2834
|
+
slug: committed.slug,
|
|
2835
|
+
scope: committed.scope,
|
|
2836
|
+
frontmatter: {
|
|
2837
|
+
...(intent.name !== undefined ? { name: intent.name } : committed.frontmatter.name !== undefined ? { name: committed.frontmatter.name } : {}),
|
|
2838
|
+
...(intent.description !== undefined ? { description: intent.description } : committed.frontmatter.description !== undefined ? { description: committed.frontmatter.description } : {}),
|
|
2839
|
+
...(intent.type !== undefined ? { type: intent.type } : committed.frontmatter.type !== undefined ? { type: committed.frontmatter.type } : {}),
|
|
2840
|
+
...(committed.frontmatter.origin !== undefined ? { origin: { ...committed.frontmatter.origin } } : {}),
|
|
2841
|
+
...(committed.frontmatter.distilled !== undefined ? { distilled: structuredClone(committed.frontmatter.distilled) } : {}),
|
|
2842
|
+
...(committed.frontmatter.provenance !== undefined ? { provenance: { ...committed.frontmatter.provenance } } : {}),
|
|
2843
|
+
...(committed.frontmatter.trust !== undefined ? { trust: committed.frontmatter.trust } : {}),
|
|
2844
|
+
...(committed.frontmatter.extra !== undefined ? { extra: [...committed.frontmatter.extra] } : {}),
|
|
2845
|
+
},
|
|
2846
|
+
body: intent.body,
|
|
2847
|
+
rev: "",
|
|
2848
|
+
};
|
|
2849
|
+
terminal.rev = computeEntryRev(terminal);
|
|
2850
|
+
const findings = scanMemoryWrite(serializeEntryFile(terminal), { maxBytes: this.perFileBytes });
|
|
2851
|
+
if (findings.length > 0) {
|
|
2852
|
+
wholeReasons.push(`intent ${JSON.stringify(intent.requestId)}: scan gate (${findings.map((f) => `${f.code}: ${f.reason}`).join("; ")})`);
|
|
2853
|
+
continue;
|
|
2854
|
+
}
|
|
2855
|
+
if (committed.rev === terminal.rev) {
|
|
2856
|
+
directed.push({ intentRequestId: intent.requestId, op: "update", id: intent.entryId, baseRev: committed.rev, plannedPostRev: terminal.rev, entry: terminal, state: "satisfied" });
|
|
2857
|
+
}
|
|
2858
|
+
else {
|
|
2859
|
+
directed.push({ intentRequestId: intent.requestId, op: "update", id: intent.entryId, baseRev: committed.rev, plannedPostRev: terminal.rev, entry: terminal, state: "pending" });
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
if (wholeReasons.length > 0)
|
|
2863
|
+
throw new ConsolidationRefusedError("memory.consolidation_refused", "memory consolidation plan refused", wholeReasons);
|
|
2864
|
+
try {
|
|
2865
|
+
const seatNow = readConsolidationGateFile(this.controlDir).scopes[scope]?.openPlanId;
|
|
2866
|
+
if (seatNow !== planId) {
|
|
2867
|
+
throw new ConsolidationRefusedError("memory.consolidation_busy", `memory consolidation refused: the ${JSON.stringify(scope)} seat was reclaimed during the freeze (a reconcile judged the claim stale) — retake the snapshot and commit again.`);
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
catch (err) {
|
|
2871
|
+
if (err instanceof ConsolidationRefusedError)
|
|
2872
|
+
throw err;
|
|
2873
|
+
throw new ConsolidationRefusedError("memory.consolidation_gate_corrupt", `memory consolidation refused: the gate store could not be re-read at the freeze boundary (fail-closed): ${err instanceof Error ? err.message : String(err)}`);
|
|
2874
|
+
}
|
|
2875
|
+
const plan = {
|
|
2876
|
+
v: 1,
|
|
2877
|
+
planId,
|
|
2878
|
+
scope,
|
|
2879
|
+
requestId: input.requestId,
|
|
2880
|
+
createdAt: at,
|
|
2881
|
+
cycleToken: input.cycleToken,
|
|
2882
|
+
epoch: snapshot.epoch,
|
|
2883
|
+
visibleMarked,
|
|
2884
|
+
...(foldedOrigin !== undefined ? { foldedOrigin } : {}),
|
|
2885
|
+
products: assembled,
|
|
2886
|
+
productStates: Object.fromEntries(assembled.map((e) => [e.id, "pending"])),
|
|
2887
|
+
directed,
|
|
2888
|
+
intents: intents.map((it) => ({ requestId: it.requestId, state: "pending" })),
|
|
2889
|
+
state: "open",
|
|
2890
|
+
audit: [
|
|
2891
|
+
{ at, event: "frozen", requestId: input.requestId, ...(refusedProducts.length > 0 ? { detail: `${refusedProducts.length} product(s) refused at freeze: ${refusedProducts.map((r) => `#${r.index}: ${r.reason}`).join(" | ")}` } : {}) },
|
|
2892
|
+
],
|
|
2893
|
+
};
|
|
2894
|
+
writeConsolidationPlan(this.controlDir, plan);
|
|
2895
|
+
return { plan, refusedProducts };
|
|
2896
|
+
}
|
|
2897
|
+
async applyConsolidationPlan(plan, frozenLeaseToken) {
|
|
2898
|
+
if (plan.state === "open") {
|
|
2899
|
+
plan.state = "applying";
|
|
2900
|
+
plan.audit.push({ at: this.now(), event: "applying" });
|
|
2901
|
+
writeConsolidationPlan(this.controlDir, plan);
|
|
2902
|
+
}
|
|
2903
|
+
const face = this.committedAuditFace();
|
|
2904
|
+
const pendingProducts = plan.products.filter((p) => plan.productStates[p.id] === "pending");
|
|
2905
|
+
if (pendingProducts.length > 0) {
|
|
2906
|
+
if (!(await this.consolidationLeaseCurrent(plan.scope, frozenLeaseToken))) {
|
|
2907
|
+
throw new ConsolidationRefusedError("memory.consolidation_lease_unavailable", `memory consolidation apply paused: the global lease for scope ${JSON.stringify(plan.scope)} is no longer held at its frozen fencing token — the plan stays durable and replays under a valid holder (reconcileConsolidation).`);
|
|
2908
|
+
}
|
|
2909
|
+
const txnId = uuidv7();
|
|
2910
|
+
const lineageRows = pendingProducts.map((p) => ({ entryId: p.id, rev: p.rev, ...(p.frontmatter.origin !== undefined ? { marked: true } : {}) }));
|
|
2911
|
+
let lineageArmed = false;
|
|
2912
|
+
try {
|
|
2913
|
+
stageLineagePending(this.controlDir, txnId, `consolidation:${plan.planId}`, lineageRows, this.now);
|
|
2914
|
+
lineageArmed = true;
|
|
2915
|
+
}
|
|
2916
|
+
catch (err) {
|
|
2917
|
+
throw new ConsolidationRefusedError("memory.consolidation_governance_unreadable", `memory consolidation apply paused: the lineage write-ahead could not be staged (the plan stays durable and replays): ${err instanceof Error ? err.message : String(err)}`);
|
|
2918
|
+
}
|
|
2919
|
+
let report;
|
|
2920
|
+
try {
|
|
2921
|
+
report = await this.backend.applyPatches(pendingProducts.map(productAddPatch));
|
|
2922
|
+
}
|
|
2923
|
+
catch (err) {
|
|
2924
|
+
try {
|
|
2925
|
+
discardLineagePending(this.controlDir, txnId);
|
|
2926
|
+
}
|
|
2927
|
+
catch {
|
|
2928
|
+
}
|
|
2929
|
+
throw new ConsolidationRefusedError("memory.consolidation_backend_failed", `memory consolidation apply paused: the backend refused the product batch (the plan stays durable and replays): ${err instanceof Error ? err.message : String(err)}`);
|
|
2930
|
+
}
|
|
2931
|
+
for (const a of report.applied)
|
|
2932
|
+
if (plan.productStates[a.id] === "pending")
|
|
2933
|
+
plan.productStates[a.id] = "applied";
|
|
2934
|
+
for (const c of report.conflicts) {
|
|
2935
|
+
if (plan.productStates[c.id] === undefined)
|
|
2936
|
+
continue;
|
|
2937
|
+
const probe = (await face.getByIds([c.id])).find((e) => e.id === c.id);
|
|
2938
|
+
if (probe !== undefined && probe.rev === plan.products.find((p) => p.id === c.id)?.rev) {
|
|
2939
|
+
plan.productStates[c.id] = "applied";
|
|
2940
|
+
continue;
|
|
2941
|
+
}
|
|
2942
|
+
plan.productStates[c.id] = "conflict";
|
|
2943
|
+
plan.audit.push({ at: this.now(), event: "product_conflict", detail: `${c.id}: ${c.reason}` });
|
|
2944
|
+
}
|
|
2945
|
+
if (lineageArmed) {
|
|
2946
|
+
try {
|
|
2947
|
+
recordLineageCredential(this.controlDir, txnId, report.applied.filter((a) => a.op !== "delete").map((a) => a.id), this.now);
|
|
2948
|
+
this.settlePromotions(promoteLineagePending(this.controlDir, txnId, this.now));
|
|
2949
|
+
}
|
|
2950
|
+
catch (err) {
|
|
2951
|
+
plan.audit.push({ at: this.now(), event: "lineage_settle_failed", detail: err instanceof Error ? err.message : String(err) });
|
|
2952
|
+
const sink = this.onIncident;
|
|
2953
|
+
if (sink !== undefined) {
|
|
2954
|
+
try {
|
|
2955
|
+
const e = new Error(`memory consolidation lineage settlement failed after commit (entries stay latched until reconciled): ${err instanceof Error ? err.message : String(err)}`);
|
|
2956
|
+
e.code = "memory.lineage_settle_failed";
|
|
2957
|
+
sink(e);
|
|
2958
|
+
}
|
|
2959
|
+
catch {
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
writeConsolidationPlan(this.controlDir, plan);
|
|
2965
|
+
}
|
|
2966
|
+
const pendingDirected = plan.directed.filter((d) => d.state === "pending");
|
|
2967
|
+
if (pendingDirected.length > 0) {
|
|
2968
|
+
if (!(await this.consolidationLeaseCurrent(plan.scope, frozenLeaseToken))) {
|
|
2969
|
+
throw new ConsolidationRefusedError("memory.consolidation_lease_unavailable", `memory consolidation apply paused: the global lease for scope ${JSON.stringify(plan.scope)} is no longer held at its frozen fencing token — the plan stays durable and replays under a valid holder (reconcileConsolidation).`);
|
|
2970
|
+
}
|
|
2971
|
+
const patches = pendingDirected.map((d) => (d.op === "delete" ? { op: "delete", id: d.id, baseRev: d.baseRev } : { op: "update", id: d.id, entry: d.entry, baseRev: d.baseRev }));
|
|
2972
|
+
let report;
|
|
2973
|
+
try {
|
|
2974
|
+
report = await this.backend.applyPatches(patches);
|
|
2975
|
+
}
|
|
2976
|
+
catch (err) {
|
|
2977
|
+
throw new ConsolidationRefusedError("memory.consolidation_backend_failed", `memory consolidation apply paused: the backend refused the directed batch (the plan stays durable and replays): ${err instanceof Error ? err.message : String(err)}`);
|
|
2978
|
+
}
|
|
2979
|
+
const appliedIds = new Set(report.applied.map((a) => `${a.op}:${a.id}`));
|
|
2980
|
+
const conflicted = new Map(report.conflicts.map((c) => [`${c.op}:${c.id}`, c]));
|
|
2981
|
+
for (const d of pendingDirected) {
|
|
2982
|
+
const key = `${d.op}:${d.id}`;
|
|
2983
|
+
if (appliedIds.has(key)) {
|
|
2984
|
+
d.state = "applied";
|
|
2985
|
+
if (d.op === "delete")
|
|
2986
|
+
d.attribution = "self";
|
|
2987
|
+
continue;
|
|
2988
|
+
}
|
|
2989
|
+
const conflict = conflicted.get(key);
|
|
2990
|
+
if (conflict === undefined)
|
|
2991
|
+
continue;
|
|
2992
|
+
if (d.op === "delete") {
|
|
2993
|
+
const probe = await face.getByIds([d.id]);
|
|
2994
|
+
const standing = probe.find((e) => e.id === d.id && e.frontmatter.deleted !== true);
|
|
2995
|
+
if (standing === undefined) {
|
|
2996
|
+
d.state = "satisfied";
|
|
2997
|
+
d.attribution = "unattributed";
|
|
2998
|
+
}
|
|
2999
|
+
else {
|
|
3000
|
+
d.state = "conflict";
|
|
3001
|
+
plan.audit.push({ at: this.now(), event: "directed_conflict", detail: `${d.id}: ${conflict.reason}` });
|
|
3002
|
+
}
|
|
3003
|
+
continue;
|
|
3004
|
+
}
|
|
3005
|
+
const probe = await face.getByIds([d.id]);
|
|
3006
|
+
const current = probe.find((e) => e.id === d.id);
|
|
3007
|
+
if (current !== undefined && current.rev === d.plannedPostRev) {
|
|
3008
|
+
d.state = "satisfied";
|
|
3009
|
+
}
|
|
3010
|
+
else {
|
|
3011
|
+
d.state = "conflict";
|
|
3012
|
+
plan.audit.push({ at: this.now(), event: "directed_conflict", detail: `${d.id}: ${conflict.reason}` });
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
writeConsolidationPlan(this.controlDir, plan);
|
|
3016
|
+
}
|
|
3017
|
+
try {
|
|
3018
|
+
const post = this.readChallengeExclusions();
|
|
3019
|
+
for (const p of plan.products) {
|
|
3020
|
+
if (plan.productStates[p.id] !== "applied")
|
|
3021
|
+
continue;
|
|
3022
|
+
if (post.has(p.id))
|
|
3023
|
+
continue;
|
|
3024
|
+
const hitInputs = (p.frontmatter.distilled?.inputs ?? []).filter((i) => post.has(i.id)).map((i) => i.id);
|
|
3025
|
+
if (hitInputs.length > 0) {
|
|
3026
|
+
this.challengeEntries([p.id], `consolidation input ${hitInputs[0]} was challenged in the freeze→apply window (retroactive lineage propagation)`, `consolidation:${plan.planId}:recheck`);
|
|
3027
|
+
plan.audit.push({ at: this.now(), event: "post_apply_propagation", detail: `${p.id} challenged over inputs ${hitInputs.join(", ")}` });
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
catch (err) {
|
|
3032
|
+
plan.audit.push({ at: this.now(), event: "post_apply_recheck_unreadable", detail: err instanceof Error ? err.message : String(err) });
|
|
3033
|
+
}
|
|
3034
|
+
return await this.settleConsolidationPlan(plan);
|
|
3035
|
+
}
|
|
3036
|
+
async settleConsolidationPlan(plan) {
|
|
3037
|
+
const productStates = Object.values(plan.productStates);
|
|
3038
|
+
const anyConflict = productStates.includes("conflict") || plan.directed.some((d) => d.state === "conflict");
|
|
3039
|
+
const allTerminal = productStates.every((s) => s !== "pending") && plan.directed.every((d) => d.state !== "pending");
|
|
3040
|
+
if (!allTerminal) {
|
|
3041
|
+
writeConsolidationPlan(this.controlDir, plan);
|
|
3042
|
+
throw new ConsolidationRefusedError("memory.consolidation_backend_failed", `memory consolidation plan ${plan.planId} did not reach terminal state this pass — it stays durable and replays (reconcileConsolidation).`);
|
|
3043
|
+
}
|
|
3044
|
+
const at = this.now();
|
|
3045
|
+
const notices = [];
|
|
3046
|
+
if (anyConflict) {
|
|
3047
|
+
plan.state = "conflict";
|
|
3048
|
+
plan.audit.push({ at, event: "conflict" });
|
|
3049
|
+
writeConsolidationPlan(this.controlDir, plan);
|
|
3050
|
+
try {
|
|
3051
|
+
enqueueMemoryAnnouncement(this.controlDir, { kind: "gate", at, items: [`memory consolidation plan ${plan.planId} for scope ${inlineUntrusted(plan.scope, 80)} parked CONFLICT — resolveConsolidationPlan(planId, "retry"|"discard") is the host valve`] });
|
|
3052
|
+
}
|
|
3053
|
+
catch (err) {
|
|
3054
|
+
this.discloseAnnounceFailure("consolidation conflict enqueue", err);
|
|
3055
|
+
}
|
|
3056
|
+
notices.push(memoryConsolidationConflictNotice({ planId: plan.planId, scope: plan.scope }));
|
|
3057
|
+
return this.consolidationReceipt(plan, notices);
|
|
3058
|
+
}
|
|
3059
|
+
plan.state = "completed";
|
|
3060
|
+
plan.audit.push({ at, event: "completed" });
|
|
3061
|
+
const credentialRows = plan.intents.map((it) => ({
|
|
3062
|
+
requestId: it.requestId,
|
|
3063
|
+
state: "settled",
|
|
3064
|
+
planId: plan.planId,
|
|
3065
|
+
at,
|
|
3066
|
+
patches: plan.directed.filter((d) => d.intentRequestId === it.requestId).map((d) => ({ op: d.op, id: d.id, ...(d.attribution !== undefined ? { attribution: d.attribution } : {}) })),
|
|
3067
|
+
}));
|
|
3068
|
+
try {
|
|
3069
|
+
const written = new Set(recordIntentCredentials(this.controlDir, credentialRows));
|
|
3070
|
+
for (const it of plan.intents)
|
|
3071
|
+
it.state = "settled";
|
|
3072
|
+
for (const row2 of credentialRows) {
|
|
3073
|
+
if (!written.has(row2.requestId)) {
|
|
3074
|
+
plan.audit.push({ at, event: "credential_preowned", detail: `${row2.requestId}: another plan's credential stands (terminal-state effects converged; the standing row wins, F10)` });
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
catch (err) {
|
|
3079
|
+
plan.state = "applying";
|
|
3080
|
+
plan.audit.push({ at, event: "credential_write_failed", detail: err instanceof Error ? err.message : String(err) });
|
|
3081
|
+
writeConsolidationPlan(this.controlDir, plan);
|
|
3082
|
+
throw new ConsolidationRefusedError("memory.consolidation_governance_unreadable", `memory consolidation plan ${plan.planId} committed its writes but could not record the intent credentials — the plan stays durable and replays (reconcileConsolidation): ${err instanceof Error ? err.message : String(err)}`);
|
|
3083
|
+
}
|
|
3084
|
+
writeConsolidationPlan(this.controlDir, plan);
|
|
3085
|
+
const currentlyEligible = await this.settleFingerprintFilter(plan.scope);
|
|
3086
|
+
updateConsolidationGate(this.controlDir, (file) => {
|
|
3087
|
+
const row = file.scopes[plan.scope];
|
|
3088
|
+
if (row?.openPlanId !== plan.planId)
|
|
3089
|
+
return { result: undefined };
|
|
3090
|
+
delete row.openPlanId;
|
|
3091
|
+
row.lastRunAt = at;
|
|
3092
|
+
row.epoch += 1;
|
|
3093
|
+
row.sessions = { count: 0, ring: [] };
|
|
3094
|
+
if (row.snapshot !== undefined) {
|
|
3095
|
+
if (currentlyEligible !== undefined) {
|
|
3096
|
+
const fp = {};
|
|
3097
|
+
for (const [id, rev] of Object.entries(row.snapshot.eligible)) {
|
|
3098
|
+
if (!currentlyEligible(id, rev))
|
|
3099
|
+
continue;
|
|
3100
|
+
fp[id] = { rev };
|
|
3101
|
+
}
|
|
3102
|
+
row.fingerprint = fp;
|
|
3103
|
+
}
|
|
3104
|
+
else {
|
|
3105
|
+
delete row.fingerprint;
|
|
3106
|
+
}
|
|
3107
|
+
delete row.snapshot;
|
|
3108
|
+
}
|
|
3109
|
+
file.scopes[plan.scope] = row;
|
|
3110
|
+
return { next: file, result: undefined };
|
|
3111
|
+
});
|
|
3112
|
+
const productCount = Object.values(plan.productStates).filter((s) => s === "applied").length;
|
|
3113
|
+
const supersededCount = new Set(plan.products.flatMap((p) => (p.frontmatter.distilled?.inputs ?? []).filter((i) => i.superseded === true).map((i) => i.id))).size;
|
|
3114
|
+
try {
|
|
3115
|
+
enqueueMemoryAnnouncement(this.controlDir, {
|
|
3116
|
+
kind: "gate",
|
|
3117
|
+
at,
|
|
3118
|
+
items: [`memory consolidation plan ${plan.planId} for scope ${inlineUntrusted(plan.scope, 80)} COMPLETED: ${productCount} product(s) committed, ${supersededCount} entr${supersededCount === 1 ? "y" : "ies"} superseded (evidence retained), ${plan.intents.length} intent(s) settled`],
|
|
3119
|
+
});
|
|
3120
|
+
}
|
|
3121
|
+
catch (err) {
|
|
3122
|
+
this.discloseAnnounceFailure("consolidation completion enqueue", err);
|
|
3123
|
+
}
|
|
3124
|
+
notices.push(memoryConsolidationCommittedNotice({ planId: plan.planId, scope: plan.scope, products: productCount, superseded: supersededCount, intents: plan.intents.length }));
|
|
3125
|
+
return this.consolidationReceipt(plan, notices);
|
|
3126
|
+
}
|
|
3127
|
+
consolidationReceipt(plan, notices) {
|
|
3128
|
+
const states = Object.values(plan.productStates);
|
|
3129
|
+
return {
|
|
3130
|
+
planId: plan.planId,
|
|
3131
|
+
state: plan.state === "completed" ? "completed" : "conflict",
|
|
3132
|
+
products: { frozen: plan.products.length, applied: states.filter((s) => s === "applied").length, conflicts: states.filter((s) => s === "conflict").length, refused: [] },
|
|
3133
|
+
directed: {
|
|
3134
|
+
frozen: plan.directed.length,
|
|
3135
|
+
applied: plan.directed.filter((d) => d.state === "applied").length,
|
|
3136
|
+
satisfied: plan.directed.filter((d) => d.state === "satisfied").length,
|
|
3137
|
+
conflicts: plan.directed.filter((d) => d.state === "conflict").length,
|
|
3138
|
+
},
|
|
3139
|
+
intents: { settled: plan.intents.filter((i) => i.state === "settled").map((i) => i.requestId), pending: plan.intents.filter((i) => i.state === "pending").map((i) => i.requestId) },
|
|
3140
|
+
foldedOrigin: plan.foldedOrigin !== undefined,
|
|
3141
|
+
notices,
|
|
3142
|
+
};
|
|
3143
|
+
}
|
|
3144
|
+
async reconcileConsolidation(scope) {
|
|
3145
|
+
this.requireConsolidation();
|
|
3146
|
+
const gate = this.readConsolidationGateOrRefuse();
|
|
3147
|
+
const row = gate.scopes[scope];
|
|
3148
|
+
const out = { propagatedChallenges: [] };
|
|
3149
|
+
if (row?.openPlanId !== undefined) {
|
|
3150
|
+
const seated = row.openPlanId;
|
|
3151
|
+
const read = readConsolidationPlan(this.controlDir, seated);
|
|
3152
|
+
if (read.state === "absent") {
|
|
3153
|
+
if (this.now() - (row.seatClaimedAt ?? 0) < CONSOLIDATION_SEAT_FREEZE_GRACE_MS) {
|
|
3154
|
+
out.seatInFlight = seated;
|
|
3155
|
+
}
|
|
3156
|
+
else {
|
|
3157
|
+
this.releaseOrphanConsolidationSeat(scope, seated, out);
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
else if (read.state === "corrupt") {
|
|
3161
|
+
out.corruptPlan = { planId: seated, detail: read.detail };
|
|
3162
|
+
}
|
|
3163
|
+
else if (read.plan.state === "open" || read.plan.state === "applying") {
|
|
3164
|
+
try {
|
|
3165
|
+
const receipt = await this.applyConsolidationPlan(read.plan);
|
|
3166
|
+
out.replayed = { planId: seated, state: receipt.state };
|
|
3167
|
+
}
|
|
3168
|
+
finally {
|
|
3169
|
+
await this.releaseConsolidationLease(scope);
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
else if (read.plan.state === "completed" || read.plan.state === "discarded") {
|
|
3173
|
+
await this.settleGateForTerminalPlan(read.plan);
|
|
3174
|
+
out.replayed = { planId: seated, state: read.plan.state === "completed" ? "completed" : "conflict" };
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
await this.runConsolidationPropagationSweep(scope, out);
|
|
3178
|
+
return out;
|
|
3179
|
+
}
|
|
3180
|
+
releaseOrphanConsolidationSeat(scope, seated, out) {
|
|
3181
|
+
updateConsolidationGate(this.controlDir, (file) => {
|
|
3182
|
+
const r = file.scopes[scope];
|
|
3183
|
+
if (r?.openPlanId === seated) {
|
|
3184
|
+
delete r.openPlanId;
|
|
3185
|
+
file.scopes[scope] = r;
|
|
3186
|
+
return { next: file, result: undefined };
|
|
3187
|
+
}
|
|
3188
|
+
return { result: undefined };
|
|
3189
|
+
});
|
|
3190
|
+
try {
|
|
3191
|
+
enqueueMemoryAnnouncement(this.controlDir, { kind: "gate", at: this.now(), items: [`memory consolidation seat for scope ${inlineUntrusted(scope, 80)} released: plan ${seated} was lost before freeze (crash window) — nothing was applied`] });
|
|
3192
|
+
}
|
|
3193
|
+
catch (err) {
|
|
3194
|
+
this.discloseAnnounceFailure("consolidation orphan-seat enqueue", err);
|
|
3195
|
+
}
|
|
3196
|
+
out.releasedOrphanSeat = seated;
|
|
3197
|
+
}
|
|
3198
|
+
async runConsolidationPropagationSweep(scope, out) {
|
|
3199
|
+
try {
|
|
3200
|
+
const face = this.committedAuditFace();
|
|
3201
|
+
const headers = await face.listHeaders([scope]);
|
|
3202
|
+
const exclusions = this.readChallengeExclusions();
|
|
3203
|
+
const productHeaders = headers.filter((h) => h.distilled !== undefined && !exclusions.has(h.id));
|
|
3204
|
+
if (productHeaders.length > 0) {
|
|
3205
|
+
const products = await face.getByIds(productHeaders.map((h) => h.id));
|
|
3206
|
+
for (const p of products) {
|
|
3207
|
+
const challengedInputs = (p.frontmatter.distilled?.inputs ?? []).filter((i) => exclusions.get(i.id)?.code === "challenged").map((i) => i.id);
|
|
3208
|
+
if (challengedInputs.length === 0)
|
|
3209
|
+
continue;
|
|
3210
|
+
this.challengeEntries([p.id], `consolidation input ${challengedInputs[0]} is challenged (retroactive lineage propagation — the product distilled from it)`, `consolidation-reconcile:${uuidv7()}`);
|
|
3211
|
+
out.propagatedChallenges.push(p.id);
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
catch (err) {
|
|
3216
|
+
out.sweepError = err instanceof Error ? err.message : String(err);
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
async settleGateForTerminalPlan(plan) {
|
|
3220
|
+
const currentlyEligible = await this.settleFingerprintFilter(plan.scope);
|
|
3221
|
+
updateConsolidationGate(this.controlDir, (file) => {
|
|
3222
|
+
const row = file.scopes[plan.scope];
|
|
3223
|
+
if (row?.openPlanId !== plan.planId)
|
|
3224
|
+
return { result: undefined };
|
|
3225
|
+
delete row.openPlanId;
|
|
3226
|
+
row.epoch += 1;
|
|
3227
|
+
if (plan.state === "completed") {
|
|
3228
|
+
row.lastRunAt = this.now();
|
|
3229
|
+
row.sessions = { count: 0, ring: [] };
|
|
3230
|
+
if (row.snapshot !== undefined) {
|
|
3231
|
+
if (currentlyEligible !== undefined) {
|
|
3232
|
+
const fp = {};
|
|
3233
|
+
for (const [id, rev] of Object.entries(row.snapshot.eligible)) {
|
|
3234
|
+
if (!currentlyEligible(id, rev))
|
|
3235
|
+
continue;
|
|
3236
|
+
fp[id] = { rev };
|
|
3237
|
+
}
|
|
3238
|
+
row.fingerprint = fp;
|
|
3239
|
+
}
|
|
3240
|
+
else {
|
|
3241
|
+
delete row.fingerprint;
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
delete row.snapshot;
|
|
3246
|
+
file.scopes[plan.scope] = row;
|
|
3247
|
+
return { next: file, result: undefined };
|
|
3248
|
+
});
|
|
3249
|
+
}
|
|
3250
|
+
async settleFingerprintFilter(scope) {
|
|
3251
|
+
try {
|
|
3252
|
+
const headers = await this.committedAuditFace().listHeaders([scope]);
|
|
3253
|
+
const excl = this.readChallengeExclusions();
|
|
3254
|
+
const sup = deriveSupersededSet(headers, { excluded: (id) => excl.has(id) });
|
|
3255
|
+
const revById = new Map(headers.map((h) => [h.id, h.rev]));
|
|
3256
|
+
return (id, rev) => !excl.has(id) && !sup.has(id) && revById.get(id) === rev;
|
|
3257
|
+
}
|
|
3258
|
+
catch {
|
|
3259
|
+
return undefined;
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
async resolveConsolidationPlan(planId, action, opts) {
|
|
3263
|
+
this.requireConsolidation();
|
|
3264
|
+
if (!isValidConsolidationPlanId(planId)) {
|
|
3265
|
+
throw new ConsolidationRefusedError("memory.consolidation_refused", `memory consolidation plan id ${JSON.stringify(planId.slice(0, 80))} fails the plan-id grammar — refused before any path use.`);
|
|
3266
|
+
}
|
|
3267
|
+
const read = readConsolidationPlan(this.controlDir, planId);
|
|
3268
|
+
const at = this.now();
|
|
3269
|
+
if (read.state === "corrupt") {
|
|
3270
|
+
if (action === "retry") {
|
|
3271
|
+
throw new ConsolidationRefusedError("memory.consolidation_refused", `memory consolidation plan ${planId} is corrupt (${read.detail}) — retry has no readable intents; discard it (the bytes move to custody).`);
|
|
3272
|
+
}
|
|
3273
|
+
const custody = quarantineCorruptPlan(this.controlDir, planId);
|
|
3274
|
+
updateConsolidationGate(this.controlDir, (file) => {
|
|
3275
|
+
for (const [scope, row] of Object.entries(file.scopes)) {
|
|
3276
|
+
if (row.openPlanId === planId) {
|
|
3277
|
+
delete row.openPlanId;
|
|
3278
|
+
row.epoch += 1;
|
|
3279
|
+
delete row.snapshot;
|
|
3280
|
+
file.scopes[scope] = row;
|
|
3281
|
+
return { next: file, result: undefined };
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
return { result: undefined };
|
|
3285
|
+
});
|
|
3286
|
+
try {
|
|
3287
|
+
enqueueMemoryAnnouncement(this.controlDir, { kind: "gate", at, items: [`memory consolidation plan ${planId} DISCARDED as corrupt (requestId ${inlineUntrusted(opts.requestId, 80)})${custody !== undefined ? ` — bytes retained at ${custody}` : ""}`] });
|
|
3288
|
+
}
|
|
3289
|
+
catch (err) {
|
|
3290
|
+
this.discloseAnnounceFailure("consolidation corrupt-discard enqueue", err);
|
|
3291
|
+
}
|
|
3292
|
+
return { planId, action, state: "discarded", ...(custody !== undefined ? { custody } : {}) };
|
|
3293
|
+
}
|
|
3294
|
+
if (read.state === "absent") {
|
|
3295
|
+
throw new ConsolidationRefusedError("memory.consolidation_refused", `memory consolidation plan ${planId} does not exist.`);
|
|
3296
|
+
}
|
|
3297
|
+
const plan = read.plan;
|
|
3298
|
+
if (plan.state === "completed" || plan.state === "discarded") {
|
|
3299
|
+
throw new ConsolidationRefusedError("memory.consolidation_refused", `memory consolidation plan ${planId} is already terminal (${plan.state}) — a late valve action is refused (状态先读后写).`);
|
|
3300
|
+
}
|
|
3301
|
+
if (action === "discard") {
|
|
3302
|
+
plan.state = "discarded";
|
|
3303
|
+
plan.audit.push({ at, event: "discarded", requestId: opts.requestId });
|
|
3304
|
+
const abandoned = plan.intents.filter((i) => i.state === "pending");
|
|
3305
|
+
for (const it of abandoned)
|
|
3306
|
+
it.state = "abandoned";
|
|
3307
|
+
writeConsolidationPlan(this.controlDir, plan);
|
|
3308
|
+
try {
|
|
3309
|
+
recordIntentCredentials(this.controlDir, abandoned.map((it) => ({ requestId: it.requestId, state: "abandoned", planId, at, patches: [] })));
|
|
3310
|
+
}
|
|
3311
|
+
catch (err) {
|
|
3312
|
+
plan.audit.push({ at, event: "abandon_credential_failed", detail: err instanceof Error ? err.message : String(err) });
|
|
3313
|
+
writeConsolidationPlan(this.controlDir, plan);
|
|
3314
|
+
}
|
|
3315
|
+
await this.settleGateForTerminalPlan(plan);
|
|
3316
|
+
try {
|
|
3317
|
+
enqueueMemoryAnnouncement(this.controlDir, { kind: "gate", at, items: [`memory consolidation plan ${planId} DISCARDED by the host valve (requestId ${inlineUntrusted(opts.requestId, 80)}) — ${abandoned.length} intent(s) settled ABANDONED (given up, not fulfilled)`] });
|
|
3318
|
+
}
|
|
3319
|
+
catch (err) {
|
|
3320
|
+
this.discloseAnnounceFailure("consolidation discard enqueue", err);
|
|
3321
|
+
}
|
|
3322
|
+
return { planId, action, state: "discarded", abandonedIntents: abandoned.map((i) => i.requestId) };
|
|
3323
|
+
}
|
|
3324
|
+
if (plan.state !== "conflict") {
|
|
3325
|
+
throw new ConsolidationRefusedError("memory.consolidation_refused", `memory consolidation plan ${planId} is ${plan.state} — retry acts on a CONFLICT plan (run reconcileConsolidation to converge an in-flight one).`);
|
|
3326
|
+
}
|
|
3327
|
+
let standingCredentials = {};
|
|
3328
|
+
try {
|
|
3329
|
+
standingCredentials = readIntentCredentials(this.controlDir);
|
|
3330
|
+
}
|
|
3331
|
+
catch (err) {
|
|
3332
|
+
throw new ConsolidationRefusedError("memory.consolidation_gate_corrupt", `memory consolidation retry refused: the intent-credential account cannot be trusted (fail-closed): ${err instanceof Error ? err.message : String(err)}`);
|
|
3333
|
+
}
|
|
3334
|
+
const pendingIntents = plan.intents.filter((i) => i.state === "pending" && standingCredentials[i.requestId] === undefined).map((i) => i.requestId);
|
|
3335
|
+
const face = this.committedAuditFace();
|
|
3336
|
+
const originalIntents = await this.reconstructIntentsForRetry(plan, pendingIntents, face);
|
|
3337
|
+
const retryPlanId = uuidv7();
|
|
3338
|
+
updateConsolidationGate(this.controlDir, (file) => {
|
|
3339
|
+
const row = file.scopes[plan.scope] ?? { sessions: { count: 0, ring: [] }, epoch: 0 };
|
|
3340
|
+
if (row.openPlanId !== plan.planId && row.openPlanId !== undefined) {
|
|
3341
|
+
throw new ConsolidationRefusedError("memory.consolidation_busy", `memory consolidation retry refused: plan ${row.openPlanId} holds the ${JSON.stringify(plan.scope)} seat.`);
|
|
3342
|
+
}
|
|
3343
|
+
row.openPlanId = retryPlanId;
|
|
3344
|
+
row.seatClaimedAt = at;
|
|
3345
|
+
file.scopes[plan.scope] = row;
|
|
3346
|
+
return { next: file, result: undefined };
|
|
3347
|
+
});
|
|
3348
|
+
const retryPlan = {
|
|
3349
|
+
v: 1,
|
|
3350
|
+
planId: retryPlanId,
|
|
3351
|
+
scope: plan.scope,
|
|
3352
|
+
requestId: opts.requestId,
|
|
3353
|
+
createdAt: at,
|
|
3354
|
+
epoch: 0,
|
|
3355
|
+
visibleMarked: false,
|
|
3356
|
+
products: [],
|
|
3357
|
+
productStates: {},
|
|
3358
|
+
directed: originalIntents.directed,
|
|
3359
|
+
intents: pendingIntents.map((r) => ({ requestId: r, state: "pending" })),
|
|
3360
|
+
state: "open",
|
|
3361
|
+
audit: [
|
|
3362
|
+
{ at, event: "frozen_retry", requestId: opts.requestId, detail: `retries intents of ${planId}` },
|
|
3363
|
+
...originalIntents.reasons.map((r) => ({ at, event: "directed_conflict", detail: `${r.id}: ${r.reason}` })),
|
|
3364
|
+
],
|
|
3365
|
+
};
|
|
3366
|
+
writeConsolidationPlan(this.controlDir, retryPlan);
|
|
3367
|
+
plan.state = "discarded";
|
|
3368
|
+
plan.audit.push({ at, event: "superseded_by_retry", requestId: opts.requestId, detail: `retried as ${retryPlanId}` });
|
|
3369
|
+
writeConsolidationPlan(this.controlDir, plan);
|
|
3370
|
+
try {
|
|
3371
|
+
const receipt = await this.applyConsolidationPlan(retryPlan);
|
|
3372
|
+
return { planId, action, state: receipt.state, retryPlanId, retryReceipt: receipt };
|
|
3373
|
+
}
|
|
3374
|
+
finally {
|
|
3375
|
+
await this.releaseConsolidationLease(plan.scope);
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
async reconstructIntentsForRetry(plan, pendingIntentIds, face) {
|
|
3379
|
+
const directed = [];
|
|
3380
|
+
const reasons = [];
|
|
3381
|
+
const wanted = new Set(pendingIntentIds);
|
|
3382
|
+
const targets = new Map((await face.getByIds(plan.directed.filter((d) => wanted.has(d.intentRequestId)).map((d) => d.id))).map((e) => [e.id, e]));
|
|
3383
|
+
for (const d of plan.directed) {
|
|
3384
|
+
if (!wanted.has(d.intentRequestId))
|
|
3385
|
+
continue;
|
|
3386
|
+
const committed = targets.get(d.id);
|
|
3387
|
+
if (committed !== undefined && committed.scope !== plan.scope) {
|
|
3388
|
+
directed.push({ intentRequestId: d.intentRequestId, op: d.op, id: d.id, baseRev: "", ...(d.plannedPostRev !== undefined ? { plannedPostRev: d.plannedPostRev } : {}), state: "conflict" });
|
|
3389
|
+
reasons.push({ id: d.id, reason: `target now lives in scope ${JSON.stringify(committed.scope)}, not this plan's ${JSON.stringify(plan.scope)} — no patch was offered to the store` });
|
|
3390
|
+
continue;
|
|
3391
|
+
}
|
|
3392
|
+
if (d.op === "delete") {
|
|
3393
|
+
if (committed === undefined || committed.frontmatter.deleted === true) {
|
|
3394
|
+
directed.push({ intentRequestId: d.intentRequestId, op: "delete", id: d.id, baseRev: "", state: "satisfied", attribution: "unattributed" });
|
|
3395
|
+
}
|
|
3396
|
+
else {
|
|
3397
|
+
directed.push({ intentRequestId: d.intentRequestId, op: "delete", id: d.id, baseRev: committed.rev, state: "pending" });
|
|
3398
|
+
}
|
|
3399
|
+
continue;
|
|
3400
|
+
}
|
|
3401
|
+
if (d.entry === undefined) {
|
|
3402
|
+
const satisfied = committed !== undefined && committed.rev === d.plannedPostRev;
|
|
3403
|
+
directed.push({ intentRequestId: d.intentRequestId, op: "update", id: d.id, baseRev: "", ...(d.plannedPostRev !== undefined ? { plannedPostRev: d.plannedPostRev } : {}), state: satisfied ? "satisfied" : "conflict" });
|
|
3404
|
+
if (!satisfied)
|
|
3405
|
+
reasons.push({ id: d.id, reason: "the frozen row carries no terminal entry to replay and the committed rev is not the planned one — no patch was offered to the store" });
|
|
3406
|
+
continue;
|
|
3407
|
+
}
|
|
3408
|
+
if (committed === undefined || committed.frontmatter.deleted === true) {
|
|
3409
|
+
directed.push({ intentRequestId: d.intentRequestId, op: "update", id: d.id, baseRev: "", ...(d.plannedPostRev !== undefined ? { plannedPostRev: d.plannedPostRev } : {}), state: "conflict" });
|
|
3410
|
+
reasons.push({ id: d.id, reason: "the rewrite target is gone (absent or tombstoned) — a terminal cannot stand on a missing entry, and no patch was offered to the store" });
|
|
3411
|
+
continue;
|
|
3412
|
+
}
|
|
3413
|
+
if (committed.rev === d.plannedPostRev) {
|
|
3414
|
+
directed.push({ intentRequestId: d.intentRequestId, op: "update", id: d.id, baseRev: committed.rev, ...(d.plannedPostRev !== undefined ? { plannedPostRev: d.plannedPostRev } : {}), state: "satisfied" });
|
|
3415
|
+
}
|
|
3416
|
+
else {
|
|
3417
|
+
directed.push({ intentRequestId: d.intentRequestId, op: "update", id: d.id, baseRev: committed.rev, ...(d.plannedPostRev !== undefined ? { plannedPostRev: d.plannedPostRev } : {}), entry: d.entry, state: "pending" });
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
return { directed, reasons };
|
|
3421
|
+
}
|
|
3422
|
+
listConsolidationPlans() {
|
|
3423
|
+
const out = [];
|
|
3424
|
+
for (const planId of listConsolidationPlanIds(this.controlDir)) {
|
|
3425
|
+
const read = readConsolidationPlan(this.controlDir, planId);
|
|
3426
|
+
if (read.state === "corrupt") {
|
|
3427
|
+
out.push({ planId, corrupt: true });
|
|
3428
|
+
continue;
|
|
3429
|
+
}
|
|
3430
|
+
if (read.state === "absent")
|
|
3431
|
+
continue;
|
|
3432
|
+
const p = read.plan;
|
|
3433
|
+
out.push({ planId, scope: p.scope, state: p.state, createdAt: p.createdAt, products: p.products.length, directed: p.directed.length, intents: p.intents.length });
|
|
3434
|
+
}
|
|
3435
|
+
return out;
|
|
3436
|
+
}
|
|
3437
|
+
readConsolidationGate(scope) {
|
|
3438
|
+
try {
|
|
3439
|
+
return readConsolidationGateFile(this.controlDir).scopes[scope];
|
|
3440
|
+
}
|
|
3441
|
+
catch {
|
|
3442
|
+
return undefined;
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
2350
3445
|
committedAuditFace() {
|
|
2351
3446
|
const b = this.backend;
|
|
2352
3447
|
return b.restrictedAdoptionView?.({ audit: false }) ?? b.retrievalView?.() ?? this.backend;
|
|
@@ -2608,6 +3703,8 @@ export class MemoryEngine {
|
|
|
2608
3703
|
const carryIndex = this.provenance === "carry";
|
|
2609
3704
|
const exposedById = new Map();
|
|
2610
3705
|
const exposedProjectionTargets = new Set();
|
|
3706
|
+
const supersededIds = deriveSupersededSet(headers, { excluded: (id) => excludedIds.has(id) });
|
|
3707
|
+
const supersededTargets = new Set();
|
|
2611
3708
|
for (const h of headers) {
|
|
2612
3709
|
const repoTarget = handle.repoIndexTargets?.get(h.id);
|
|
2613
3710
|
const target = repoTarget ?? relative(handle.writableRoot, canonicalize(join(scopeDirFor(this.memoryDir, this.controlDir, h.scope), `${h.slug}.md`)));
|
|
@@ -2615,11 +3712,17 @@ export class MemoryEngine {
|
|
|
2615
3712
|
excludedTargets.add(target);
|
|
2616
3713
|
continue;
|
|
2617
3714
|
}
|
|
3715
|
+
const superseded = supersededIds.has(h.id);
|
|
2618
3716
|
if (carryIndex && h.exposure === "external") {
|
|
2619
|
-
|
|
3717
|
+
if (!superseded)
|
|
3718
|
+
exposedById.set(h.id, h);
|
|
2620
3719
|
exposedProjectionTargets.add(target);
|
|
2621
3720
|
continue;
|
|
2622
3721
|
}
|
|
3722
|
+
if (superseded) {
|
|
3723
|
+
supersededTargets.add(target);
|
|
3724
|
+
continue;
|
|
3725
|
+
}
|
|
2623
3726
|
expected.set(target, h);
|
|
2624
3727
|
}
|
|
2625
3728
|
const covered = new Set();
|
|
@@ -2637,7 +3740,7 @@ export class MemoryEngine {
|
|
|
2637
3740
|
return prev;
|
|
2638
3741
|
};
|
|
2639
3742
|
const foldTarget = (t) => foldDots(t.toLowerCase());
|
|
2640
|
-
const knownTargetsFolded = new Set([...expected.keys(), ...excludedTargets, ...exposedProjectionTargets].map(foldTarget));
|
|
3743
|
+
const knownTargetsFolded = new Set([...expected.keys(), ...excludedTargets, ...exposedProjectionTargets, ...supersededTargets].map(foldTarget));
|
|
2641
3744
|
const pathTokenAt = (folded, idx, len) => {
|
|
2642
3745
|
const isPathChar = (ch) => /[a-z0-9._\-/\\]/.test(ch);
|
|
2643
3746
|
let s = idx;
|
|
@@ -2685,6 +3788,10 @@ export class MemoryEngine {
|
|
|
2685
3788
|
warnings?.push(`MEMORY.md index line removed: ${target} is withheld (challenged or unsettled) — its content stays off the injected index`);
|
|
2686
3789
|
continue;
|
|
2687
3790
|
}
|
|
3791
|
+
if (supersededTargets.has(target)) {
|
|
3792
|
+
warnings?.push(`MEMORY.md index line for ${target} removed: a newer consolidated entry supersedes it (the entry stays readable via memory_get)`);
|
|
3793
|
+
continue;
|
|
3794
|
+
}
|
|
2688
3795
|
if (covered.has(target)) {
|
|
2689
3796
|
if (line === keptByTarget.get(target))
|
|
2690
3797
|
continue;
|