@remnic/core 9.3.745 → 9.3.746
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/dist/access-boundary.d.ts +2 -2
- package/dist/access-cli.js +1 -1
- package/dist/access-http.d.ts +2 -2
- package/dist/access-mcp.d.ts +2 -2
- package/dist/access-operations.d.ts +2 -2
- package/dist/{access-service-CdY88L86.d.ts → access-service-Cx16nmCv.d.ts} +1 -1
- package/dist/access-service.d.ts +2 -2
- package/dist/access-surface-catalog.d.ts +2 -2
- package/dist/bootstrap.d.ts +1 -1
- package/dist/{chunk-YQX63P3W.js → chunk-PJE6D3NH.js} +11513 -11474
- package/dist/chunk-PJE6D3NH.js.map +1 -0
- package/dist/{cli-BNJtn0R0.d.ts → cli-D1y3vooB.d.ts} +2 -2
- package/dist/cli.d.ts +3 -3
- package/dist/explicit-capture.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/mcp-memory-inspector-app.d.ts +2 -2
- package/dist/{orchestrator-BBGXLd20.d.ts → orchestrator-CnCE3Hvq.d.ts} +11 -5
- package/dist/orchestrator.d.ts +1 -1
- package/dist/orchestrator.js +1 -1
- package/package.json +2 -2
- package/src/orchestration/consolidation-run.ts +633 -0
- package/src/orchestrator.ts +39 -538
- package/dist/chunk-YQX63P3W.js.map +0 -1
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consolidation-run coordinator — extracted from the orchestrator
|
|
3
|
+
* (issue #1526, seam 17).
|
|
4
|
+
*
|
|
5
|
+
* Owns the full consolidation maintenance pass:
|
|
6
|
+
* - `run()` — the main consolidation cycle (LLM merge/invalidate/update
|
|
7
|
+
* → entity-file merge → commitment cleanup → TTL cleanup → lifecycle
|
|
8
|
+
* policy → compression guideline learning → tier migration → fact
|
|
9
|
+
* archival → semantic consolidation → identity consolidation → profile
|
|
10
|
+
* consolidation → summarization → topic extraction → TMT rebuild)
|
|
11
|
+
* - `recordScheduledDreamsPhaseRun` — dream-phase ledger recording
|
|
12
|
+
* - `runFactArchival` / `runSummarization` / `runTopicExtraction` —
|
|
13
|
+
* thin delegates to LifecyclePolicyCoordinator
|
|
14
|
+
* - `runCompressionGuidelineLearningPass` — thin delegate to
|
|
15
|
+
* CompressionGuidelineCoordinator
|
|
16
|
+
*
|
|
17
|
+
* The orchestrator constructs one instance and delegates the consolidation
|
|
18
|
+
* pass to it. Everything that must stay orchestrator-owned (access tracking,
|
|
19
|
+
* embedding index, entity synthesis, tier migration, semantic consolidation,
|
|
20
|
+
* identity consolidation, recall section config, fast LLM) arrives as
|
|
21
|
+
* injectable delegates.
|
|
22
|
+
*
|
|
23
|
+
* Behavior-preserving move from orchestrator.ts. No logic changes — the
|
|
24
|
+
* orchestrator keeps a thin delegating method so existing call sites
|
|
25
|
+
* (runConsolidationNow, maintenance scheduler) and tests continue to work.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import path from "node:path";
|
|
29
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
30
|
+
import { log } from "../logger.js";
|
|
31
|
+
import { applyCommitmentLedgerLifecycle } from "../commitment-ledger.js";
|
|
32
|
+
import { recordDreamsPhaseRun } from "../maintenance/dreams-ledger.js";
|
|
33
|
+
import { clearIndexes, deindexMemory, indexMemoriesBatch, indexesExist } from "../temporal-index.js";
|
|
34
|
+
import { isActiveMemoryStatus } from "../memory-lifecycle-ledger-utils.js";
|
|
35
|
+
import {
|
|
36
|
+
resolveConsolidationCapabilities,
|
|
37
|
+
resolveCreationMemoryCapabilities,
|
|
38
|
+
resolveIndexingCapabilities,
|
|
39
|
+
resolveMemoryLifecycleCapabilities,
|
|
40
|
+
resolvePipelineProcessingCapabilities,
|
|
41
|
+
resolvePresentationCapabilities,
|
|
42
|
+
resolveRecallEnhancementCapabilities,
|
|
43
|
+
type MemoryLifecycleCapabilitySet,
|
|
44
|
+
} from "../capabilities.js";
|
|
45
|
+
import type { LifecyclePolicyCoordinator } from "./lifecycle-policy-coordinator.js";
|
|
46
|
+
import type { CompressionGuidelineCoordinator } from "./compression-guideline-coordinator.js";
|
|
47
|
+
import type { SemanticConsolidationCoordinator } from "./semantic-consolidation-coordinator.js";
|
|
48
|
+
import type { EntitySynthesisCoordinator } from "./entity-synthesis-coordinator.js";
|
|
49
|
+
import type { RecallSectionCoordinator } from "./recall-section-coordinator.js";
|
|
50
|
+
import type { TierMigrationCoordinator } from "./tier-migration-coordinator.js";
|
|
51
|
+
import type { StorageManager } from "../index.js";
|
|
52
|
+
import type { NamespaceStorageRouter } from "../namespaces/storage.js";
|
|
53
|
+
import type { ExtractionEngine } from "../extraction.js";
|
|
54
|
+
import type { EmbeddingFallback } from "../embedding-fallback.js";
|
|
55
|
+
import type { TmtBuilder } from "../tmt.js";
|
|
56
|
+
import type {
|
|
57
|
+
ConsolidationObservation,
|
|
58
|
+
MemoryFile,
|
|
59
|
+
PluginConfig,
|
|
60
|
+
RecallSectionConfig,
|
|
61
|
+
} from "../types.js";
|
|
62
|
+
|
|
63
|
+
/** Dependencies injected by the orchestrator. Stable references or live
|
|
64
|
+
* accessors — lazy getters for mutable fields tests reassign
|
|
65
|
+
* post-construction (storage, extraction, storageRouter). */
|
|
66
|
+
export interface ConsolidationRunCoordinatorDeps {
|
|
67
|
+
config: PluginConfig;
|
|
68
|
+
getStorage: () => StorageManager;
|
|
69
|
+
getStorageRouter: () => NamespaceStorageRouter;
|
|
70
|
+
getExtraction: () => ExtractionEngine;
|
|
71
|
+
embeddingFallback: EmbeddingFallback;
|
|
72
|
+
tmtBuilder: TmtBuilder;
|
|
73
|
+
consolidationObservers: Set<
|
|
74
|
+
(observation: ConsolidationObservation) => Promise<void> | void
|
|
75
|
+
>;
|
|
76
|
+
getAccessTrackingBuffer: () => Map<
|
|
77
|
+
string,
|
|
78
|
+
{ count: number; lastAccessed: string }
|
|
79
|
+
>;
|
|
80
|
+
|
|
81
|
+
lifecyclePolicyCoordinator: LifecyclePolicyCoordinator;
|
|
82
|
+
compressionGuidelineCoordinator: CompressionGuidelineCoordinator;
|
|
83
|
+
semanticConsolidationCoordinator: SemanticConsolidationCoordinator;
|
|
84
|
+
entitySynthesisCoordinator: EntitySynthesisCoordinator;
|
|
85
|
+
recallSectionCoordinator: RecallSectionCoordinator;
|
|
86
|
+
tierMigrationCoordinator: TierMigrationCoordinator;
|
|
87
|
+
|
|
88
|
+
flushAccessTracking: () => Promise<void>;
|
|
89
|
+
indexPersistedMemory: (storage: StorageManager, memoryId: string) => Promise<void>;
|
|
90
|
+
autoConsolidateIdentity: () => Promise<void>;
|
|
91
|
+
fastChatCompletion: (
|
|
92
|
+
messages: Array<{ role: string; content: string }>,
|
|
93
|
+
options: {
|
|
94
|
+
temperature?: number;
|
|
95
|
+
maxTokens?: number;
|
|
96
|
+
timeoutMs?: number;
|
|
97
|
+
operation?: string;
|
|
98
|
+
priority?: "background" | "recall-critical";
|
|
99
|
+
},
|
|
100
|
+
) => Promise<{ content: string } | null>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export class ConsolidationRunCoordinator {
|
|
104
|
+
constructor(private readonly deps: ConsolidationRunCoordinatorDeps) {}
|
|
105
|
+
|
|
106
|
+
async run(): Promise<{
|
|
107
|
+
memoriesProcessed: number;
|
|
108
|
+
merged: number;
|
|
109
|
+
invalidated: number;
|
|
110
|
+
}> {
|
|
111
|
+
const config = this.deps.config;
|
|
112
|
+
const storage = this.deps.getStorage();
|
|
113
|
+
const lifecycleCaps = resolveMemoryLifecycleCapabilities(config);
|
|
114
|
+
log.info("running consolidation pass");
|
|
115
|
+
let merged = 0;
|
|
116
|
+
let invalidated = 0;
|
|
117
|
+
// Tracks whether any consolidation memory-item action (UPDATE / MERGE /
|
|
118
|
+
// INVALIDATE) durably rewrote memory state. A consolidation pass that only
|
|
119
|
+
// mutates memory items (no profile/entity updates) still changes the default
|
|
120
|
+
// namespace's data, so its catalog `lastWriteAt` must refresh too (NIBOi).
|
|
121
|
+
let memoryItemMutated = false;
|
|
122
|
+
|
|
123
|
+
// Flush access tracking buffer first
|
|
124
|
+
if (this.deps.getAccessTrackingBuffer().size > 0) {
|
|
125
|
+
await this.deps.flushAccessTracking();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let allMemories = await storage.readAllMemories();
|
|
129
|
+
if (allMemories.length < 5) {
|
|
130
|
+
return { memoriesProcessed: allMemories.length, merged, invalidated };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const recent = allMemories
|
|
134
|
+
.sort(
|
|
135
|
+
(a, b) =>
|
|
136
|
+
new Date(b.frontmatter.created).getTime() -
|
|
137
|
+
new Date(a.frontmatter.created).getTime(),
|
|
138
|
+
)
|
|
139
|
+
.slice(0, 20);
|
|
140
|
+
|
|
141
|
+
const older = allMemories.sort(
|
|
142
|
+
(a, b) =>
|
|
143
|
+
new Date(a.frontmatter.created).getTime() -
|
|
144
|
+
new Date(b.frontmatter.created).getTime(),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const profile = await storage.readProfile();
|
|
148
|
+
const result = await this.deps.getExtraction().consolidate(recent, older, profile);
|
|
149
|
+
|
|
150
|
+
// Build a lookup map from the already-loaded corpus to avoid repeated
|
|
151
|
+
// readAllMemories() scans inside getMemoryById for pre-action deindex reads.
|
|
152
|
+
const memoryLookup = resolveIndexingCapabilities(config).queryAwareIndexing
|
|
153
|
+
? new Map(allMemories.map((m) => [m.frontmatter.id, m]))
|
|
154
|
+
: null;
|
|
155
|
+
|
|
156
|
+
for (const item of result.items) {
|
|
157
|
+
switch (item.action) {
|
|
158
|
+
case "INVALIDATE": {
|
|
159
|
+
// Capture path/frontmatter before invalidation for index cleanup
|
|
160
|
+
const toInvalidate = resolveIndexingCapabilities(config).queryAwareIndexing
|
|
161
|
+
? (memoryLookup?.get(item.existingId) ?? null)
|
|
162
|
+
: null;
|
|
163
|
+
if (await storage.invalidateMemory(item.existingId)) {
|
|
164
|
+
invalidated += 1;
|
|
165
|
+
memoryItemMutated = true;
|
|
166
|
+
await this.deps.embeddingFallback.removeFromIndex(item.existingId);
|
|
167
|
+
if (toInvalidate?.path && toInvalidate.frontmatter?.created) {
|
|
168
|
+
deindexMemory(
|
|
169
|
+
config.memoryDir,
|
|
170
|
+
toInvalidate.path,
|
|
171
|
+
toInvalidate.frontmatter.created,
|
|
172
|
+
toInvalidate.frontmatter.tags ?? [],
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
case "UPDATE":
|
|
179
|
+
if (item.updatedContent) {
|
|
180
|
+
await storage.updateMemory(
|
|
181
|
+
item.existingId,
|
|
182
|
+
item.updatedContent,
|
|
183
|
+
{
|
|
184
|
+
lineage: [item.existingId],
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
memoryItemMutated = true;
|
|
188
|
+
await this.deps.indexPersistedMemory(storage, item.existingId);
|
|
189
|
+
// updateMemory() only changes content/updated/lineage — path, created, and tags
|
|
190
|
+
// are preserved, so the temporal/tag index entry is already correct; no reindex needed.
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
case "MERGE":
|
|
194
|
+
if (item.updatedContent && item.mergeWith) {
|
|
195
|
+
await storage.updateMemory(
|
|
196
|
+
item.existingId,
|
|
197
|
+
item.updatedContent,
|
|
198
|
+
{
|
|
199
|
+
supersedes: item.mergeWith,
|
|
200
|
+
lineage: [item.existingId, item.mergeWith],
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
memoryItemMutated = true;
|
|
204
|
+
await this.deps.indexPersistedMemory(storage, item.existingId);
|
|
205
|
+
// updateMemory() only changes content/updated/supersedes/lineage — path, created, and tags
|
|
206
|
+
// are preserved, so the temporal/tag index entry for the survivor is already correct.
|
|
207
|
+
// Capture before invalidation for index cleanup
|
|
208
|
+
const toMergeInvalidate = resolveIndexingCapabilities(config).queryAwareIndexing
|
|
209
|
+
? (memoryLookup?.get(item.mergeWith) ?? null)
|
|
210
|
+
: null;
|
|
211
|
+
if (await storage.invalidateMemory(item.mergeWith)) {
|
|
212
|
+
invalidated += 1;
|
|
213
|
+
merged += 1;
|
|
214
|
+
await this.deps.embeddingFallback.removeFromIndex(item.mergeWith);
|
|
215
|
+
if (
|
|
216
|
+
toMergeInvalidate?.path &&
|
|
217
|
+
toMergeInvalidate.frontmatter?.created
|
|
218
|
+
) {
|
|
219
|
+
deindexMemory(
|
|
220
|
+
config.memoryDir,
|
|
221
|
+
toMergeInvalidate.path,
|
|
222
|
+
toMergeInvalidate.frontmatter.created,
|
|
223
|
+
toMergeInvalidate.frontmatter.tags ?? [],
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (result.profileUpdates.length > 0) {
|
|
233
|
+
await storage.appendToProfile(result.profileUpdates);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
for (const entity of result.entityUpdates) {
|
|
237
|
+
const safeFacts = Array.isArray(entity.facts)
|
|
238
|
+
? entity.facts.filter((f): f is string => typeof f === "string")
|
|
239
|
+
: [];
|
|
240
|
+
await storage.writeEntity(entity.name, entity.type, safeFacts, {
|
|
241
|
+
source: "consolidation",
|
|
242
|
+
structuredSections: Array.isArray(entity.structuredSections)
|
|
243
|
+
? entity.structuredSections
|
|
244
|
+
: undefined,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Catalog write touch accounting (issue #1499 sweep): consolidation persists
|
|
249
|
+
// durable mutations directly to the default-namespace storage, bypassing
|
|
250
|
+
// the extraction write path. We do NOT touch here — later maintenance steps in
|
|
251
|
+
// this same function (entity-file merges, expired-commitment / TTL cleanup,
|
|
252
|
+
// fact archival) can ALSO mutate the namespace on a run with no LLM outputs
|
|
253
|
+
// (NIjwl). So we accumulate every durable mutation into `memoryItemMutated` and
|
|
254
|
+
// record ONE consolidated touch AFTER all mutation-producing steps complete,
|
|
255
|
+
// just before returning (rule #25: touch after the write commits). LLM
|
|
256
|
+
// profile/entity updates and memory-item actions (UPDATE / MERGE / INVALIDATE)
|
|
257
|
+
// count here (NIBOi).
|
|
258
|
+
if (result.profileUpdates.length > 0 || result.entityUpdates.length > 0) {
|
|
259
|
+
memoryItemMutated = true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Merge fragmented entity files
|
|
263
|
+
const entitiesMerged = await storage.mergeFragmentedEntities();
|
|
264
|
+
if (entitiesMerged > 0) {
|
|
265
|
+
memoryItemMutated = true;
|
|
266
|
+
log.info(`merged ${entitiesMerged} fragmented entity files`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (resolvePresentationCapabilities(config).entitySummary) {
|
|
270
|
+
try {
|
|
271
|
+
const synthesized = await this.deps.entitySynthesisCoordinator.processQueue(
|
|
272
|
+
config.defaultNamespace,
|
|
273
|
+
5,
|
|
274
|
+
);
|
|
275
|
+
if (synthesized > 0) {
|
|
276
|
+
// Entity synthesis rewrites entity files — a durable namespace mutation,
|
|
277
|
+
// so record it for the catalog touch even when it is the only change in
|
|
278
|
+
// the pass (codex). Otherwise lastWriteAt goes stale.
|
|
279
|
+
memoryItemMutated = true;
|
|
280
|
+
log.info(`refreshed ${synthesized} entity syntheses`);
|
|
281
|
+
}
|
|
282
|
+
} catch (err) {
|
|
283
|
+
log.debug(`entity synthesis pass failed: ${err}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Clean expired commitments
|
|
288
|
+
const deletedCommitments = await storage.cleanExpiredCommitments(
|
|
289
|
+
config.commitmentDecayDays,
|
|
290
|
+
);
|
|
291
|
+
if (deletedCommitments.length > 0) {
|
|
292
|
+
memoryItemMutated = true;
|
|
293
|
+
log.info(`cleaned ${deletedCommitments.length} expired commitments`);
|
|
294
|
+
if (resolveIndexingCapabilities(config).queryAwareIndexing) {
|
|
295
|
+
for (const m of deletedCommitments) {
|
|
296
|
+
deindexMemory(
|
|
297
|
+
config.memoryDir,
|
|
298
|
+
m.path,
|
|
299
|
+
m.frontmatter.created,
|
|
300
|
+
m.frontmatter.tags ?? [],
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (
|
|
307
|
+
resolveCreationMemoryCapabilities(config).creationMemory &&
|
|
308
|
+
resolveCreationMemoryCapabilities(config).commitmentLedger &&
|
|
309
|
+
resolveCreationMemoryCapabilities(config).commitmentLifecycle
|
|
310
|
+
) {
|
|
311
|
+
try {
|
|
312
|
+
const lifecycle = await applyCommitmentLedgerLifecycle({
|
|
313
|
+
memoryDir: config.memoryDir,
|
|
314
|
+
commitmentLedgerDir: config.commitmentLedgerDir,
|
|
315
|
+
enabled: true,
|
|
316
|
+
decayDays: config.commitmentDecayDays,
|
|
317
|
+
});
|
|
318
|
+
if (
|
|
319
|
+
lifecycle.transitionedToExpired.length > 0 ||
|
|
320
|
+
lifecycle.deletedResolved.length > 0
|
|
321
|
+
) {
|
|
322
|
+
memoryItemMutated = true;
|
|
323
|
+
log.info(
|
|
324
|
+
`commitment ledger lifecycle: expired ${lifecycle.transitionedToExpired.length}, cleaned ${lifecycle.deletedResolved.length}`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
} catch (err) {
|
|
328
|
+
log.debug(`commitment ledger lifecycle pass failed: ${err}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Clean memories past their TTL (speculative memories auto-expire)
|
|
333
|
+
const deletedTTL = await storage.cleanExpiredTTL();
|
|
334
|
+
if (deletedTTL.length > 0) {
|
|
335
|
+
memoryItemMutated = true;
|
|
336
|
+
log.info(`cleaned ${deletedTTL.length} TTL-expired memories`);
|
|
337
|
+
if (resolveIndexingCapabilities(config).queryAwareIndexing) {
|
|
338
|
+
for (const m of deletedTTL) {
|
|
339
|
+
deindexMemory(
|
|
340
|
+
config.memoryDir,
|
|
341
|
+
m.path,
|
|
342
|
+
m.frontmatter.created,
|
|
343
|
+
m.frontmatter.tags ?? [],
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// v8.3 Lifecycle policy pass — deterministic promotion/decay metadata
|
|
350
|
+
if (lifecycleCaps.lifecyclePolicy) {
|
|
351
|
+
try {
|
|
352
|
+
const lightSleepStartedAt = new Date().toISOString();
|
|
353
|
+
const lifecycleCorpus = await storage.readAllMemories();
|
|
354
|
+
// Lifecycle frontmatter writes count as durable mutations for the catalog
|
|
355
|
+
// touch below (codex NR-tS), even when no other consolidation step set
|
|
356
|
+
// memoryItemMutated.
|
|
357
|
+
if ((await this.deps.lifecyclePolicyCoordinator.runLifecyclePolicyPass(lifecycleCorpus, storage)) > 0) {
|
|
358
|
+
memoryItemMutated = true;
|
|
359
|
+
}
|
|
360
|
+
await this.recordScheduledDreamsPhaseRun(
|
|
361
|
+
"lightSleep",
|
|
362
|
+
lifecycleCorpus.length,
|
|
363
|
+
`scheduled lifecycle policy pass assessed ${lifecycleCorpus.length} memories`,
|
|
364
|
+
{
|
|
365
|
+
startedAt: lightSleepStartedAt,
|
|
366
|
+
completedAt: new Date().toISOString(),
|
|
367
|
+
},
|
|
368
|
+
);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
log.warn(`lifecycle policy pass failed (ignored): ${err}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// v8.3 Compression guideline learning pass (default off, fail-open).
|
|
375
|
+
await this.deps.compressionGuidelineCoordinator.runCompressionGuidelineLearningPass();
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
const deepSleepStartedAt = new Date().toISOString();
|
|
379
|
+
// Tier migrations move/rewrite memory files; count them as durable
|
|
380
|
+
// mutations for the catalog touch below (codex NThSW).
|
|
381
|
+
const tierMigration = await this.deps.tierMigrationCoordinator.runCycle(storage, "maintenance");
|
|
382
|
+
if (tierMigration.migrated > 0) memoryItemMutated = true;
|
|
383
|
+
allMemories = await storage.readAllMemories();
|
|
384
|
+
|
|
385
|
+
// Fact archival pass (v6.0) — move old, low-importance, rarely-accessed facts to archive/
|
|
386
|
+
if (resolveRecallEnhancementCapabilities(config).factArchival) {
|
|
387
|
+
const archived = await this.deps.lifecyclePolicyCoordinator.runFactArchival(allMemories);
|
|
388
|
+
if (archived > 0) {
|
|
389
|
+
memoryItemMutated = true;
|
|
390
|
+
log.info(`archived ${archived} old low-importance facts`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
await this.recordScheduledDreamsPhaseRun(
|
|
394
|
+
"deepSleep",
|
|
395
|
+
allMemories.length,
|
|
396
|
+
`scheduled deep-sleep maintenance assessed ${allMemories.length} memories`,
|
|
397
|
+
{
|
|
398
|
+
startedAt: deepSleepStartedAt,
|
|
399
|
+
completedAt: new Date().toISOString(),
|
|
400
|
+
},
|
|
401
|
+
);
|
|
402
|
+
} catch (err) {
|
|
403
|
+
log.warn(`deep-sleep maintenance pass failed (ignored): ${err}`);
|
|
404
|
+
try {
|
|
405
|
+
allMemories = await storage.readAllMemories();
|
|
406
|
+
} catch (readErr) {
|
|
407
|
+
log.warn(`deep-sleep maintenance recovery read failed: ${readErr}`);
|
|
408
|
+
throw err;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Semantic consolidation pass — find similar memories, synthesize canonical versions
|
|
413
|
+
if (resolveConsolidationCapabilities(config).semanticConsolidation) {
|
|
414
|
+
try {
|
|
415
|
+
const stateFilePath = path.join(
|
|
416
|
+
config.memoryDir,
|
|
417
|
+
"state",
|
|
418
|
+
"semantic-consolidation-last-run.json",
|
|
419
|
+
);
|
|
420
|
+
let shouldRun = true;
|
|
421
|
+
try {
|
|
422
|
+
const stateRaw = await readFile(stateFilePath, "utf-8");
|
|
423
|
+
const stateData = JSON.parse(stateRaw) as { lastRunAt?: string };
|
|
424
|
+
if (stateData.lastRunAt) {
|
|
425
|
+
const lastRunMs = new Date(stateData.lastRunAt).getTime();
|
|
426
|
+
const intervalMs =
|
|
427
|
+
config.semanticConsolidationIntervalHours * 60 * 60 * 1000;
|
|
428
|
+
if (Date.now() - lastRunMs < intervalMs) {
|
|
429
|
+
shouldRun = false;
|
|
430
|
+
log.debug(
|
|
431
|
+
"[semantic-consolidation] skipping — not enough time since last run",
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
} catch {
|
|
436
|
+
// No state file yet — first run
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (shouldRun) {
|
|
440
|
+
const remStartedAt = new Date().toISOString();
|
|
441
|
+
const semResult = await this.deps.semanticConsolidationCoordinator.runSemanticConsolidation();
|
|
442
|
+
let remItemsProcessed = allMemories.length;
|
|
443
|
+
try {
|
|
444
|
+
allMemories = await storage.readAllMemories();
|
|
445
|
+
remItemsProcessed = allMemories.length;
|
|
446
|
+
} catch (err) {
|
|
447
|
+
log.warn(
|
|
448
|
+
`[semantic-consolidation] post-run telemetry refresh failed (non-fatal): ${err}`,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
await this.recordScheduledDreamsPhaseRun(
|
|
452
|
+
"rem",
|
|
453
|
+
remItemsProcessed,
|
|
454
|
+
`scheduled REM consolidation found ${semResult.clustersFound} clusters`,
|
|
455
|
+
{
|
|
456
|
+
startedAt: remStartedAt,
|
|
457
|
+
completedAt: new Date().toISOString(),
|
|
458
|
+
},
|
|
459
|
+
);
|
|
460
|
+
if (semResult.memoriesArchived > 0) {
|
|
461
|
+
log.info(
|
|
462
|
+
`[semantic-consolidation] archived ${semResult.memoriesArchived} memories during maintenance`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
// Only persist last-run timestamp if the run succeeded (had no errors or made progress)
|
|
466
|
+
if (semResult.errors === 0 || semResult.memoriesArchived > 0) {
|
|
467
|
+
const stateDir = path.join(config.memoryDir, "state");
|
|
468
|
+
await mkdir(stateDir, { recursive: true });
|
|
469
|
+
await writeFile(
|
|
470
|
+
stateFilePath,
|
|
471
|
+
JSON.stringify({ lastRunAt: new Date().toISOString() }),
|
|
472
|
+
"utf-8",
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
} catch (err) {
|
|
477
|
+
log.warn(
|
|
478
|
+
`[semantic-consolidation] maintenance pass failed (non-fatal): ${err}`,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Auto-consolidate IDENTITY.md if it's getting large
|
|
484
|
+
if (resolveRecallEnhancementCapabilities(config).identity) {
|
|
485
|
+
await this.deps.autoConsolidateIdentity();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Auto-consolidate profile.md if it exceeds max lines
|
|
489
|
+
const profileSection = this.deps.recallSectionCoordinator.getRecallSectionEntry("profile");
|
|
490
|
+
const profileConsolidationTriggerLines =
|
|
491
|
+
typeof profileSection?.consolidateTriggerLines === "number"
|
|
492
|
+
? Math.max(0, Math.floor(profileSection.consolidateTriggerLines))
|
|
493
|
+
: undefined;
|
|
494
|
+
const profileConsolidationTargetLines =
|
|
495
|
+
typeof profileSection?.consolidateTargetLines === "number"
|
|
496
|
+
? Math.max(0, Math.floor(profileSection.consolidateTargetLines))
|
|
497
|
+
: 50;
|
|
498
|
+
if (
|
|
499
|
+
await storage.profileNeedsConsolidation(
|
|
500
|
+
profileConsolidationTriggerLines,
|
|
501
|
+
)
|
|
502
|
+
) {
|
|
503
|
+
log.info("profile.md exceeds max lines — running smart consolidation");
|
|
504
|
+
const currentProfile = await storage.readProfile();
|
|
505
|
+
if (currentProfile) {
|
|
506
|
+
const profileResult = await this.deps.getExtraction().consolidateProfile(
|
|
507
|
+
currentProfile,
|
|
508
|
+
profileConsolidationTargetLines,
|
|
509
|
+
);
|
|
510
|
+
if (profileResult) {
|
|
511
|
+
await storage.writeProfile(profileResult.consolidatedProfile);
|
|
512
|
+
// Profile consolidation rewrites profile.md — a durable namespace
|
|
513
|
+
// mutation; record it for the catalog touch even when it is the only
|
|
514
|
+
// change in the pass (codex). Otherwise lastWriteAt goes stale.
|
|
515
|
+
memoryItemMutated = true;
|
|
516
|
+
log.info(
|
|
517
|
+
`profile.md consolidated: removed ${profileResult.removedCount} items — ${profileResult.summary}`,
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Memory Summarization (Phase 4A)
|
|
524
|
+
if (resolvePipelineProcessingCapabilities(config).summarization) {
|
|
525
|
+
await this.deps.lifecyclePolicyCoordinator.runSummarization(allMemories);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Topic Extraction (Phase 4B)
|
|
529
|
+
if (resolvePipelineProcessingCapabilities(config).topicExtraction) {
|
|
530
|
+
await this.deps.lifecyclePolicyCoordinator.runTopicExtraction(allMemories);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const meta = await storage.loadMeta();
|
|
534
|
+
meta.lastConsolidationAt = new Date().toISOString();
|
|
535
|
+
await storage.saveMeta(meta);
|
|
536
|
+
|
|
537
|
+
// Temporal Memory Tree (v8.2) — rebuild nodes from all memories, fail-open
|
|
538
|
+
if (lifecycleCaps.temporalMemoryTree) {
|
|
539
|
+
try {
|
|
540
|
+
const tmtEntries = allMemories
|
|
541
|
+
.filter(
|
|
542
|
+
(m) =>
|
|
543
|
+
m.frontmatter.status !== "superseded" && m.frontmatter.status !== "archived" &&
|
|
544
|
+
m.frontmatter.status !== "forgotten" && m.frontmatter.status !== "pending_review", // #1576: unfaithful queue items must not feed TMT clusters
|
|
545
|
+
)
|
|
546
|
+
.map((m) => ({
|
|
547
|
+
path: m.path,
|
|
548
|
+
id: m.frontmatter.id,
|
|
549
|
+
created: m.frontmatter.created,
|
|
550
|
+
content: m.content,
|
|
551
|
+
}));
|
|
552
|
+
await this.deps.tmtBuilder.maybeRebuildNodes(
|
|
553
|
+
tmtEntries,
|
|
554
|
+
async (texts, level) => {
|
|
555
|
+
const prompt = `You are a memory archivist. Summarize the following ${level}-level memories into 3\u20135 sentences, preserving key facts, decisions, and preferences.\n\n${texts.map((t, i) => `[${i + 1}] ${t}`).join("\n\n")}`;
|
|
556
|
+
const response = await this.deps.fastChatCompletion(
|
|
557
|
+
[
|
|
558
|
+
{
|
|
559
|
+
role: "system",
|
|
560
|
+
content:
|
|
561
|
+
"Respond with a 3\u20135 sentence narrative summary. No JSON, just plain prose.",
|
|
562
|
+
},
|
|
563
|
+
{ role: "user", content: prompt },
|
|
564
|
+
],
|
|
565
|
+
{
|
|
566
|
+
temperature: 0.3,
|
|
567
|
+
maxTokens: config.tmtSummaryMaxTokens,
|
|
568
|
+
operation: "tmt_summary",
|
|
569
|
+
priority: "background",
|
|
570
|
+
},
|
|
571
|
+
);
|
|
572
|
+
return response?.content?.trim() || texts.slice(0, 3).join(" ");
|
|
573
|
+
},
|
|
574
|
+
);
|
|
575
|
+
} catch (err) {
|
|
576
|
+
log.warn(`tmt: consolidation hook failed (ignored): ${err}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (this.deps.consolidationObservers.size > 0) {
|
|
581
|
+
const observation: ConsolidationObservation = {
|
|
582
|
+
runAt: new Date().toISOString(),
|
|
583
|
+
recentMemories: recent,
|
|
584
|
+
existingMemories: older.slice(-50),
|
|
585
|
+
profile,
|
|
586
|
+
result,
|
|
587
|
+
merged,
|
|
588
|
+
invalidated,
|
|
589
|
+
};
|
|
590
|
+
for (const observer of this.deps.consolidationObservers) {
|
|
591
|
+
try {
|
|
592
|
+
await observer(observation);
|
|
593
|
+
} catch (err) {
|
|
594
|
+
log.warn(`consolidation observer failed (ignored): ${err}`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Consolidated catalog write touch — belt-and-suspenders for cleanup-only
|
|
600
|
+
// passes that mutate the store via delete-only paths (entity merges, TTL
|
|
601
|
+
// cleanup) without triggering the storage chokepoint's post-write hook.
|
|
602
|
+
// Gated on memoryItemMutated (set by every durable mutation including cleanup-only passes)
|
|
603
|
+
// Best-effort and failure-tolerant.
|
|
604
|
+
if (memoryItemMutated) {
|
|
605
|
+
this.deps.getStorageRouter().recordWrite(config.defaultNamespace, storage.dir);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
log.info("consolidation complete");
|
|
609
|
+
return { memoriesProcessed: allMemories.length, merged, invalidated };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private async recordScheduledDreamsPhaseRun(
|
|
613
|
+
phase: "lightSleep" | "rem" | "deepSleep",
|
|
614
|
+
itemsProcessed: number,
|
|
615
|
+
notes: string,
|
|
616
|
+
timing: { startedAt?: string; completedAt?: string } = {},
|
|
617
|
+
): Promise<void> {
|
|
618
|
+
try {
|
|
619
|
+
await recordDreamsPhaseRun({
|
|
620
|
+
memoryDir: this.deps.getStorage().dir,
|
|
621
|
+
phase,
|
|
622
|
+
trigger: "scheduled",
|
|
623
|
+
itemsProcessed,
|
|
624
|
+
notes,
|
|
625
|
+
startedAt: timing.startedAt,
|
|
626
|
+
completedAt: timing.completedAt,
|
|
627
|
+
});
|
|
628
|
+
} catch (error) {
|
|
629
|
+
log.debug(`dreams ledger scheduled ${phase} write failed (non-fatal): ${error}`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
}
|