@remnic/core 9.3.761 → 9.3.763
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.js +10 -9
- package/dist/access-boundary.js +11 -10
- package/dist/access-cli.js +45 -44
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.js +17 -16
- package/dist/access-identity-continuity-surface.js +10 -9
- package/dist/access-lcm-surface.js +10 -9
- package/dist/access-mcp.js +14 -13
- package/dist/access-observe-write-surface.js +10 -9
- package/dist/access-operations-batch.js +12 -11
- package/dist/access-operations.js +13 -12
- package/dist/access-recall-surface.js +10 -9
- package/dist/access-service.js +10 -9
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/briefing.js +5 -4
- package/dist/causal-consolidation.js +6 -5
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/{chunk-742MWWVL.js → chunk-2RLECZ6J.js} +2 -2
- package/dist/{chunk-CFHDI5CV.js → chunk-6YEENRE3.js} +4 -4
- package/dist/{chunk-JYH3XYWM.js → chunk-6ZFE2TPO.js} +3 -3
- package/dist/{chunk-LG2MCUBC.js → chunk-A4FMQIST.js} +2 -2
- package/dist/{chunk-APJQ6UEA.js → chunk-AGNBY3VG.js} +4 -4
- package/dist/{chunk-OCPVA4TH.js → chunk-AS2NRB45.js} +2 -2
- package/dist/{chunk-W4ADKEIY.js → chunk-BAF7MBEH.js} +22 -22
- package/dist/{chunk-ICVNVSKN.js → chunk-BRYPUVJU.js} +2 -2
- package/dist/{chunk-IV4INTAU.js → chunk-CVINPTKG.js} +2 -2
- package/dist/{chunk-IRFHFRHP.js → chunk-D6YTILJO.js} +25 -25
- package/dist/chunk-DQC3ZZYM.js +35 -0
- package/dist/chunk-DQC3ZZYM.js.map +1 -0
- package/dist/{chunk-3DAIQSVS.js → chunk-DTTVX6EO.js} +23 -8
- package/dist/chunk-DTTVX6EO.js.map +1 -0
- package/dist/{chunk-P7UA66OG.js → chunk-IVFIYAIP.js} +2 -2
- package/dist/{chunk-RKNJBZ55.js → chunk-JBPKEARU.js} +4 -4
- package/dist/{chunk-Y3XEN5TL.js → chunk-K4EDULCP.js} +3 -3
- package/dist/{chunk-P7N2DQLC.js → chunk-KXG7HHIQ.js} +3 -3
- package/dist/{chunk-4WOXD56B.js → chunk-LDCFFJ67.js} +5 -5
- package/dist/{chunk-5AZQZFJG.js → chunk-N3QDLE2N.js} +5 -5
- package/dist/{chunk-2B3BTQN4.js → chunk-N4ZA2I7M.js} +2 -2
- package/dist/chunk-NDXJJKT2.js +118 -0
- package/dist/chunk-NDXJJKT2.js.map +1 -0
- package/dist/{chunk-HP23UD6W.js → chunk-NFWTMM2M.js} +2 -2
- package/dist/{chunk-3QLPBCFG.js → chunk-NKP6II6G.js} +4 -4
- package/dist/{chunk-HHCMZEUC.js → chunk-OX272PBW.js} +2 -2
- package/dist/{chunk-YU4CLTZY.js → chunk-QPTP6SKC.js} +2 -2
- package/dist/{chunk-6S2C5FQ4.js → chunk-R3K4CXPR.js} +2 -2
- package/dist/{chunk-XDFWMY4X.js → chunk-SFNQDXXL.js} +108 -14
- package/dist/chunk-SFNQDXXL.js.map +1 -0
- package/dist/{chunk-MB5RSUW6.js → chunk-SXN3LCUT.js} +114 -28
- package/dist/chunk-SXN3LCUT.js.map +1 -0
- package/dist/{chunk-3CBSKDRL.js → chunk-TIOXQPD7.js} +9 -9
- package/dist/{chunk-CXW6Y7SI.js → chunk-V5RVMULT.js} +4 -4
- package/dist/{chunk-DUU4VLOW.js → chunk-VKHZFF5P.js} +2 -2
- package/dist/{chunk-NPZTEHR7.js → chunk-X7HTU4HO.js} +2 -2
- package/dist/{chunk-ZAIU7ZEA.js → chunk-YDYC6PIS.js} +2 -2
- package/dist/cli.js +34 -33
- package/dist/compounding/engine.js +5 -4
- package/dist/connectors/codex-materialize-runner.js +5 -4
- package/dist/connectors/index.js +5 -4
- package/dist/entity-retrieval.js +5 -4
- package/dist/{forget-OEE5OELK.js → forget-WYLR4ZFV.js} +2 -2
- package/dist/index.js +47 -46
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +4 -4
- package/dist/maintenance/memory-governance.js +5 -4
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +11 -1
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +5 -4
- package/dist/maintenance/rebuild-memory-projection.d.ts +22 -1
- package/dist/maintenance/rebuild-memory-projection.js +6 -5
- package/dist/memory-projection-store.d.ts +6 -2
- package/dist/memory-projection-store.js +9 -3
- package/dist/namespaces/migrate.js +9 -8
- package/dist/namespaces/search.js +3 -3
- package/dist/namespaces/storage.js +5 -4
- package/dist/operator-toolkit.js +12 -11
- package/dist/orchestration/maintenance.js +7 -6
- package/dist/orchestrator.js +45 -44
- package/dist/{purge-6ATBGT77.js → purge-IHRX4Z7X.js} +7 -1
- package/dist/purge-IHRX4Z7X.js.map +1 -0
- package/dist/schemas.d.ts +22 -22
- package/dist/search/factory.js +2 -2
- package/dist/search/index.js +4 -4
- package/dist/semantic-consolidation.js +6 -5
- package/dist/semantic-rule-promotion.js +5 -4
- package/dist/semantic-rule-verifier.js +5 -4
- 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 +4 -3
- package/dist/transfer/import-sqlite.js +2 -2
- package/dist/transfer/types.d.ts +12 -12
- package/dist/verified-recall.js +5 -4
- package/package.json +2 -2
- package/src/maintenance/purge.test.ts +76 -0
- package/src/maintenance/purge.ts +3 -0
- package/src/maintenance/rebuild-memory-lifecycle-ledger.ts +59 -8
- package/src/maintenance/rebuild-memory-projection.ts +154 -10
- package/src/memory-projection-store.ts +143 -35
- package/src/storage-guards.ts +30 -0
- package/src/storage.ts +16 -16
- package/dist/chunk-3DAIQSVS.js.map +0 -1
- package/dist/chunk-GYJESQS5.js +0 -77
- package/dist/chunk-GYJESQS5.js.map +0 -1
- package/dist/chunk-MB5RSUW6.js.map +0 -1
- package/dist/chunk-XDFWMY4X.js.map +0 -1
- package/dist/purge-6ATBGT77.js.map +0 -1
- /package/dist/{chunk-742MWWVL.js.map → chunk-2RLECZ6J.js.map} +0 -0
- /package/dist/{chunk-CFHDI5CV.js.map → chunk-6YEENRE3.js.map} +0 -0
- /package/dist/{chunk-JYH3XYWM.js.map → chunk-6ZFE2TPO.js.map} +0 -0
- /package/dist/{chunk-LG2MCUBC.js.map → chunk-A4FMQIST.js.map} +0 -0
- /package/dist/{chunk-APJQ6UEA.js.map → chunk-AGNBY3VG.js.map} +0 -0
- /package/dist/{chunk-OCPVA4TH.js.map → chunk-AS2NRB45.js.map} +0 -0
- /package/dist/{chunk-W4ADKEIY.js.map → chunk-BAF7MBEH.js.map} +0 -0
- /package/dist/{chunk-ICVNVSKN.js.map → chunk-BRYPUVJU.js.map} +0 -0
- /package/dist/{chunk-IV4INTAU.js.map → chunk-CVINPTKG.js.map} +0 -0
- /package/dist/{chunk-IRFHFRHP.js.map → chunk-D6YTILJO.js.map} +0 -0
- /package/dist/{chunk-P7UA66OG.js.map → chunk-IVFIYAIP.js.map} +0 -0
- /package/dist/{chunk-RKNJBZ55.js.map → chunk-JBPKEARU.js.map} +0 -0
- /package/dist/{chunk-Y3XEN5TL.js.map → chunk-K4EDULCP.js.map} +0 -0
- /package/dist/{chunk-P7N2DQLC.js.map → chunk-KXG7HHIQ.js.map} +0 -0
- /package/dist/{chunk-4WOXD56B.js.map → chunk-LDCFFJ67.js.map} +0 -0
- /package/dist/{chunk-5AZQZFJG.js.map → chunk-N3QDLE2N.js.map} +0 -0
- /package/dist/{chunk-2B3BTQN4.js.map → chunk-N4ZA2I7M.js.map} +0 -0
- /package/dist/{chunk-HP23UD6W.js.map → chunk-NFWTMM2M.js.map} +0 -0
- /package/dist/{chunk-3QLPBCFG.js.map → chunk-NKP6II6G.js.map} +0 -0
- /package/dist/{chunk-HHCMZEUC.js.map → chunk-OX272PBW.js.map} +0 -0
- /package/dist/{chunk-YU4CLTZY.js.map → chunk-QPTP6SKC.js.map} +0 -0
- /package/dist/{chunk-6S2C5FQ4.js.map → chunk-R3K4CXPR.js.map} +0 -0
- /package/dist/{chunk-3CBSKDRL.js.map → chunk-TIOXQPD7.js.map} +0 -0
- /package/dist/{chunk-CXW6Y7SI.js.map → chunk-V5RVMULT.js.map} +0 -0
- /package/dist/{chunk-DUU4VLOW.js.map → chunk-VKHZFF5P.js.map} +0 -0
- /package/dist/{chunk-NPZTEHR7.js.map → chunk-X7HTU4HO.js.map} +0 -0
- /package/dist/{chunk-ZAIU7ZEA.js.map → chunk-YDYC6PIS.js.map} +0 -0
- /package/dist/{forget-OEE5OELK.js.map → forget-WYLR4ZFV.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/core",
|
|
3
|
-
"version": "9.3.
|
|
3
|
+
"version": "9.3.763",
|
|
4
4
|
"description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -3000,7 +3000,7 @@
|
|
|
3000
3000
|
"core"
|
|
3001
3001
|
],
|
|
3002
3002
|
"peerDependencies": {
|
|
3003
|
-
"@remnic/coding-graph": "^9.3.
|
|
3003
|
+
"@remnic/coding-graph": "^9.3.763"
|
|
3004
3004
|
},
|
|
3005
3005
|
"peerDependenciesMeta": {
|
|
3006
3006
|
"@remnic/coding-graph": {
|
|
@@ -5,6 +5,9 @@ import path from "node:path";
|
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
|
|
7
7
|
import { purgeMemories } from "./purge.js";
|
|
8
|
+
import { rebuildMemoryProjection } from "./rebuild-memory-projection.js";
|
|
9
|
+
import { readProjectedMemoryBrowse } from "../memory-projection-store.js";
|
|
10
|
+
import { StorageManager } from "../storage.js";
|
|
8
11
|
import type { MemoryFile } from "../types.js";
|
|
9
12
|
|
|
10
13
|
test("purgeMemories records audit errors without blocking hard delete", async () => {
|
|
@@ -54,6 +57,79 @@ test("purgeMemories records audit errors without blocking hard delete", async ()
|
|
|
54
57
|
}
|
|
55
58
|
});
|
|
56
59
|
|
|
60
|
+
test("purgeMemories invalidates projection rows for hard-deleted memories", async () => {
|
|
61
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-purge-projection-"));
|
|
62
|
+
try {
|
|
63
|
+
const storage = new StorageManager(dir);
|
|
64
|
+
const { id } = await storage.writeMemory("fact", "purge projection target", {
|
|
65
|
+
source: "test",
|
|
66
|
+
});
|
|
67
|
+
const memory = await storage.getMemoryById(id);
|
|
68
|
+
assert.ok(memory);
|
|
69
|
+
await rebuildMemoryProjection({ memoryDir: dir, dryRun: false });
|
|
70
|
+
assert.equal(readProjectedMemoryBrowse(dir, { limit: 5, offset: 0 })?.total, 1);
|
|
71
|
+
|
|
72
|
+
const result = await purgeMemories({
|
|
73
|
+
storage,
|
|
74
|
+
olderThanMs: 1,
|
|
75
|
+
tier: "all",
|
|
76
|
+
dryRun: false,
|
|
77
|
+
now: () => new Date(Date.now() + 86_400_000),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
assert.equal(result.purgedCount, 1);
|
|
81
|
+
assert.equal(readProjectedMemoryBrowse(dir, { limit: 5, offset: 0 })?.total, 0);
|
|
82
|
+
} finally {
|
|
83
|
+
await rm(dir, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("purgeMemories invalidates projection rows for already-absent files but not failed deletes, and dry-run never mutates", async () => {
|
|
88
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-purge-projection-branches-"));
|
|
89
|
+
try {
|
|
90
|
+
// Real projection with two rows; candidates come from a mock storage so the
|
|
91
|
+
// unlink outcomes are deterministic: one path is already absent (ENOENT),
|
|
92
|
+
// the other is a non-empty directory (unlink fails, not ENOENT).
|
|
93
|
+
const realStorage = new StorageManager(dir);
|
|
94
|
+
const first = await realStorage.writeMemory("fact", "purge branch enoent", { source: "test" });
|
|
95
|
+
const second = await realStorage.writeMemory("fact", "purge branch failure", { source: "test" });
|
|
96
|
+
await rebuildMemoryProjection({ memoryDir: dir, dryRun: false });
|
|
97
|
+
assert.equal(readProjectedMemoryBrowse(dir, { limit: 5, offset: 0 })?.total, 2);
|
|
98
|
+
|
|
99
|
+
const firstMemory = await realStorage.getMemoryById(first.id);
|
|
100
|
+
const secondMemory = await realStorage.getMemoryById(second.id);
|
|
101
|
+
assert.ok(firstMemory && secondMemory);
|
|
102
|
+
await rm(firstMemory.path);
|
|
103
|
+
await rm(secondMemory.path);
|
|
104
|
+
await mkdir(secondMemory.path);
|
|
105
|
+
await writeFile(path.join(secondMemory.path, "block.txt"), "x", "utf8");
|
|
106
|
+
|
|
107
|
+
const mockStorage = {
|
|
108
|
+
dir,
|
|
109
|
+
readAllMemories: async () => [firstMemory, secondMemory],
|
|
110
|
+
readAllColdMemories: async () => [],
|
|
111
|
+
readArchivedMemories: async () => [],
|
|
112
|
+
};
|
|
113
|
+
const future = () => new Date(Date.now() + 86_400_000);
|
|
114
|
+
|
|
115
|
+
// Dry-run: no projection mutation.
|
|
116
|
+
await purgeMemories({ storage: mockStorage as never, olderThanMs: 1, tier: "all", dryRun: true, now: future });
|
|
117
|
+
assert.equal(readProjectedMemoryBrowse(dir, { limit: 5, offset: 0 })?.total, 2);
|
|
118
|
+
|
|
119
|
+
const result = await purgeMemories({ storage: mockStorage as never, olderThanMs: 1, tier: "all", dryRun: false, now: future });
|
|
120
|
+
|
|
121
|
+
assert.equal(result.alreadyAbsentCount, 1);
|
|
122
|
+
assert.equal(result.purgedCount, 0);
|
|
123
|
+
assert.equal(result.errors.filter((e) => e.id === second.id).length, 1);
|
|
124
|
+
const page = readProjectedMemoryBrowse(dir, { limit: 5, offset: 0 });
|
|
125
|
+
assert.ok(page);
|
|
126
|
+
// ENOENT row invalidated; failed-delete row keeps its projection entry.
|
|
127
|
+
assert.equal(page.total, 1);
|
|
128
|
+
} finally {
|
|
129
|
+
await rm(dir, { recursive: true, force: true });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
57
133
|
async function fileExists(filePath: string): Promise<boolean> {
|
|
58
134
|
try {
|
|
59
135
|
await access(filePath);
|
package/src/maintenance/purge.ts
CHANGED
|
@@ -23,6 +23,7 @@ import path from "node:path";
|
|
|
23
23
|
import { appendFile, mkdir, unlink } from "node:fs/promises";
|
|
24
24
|
import type { StorageManager } from "../storage.js";
|
|
25
25
|
import type { MemoryFile } from "../types.js";
|
|
26
|
+
import { markProjectedMemoryPathInvalid } from "../memory-projection-store.js";
|
|
26
27
|
import type { SearchBackend } from "../search/port.js";
|
|
27
28
|
|
|
28
29
|
export type PurgeTierFilter = "cold" | "all";
|
|
@@ -236,12 +237,14 @@ export async function purgeMemories(
|
|
|
236
237
|
await unlink(candidate.path);
|
|
237
238
|
actuallyPurged.push(candidate);
|
|
238
239
|
addCollectionForCandidate(candidate);
|
|
240
|
+
markProjectedMemoryPathInvalid(storage.dir, candidate.id);
|
|
239
241
|
} catch (err) {
|
|
240
242
|
const message = err instanceof Error ? err.message : String(err);
|
|
241
243
|
// ENOENT is fine — already gone
|
|
242
244
|
if (hasErrorCode(err, "ENOENT")) {
|
|
243
245
|
alreadyAbsent.push(candidate);
|
|
244
246
|
addCollectionForCandidate(candidate);
|
|
247
|
+
markProjectedMemoryPathInvalid(storage.dir, candidate.id);
|
|
245
248
|
} else {
|
|
246
249
|
errors.push({ id: candidate.id, path: candidate.path, error: message });
|
|
247
250
|
}
|
|
@@ -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,
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
import {
|
|
24
25
|
getMemoryProjectionPath,
|
|
25
26
|
initializeMemoryProjectionDb,
|
|
27
|
+
isProjectedMemoryPathValid,
|
|
26
28
|
type ProjectedEntityMentionRow,
|
|
27
29
|
type MemoryProjectionGovernanceAppliedActionRow,
|
|
28
30
|
type MemoryProjectionGovernanceReviewQueueRow,
|
|
@@ -51,6 +53,27 @@ export interface RebuildMemoryProjectionOptions {
|
|
|
51
53
|
updatedBefore?: string;
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
export interface SkippedDuplicateMemory {
|
|
57
|
+
memoryId: string;
|
|
58
|
+
keptPath: string;
|
|
59
|
+
skippedPath: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface SkippedBlankIdMemory {
|
|
63
|
+
path: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SkippedDuplicateTimelineEvent {
|
|
67
|
+
eventId: string;
|
|
68
|
+
keptPath: string;
|
|
69
|
+
skippedPath: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface SkippedBlankIdTimelineEvent {
|
|
73
|
+
eventId: string;
|
|
74
|
+
path: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
54
77
|
export interface RebuildMemoryProjectionResult {
|
|
55
78
|
dryRun: boolean;
|
|
56
79
|
scannedMemories: number;
|
|
@@ -62,6 +85,10 @@ export interface RebuildMemoryProjectionResult {
|
|
|
62
85
|
outputPath: string;
|
|
63
86
|
backupPath?: string;
|
|
64
87
|
usedLifecycleLedger: boolean;
|
|
88
|
+
skippedDuplicateMemories: SkippedDuplicateMemory[];
|
|
89
|
+
skippedBlankIdMemories: SkippedBlankIdMemory[];
|
|
90
|
+
skippedDuplicateTimelineEvents: SkippedDuplicateTimelineEvent[];
|
|
91
|
+
skippedBlankIdTimelineEvents: SkippedBlankIdTimelineEvent[];
|
|
65
92
|
scope: {
|
|
66
93
|
updatedAfter: string | null;
|
|
67
94
|
updatedBefore: string | null;
|
|
@@ -435,6 +462,91 @@ function serializeGovernanceReviewQueueRow(
|
|
|
435
462
|
});
|
|
436
463
|
}
|
|
437
464
|
|
|
465
|
+
function selectProjectionMemories(
|
|
466
|
+
tiers: MemoryFile[][],
|
|
467
|
+
): {
|
|
468
|
+
allMemories: MemoryFile[];
|
|
469
|
+
memories: MemoryFile[];
|
|
470
|
+
skippedDuplicateMemories: SkippedDuplicateMemory[];
|
|
471
|
+
skippedBlankIdMemories: SkippedBlankIdMemory[];
|
|
472
|
+
} {
|
|
473
|
+
const allMemories = tiers.flat();
|
|
474
|
+
const memories: MemoryFile[] = [];
|
|
475
|
+
const keptById = new Map<string, MemoryFile>();
|
|
476
|
+
const skippedDuplicateMemories: SkippedDuplicateMemory[] = [];
|
|
477
|
+
const skippedBlankIdMemories: SkippedBlankIdMemory[] = [];
|
|
478
|
+
|
|
479
|
+
// Tier order is authoritative: hot wins over cold, and cold wins over archive.
|
|
480
|
+
// Paths break ties within a tier so the winner does not depend on readdir order.
|
|
481
|
+
for (const tier of tiers) {
|
|
482
|
+
for (const memory of [...tier].sort((left, right) => left.path.localeCompare(right.path))) {
|
|
483
|
+
const memoryId = memory.frontmatter.id.trim();
|
|
484
|
+
if (!memoryId) {
|
|
485
|
+
skippedBlankIdMemories.push({ path: memory.path });
|
|
486
|
+
log.warn(`projection rebuild skipped blank memory id: path=${memory.path}`);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const kept = keptById.get(memoryId);
|
|
490
|
+
if (kept) {
|
|
491
|
+
skippedDuplicateMemories.push({
|
|
492
|
+
memoryId,
|
|
493
|
+
keptPath: kept.path,
|
|
494
|
+
skippedPath: memory.path,
|
|
495
|
+
});
|
|
496
|
+
log.warn(
|
|
497
|
+
`projection rebuild skipped duplicate memory id=${memoryId}: kept=${kept.path} skipped=${memory.path}`,
|
|
498
|
+
);
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
keptById.set(memoryId, memory);
|
|
502
|
+
memories.push(memory);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return { allMemories, memories, skippedDuplicateMemories, skippedBlankIdMemories };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function deduplicateTimelineEvents(
|
|
510
|
+
events: MemoryLifecycleEvent[],
|
|
511
|
+
sourcePathFor: (event: MemoryLifecycleEvent) => string,
|
|
512
|
+
): {
|
|
513
|
+
events: MemoryLifecycleEvent[];
|
|
514
|
+
skipped: SkippedDuplicateTimelineEvent[];
|
|
515
|
+
skippedBlankIds: SkippedBlankIdTimelineEvent[];
|
|
516
|
+
} {
|
|
517
|
+
const keptById = new Map<string, { event: MemoryLifecycleEvent; path: string }>();
|
|
518
|
+
const unique: MemoryLifecycleEvent[] = [];
|
|
519
|
+
const skipped: SkippedDuplicateTimelineEvent[] = [];
|
|
520
|
+
const skippedBlankIds: SkippedBlankIdTimelineEvent[] = [];
|
|
521
|
+
for (const event of sortMemoryLifecycleEvents(events)) {
|
|
522
|
+
const sourcePath = sourcePathFor(event);
|
|
523
|
+
if (!event.memoryId.trim()) {
|
|
524
|
+
skippedBlankIds.push({ eventId: event.eventId, path: sourcePath });
|
|
525
|
+
log.warn(
|
|
526
|
+
`projection rebuild skipped timeline event with blank memory id: `
|
|
527
|
+
+ `event_id=${event.eventId} path=${sourcePath}`,
|
|
528
|
+
);
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
const kept = keptById.get(event.eventId);
|
|
532
|
+
if (kept) {
|
|
533
|
+
skipped.push({
|
|
534
|
+
eventId: event.eventId,
|
|
535
|
+
keptPath: kept.path,
|
|
536
|
+
skippedPath: sourcePath,
|
|
537
|
+
});
|
|
538
|
+
log.warn(
|
|
539
|
+
`projection rebuild skipped duplicate timeline event_id=${event.eventId}: `
|
|
540
|
+
+ `kept=${kept.path} skipped=${sourcePath}`,
|
|
541
|
+
);
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
keptById.set(event.eventId, { event, path: sourcePath });
|
|
545
|
+
unique.push(event);
|
|
546
|
+
}
|
|
547
|
+
return { events: unique, skipped, skippedBlankIds };
|
|
548
|
+
}
|
|
549
|
+
|
|
438
550
|
async function loadAuthoritativeProjectionSnapshot(options: {
|
|
439
551
|
memoryDir: string;
|
|
440
552
|
defaultNamespace?: string;
|
|
@@ -464,22 +576,43 @@ async function loadAuthoritativeProjectionSnapshot(options: {
|
|
|
464
576
|
updatedAfter: string | null;
|
|
465
577
|
updatedBefore: string | null;
|
|
466
578
|
};
|
|
579
|
+
skippedDuplicateMemories: SkippedDuplicateMemory[];
|
|
580
|
+
skippedBlankIdMemories: SkippedBlankIdMemory[];
|
|
581
|
+
skippedDuplicateTimelineEvents: SkippedDuplicateTimelineEvent[];
|
|
582
|
+
skippedBlankIdTimelineEvents: SkippedBlankIdTimelineEvent[];
|
|
467
583
|
}> {
|
|
468
584
|
const storage = new StorageManager(options.memoryDir);
|
|
469
585
|
// Force a fresh disk read — projection verify/rebuild must see the true
|
|
470
586
|
// on-disk state, not a potentially stale in-process cache.
|
|
471
587
|
storage.invalidateAllMemoriesCacheForDir();
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
]
|
|
477
|
-
|
|
588
|
+
const selected = selectProjectionMemories([
|
|
589
|
+
await storage.readAllMemories(),
|
|
590
|
+
await storage.readAllColdMemories(),
|
|
591
|
+
await storage.readArchivedMemories(),
|
|
592
|
+
]);
|
|
593
|
+
const allMemories = selected.allMemories;
|
|
594
|
+
const projectionMemories = selected.memories;
|
|
478
595
|
const lifecycleEvents = await storage.readAllMemoryLifecycleEvents();
|
|
479
|
-
const
|
|
480
|
-
const
|
|
596
|
+
const timeline = loadTimelineEvents(projectionMemories, lifecycleEvents);
|
|
597
|
+
const lifecycleLedgerPath = path.join(
|
|
598
|
+
options.memoryDir,
|
|
599
|
+
"state",
|
|
600
|
+
"memory-lifecycle-ledger.jsonl",
|
|
601
|
+
);
|
|
602
|
+
const memoryPathById = new Map(
|
|
603
|
+
projectionMemories.map((memory) => [memory.frontmatter.id, memory.path]),
|
|
604
|
+
);
|
|
605
|
+
const deduplicatedTimeline = deduplicateTimelineEvents(
|
|
606
|
+
timeline.events,
|
|
607
|
+
(event) => timeline.usedLifecycleLedger
|
|
608
|
+
? lifecycleLedgerPath
|
|
609
|
+
: memoryPathById.get(event.memoryId) ?? lifecycleLedgerPath,
|
|
610
|
+
);
|
|
611
|
+
const events = deduplicatedTimeline.events;
|
|
612
|
+
const usedLifecycleLedger = timeline.usedLifecycleLedger;
|
|
613
|
+
const currentRows = projectionMemories.map((memory) => toCurrentStateRow(options.memoryDir, memory));
|
|
481
614
|
const scope = normalizeProjectionScope(options);
|
|
482
|
-
const scopedMemories = filterMemoriesForProjectionScope(
|
|
615
|
+
const scopedMemories = filterMemoriesForProjectionScope(projectionMemories, scope);
|
|
483
616
|
const scopedMemoryIds = new Set(scopedMemories.map((memory) => memory.frontmatter.id));
|
|
484
617
|
const nativeKnowledgeRows = await loadPersistedNativeKnowledgeChunks({
|
|
485
618
|
memoryDir: options.memoryDir,
|
|
@@ -516,6 +649,10 @@ async function loadAuthoritativeProjectionSnapshot(options: {
|
|
|
516
649
|
governance,
|
|
517
650
|
usedLifecycleLedger,
|
|
518
651
|
scope,
|
|
652
|
+
skippedDuplicateMemories: selected.skippedDuplicateMemories,
|
|
653
|
+
skippedBlankIdMemories: selected.skippedBlankIdMemories,
|
|
654
|
+
skippedDuplicateTimelineEvents: deduplicatedTimeline.skipped,
|
|
655
|
+
skippedBlankIdTimelineEvents: deduplicatedTimeline.skippedBlankIds,
|
|
519
656
|
};
|
|
520
657
|
}
|
|
521
658
|
|
|
@@ -658,6 +795,7 @@ function writeProjectionDb(
|
|
|
658
795
|
usedLifecycleLedger: boolean,
|
|
659
796
|
): void {
|
|
660
797
|
const db = openBetterSqlite3(dbPath);
|
|
798
|
+
const memoryDir = path.dirname(path.dirname(dbPath));
|
|
661
799
|
try {
|
|
662
800
|
initializeMemoryProjectionDb(db);
|
|
663
801
|
|
|
@@ -674,6 +812,7 @@ function writeProjectionDb(
|
|
|
674
812
|
status,
|
|
675
813
|
lifecycle_state,
|
|
676
814
|
path_rel,
|
|
815
|
+
path_valid,
|
|
677
816
|
created_at,
|
|
678
817
|
updated_at,
|
|
679
818
|
archived_at,
|
|
@@ -687,7 +826,7 @@ function writeProjectionDb(
|
|
|
687
826
|
last_accessed,
|
|
688
827
|
tags_json,
|
|
689
828
|
preview_text
|
|
690
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
829
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
691
830
|
`);
|
|
692
831
|
|
|
693
832
|
const insertTimeline = db.prepare(`
|
|
@@ -785,6 +924,7 @@ function writeProjectionDb(
|
|
|
785
924
|
row.status,
|
|
786
925
|
row.lifecycleState ?? null,
|
|
787
926
|
row.pathRel,
|
|
927
|
+
isProjectedMemoryPathValid(memoryDir, row.pathRel) ? 1 : 0,
|
|
788
928
|
row.created,
|
|
789
929
|
row.updated,
|
|
790
930
|
row.archivedAt ?? null,
|
|
@@ -1040,6 +1180,10 @@ export async function rebuildMemoryProjection(
|
|
|
1040
1180
|
outputPath,
|
|
1041
1181
|
backupPath,
|
|
1042
1182
|
usedLifecycleLedger: snapshot.usedLifecycleLedger,
|
|
1183
|
+
skippedDuplicateMemories: snapshot.skippedDuplicateMemories,
|
|
1184
|
+
skippedBlankIdMemories: snapshot.skippedBlankIdMemories,
|
|
1185
|
+
skippedDuplicateTimelineEvents: snapshot.skippedDuplicateTimelineEvents,
|
|
1186
|
+
skippedBlankIdTimelineEvents: snapshot.skippedBlankIdTimelineEvents,
|
|
1043
1187
|
scope: snapshot.scope,
|
|
1044
1188
|
};
|
|
1045
1189
|
}
|