@remnic/core 9.3.760 → 9.3.762
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-admin-ops-surface.d.ts +3 -3
- package/dist/access-admin-ops-surface.js +11 -10
- package/dist/access-boundary.d.ts +3 -3
- package/dist/access-boundary.js +12 -11
- package/dist/access-cli.js +46 -45
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +3 -3
- package/dist/access-http.js +18 -17
- package/dist/access-identity-continuity-surface.d.ts +2 -2
- package/dist/access-identity-continuity-surface.js +11 -10
- package/dist/access-lcm-surface.d.ts +3 -3
- package/dist/access-lcm-surface.js +11 -10
- package/dist/access-mcp.d.ts +3 -3
- package/dist/access-mcp.js +15 -14
- package/dist/access-observe-write-surface.d.ts +3 -3
- package/dist/access-observe-write-surface.js +11 -10
- package/dist/access-operations-batch.js +13 -12
- package/dist/access-operations.d.ts +7 -7
- package/dist/access-operations.js +14 -13
- package/dist/access-recall-surface.d.ts +3 -3
- package/dist/access-recall-surface.js +11 -10
- package/dist/access-schema.d.ts +68 -68
- package/dist/{access-service-BW-gb3DL.d.ts → access-service-CaWJP_f6.d.ts} +2 -2
- package/dist/access-service.d.ts +3 -3
- package/dist/access-service.js +11 -10
- package/dist/access-surface-catalog.d.ts +3 -3
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/bootstrap.d.ts +2 -2
- package/dist/briefing.js +4 -3
- package/dist/{catalog-DaKAFbSJ.d.ts → catalog-bflYNWsM.d.ts} +8 -4
- package/dist/causal-consolidation.js +5 -4
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/{chunk-OLEBPUZ7.js → chunk-2KYXXMTE.js} +6 -6
- package/dist/{chunk-6CUUFFU7.js → chunk-2PGB63VU.js} +131 -39
- package/dist/chunk-2PGB63VU.js.map +1 -0
- package/dist/{chunk-FYJWEGYP.js → chunk-4HYJETVO.js} +2 -2
- package/dist/{chunk-APJQ6UEA.js → chunk-AGNBY3VG.js} +4 -4
- package/dist/{chunk-W3M34XT4.js → chunk-BAVVI5GP.js} +2 -2
- package/dist/{chunk-XQT5GSWU.js → chunk-BRYPUVJU.js} +3 -3
- package/dist/{chunk-2XE4F3VZ.js → chunk-DB647OZ4.js} +24 -24
- package/dist/{chunk-3JBW3GHN.js → chunk-DPZ5GJMD.js} +2 -2
- package/dist/chunk-DQC3ZZYM.js +35 -0
- package/dist/chunk-DQC3ZZYM.js.map +1 -0
- package/dist/{chunk-SMLMH7S5.js → chunk-EDC7RZCW.js} +24 -24
- package/dist/{chunk-ZFXCQPNO.js → chunk-EGGE52UU.js} +2 -2
- package/dist/chunk-EGGE52UU.js.map +1 -0
- package/dist/{chunk-IOZZG75J.js → chunk-ES3GAMSQ.js} +2 -2
- package/dist/{chunk-QUGPXPFJ.js → chunk-ESDBHQY6.js} +5 -5
- package/dist/{chunk-ILVF6TIY.js → chunk-HDFS6RWQ.js} +2 -2
- package/dist/{chunk-SN2EWIAI.js → chunk-HDYFNXVO.js} +2 -2
- package/dist/{chunk-ZEQVS6AZ.js → chunk-HMUEAKSD.js} +2 -2
- package/dist/{chunk-P26O6R3Q.js → chunk-IIURIWJG.js} +102 -12
- package/dist/chunk-IIURIWJG.js.map +1 -0
- package/dist/{chunk-RKNJBZ55.js → chunk-JBPKEARU.js} +4 -4
- package/dist/{chunk-QQJMVF6P.js → chunk-JU6BKZHX.js} +3 -3
- package/dist/{chunk-SLAZZXBU.js → chunk-KZWUJC7Y.js} +2 -2
- package/dist/{chunk-IZLGHKWA.js → chunk-LAJIXABF.js} +10 -10
- package/dist/{chunk-3QLPBCFG.js → chunk-NKP6II6G.js} +4 -4
- package/dist/{chunk-WIXQ3F5P.js → chunk-OQ6F3WBX.js} +2 -2
- package/dist/{chunk-YP23FQPT.js → chunk-P7W4CDX6.js} +92 -41
- package/dist/chunk-P7W4CDX6.js.map +1 -0
- package/dist/{chunk-EGQ3AVZV.js → chunk-PNFN7SIN.js} +3 -3
- package/dist/{chunk-M2AAYJVI.js → chunk-QPFO54VC.js} +4 -4
- package/dist/{chunk-7HWOEYHW.js → chunk-QWIE43BS.js} +2 -2
- package/dist/chunk-TEKTCF2V.js +118 -0
- package/dist/chunk-TEKTCF2V.js.map +1 -0
- package/dist/{chunk-QOM3F6YW.js → chunk-TIWRP5NW.js} +5 -5
- package/dist/{chunk-CXW6Y7SI.js → chunk-V5RVMULT.js} +4 -4
- package/dist/{chunk-DTSOB2YF.js → chunk-VHYFY3JI.js} +2 -2
- package/dist/{chunk-AZSSKOG6.js → chunk-WAIMHMO3.js} +3 -3
- package/dist/{chunk-PKRRBRY2.js → chunk-ZTBH2OHD.js} +2 -2
- package/dist/{cli-D551dnva.d.ts → cli-CvWpIbMD.d.ts} +2 -2
- package/dist/cli.d.ts +4 -4
- package/dist/cli.js +35 -34
- package/dist/compounding/engine.js +4 -3
- package/dist/connectors/codex-materialize-runner.js +4 -3
- package/dist/connectors/index.js +4 -3
- package/dist/entity-retrieval.js +4 -3
- package/dist/explicit-capture.d.ts +2 -2
- package/dist/{forget-OEE5OELK.js → forget-WYLR4ZFV.js} +2 -2
- package/dist/index.d.ts +5 -5
- package/dist/index.js +48 -47
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +4 -4
- package/dist/maintenance/memory-governance.js +4 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +11 -1
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -3
- package/dist/maintenance/rebuild-memory-projection.d.ts +22 -1
- package/dist/maintenance/rebuild-memory-projection.js +5 -4
- package/dist/mcp-memory-inspector-app.d.ts +3 -3
- package/dist/namespaces/migrate.d.ts +1 -1
- package/dist/namespaces/migrate.js +9 -8
- package/dist/namespaces/search.js +4 -4
- package/dist/namespaces/storage.d.ts +1 -1
- package/dist/namespaces/storage.js +5 -4
- package/dist/operator-toolkit.js +13 -12
- package/dist/orchestration/maintenance.d.ts +1 -1
- package/dist/orchestration/maintenance.js +8 -7
- package/dist/{orchestrator-BnM187cb.d.ts → orchestrator-C04Q4Zao.d.ts} +1 -1
- package/dist/orchestrator.d.ts +2 -2
- package/dist/orchestrator.js +46 -45
- package/dist/schemas.d.ts +84 -84
- package/dist/search/factory.js +2 -2
- package/dist/search/index.js +4 -4
- package/dist/semantic-consolidation.js +5 -4
- package/dist/semantic-rule-promotion.js +4 -3
- package/dist/semantic-rule-verifier.js +4 -3
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/storage-guards.d.ts +9 -0
- package/dist/storage-guards.js +11 -0
- package/dist/storage-guards.js.map +1 -0
- package/dist/storage.js +3 -2
- package/dist/transfer/import-sqlite.js +2 -2
- package/dist/transfer/types.d.ts +66 -66
- package/dist/verified-recall.js +4 -3
- package/package.json +2 -2
- package/src/maintenance/rebuild-memory-lifecycle-ledger.ts +59 -8
- package/src/maintenance/rebuild-memory-projection.ts +149 -9
- package/src/namespaces/catalog.test.ts +133 -1
- package/src/namespaces/catalog.ts +163 -48
- package/src/namespaces/identity.ts +2 -2
- package/src/storage/secure-line-reader.ts +79 -0
- package/src/storage-guards.ts +30 -0
- package/src/storage-memory-lifecycle-ledger.test.ts +130 -0
- package/src/storage.ts +24 -41
- package/dist/chunk-6CUUFFU7.js.map +0 -1
- package/dist/chunk-LLVKDFNU.js +0 -77
- package/dist/chunk-LLVKDFNU.js.map +0 -1
- package/dist/chunk-P26O6R3Q.js.map +0 -1
- package/dist/chunk-YP23FQPT.js.map +0 -1
- package/dist/chunk-ZFXCQPNO.js.map +0 -1
- /package/dist/{chunk-OLEBPUZ7.js.map → chunk-2KYXXMTE.js.map} +0 -0
- /package/dist/{chunk-FYJWEGYP.js.map → chunk-4HYJETVO.js.map} +0 -0
- /package/dist/{chunk-APJQ6UEA.js.map → chunk-AGNBY3VG.js.map} +0 -0
- /package/dist/{chunk-W3M34XT4.js.map → chunk-BAVVI5GP.js.map} +0 -0
- /package/dist/{chunk-XQT5GSWU.js.map → chunk-BRYPUVJU.js.map} +0 -0
- /package/dist/{chunk-2XE4F3VZ.js.map → chunk-DB647OZ4.js.map} +0 -0
- /package/dist/{chunk-3JBW3GHN.js.map → chunk-DPZ5GJMD.js.map} +0 -0
- /package/dist/{chunk-SMLMH7S5.js.map → chunk-EDC7RZCW.js.map} +0 -0
- /package/dist/{chunk-IOZZG75J.js.map → chunk-ES3GAMSQ.js.map} +0 -0
- /package/dist/{chunk-QUGPXPFJ.js.map → chunk-ESDBHQY6.js.map} +0 -0
- /package/dist/{chunk-ILVF6TIY.js.map → chunk-HDFS6RWQ.js.map} +0 -0
- /package/dist/{chunk-SN2EWIAI.js.map → chunk-HDYFNXVO.js.map} +0 -0
- /package/dist/{chunk-ZEQVS6AZ.js.map → chunk-HMUEAKSD.js.map} +0 -0
- /package/dist/{chunk-RKNJBZ55.js.map → chunk-JBPKEARU.js.map} +0 -0
- /package/dist/{chunk-QQJMVF6P.js.map → chunk-JU6BKZHX.js.map} +0 -0
- /package/dist/{chunk-SLAZZXBU.js.map → chunk-KZWUJC7Y.js.map} +0 -0
- /package/dist/{chunk-IZLGHKWA.js.map → chunk-LAJIXABF.js.map} +0 -0
- /package/dist/{chunk-3QLPBCFG.js.map → chunk-NKP6II6G.js.map} +0 -0
- /package/dist/{chunk-WIXQ3F5P.js.map → chunk-OQ6F3WBX.js.map} +0 -0
- /package/dist/{chunk-EGQ3AVZV.js.map → chunk-PNFN7SIN.js.map} +0 -0
- /package/dist/{chunk-M2AAYJVI.js.map → chunk-QPFO54VC.js.map} +0 -0
- /package/dist/{chunk-7HWOEYHW.js.map → chunk-QWIE43BS.js.map} +0 -0
- /package/dist/{chunk-QOM3F6YW.js.map → chunk-TIWRP5NW.js.map} +0 -0
- /package/dist/{chunk-CXW6Y7SI.js.map → chunk-V5RVMULT.js.map} +0 -0
- /package/dist/{chunk-DTSOB2YF.js.map → chunk-VHYFY3JI.js.map} +0 -0
- /package/dist/{chunk-AZSSKOG6.js.map → chunk-WAIMHMO3.js.map} +0 -0
- /package/dist/{chunk-PKRRBRY2.js.map → chunk-ZTBH2OHD.js.map} +0 -0
- /package/dist/{forget-OEE5OELK.js.map → forget-WYLR4ZFV.js.map} +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { stat } from "node:fs/promises";
|
|
3
3
|
import { StorageManager } from "../storage.js";
|
|
4
|
+
import { log } from "../logger.js";
|
|
4
5
|
import type { MemoryLifecycleEvent } from "../types.js";
|
|
5
6
|
import { toBackupStamp } from "./backup-stamp.js";
|
|
6
7
|
import { writeFileAtomically } from "./atomic-file.js";
|
|
@@ -15,12 +16,24 @@ export interface RebuildMemoryLifecycleLedgerOptions {
|
|
|
15
16
|
now?: Date;
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
export interface SkippedLifecycleBlankIdMemory {
|
|
20
|
+
path: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SkippedDuplicateLifecycleEvent {
|
|
24
|
+
eventId: string;
|
|
25
|
+
keptPath: string;
|
|
26
|
+
skippedPath: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
export interface RebuildMemoryLifecycleLedgerResult {
|
|
19
30
|
dryRun: boolean;
|
|
20
31
|
scannedMemories: number;
|
|
21
32
|
rebuiltRows: number;
|
|
22
33
|
outputPath: string;
|
|
23
34
|
backupPath?: string;
|
|
35
|
+
skippedBlankIdMemories: SkippedLifecycleBlankIdMemory[];
|
|
36
|
+
skippedDuplicateEvents: SkippedDuplicateLifecycleEvent[];
|
|
24
37
|
}
|
|
25
38
|
|
|
26
39
|
function isNodeError(err: unknown): err is NodeJS.ErrnoException {
|
|
@@ -59,16 +72,52 @@ export async function rebuildMemoryLifecycleLedger(
|
|
|
59
72
|
const now = options.now ?? new Date();
|
|
60
73
|
const outputPath = path.join(options.memoryDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
61
74
|
const storage = new StorageManager(options.memoryDir);
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
]
|
|
67
|
-
|
|
75
|
+
const tiers = [
|
|
76
|
+
await storage.readAllMemories(),
|
|
77
|
+
await storage.readAllColdMemories(),
|
|
78
|
+
await storage.readArchivedMemories(),
|
|
79
|
+
];
|
|
80
|
+
const allMemories = tiers.flat();
|
|
81
|
+
const skippedBlankIdMemories: SkippedLifecycleBlankIdMemory[] = [];
|
|
82
|
+
const eventCandidates: Array<{ event: MemoryLifecycleEvent; path: string }> = [];
|
|
83
|
+
for (const tier of tiers) {
|
|
84
|
+
for (const memory of [...tier].sort((left, right) => left.path.localeCompare(right.path))) {
|
|
85
|
+
if (!memory.frontmatter.id.trim()) {
|
|
86
|
+
skippedBlankIdMemories.push({ path: memory.path });
|
|
87
|
+
log.warn(`lifecycle ledger rebuild skipped blank memory id: path=${memory.path}`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
for (const event of buildLifecycleEventsForMemory(memory)) {
|
|
91
|
+
eventCandidates.push({ event, path: memory.path });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
68
95
|
|
|
69
|
-
const
|
|
70
|
-
|
|
96
|
+
const orderedEvents = sortMemoryLifecycleEvents(eventCandidates.map((candidate) => candidate.event));
|
|
97
|
+
const candidateByEvent = new Map(
|
|
98
|
+
eventCandidates.map((candidate) => [candidate.event, candidate]),
|
|
71
99
|
);
|
|
100
|
+
const keptByEventId = new Map<string, { event: MemoryLifecycleEvent; path: string }>();
|
|
101
|
+
const events: MemoryLifecycleEvent[] = [];
|
|
102
|
+
const skippedDuplicateEvents: SkippedDuplicateLifecycleEvent[] = [];
|
|
103
|
+
for (const event of orderedEvents) {
|
|
104
|
+
const candidate = candidateByEvent.get(event)!;
|
|
105
|
+
const kept = keptByEventId.get(event.eventId);
|
|
106
|
+
if (kept) {
|
|
107
|
+
skippedDuplicateEvents.push({
|
|
108
|
+
eventId: event.eventId,
|
|
109
|
+
keptPath: kept.path,
|
|
110
|
+
skippedPath: candidate.path,
|
|
111
|
+
});
|
|
112
|
+
log.warn(
|
|
113
|
+
`lifecycle ledger rebuild skipped duplicate event_id=${event.eventId}: `
|
|
114
|
+
+ `kept=${kept.path} skipped=${candidate.path}`,
|
|
115
|
+
);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
keptByEventId.set(event.eventId, candidate);
|
|
119
|
+
events.push(event);
|
|
120
|
+
}
|
|
72
121
|
|
|
73
122
|
let backupPath: string | undefined;
|
|
74
123
|
if (!dryRun) {
|
|
@@ -87,5 +136,7 @@ export async function rebuildMemoryLifecycleLedger(
|
|
|
87
136
|
rebuiltRows: events.length,
|
|
88
137
|
outputPath,
|
|
89
138
|
backupPath,
|
|
139
|
+
skippedBlankIdMemories,
|
|
140
|
+
skippedDuplicateEvents,
|
|
90
141
|
};
|
|
91
142
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { mkdir, rm, stat } from "node:fs/promises";
|
|
3
3
|
import { StorageManager } from "../storage.js";
|
|
4
|
+
import { log } from "../logger.js";
|
|
4
5
|
import {
|
|
5
6
|
listMemoryGovernanceRuns,
|
|
6
7
|
readMemoryGovernanceRunArtifact,
|
|
@@ -51,6 +52,27 @@ export interface RebuildMemoryProjectionOptions {
|
|
|
51
52
|
updatedBefore?: string;
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
export interface SkippedDuplicateMemory {
|
|
56
|
+
memoryId: string;
|
|
57
|
+
keptPath: string;
|
|
58
|
+
skippedPath: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SkippedBlankIdMemory {
|
|
62
|
+
path: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface SkippedDuplicateTimelineEvent {
|
|
66
|
+
eventId: string;
|
|
67
|
+
keptPath: string;
|
|
68
|
+
skippedPath: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface SkippedBlankIdTimelineEvent {
|
|
72
|
+
eventId: string;
|
|
73
|
+
path: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
54
76
|
export interface RebuildMemoryProjectionResult {
|
|
55
77
|
dryRun: boolean;
|
|
56
78
|
scannedMemories: number;
|
|
@@ -62,6 +84,10 @@ export interface RebuildMemoryProjectionResult {
|
|
|
62
84
|
outputPath: string;
|
|
63
85
|
backupPath?: string;
|
|
64
86
|
usedLifecycleLedger: boolean;
|
|
87
|
+
skippedDuplicateMemories: SkippedDuplicateMemory[];
|
|
88
|
+
skippedBlankIdMemories: SkippedBlankIdMemory[];
|
|
89
|
+
skippedDuplicateTimelineEvents: SkippedDuplicateTimelineEvent[];
|
|
90
|
+
skippedBlankIdTimelineEvents: SkippedBlankIdTimelineEvent[];
|
|
65
91
|
scope: {
|
|
66
92
|
updatedAfter: string | null;
|
|
67
93
|
updatedBefore: string | null;
|
|
@@ -435,6 +461,91 @@ function serializeGovernanceReviewQueueRow(
|
|
|
435
461
|
});
|
|
436
462
|
}
|
|
437
463
|
|
|
464
|
+
function selectProjectionMemories(
|
|
465
|
+
tiers: MemoryFile[][],
|
|
466
|
+
): {
|
|
467
|
+
allMemories: MemoryFile[];
|
|
468
|
+
memories: MemoryFile[];
|
|
469
|
+
skippedDuplicateMemories: SkippedDuplicateMemory[];
|
|
470
|
+
skippedBlankIdMemories: SkippedBlankIdMemory[];
|
|
471
|
+
} {
|
|
472
|
+
const allMemories = tiers.flat();
|
|
473
|
+
const memories: MemoryFile[] = [];
|
|
474
|
+
const keptById = new Map<string, MemoryFile>();
|
|
475
|
+
const skippedDuplicateMemories: SkippedDuplicateMemory[] = [];
|
|
476
|
+
const skippedBlankIdMemories: SkippedBlankIdMemory[] = [];
|
|
477
|
+
|
|
478
|
+
// Tier order is authoritative: hot wins over cold, and cold wins over archive.
|
|
479
|
+
// Paths break ties within a tier so the winner does not depend on readdir order.
|
|
480
|
+
for (const tier of tiers) {
|
|
481
|
+
for (const memory of [...tier].sort((left, right) => left.path.localeCompare(right.path))) {
|
|
482
|
+
const memoryId = memory.frontmatter.id.trim();
|
|
483
|
+
if (!memoryId) {
|
|
484
|
+
skippedBlankIdMemories.push({ path: memory.path });
|
|
485
|
+
log.warn(`projection rebuild skipped blank memory id: path=${memory.path}`);
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
const kept = keptById.get(memoryId);
|
|
489
|
+
if (kept) {
|
|
490
|
+
skippedDuplicateMemories.push({
|
|
491
|
+
memoryId,
|
|
492
|
+
keptPath: kept.path,
|
|
493
|
+
skippedPath: memory.path,
|
|
494
|
+
});
|
|
495
|
+
log.warn(
|
|
496
|
+
`projection rebuild skipped duplicate memory id=${memoryId}: kept=${kept.path} skipped=${memory.path}`,
|
|
497
|
+
);
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
keptById.set(memoryId, memory);
|
|
501
|
+
memories.push(memory);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return { allMemories, memories, skippedDuplicateMemories, skippedBlankIdMemories };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function deduplicateTimelineEvents(
|
|
509
|
+
events: MemoryLifecycleEvent[],
|
|
510
|
+
sourcePathFor: (event: MemoryLifecycleEvent) => string,
|
|
511
|
+
): {
|
|
512
|
+
events: MemoryLifecycleEvent[];
|
|
513
|
+
skipped: SkippedDuplicateTimelineEvent[];
|
|
514
|
+
skippedBlankIds: SkippedBlankIdTimelineEvent[];
|
|
515
|
+
} {
|
|
516
|
+
const keptById = new Map<string, { event: MemoryLifecycleEvent; path: string }>();
|
|
517
|
+
const unique: MemoryLifecycleEvent[] = [];
|
|
518
|
+
const skipped: SkippedDuplicateTimelineEvent[] = [];
|
|
519
|
+
const skippedBlankIds: SkippedBlankIdTimelineEvent[] = [];
|
|
520
|
+
for (const event of sortMemoryLifecycleEvents(events)) {
|
|
521
|
+
const sourcePath = sourcePathFor(event);
|
|
522
|
+
if (!event.memoryId.trim()) {
|
|
523
|
+
skippedBlankIds.push({ eventId: event.eventId, path: sourcePath });
|
|
524
|
+
log.warn(
|
|
525
|
+
`projection rebuild skipped timeline event with blank memory id: `
|
|
526
|
+
+ `event_id=${event.eventId} path=${sourcePath}`,
|
|
527
|
+
);
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
const kept = keptById.get(event.eventId);
|
|
531
|
+
if (kept) {
|
|
532
|
+
skipped.push({
|
|
533
|
+
eventId: event.eventId,
|
|
534
|
+
keptPath: kept.path,
|
|
535
|
+
skippedPath: sourcePath,
|
|
536
|
+
});
|
|
537
|
+
log.warn(
|
|
538
|
+
`projection rebuild skipped duplicate timeline event_id=${event.eventId}: `
|
|
539
|
+
+ `kept=${kept.path} skipped=${sourcePath}`,
|
|
540
|
+
);
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
keptById.set(event.eventId, { event, path: sourcePath });
|
|
544
|
+
unique.push(event);
|
|
545
|
+
}
|
|
546
|
+
return { events: unique, skipped, skippedBlankIds };
|
|
547
|
+
}
|
|
548
|
+
|
|
438
549
|
async function loadAuthoritativeProjectionSnapshot(options: {
|
|
439
550
|
memoryDir: string;
|
|
440
551
|
defaultNamespace?: string;
|
|
@@ -464,22 +575,43 @@ async function loadAuthoritativeProjectionSnapshot(options: {
|
|
|
464
575
|
updatedAfter: string | null;
|
|
465
576
|
updatedBefore: string | null;
|
|
466
577
|
};
|
|
578
|
+
skippedDuplicateMemories: SkippedDuplicateMemory[];
|
|
579
|
+
skippedBlankIdMemories: SkippedBlankIdMemory[];
|
|
580
|
+
skippedDuplicateTimelineEvents: SkippedDuplicateTimelineEvent[];
|
|
581
|
+
skippedBlankIdTimelineEvents: SkippedBlankIdTimelineEvent[];
|
|
467
582
|
}> {
|
|
468
583
|
const storage = new StorageManager(options.memoryDir);
|
|
469
584
|
// Force a fresh disk read — projection verify/rebuild must see the true
|
|
470
585
|
// on-disk state, not a potentially stale in-process cache.
|
|
471
586
|
storage.invalidateAllMemoriesCacheForDir();
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
]
|
|
477
|
-
|
|
587
|
+
const selected = selectProjectionMemories([
|
|
588
|
+
await storage.readAllMemories(),
|
|
589
|
+
await storage.readAllColdMemories(),
|
|
590
|
+
await storage.readArchivedMemories(),
|
|
591
|
+
]);
|
|
592
|
+
const allMemories = selected.allMemories;
|
|
593
|
+
const projectionMemories = selected.memories;
|
|
478
594
|
const lifecycleEvents = await storage.readAllMemoryLifecycleEvents();
|
|
479
|
-
const
|
|
480
|
-
const
|
|
595
|
+
const timeline = loadTimelineEvents(projectionMemories, lifecycleEvents);
|
|
596
|
+
const lifecycleLedgerPath = path.join(
|
|
597
|
+
options.memoryDir,
|
|
598
|
+
"state",
|
|
599
|
+
"memory-lifecycle-ledger.jsonl",
|
|
600
|
+
);
|
|
601
|
+
const memoryPathById = new Map(
|
|
602
|
+
projectionMemories.map((memory) => [memory.frontmatter.id, memory.path]),
|
|
603
|
+
);
|
|
604
|
+
const deduplicatedTimeline = deduplicateTimelineEvents(
|
|
605
|
+
timeline.events,
|
|
606
|
+
(event) => timeline.usedLifecycleLedger
|
|
607
|
+
? lifecycleLedgerPath
|
|
608
|
+
: memoryPathById.get(event.memoryId) ?? lifecycleLedgerPath,
|
|
609
|
+
);
|
|
610
|
+
const events = deduplicatedTimeline.events;
|
|
611
|
+
const usedLifecycleLedger = timeline.usedLifecycleLedger;
|
|
612
|
+
const currentRows = projectionMemories.map((memory) => toCurrentStateRow(options.memoryDir, memory));
|
|
481
613
|
const scope = normalizeProjectionScope(options);
|
|
482
|
-
const scopedMemories = filterMemoriesForProjectionScope(
|
|
614
|
+
const scopedMemories = filterMemoriesForProjectionScope(projectionMemories, scope);
|
|
483
615
|
const scopedMemoryIds = new Set(scopedMemories.map((memory) => memory.frontmatter.id));
|
|
484
616
|
const nativeKnowledgeRows = await loadPersistedNativeKnowledgeChunks({
|
|
485
617
|
memoryDir: options.memoryDir,
|
|
@@ -516,6 +648,10 @@ async function loadAuthoritativeProjectionSnapshot(options: {
|
|
|
516
648
|
governance,
|
|
517
649
|
usedLifecycleLedger,
|
|
518
650
|
scope,
|
|
651
|
+
skippedDuplicateMemories: selected.skippedDuplicateMemories,
|
|
652
|
+
skippedBlankIdMemories: selected.skippedBlankIdMemories,
|
|
653
|
+
skippedDuplicateTimelineEvents: deduplicatedTimeline.skipped,
|
|
654
|
+
skippedBlankIdTimelineEvents: deduplicatedTimeline.skippedBlankIds,
|
|
519
655
|
};
|
|
520
656
|
}
|
|
521
657
|
|
|
@@ -1040,6 +1176,10 @@ export async function rebuildMemoryProjection(
|
|
|
1040
1176
|
outputPath,
|
|
1041
1177
|
backupPath,
|
|
1042
1178
|
usedLifecycleLedger: snapshot.usedLifecycleLedger,
|
|
1179
|
+
skippedDuplicateMemories: snapshot.skippedDuplicateMemories,
|
|
1180
|
+
skippedBlankIdMemories: snapshot.skippedBlankIdMemories,
|
|
1181
|
+
skippedDuplicateTimelineEvents: snapshot.skippedDuplicateTimelineEvents,
|
|
1182
|
+
skippedBlankIdTimelineEvents: snapshot.skippedBlankIdTimelineEvents,
|
|
1043
1183
|
scope: snapshot.scope,
|
|
1044
1184
|
};
|
|
1045
1185
|
}
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
appendFile,
|
|
4
|
+
mkdir,
|
|
5
|
+
mkdtemp,
|
|
6
|
+
readFile,
|
|
7
|
+
realpath,
|
|
8
|
+
rm,
|
|
9
|
+
stat,
|
|
10
|
+
symlink,
|
|
11
|
+
utimes,
|
|
12
|
+
writeFile,
|
|
13
|
+
} from "node:fs/promises";
|
|
3
14
|
import os from "node:os";
|
|
4
15
|
import path from "node:path";
|
|
5
16
|
import test from "node:test";
|
|
@@ -32,6 +43,21 @@ async function mkMemoryDir(): Promise<string> {
|
|
|
32
43
|
return await mkdtemp(path.join(os.tmpdir(), "remnic-ns-catalog-"));
|
|
33
44
|
}
|
|
34
45
|
|
|
46
|
+
class CountingNamespaceCatalog extends NamespaceCatalog {
|
|
47
|
+
catalogReadCount = 0;
|
|
48
|
+
|
|
49
|
+
constructor(config: PluginConfig) {
|
|
50
|
+
super(config);
|
|
51
|
+
this.onCatalogReadForTest = () => {
|
|
52
|
+
this.catalogReadCount++;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setAfterAppendHook(hook: () => Promise<void>): void {
|
|
57
|
+
this.onAfterCatalogAppendForTest = hook;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
35
61
|
test("markWrite registers a dynamic project namespace in the catalog", async () => {
|
|
36
62
|
const memoryDir = await mkMemoryDir();
|
|
37
63
|
try {
|
|
@@ -50,6 +76,85 @@ test("markWrite registers a dynamic project namespace in the catalog", async ()
|
|
|
50
76
|
}
|
|
51
77
|
});
|
|
52
78
|
|
|
79
|
+
test("touch reloads the catalog after an external append", async () => {
|
|
80
|
+
const memoryDir = await mkMemoryDir();
|
|
81
|
+
try {
|
|
82
|
+
const catalog = new NamespaceCatalog(makeConfig(memoryDir));
|
|
83
|
+
await catalog.markWrite("alpha", { discoveredBy: "write" });
|
|
84
|
+
|
|
85
|
+
const catalogPath = path.join(memoryDir, "state", "namespaces.jsonl");
|
|
86
|
+
const alpha = JSON.parse((await readFile(catalogPath, "utf8")).trim());
|
|
87
|
+
const externalNamespace = "external";
|
|
88
|
+
await appendFile(
|
|
89
|
+
catalogPath,
|
|
90
|
+
`${JSON.stringify({
|
|
91
|
+
...alpha,
|
|
92
|
+
namespace: externalNamespace,
|
|
93
|
+
identityToken: namespaceIdentityToken(externalNamespace),
|
|
94
|
+
storageDir: path.join(memoryDir, "namespaces", namespaceIdentityToken(externalNamespace)),
|
|
95
|
+
})}\n`,
|
|
96
|
+
"utf8",
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
await catalog.markRead("alpha");
|
|
100
|
+
|
|
101
|
+
assert.ok(
|
|
102
|
+
(await catalog.listNamespaces()).some((record) => record.namespace === externalNamespace),
|
|
103
|
+
"the touch must fold in a row appended through another file descriptor",
|
|
104
|
+
);
|
|
105
|
+
} finally {
|
|
106
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("repeated touches do not re-read an unchanged catalog", async () => {
|
|
111
|
+
const memoryDir = await mkMemoryDir();
|
|
112
|
+
try {
|
|
113
|
+
const catalog = new CountingNamespaceCatalog(makeConfig(memoryDir));
|
|
114
|
+
|
|
115
|
+
await catalog.markWrite("alpha", { discoveredBy: "write" });
|
|
116
|
+
await catalog.markRead("alpha");
|
|
117
|
+
await catalog.markMaintenance("alpha", "test");
|
|
118
|
+
|
|
119
|
+
assert.equal(catalog.catalogReadCount, 1);
|
|
120
|
+
} finally {
|
|
121
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("a foreign append between append and stat invalidates the cache", async () => {
|
|
126
|
+
const memoryDir = await mkMemoryDir();
|
|
127
|
+
try {
|
|
128
|
+
const catalog = new CountingNamespaceCatalog(makeConfig(memoryDir));
|
|
129
|
+
await catalog.markWrite("alpha", { discoveredBy: "write" });
|
|
130
|
+
|
|
131
|
+
const catalogPath = path.join(memoryDir, "state", "namespaces.jsonl");
|
|
132
|
+
const alpha = JSON.parse((await readFile(catalogPath, "utf8")).trim());
|
|
133
|
+
const externalNamespace = "racing-external";
|
|
134
|
+
catalog.setAfterAppendHook(async () => {
|
|
135
|
+
await appendFile(
|
|
136
|
+
catalogPath,
|
|
137
|
+
`${JSON.stringify({
|
|
138
|
+
...alpha,
|
|
139
|
+
namespace: externalNamespace,
|
|
140
|
+
identityToken: namespaceIdentityToken(externalNamespace),
|
|
141
|
+
storageDir: path.join(memoryDir, "namespaces", namespaceIdentityToken(externalNamespace)),
|
|
142
|
+
})}\n`,
|
|
143
|
+
"utf8",
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await catalog.markRead("alpha");
|
|
148
|
+
const readsBeforeReload = catalog.catalogReadCount;
|
|
149
|
+
const records = await catalog.listNamespaces();
|
|
150
|
+
|
|
151
|
+
assert.equal(catalog.catalogReadCount, readsBeforeReload + 1);
|
|
152
|
+
assert.ok(records.some((record) => record.namespace === externalNamespace));
|
|
153
|
+
} finally {
|
|
154
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
53
158
|
test("configured default and shared namespaces appear in the catalog after register", async () => {
|
|
54
159
|
const memoryDir = await mkMemoryDir();
|
|
55
160
|
try {
|
|
@@ -89,6 +194,19 @@ test("rebuildFromDisk finds existing tokenized namespace directories", async ()
|
|
|
89
194
|
}
|
|
90
195
|
});
|
|
91
196
|
|
|
197
|
+
test("rebuildFromDisk accepts config without sharedNamespace", async () => {
|
|
198
|
+
const memoryDir = await mkMemoryDir();
|
|
199
|
+
try {
|
|
200
|
+
const config = makeConfig(memoryDir);
|
|
201
|
+
delete (config as Partial<PluginConfig>).sharedNamespace;
|
|
202
|
+
const catalog = new NamespaceCatalog(config);
|
|
203
|
+
|
|
204
|
+
await assert.doesNotReject(catalog.rebuildFromDisk());
|
|
205
|
+
} finally {
|
|
206
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
92
210
|
test("rebuildFromDisk dry-run does not write the catalog file", async () => {
|
|
93
211
|
const memoryDir = await mkMemoryDir();
|
|
94
212
|
try {
|
|
@@ -405,6 +523,20 @@ test("catalog tolerates corrupt / non-object JSONL lines on read", async () => {
|
|
|
405
523
|
}
|
|
406
524
|
});
|
|
407
525
|
|
|
526
|
+
test("catalog read fails open when the catalog path is a directory", async () => {
|
|
527
|
+
const memoryDir = await mkMemoryDir();
|
|
528
|
+
try {
|
|
529
|
+
await mkdir(path.join(memoryDir, "state", "namespaces.jsonl"), { recursive: true });
|
|
530
|
+
const catalog = new NamespaceCatalog(makeConfig(memoryDir));
|
|
531
|
+
|
|
532
|
+
await assert.doesNotReject(async () => {
|
|
533
|
+
assert.deepEqual(await catalog.listNamespaces(), []);
|
|
534
|
+
});
|
|
535
|
+
} finally {
|
|
536
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
|
|
408
540
|
test("catalog quarantines malformed JSONL record fields at the parse boundary", async () => {
|
|
409
541
|
const memoryDir = await mkMemoryDir();
|
|
410
542
|
try {
|