@remnic/core 9.35.2 → 9.35.4
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 +5 -5
- package/dist/access-admin-ops-surface.js +19 -18
- package/dist/access-authorization-probe.d.ts +5 -5
- package/dist/access-authorization-probe.js +20 -19
- package/dist/access-boundary.d.ts +5 -5
- package/dist/access-boundary.js +19 -18
- package/dist/access-cli.js +39 -38
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.d.ts +5 -5
- package/dist/access-extraction-force-flush.js +19 -18
- package/dist/access-http-lcm-compaction.d.ts +5 -5
- package/dist/access-http-lifecycle-flush.d.ts +5 -5
- package/dist/access-http.d.ts +11 -5
- package/dist/access-http.js +24 -23
- package/dist/access-identity-continuity-surface.d.ts +4 -4
- package/dist/access-identity-continuity-surface.js +19 -18
- package/dist/access-lcm-surface.d.ts +5 -5
- package/dist/access-lcm-surface.js +19 -18
- package/dist/access-mcp.d.ts +5 -5
- package/dist/access-mcp.js +22 -21
- package/dist/access-namespace-preflight.js +19 -18
- package/dist/access-observe-write-surface.d.ts +5 -5
- package/dist/access-observe-write-surface.js +19 -18
- package/dist/access-operations-batch.js +20 -19
- package/dist/access-operations.d.ts +8 -8
- package/dist/access-operations.js +21 -20
- package/dist/access-recall-concurrency.d.ts +5 -5
- package/dist/access-recall-concurrency.js +19 -18
- package/dist/access-recall-response.d.ts +5 -5
- package/dist/access-recall-response.js +19 -18
- package/dist/access-recall-surface.d.ts +5 -5
- package/dist/access-recall-surface.js +19 -18
- package/dist/access-schema.d.ts +88 -88
- package/dist/{access-service-D9zIBlpp.d.ts → access-service-C2WYT1pO.d.ts} +3 -3
- package/dist/access-service-helpers.d.ts +5 -5
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +19 -18
- package/dist/access-surface-catalog.d.ts +5 -5
- package/dist/access-wearables-meetings-surface.d.ts +2 -2
- package/dist/bootstrap.d.ts +4 -4
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +10 -9
- package/dist/buffer.d.ts +1 -1
- package/dist/{capsule-merge-2H2IUTBT.js → capsule-merge-IDWJPCWK.js} +77 -8
- package/dist/capsule-merge-IDWJPCWK.js.map +1 -0
- package/dist/causal-consolidation.js +11 -10
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/{chunk-65ZPVYTA.js → chunk-3CE77VAP.js} +55 -5
- package/dist/chunk-3CE77VAP.js.map +1 -0
- package/dist/chunk-4J7G5SY4.js +338 -0
- package/dist/chunk-4J7G5SY4.js.map +1 -0
- package/dist/{chunk-EO7X2LDK.js → chunk-4TAD3B56.js} +2 -2
- package/dist/{chunk-QHYB4MJV.js → chunk-5ZGHUWTT.js} +2 -2
- package/dist/{chunk-TCIWHK3N.js → chunk-6D5D5A4M.js} +5 -5
- package/dist/{chunk-BIXWNHIL.js → chunk-6TF4M2LB.js} +5 -5
- package/dist/{chunk-FBUE6PD3.js → chunk-BEQOZOYZ.js} +21 -4
- package/dist/chunk-BEQOZOYZ.js.map +1 -0
- package/dist/{chunk-SA3WJJF2.js → chunk-BGLDKAYY.js} +2 -2
- package/dist/{chunk-OPRP6Q4Z.js → chunk-DKKZ6SO2.js} +3 -3
- package/dist/{chunk-6TAUHL33.js → chunk-DLVPUX7U.js} +10 -2
- package/dist/chunk-DLVPUX7U.js.map +1 -0
- package/dist/{chunk-QNWXCJ76.js → chunk-ENTCVTQR.js} +18 -1
- package/dist/{chunk-QNWXCJ76.js.map → chunk-ENTCVTQR.js.map} +1 -1
- package/dist/{chunk-AEPYA55F.js → chunk-FE7MUX7M.js} +2 -2
- package/dist/{chunk-GHR7WHZX.js → chunk-GORHSN5C.js} +2 -2
- package/dist/{chunk-7TEZUUGM.js → chunk-HUS2XN3T.js} +2 -2
- package/dist/{chunk-SLPLPGJQ.js → chunk-IE2CS5BM.js} +17 -10
- package/dist/chunk-IE2CS5BM.js.map +1 -0
- package/dist/{chunk-XWDZQIYU.js → chunk-IJAQCBGV.js} +2 -2
- package/dist/{chunk-TJLLYV2X.js → chunk-IMEXE4XS.js} +7 -7
- package/dist/{chunk-IZXTZ7BF.js → chunk-JYLN6KHH.js} +6 -6
- package/dist/{chunk-EH5U2YCX.js → chunk-M3DBB2ND.js} +2 -2
- package/dist/{chunk-XABS455Y.js → chunk-NBAEGFXT.js} +5 -5
- package/dist/{chunk-T4HH5Q3U.js → chunk-OJRLTZQM.js} +3 -3
- package/dist/{chunk-7ZHGPX56.js → chunk-PRP3M5Y3.js} +2 -2
- package/dist/{chunk-Y2ZD2RVG.js → chunk-PYTUBWAS.js} +2 -2
- package/dist/{chunk-4TGZCYHH.js → chunk-Q3CVY7LV.js} +1954 -1642
- package/dist/chunk-Q3CVY7LV.js.map +1 -0
- package/dist/{chunk-PH3ZWL3W.js → chunk-Q53RLEW7.js} +2 -2
- package/dist/{chunk-Y6OWJ3V5.js → chunk-RY2XZTFN.js} +2 -2
- package/dist/{chunk-2SHIWGY7.js → chunk-RY7CHVGE.js} +2 -2
- package/dist/{chunk-7VUAG236.js → chunk-T3FVAHM7.js} +4 -4
- package/dist/{chunk-2LTDWSR6.js → chunk-TSH56VPU.js} +2 -2
- package/dist/{chunk-J6SLNQRO.js → chunk-UBWOCQ3I.js} +6 -2
- package/dist/chunk-UBWOCQ3I.js.map +1 -0
- package/dist/{chunk-5FHE4JI7.js → chunk-VEUVON5I.js} +2 -2
- package/dist/{chunk-KEWUNL7P.js → chunk-WET4M3R6.js} +2 -2
- package/dist/{chunk-HEVWWPTL.js → chunk-Y2UUEB4I.js} +17 -17
- package/dist/{chunk-AHZYNPLX.js → chunk-YVMVVE4B.js} +2 -2
- package/dist/{chunk-5HTAJCTO.js → chunk-ZB3YODID.js} +15 -15
- package/dist/{chunk-FHYGLITW.js → chunk-ZJMVZLPK.js} +217 -45
- package/dist/{chunk-FHYGLITW.js.map → chunk-ZJMVZLPK.js.map} +1 -1
- package/dist/{cli-mFX-QQvx.d.ts → cli-jkmzdT8P.d.ts} +3 -3
- package/dist/cli.d.ts +6 -6
- package/dist/cli.js +34 -33
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +10 -9
- package/dist/connectors/codex-materialize-runner.js +10 -9
- package/dist/connectors/index.js +10 -9
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-provenance-check.js +8 -2
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/consolidation-undo.js +10 -1
- package/dist/consolidation-undo.js.map +1 -1
- package/dist/contradiction/index.d.ts +1 -1
- package/dist/corpus-watermark.js +12 -11
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +10 -9
- package/dist/explicit-capture.d.ts +4 -4
- package/dist/explicit-capture.js +6 -3
- package/dist/extraction.js +10 -9
- package/dist/{forget-4UY2EOKH.js → forget-DNAJ66ZD.js} +2 -2
- package/dist/index.d.ts +396 -396
- package/dist/index.js +39 -38
- package/dist/maintenance/memory-governance.js +10 -9
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +1 -1
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -9
- package/dist/maintenance/rebuild-memory-projection.d.ts +1 -1
- package/dist/maintenance/rebuild-memory-projection.js +11 -10
- package/dist/{maintenance-70Wq118b.d.ts → maintenance-Ha9Dg93M.d.ts} +1 -1
- package/dist/mcp-memory-inspector-app.d.ts +5 -5
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +1 -1
- package/dist/namespaces/migrate.js +11 -10
- package/dist/namespaces/storage.d.ts +1 -1
- package/dist/namespaces/storage.js +10 -9
- package/dist/offline-sync-impression-drain.js +3 -3
- package/dist/operator-doctor-corpus.js +13 -12
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +17 -16
- package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +15 -14
- package/dist/{orchestrator-BH8LT-NX.d.ts → orchestrator-DV_u9bfx.d.ts} +3 -3
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +39 -38
- package/dist/page-versioning.js +7 -1
- package/dist/schemas.d.ts +54 -54
- package/dist/semantic-consolidation.js +11 -10
- package/dist/semantic-rule-promotion.js +10 -9
- package/dist/semantic-rule-verifier.js +10 -9
- package/dist/{service-CrxXHz5T.d.ts → service-JxXXY-Eh.d.ts} +1 -1
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/source-agent-qualifier.js +10 -9
- package/dist/{storage-CyWa1Jm6.d.ts → storage-BbH4HDUS.d.ts} +30 -26
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +9 -8
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/transfer/capsule-import.js +8 -2
- package/dist/transfer/types.d.ts +66 -66
- package/dist/verified-recall.js +10 -9
- package/package.json +2 -2
- package/src/access-http.ts +14 -9
- package/src/binary-lifecycle/pipeline.ts +52 -1
- package/src/consolidation-undo.ts +4 -0
- package/src/curation/index.ts +41 -5
- package/src/lifecycle/tombstones.ts +40 -0
- package/src/maintenance/memory-governance.ts +4 -0
- package/src/page-versioning.ts +16 -1
- package/src/review/index.ts +97 -13
- package/src/spaces/index.ts +127 -6
- package/src/storage/citation-hash-source.ts +97 -0
- package/src/storage/entity-canonical-id-lock.ts +33 -0
- package/src/storage/entity-canonical-id-migration-adapter.ts +8 -4
- package/src/storage/entity-canonical-id-migration.ts +129 -77
- package/src/storage/entity-canonical-id-references.ts +557 -0
- package/src/storage/entity-ref-repair.ts +171 -0
- package/src/storage/entity-store.ts +163 -2
- package/src/storage/tombstone-blocked-capture-sync.ts +18 -0
- package/src/storage.ts +363 -396
- package/src/transfer/capsule-import.ts +91 -3
- package/src/transfer/capsule-merge.ts +128 -10
- package/dist/capsule-merge-2H2IUTBT.js.map +0 -1
- package/dist/chunk-4TGZCYHH.js.map +0 -1
- package/dist/chunk-65ZPVYTA.js.map +0 -1
- package/dist/chunk-6TAUHL33.js.map +0 -1
- package/dist/chunk-FBUE6PD3.js.map +0 -1
- package/dist/chunk-J6SLNQRO.js.map +0 -1
- package/dist/chunk-SLPLPGJQ.js.map +0 -1
- /package/dist/{chunk-EO7X2LDK.js.map → chunk-4TAD3B56.js.map} +0 -0
- /package/dist/{chunk-QHYB4MJV.js.map → chunk-5ZGHUWTT.js.map} +0 -0
- /package/dist/{chunk-TCIWHK3N.js.map → chunk-6D5D5A4M.js.map} +0 -0
- /package/dist/{chunk-BIXWNHIL.js.map → chunk-6TF4M2LB.js.map} +0 -0
- /package/dist/{chunk-SA3WJJF2.js.map → chunk-BGLDKAYY.js.map} +0 -0
- /package/dist/{chunk-OPRP6Q4Z.js.map → chunk-DKKZ6SO2.js.map} +0 -0
- /package/dist/{chunk-AEPYA55F.js.map → chunk-FE7MUX7M.js.map} +0 -0
- /package/dist/{chunk-GHR7WHZX.js.map → chunk-GORHSN5C.js.map} +0 -0
- /package/dist/{chunk-7TEZUUGM.js.map → chunk-HUS2XN3T.js.map} +0 -0
- /package/dist/{chunk-XWDZQIYU.js.map → chunk-IJAQCBGV.js.map} +0 -0
- /package/dist/{chunk-TJLLYV2X.js.map → chunk-IMEXE4XS.js.map} +0 -0
- /package/dist/{chunk-IZXTZ7BF.js.map → chunk-JYLN6KHH.js.map} +0 -0
- /package/dist/{chunk-EH5U2YCX.js.map → chunk-M3DBB2ND.js.map} +0 -0
- /package/dist/{chunk-XABS455Y.js.map → chunk-NBAEGFXT.js.map} +0 -0
- /package/dist/{chunk-T4HH5Q3U.js.map → chunk-OJRLTZQM.js.map} +0 -0
- /package/dist/{chunk-7ZHGPX56.js.map → chunk-PRP3M5Y3.js.map} +0 -0
- /package/dist/{chunk-Y2ZD2RVG.js.map → chunk-PYTUBWAS.js.map} +0 -0
- /package/dist/{chunk-PH3ZWL3W.js.map → chunk-Q53RLEW7.js.map} +0 -0
- /package/dist/{chunk-Y6OWJ3V5.js.map → chunk-RY2XZTFN.js.map} +0 -0
- /package/dist/{chunk-2SHIWGY7.js.map → chunk-RY7CHVGE.js.map} +0 -0
- /package/dist/{chunk-7VUAG236.js.map → chunk-T3FVAHM7.js.map} +0 -0
- /package/dist/{chunk-2LTDWSR6.js.map → chunk-TSH56VPU.js.map} +0 -0
- /package/dist/{chunk-5FHE4JI7.js.map → chunk-VEUVON5I.js.map} +0 -0
- /package/dist/{chunk-KEWUNL7P.js.map → chunk-WET4M3R6.js.map} +0 -0
- /package/dist/{chunk-HEVWWPTL.js.map → chunk-Y2UUEB4I.js.map} +0 -0
- /package/dist/{chunk-AHZYNPLX.js.map → chunk-YVMVVE4B.js.map} +0 -0
- /package/dist/{chunk-5HTAJCTO.js.map → chunk-ZB3YODID.js.map} +0 -0
- /package/dist/{forget-4UY2EOKH.js.map → forget-DNAJ66ZD.js.map} +0 -0
|
@@ -11,14 +11,10 @@ import {
|
|
|
11
11
|
} from "./chunk-UHVXE24Y.js";
|
|
12
12
|
import {
|
|
13
13
|
TombstoneStore,
|
|
14
|
+
applyTombstoneResurrectionGate,
|
|
14
15
|
buildRetiredFactTombstoneInputs,
|
|
15
16
|
collectRetiredMemoriesForRebuild
|
|
16
|
-
} from "./chunk-
|
|
17
|
-
import {
|
|
18
|
-
computeSupersessionKey,
|
|
19
|
-
normalizeSupersessionKey,
|
|
20
|
-
supersessionKeysForFact
|
|
21
|
-
} from "./chunk-DNHVPZOD.js";
|
|
17
|
+
} from "./chunk-ENTCVTQR.js";
|
|
22
18
|
import {
|
|
23
19
|
assertMemoryFrontmatterId,
|
|
24
20
|
warnProjectionFallback
|
|
@@ -42,7 +38,7 @@ import {
|
|
|
42
38
|
readMaybeEncryptedLines,
|
|
43
39
|
readMemoryActionEventRowsFromLines,
|
|
44
40
|
serializeLifecycleAppendPayload
|
|
45
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-T3FVAHM7.js";
|
|
46
42
|
import {
|
|
47
43
|
namespaceIdentityFromToken
|
|
48
44
|
} from "./chunk-EGGE52UU.js";
|
|
@@ -82,7 +78,7 @@ import {
|
|
|
82
78
|
TombstoneBlockedCaptureIndexHost,
|
|
83
79
|
buildExplicitCaptureDedupKey,
|
|
84
80
|
parseTombstoneBlockedOfflineSyncMemory
|
|
85
|
-
} from "./chunk-
|
|
81
|
+
} from "./chunk-BEQOZOYZ.js";
|
|
86
82
|
import {
|
|
87
83
|
composeMemoryEnvelope,
|
|
88
84
|
isSealedMemoryEnvelope,
|
|
@@ -92,9 +88,6 @@ import {
|
|
|
92
88
|
assemblePersistedBody,
|
|
93
89
|
stripAttributesSuffix
|
|
94
90
|
} from "./chunk-R3DJVOSM.js";
|
|
95
|
-
import {
|
|
96
|
-
withHeldFileLock
|
|
97
|
-
} from "./chunk-FFAZ2XFU.js";
|
|
98
91
|
import {
|
|
99
92
|
markProjectedMemoryPathInvalid,
|
|
100
93
|
openProjectionReadonly,
|
|
@@ -117,7 +110,7 @@ import {
|
|
|
117
110
|
} from "./chunk-G7D6GZ5J.js";
|
|
118
111
|
import {
|
|
119
112
|
createVersion
|
|
120
|
-
} from "./chunk-
|
|
113
|
+
} from "./chunk-DLVPUX7U.js";
|
|
121
114
|
import {
|
|
122
115
|
clearInFlightReads,
|
|
123
116
|
deleteInFlightRead,
|
|
@@ -125,6 +118,27 @@ import {
|
|
|
125
118
|
getInFlightRead,
|
|
126
119
|
setInFlightRead
|
|
127
120
|
} from "./chunk-PDJTHRAW.js";
|
|
121
|
+
import {
|
|
122
|
+
ENTITY_CANONICAL_ID_MIGRATION_FILE,
|
|
123
|
+
ENTITY_CANONICAL_ID_RECONCILE_CONSUMING_MARKER,
|
|
124
|
+
ENTITY_CANONICAL_ID_RECONCILE_MARKER,
|
|
125
|
+
HistoricalEntityCanonicalIdCache,
|
|
126
|
+
appendCanonicalizedTombstone,
|
|
127
|
+
canonicalizeEntityRefOption,
|
|
128
|
+
reconcileIfJournalMovedSync,
|
|
129
|
+
repairEntityRefAfterJournalMove,
|
|
130
|
+
requestEntityCanonicalIdReconcileSync,
|
|
131
|
+
resolveHistoricalEntityCanonicalId,
|
|
132
|
+
withEntityCanonicalMutationLock
|
|
133
|
+
} from "./chunk-4J7G5SY4.js";
|
|
134
|
+
import {
|
|
135
|
+
computeSupersessionKey,
|
|
136
|
+
normalizeSupersessionKey,
|
|
137
|
+
supersessionKeysForFact
|
|
138
|
+
} from "./chunk-DNHVPZOD.js";
|
|
139
|
+
import {
|
|
140
|
+
withHeldFileLock
|
|
141
|
+
} from "./chunk-FFAZ2XFU.js";
|
|
128
142
|
import {
|
|
129
143
|
clearMemoryCache,
|
|
130
144
|
getCachedEntities,
|
|
@@ -214,11 +228,11 @@ import {
|
|
|
214
228
|
stat as stat2,
|
|
215
229
|
writeFile as writeFile2,
|
|
216
230
|
mkdir,
|
|
217
|
-
unlink as
|
|
231
|
+
unlink as unlink4,
|
|
218
232
|
appendFile,
|
|
219
233
|
open
|
|
220
234
|
} from "fs/promises";
|
|
221
|
-
import { appendFileSync, createReadStream, mkdirSync, readFileSync
|
|
235
|
+
import { appendFileSync, createReadStream, mkdirSync, readFileSync, statSync } from "fs";
|
|
222
236
|
import { createHash as createHash5 } from "crypto";
|
|
223
237
|
import path9 from "path";
|
|
224
238
|
|
|
@@ -1420,1442 +1434,1641 @@ function selfDeps(self) {
|
|
|
1420
1434
|
}
|
|
1421
1435
|
|
|
1422
1436
|
// src/storage/entity-store.ts
|
|
1423
|
-
import { readdir as
|
|
1424
|
-
import path4 from "path";
|
|
1425
|
-
|
|
1426
|
-
// src/storage/entity-canonical-id-migration.ts
|
|
1427
|
-
import { randomUUID } from "crypto";
|
|
1428
|
-
import {
|
|
1429
|
-
access,
|
|
1430
|
-
lstat as lstat2,
|
|
1431
|
-
readFile,
|
|
1432
|
-
readdir as readdir2,
|
|
1433
|
-
realpath as realpath2,
|
|
1434
|
-
rename,
|
|
1435
|
-
stat as statFile,
|
|
1436
|
-
unlink,
|
|
1437
|
-
writeFile
|
|
1438
|
-
} from "fs/promises";
|
|
1437
|
+
import { readdir as readdir2, unlink } from "fs/promises";
|
|
1439
1438
|
import path3 from "path";
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
var ENTITY_CANONICAL_ID_MIGRATION_FILE = "entity-canonical-id-migration-v1.json";
|
|
1444
|
-
var ENTITY_CANONICAL_ID_MIGRATION_LOCK_STALE_MS = 6e4;
|
|
1445
|
-
var ENTITY_CANONICAL_ID_MIGRATION_LOCK_MAX_WAIT_MS = 3e5;
|
|
1446
|
-
var TOMBSTONE_LOCK_STALE_MS = 3e4;
|
|
1447
|
-
var TOMBSTONE_LOCK_MAX_WAIT_MS = 5e3;
|
|
1448
|
-
var ENTITY_CANONICAL_ID_MUTATION_LOCK_STALE_MS = 6e4;
|
|
1449
|
-
var ENTITY_CANONICAL_ID_MUTATION_LOCK_MAX_WAIT_MS = 3e5;
|
|
1450
|
-
var MEMORY_REWRITE_MAX_PASSES = 3;
|
|
1451
|
-
var ENTITY_MAPPING_RESCAN_MAX_PASSES = 3;
|
|
1452
|
-
function loadHistoricalEntityCanonicalIds(stateDir) {
|
|
1453
|
-
const statePath2 = path3.join(stateDir, ENTITY_CANONICAL_ID_MIGRATION_FILE);
|
|
1454
|
-
try {
|
|
1455
|
-
const parsed = JSON.parse(readFileSync(statePath2, "utf-8"));
|
|
1456
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
1457
|
-
const mappings = parsed.mappings;
|
|
1458
|
-
if (!mappings || typeof mappings !== "object" || Array.isArray(mappings)) return {};
|
|
1459
|
-
const cleaned = {};
|
|
1460
|
-
for (const [legacyId, canonicalId] of Object.entries(mappings)) {
|
|
1461
|
-
if (legacyId.length > 0 && typeof canonicalId === "string" && canonicalId.length > 0) {
|
|
1462
|
-
cleaned[legacyId] = canonicalId;
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
return cleaned;
|
|
1466
|
-
} catch {
|
|
1467
|
-
return {};
|
|
1439
|
+
var EntityStore = class {
|
|
1440
|
+
constructor(deps) {
|
|
1441
|
+
this.deps = deps;
|
|
1468
1442
|
}
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
while (!seen.has(current)) {
|
|
1474
|
-
seen.add(current);
|
|
1475
|
-
const next = mappings[current];
|
|
1476
|
-
if (!next || next === current) break;
|
|
1477
|
-
current = next;
|
|
1443
|
+
deps;
|
|
1444
|
+
/** Normalize an entity name using this store's alias table. */
|
|
1445
|
+
normalizeEntityName(raw, type) {
|
|
1446
|
+
return normalizeEntityName(raw, type, this.deps.userAliases);
|
|
1478
1447
|
}
|
|
1479
|
-
return current;
|
|
1480
|
-
}
|
|
1481
|
-
var EntityCanonicalIdMigrationStateSchema = z.object({
|
|
1482
|
-
version: z.literal(ENTITY_CANONICAL_ID_MIGRATION_VERSION),
|
|
1483
|
-
complete: z.boolean(),
|
|
1484
|
-
mappings: z.record(z.string().min(1)),
|
|
1485
1448
|
/**
|
|
1486
|
-
*
|
|
1487
|
-
*
|
|
1488
|
-
*
|
|
1489
|
-
* still
|
|
1490
|
-
*
|
|
1449
|
+
* Read an entity file by id, resolving through the migration journal with a
|
|
1450
|
+
* fallback to the caller's original id (issue #2213): mid-migration, the
|
|
1451
|
+
* journal maps legacy → canonical BEFORE the file moves — writing the
|
|
1452
|
+
* still-existing legacy file is safe (the rename retry carries it), while
|
|
1453
|
+
* resolving only the canonical path silently drops the mutation.
|
|
1491
1454
|
*/
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1455
|
+
async readEntityForMutation(name, method, historicalIds) {
|
|
1456
|
+
const canonical = resolveHistoricalEntityCanonicalId(name, historicalIds);
|
|
1457
|
+
for (const candidate of canonical === name ? [name] : [canonical, name]) {
|
|
1458
|
+
const filePath = path3.join(this.deps.entitiesDir, `${candidate}.md`);
|
|
1459
|
+
try {
|
|
1460
|
+
const entity = parseEntityFile(await this.deps.readStorageSecureFile(filePath), this.deps.entitySchemas);
|
|
1461
|
+
return { filePath, entity };
|
|
1462
|
+
} catch (err) {
|
|
1463
|
+
if (err instanceof SecureStoreLockedError) throw err;
|
|
1464
|
+
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
log.debug(`${method}: entity file ${canonical}.md not found`);
|
|
1468
|
+
return null;
|
|
1500
1469
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1470
|
+
/**
|
|
1471
|
+
* Locked read-modify-write for the entity-file mutators (issue #2213):
|
|
1472
|
+
* holding the entity-mutation lock keeps `migrateEntityFilePair` (which
|
|
1473
|
+
* takes the same lock) from moving the file between the fallback read and
|
|
1474
|
+
* this write — an unlocked write could recreate a just-renamed legacy file.
|
|
1475
|
+
* The journal itself is NOT serialized by this lock (`pruneBlocked` can
|
|
1476
|
+
* park a mapping mid-mutation), so the journal identity is REVALIDATED
|
|
1477
|
+
* just before committing — a moved journal restarts resolution against the
|
|
1478
|
+
* fresh table instead of writing into a since-contested claimant — and
|
|
1479
|
+
* checked once more after the write for a move across the write itself.
|
|
1480
|
+
* `mutate` returns false to skip the write. Resolves true when written.
|
|
1481
|
+
*/
|
|
1482
|
+
async mutateEntityFile(name, method, mutate) {
|
|
1483
|
+
const stateDir = path3.join(this.deps.baseDir, "state");
|
|
1484
|
+
return withEntityCanonicalMutationLock(stateDir, async () => {
|
|
1485
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1486
|
+
const idsAtResolve = this.deps.currentHistoricalIds();
|
|
1487
|
+
const located = await this.readEntityForMutation(name, method, idsAtResolve);
|
|
1488
|
+
const mutated = located !== null && mutate(located.entity, idsAtResolve);
|
|
1489
|
+
if (this.deps.currentHistoricalIds() !== idsAtResolve) continue;
|
|
1490
|
+
if (!located || !mutated) return false;
|
|
1491
|
+
located.entity.updated = (/* @__PURE__ */ new Date()).toISOString();
|
|
1492
|
+
await this.deps.writeStorageSecureFile(
|
|
1493
|
+
located.filePath,
|
|
1494
|
+
serializeEntityFile(located.entity, this.deps.entitySchemas)
|
|
1495
|
+
);
|
|
1496
|
+
reconcileIfJournalMovedSync(stateDir, idsAtResolve, this.deps.currentHistoricalIds());
|
|
1497
|
+
this.deps.invalidateKnowledgeIndexCache();
|
|
1498
|
+
this.deps.bumpEntityMutationVersion();
|
|
1499
|
+
return true;
|
|
1500
|
+
}
|
|
1501
|
+
requestEntityCanonicalIdReconcileSync(stateDir);
|
|
1502
|
+
throw new Error(
|
|
1503
|
+
`${method}: entity canonical-id journal kept changing; mutation not applied \u2014 retry`
|
|
1504
|
+
);
|
|
1505
|
+
});
|
|
1517
1506
|
}
|
|
1518
|
-
|
|
1519
|
-
|
|
1507
|
+
/** Add a relationship to an entity file. Deduplicates by target+label. */
|
|
1508
|
+
async addEntityRelationship(name, rel) {
|
|
1509
|
+
await this.mutateEntityFile(name, "addEntityRelationship", (entity, historicalIds) => {
|
|
1510
|
+
const target = resolveHistoricalEntityCanonicalId(rel.target, historicalIds);
|
|
1511
|
+
const duplicate = entity.relationships.some(
|
|
1512
|
+
(r) => resolveHistoricalEntityCanonicalId(r.target, historicalIds) === target && r.label === rel.label
|
|
1513
|
+
);
|
|
1514
|
+
if (duplicate) return false;
|
|
1515
|
+
entity.relationships.push({ ...rel, target });
|
|
1516
|
+
return true;
|
|
1517
|
+
});
|
|
1520
1518
|
}
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
];
|
|
1529
|
-
for (const root of roots) {
|
|
1530
|
-
let rootStat;
|
|
1531
|
-
try {
|
|
1532
|
-
rootStat = await lstat2(root);
|
|
1533
|
-
} catch (error) {
|
|
1534
|
-
if (isErrnoCode(error, "ENOENT")) continue;
|
|
1535
|
-
throw error;
|
|
1536
|
-
}
|
|
1537
|
-
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
1538
|
-
throw new Error(`Refusing entity canonical-id migration through unsafe memory root: ${root}.`);
|
|
1539
|
-
}
|
|
1540
|
-
assertPathInsideRoot(memoryRoot, await realpath2(root), root);
|
|
1519
|
+
/** Add an activity entry to an entity file; prunes oldest beyond maxEntries. */
|
|
1520
|
+
async addEntityActivity(name, entry, maxEntries) {
|
|
1521
|
+
await this.mutateEntityFile(name, "addEntityActivity", (entity) => {
|
|
1522
|
+
entity.activity.unshift(entry);
|
|
1523
|
+
if (entity.activity.length > maxEntries) entity.activity = entity.activity.slice(0, maxEntries);
|
|
1524
|
+
return true;
|
|
1525
|
+
});
|
|
1541
1526
|
}
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
try {
|
|
1550
|
-
fileStat = await lstat2(migrationStatePath);
|
|
1551
|
-
} catch (error) {
|
|
1552
|
-
if (isErrnoCode(error, "ENOENT")) return;
|
|
1553
|
-
throw error;
|
|
1527
|
+
async addEntityAlias(name, alias) {
|
|
1528
|
+
const wrote = await this.mutateEntityFile(name, "addEntityAlias", (entity) => {
|
|
1529
|
+
if (entity.aliases.includes(alias)) return false;
|
|
1530
|
+
entity.aliases.push(alias);
|
|
1531
|
+
return true;
|
|
1532
|
+
});
|
|
1533
|
+
if (wrote) this.deps.bumpMemoryStatusVersion();
|
|
1554
1534
|
}
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1535
|
+
async writeEntity(name, type, facts, options = {}) {
|
|
1536
|
+
await this.deps.ensureDirectories();
|
|
1537
|
+
return withEntityCanonicalMutationLock(
|
|
1538
|
+
path3.join(this.deps.baseDir, "state"),
|
|
1539
|
+
() => this.writeEntityUnlocked(name, type, facts, options)
|
|
1558
1540
|
);
|
|
1559
1541
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1542
|
+
async writeEntityUnlocked(name, type, facts, options = {}) {
|
|
1543
|
+
if (typeof name !== "string" || !name.trim() || typeof type !== "string" || !type.trim()) {
|
|
1544
|
+
log.warn("writeEntity: invalid entity payload, skipping", {
|
|
1545
|
+
nameType: typeof name,
|
|
1546
|
+
typeType: typeof type
|
|
1547
|
+
});
|
|
1548
|
+
return "";
|
|
1549
|
+
}
|
|
1550
|
+
const safeFacts = Array.isArray(facts) ? [...new Set(
|
|
1551
|
+
facts.filter((fact) => typeof fact === "string").map((fact) => fact.trim()).filter((fact) => fact.length > 0)
|
|
1552
|
+
)] : [];
|
|
1553
|
+
let normalized = this.deps.normalizeEntityName(name, type);
|
|
1554
|
+
const match = await this.deps.findMatchingEntity(name, type);
|
|
1555
|
+
if (match && match !== normalized) {
|
|
1556
|
+
log.debug(`fuzzy match: "${normalized}" \u2192 existing "${match}"`);
|
|
1557
|
+
normalized = match;
|
|
1558
|
+
}
|
|
1559
|
+
const filePath = path3.join(this.deps.entitiesDir, `${normalized}.md`);
|
|
1560
|
+
let entity = {
|
|
1561
|
+
name,
|
|
1562
|
+
type,
|
|
1563
|
+
created: "",
|
|
1564
|
+
updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1565
|
+
facts: [],
|
|
1566
|
+
summary: void 0,
|
|
1567
|
+
synthesis: void 0,
|
|
1568
|
+
synthesisUpdatedAt: void 0,
|
|
1569
|
+
synthesisVersion: void 0,
|
|
1570
|
+
synthesisStructuredFactCount: void 0,
|
|
1571
|
+
synthesisStructuredFactDigest: void 0,
|
|
1572
|
+
timeline: [],
|
|
1573
|
+
relationships: [],
|
|
1574
|
+
activity: [],
|
|
1575
|
+
aliases: []
|
|
1576
|
+
};
|
|
1577
|
+
try {
|
|
1578
|
+
const existing = await this.deps.readStorageSecureFile(filePath);
|
|
1579
|
+
entity = parseEntityFile(existing, this.deps.entitySchemas);
|
|
1580
|
+
} catch (err) {
|
|
1581
|
+
if (err instanceof SecureStoreLockedError) throw err;
|
|
1582
|
+
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
1583
|
+
}
|
|
1584
|
+
const timestamp = options.timestamp?.trim() || (/* @__PURE__ */ new Date()).toISOString();
|
|
1585
|
+
const source = options.source?.trim() || void 0;
|
|
1586
|
+
const sessionKey = options.sessionKey?.trim() || void 0;
|
|
1587
|
+
const principal = options.principal?.trim() || void 0;
|
|
1588
|
+
const structuredSectionMap = new Map(
|
|
1589
|
+
(entity.structuredSections ?? []).map((section) => [section.key, {
|
|
1590
|
+
...section,
|
|
1591
|
+
facts: [...section.facts]
|
|
1592
|
+
}])
|
|
1577
1593
|
);
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
},
|
|
1596
|
-
async (acquired, lock) => {
|
|
1597
|
-
if (!acquired) throw new Error("Timed out waiting for entity mutation lock.");
|
|
1598
|
-
return task(() => lock.refresh());
|
|
1594
|
+
for (const section of options.structuredSections ?? []) {
|
|
1595
|
+
const normalizedSection = normalizeEntityStructuredSection(type, section, this.deps.entitySchemas);
|
|
1596
|
+
const normalizedFacts = normalizeStructuredSectionFacts(section.facts);
|
|
1597
|
+
if (normalizedFacts.length === 0) continue;
|
|
1598
|
+
const existingSection = structuredSectionMap.get(normalizedSection.key);
|
|
1599
|
+
if (!existingSection) {
|
|
1600
|
+
structuredSectionMap.set(normalizedSection.key, {
|
|
1601
|
+
key: normalizedSection.key,
|
|
1602
|
+
title: normalizedSection.title,
|
|
1603
|
+
facts: normalizedFacts
|
|
1604
|
+
});
|
|
1605
|
+
continue;
|
|
1606
|
+
}
|
|
1607
|
+
existingSection.facts = normalizeStructuredSectionFacts([...existingSection.facts, ...normalizedFacts]);
|
|
1608
|
+
if (!existingSection.title.trim() && normalizedSection.title.trim()) {
|
|
1609
|
+
existingSection.title = normalizedSection.title;
|
|
1610
|
+
}
|
|
1599
1611
|
}
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
parsed = JSON.parse(raw);
|
|
1614
|
-
} catch {
|
|
1615
|
-
throw new Error("Invalid entity canonical-id migration state.");
|
|
1616
|
-
}
|
|
1617
|
-
const result = EntityCanonicalIdMigrationStateSchema.safeParse(parsed);
|
|
1618
|
-
if (!result.success) throw new Error("Invalid entity canonical-id migration state.");
|
|
1619
|
-
return result.data;
|
|
1620
|
-
}
|
|
1621
|
-
async function writeState(deps, state) {
|
|
1622
|
-
const destination = statePath(deps);
|
|
1623
|
-
const temporary = `${destination}.${process.pid}.${randomUUID()}.tmp`;
|
|
1624
|
-
try {
|
|
1625
|
-
await writeFile(temporary, `${JSON.stringify(state, null, 2)}
|
|
1626
|
-
`, "utf-8");
|
|
1627
|
-
await rename(temporary, destination);
|
|
1628
|
-
} finally {
|
|
1629
|
-
await unlink(temporary).catch(() => void 0);
|
|
1630
|
-
}
|
|
1631
|
-
}
|
|
1632
|
-
async function fileExists(filePath) {
|
|
1633
|
-
try {
|
|
1634
|
-
await access(filePath);
|
|
1635
|
-
return true;
|
|
1636
|
-
} catch (error) {
|
|
1637
|
-
if (isErrnoCode(error, "ENOENT")) return false;
|
|
1638
|
-
throw error;
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
async function sameFileIdentity(leftPath, rightPath) {
|
|
1642
|
-
try {
|
|
1643
|
-
const [left, right] = await Promise.all([statFile(leftPath), statFile(rightPath)]);
|
|
1644
|
-
return left.dev === right.dev && left.ino === right.ino;
|
|
1645
|
-
} catch (error) {
|
|
1646
|
-
if (isErrnoCode(error, "ENOENT")) return false;
|
|
1647
|
-
throw error;
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
|
-
function assertSafeEntityId(entityId) {
|
|
1651
|
-
if (/[\/\\\0]/.test(entityId)) {
|
|
1652
|
-
throw new Error(`Refusing entity canonical-id migration through unsafe entity id: ${entityId}.`);
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1655
|
-
async function assertNotSymlink(filePath, label) {
|
|
1656
|
-
let stat3;
|
|
1657
|
-
try {
|
|
1658
|
-
stat3 = await lstat2(filePath);
|
|
1659
|
-
} catch (error) {
|
|
1660
|
-
if (isErrnoCode(error, "ENOENT")) return;
|
|
1661
|
-
throw error;
|
|
1662
|
-
}
|
|
1663
|
-
if (stat3.isSymbolicLink()) {
|
|
1664
|
-
throw new Error(`Refusing entity canonical-id migration through symlink: ${label}.`);
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
async function assertNotMemorySymlink(filePath) {
|
|
1668
|
-
let stat3;
|
|
1669
|
-
try {
|
|
1670
|
-
stat3 = await lstat2(filePath);
|
|
1671
|
-
} catch (error) {
|
|
1672
|
-
if (isErrnoCode(error, "ENOENT")) return;
|
|
1673
|
-
throw error;
|
|
1674
|
-
}
|
|
1675
|
-
if (stat3.isSymbolicLink()) {
|
|
1676
|
-
throw new Error(`Refusing entity canonical-id migration through symlinked memory: ${filePath}.`);
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
async function assertEntityContentUnchanged(deps, filePath, expectedContent, legacyId) {
|
|
1680
|
-
const latestContent = await deps.readStorageSecureFile(filePath);
|
|
1681
|
-
if (latestContent !== expectedContent) {
|
|
1682
|
-
throw new Error(`Legacy entity id ${legacyId} changed while migration was running; retry migration.`);
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
async function readSafeEntityEntries(entitiesDir) {
|
|
1686
|
-
const rootStat = await lstat2(entitiesDir);
|
|
1687
|
-
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
1688
|
-
throw new Error("Refusing entity canonical-id migration through a symlinked or non-directory entities root.");
|
|
1689
|
-
}
|
|
1690
|
-
const entries = await readdir2(entitiesDir);
|
|
1691
|
-
for (const entry of entries) {
|
|
1692
|
-
await assertNotSymlink(path3.join(entitiesDir, entry), entry);
|
|
1693
|
-
}
|
|
1694
|
-
return entries;
|
|
1695
|
-
}
|
|
1696
|
-
async function discoverMappings(deps, knownMappings = {}, blocked = /* @__PURE__ */ new Map()) {
|
|
1697
|
-
const mappings = {};
|
|
1698
|
-
const canonicalOwners = /* @__PURE__ */ new Map();
|
|
1699
|
-
blocked.clear();
|
|
1700
|
-
const entityEntries = await readSafeEntityEntries(deps.entitiesDir);
|
|
1701
|
-
for (const entry of entityEntries) {
|
|
1702
|
-
if (!entry.endsWith(".md")) continue;
|
|
1703
|
-
const legacyId = entry.slice(0, -".md".length);
|
|
1704
|
-
assertSafeEntityId(legacyId);
|
|
1705
|
-
const legacyPath = deps.resolveEntityFilePath(legacyId);
|
|
1706
|
-
if (legacyPath === null) continue;
|
|
1707
|
-
const entityContent = await deps.readStorageSecureFile(legacyPath);
|
|
1708
|
-
if (!/^#[ \t]+[^\r\n \t]/m.test(entityContent) || !/^\*\*Type:\*\*[ \t]*[^\r\n \t]/m.test(entityContent)) {
|
|
1709
|
-
throw new Error(`Cannot migrate malformed entity file ${entry}: missing name heading or type.`);
|
|
1612
|
+
for (const fact of safeFacts) {
|
|
1613
|
+
const nextEntry = {
|
|
1614
|
+
timestamp,
|
|
1615
|
+
text: fact,
|
|
1616
|
+
...source ? { source } : {},
|
|
1617
|
+
...sessionKey ? { sessionKey } : {},
|
|
1618
|
+
...principal ? { principal } : {}
|
|
1619
|
+
};
|
|
1620
|
+
const alreadyPresent = entity.timeline.some(
|
|
1621
|
+
(entry) => entry.timestamp === nextEntry.timestamp && entry.text === nextEntry.text && entry.source === nextEntry.source && entry.sessionKey === nextEntry.sessionKey && entry.principal === nextEntry.principal
|
|
1622
|
+
);
|
|
1623
|
+
if (alreadyPresent) continue;
|
|
1624
|
+
entity.timeline.push(nextEntry);
|
|
1710
1625
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1626
|
+
entity.structuredSections = sortStructuredSectionsBySchema(
|
|
1627
|
+
type,
|
|
1628
|
+
Array.from(structuredSectionMap.values()).filter((section) => section.facts.length > 0),
|
|
1629
|
+
this.deps.entitySchemas
|
|
1630
|
+
);
|
|
1631
|
+
entity.facts = compileEntityFacts(entity.timeline, entity.structuredSections);
|
|
1632
|
+
entity.summary = entity.synthesis || entity.summary;
|
|
1633
|
+
entity.name = name;
|
|
1634
|
+
entity.type = type;
|
|
1635
|
+
entity.created = entity.created || timestamp;
|
|
1636
|
+
entity.updated = (/* @__PURE__ */ new Date()).toISOString();
|
|
1637
|
+
await this.deps.snapshotBeforeWrite(filePath, "write");
|
|
1638
|
+
await this.deps.writeStorageSecureFile(filePath, serializeEntityFile(entity, this.deps.entitySchemas));
|
|
1639
|
+
this.deps.invalidateKnowledgeIndexCache();
|
|
1640
|
+
this.deps.bumpMemoryStatusVersion();
|
|
1641
|
+
this.deps.bumpEntityMutationVersion();
|
|
1642
|
+
log.debug(`wrote entity ${normalized}`);
|
|
1643
|
+
return normalized;
|
|
1644
|
+
}
|
|
1645
|
+
async readEntity(name) {
|
|
1646
|
+
const filePath = this.deps.resolveEntityFilePath(name);
|
|
1647
|
+
if (filePath === null) {
|
|
1648
|
+
return "";
|
|
1714
1649
|
}
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
if (canonicalExists && await sameFileIdentity(legacyPath, canonicalPath) && knownMappings[legacyId] === canonicalId) continue;
|
|
1722
|
-
if (canonicalExists) {
|
|
1723
|
-
const canonicalContent2 = await deps.readStorageSecureFile(canonicalPath);
|
|
1724
|
-
if (entityContent !== canonicalContent2) {
|
|
1725
|
-
blocked.set(legacyId, canonicalId);
|
|
1726
|
-
continue;
|
|
1727
|
-
}
|
|
1650
|
+
try {
|
|
1651
|
+
return await this.deps.readStorageSecureFile(filePath);
|
|
1652
|
+
} catch (err) {
|
|
1653
|
+
if (err instanceof SecureStoreLockedError) throw err;
|
|
1654
|
+
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
1655
|
+
return "";
|
|
1728
1656
|
}
|
|
1729
|
-
canonicalOwners.set(canonicalId, [...canonicalOwners.get(canonicalId) ?? [], legacyId]);
|
|
1730
|
-
mappings[legacyId] = canonicalId;
|
|
1731
1657
|
}
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1658
|
+
/** Return sorted list of entity filenames (without .md extension) */
|
|
1659
|
+
async listEntityNames() {
|
|
1660
|
+
try {
|
|
1661
|
+
const entries = await readdir2(this.deps.entitiesDir);
|
|
1662
|
+
return entries.filter((e) => e.endsWith(".md")).map((e) => e.replace(".md", "")).sort();
|
|
1663
|
+
} catch {
|
|
1664
|
+
return [];
|
|
1737
1665
|
}
|
|
1738
1666
|
}
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
const previousEntityRef = memory.frontmatter.entityRef;
|
|
1758
|
-
const canonicalId = previousEntityRef ? mappings[previousEntityRef] : void 0;
|
|
1759
|
-
if (!previousEntityRef || !canonicalId) continue;
|
|
1760
|
-
await assertNotMemorySymlink(memory.path);
|
|
1761
|
-
const observedMemoryFingerprint = await fingerprintPath(memory.path);
|
|
1762
|
-
if (observedMemoryFingerprint === "missing") continue;
|
|
1763
|
-
if (!await refreshLock()) throw new Error("Lost entity canonical-id migration lock.");
|
|
1764
|
-
const latestMemory = await deps.readMemoryByPath(memory.path);
|
|
1765
|
-
if (!latestMemory) continue;
|
|
1766
|
-
const latestMemoryFingerprint = await fingerprintPath(latestMemory.path);
|
|
1767
|
-
if (latestMemoryFingerprint !== observedMemoryFingerprint) continue;
|
|
1768
|
-
const latestPreviousEntityRef = latestMemory.frontmatter.entityRef;
|
|
1769
|
-
const latestCanonicalId = latestPreviousEntityRef ? mappings[latestPreviousEntityRef] : void 0;
|
|
1770
|
-
if (!latestPreviousEntityRef || !latestCanonicalId) continue;
|
|
1771
|
-
const memoryEncrypted = await deps.isEncryptedStorageFile(latestMemory.path);
|
|
1772
|
-
if (await fingerprintPath(latestMemory.path) !== latestMemoryFingerprint) continue;
|
|
1773
|
-
await deps.snapshotBeforeWrite(latestMemory.path, "write");
|
|
1774
|
-
if (await fingerprintPath(latestMemory.path) !== latestMemoryFingerprint) continue;
|
|
1775
|
-
await deps.writeStorageSecureFile(
|
|
1776
|
-
latestMemory.path,
|
|
1777
|
-
deps.serializeMemoryWithEntityRef(latestMemory, latestCanonicalId),
|
|
1778
|
-
memoryEncrypted
|
|
1779
|
-
);
|
|
1780
|
-
await deps.rewriteProjectedMemoryEntityReference(
|
|
1781
|
-
latestMemory.frontmatter.id,
|
|
1782
|
-
latestPreviousEntityRef,
|
|
1783
|
-
latestCanonicalId
|
|
1784
|
-
);
|
|
1785
|
-
rewroteAny = true;
|
|
1786
|
-
rewroteColdMemory ||= latestMemory.path.includes(`${path3.sep}cold${path3.sep}`);
|
|
1667
|
+
/** Re-serialize every entity file into the compiled-truth timeline shape. */
|
|
1668
|
+
async migrateEntityFilesToCompiledTruthTimeline() {
|
|
1669
|
+
const entityNames = await this.listEntityNames();
|
|
1670
|
+
const stateDir = path3.join(this.deps.baseDir, "state");
|
|
1671
|
+
let migrated = 0;
|
|
1672
|
+
for (const entityName of entityNames) {
|
|
1673
|
+
const wrote = await withEntityCanonicalMutationLock(stateDir, async () => {
|
|
1674
|
+
const raw = await this.readEntity(entityName);
|
|
1675
|
+
if (!raw) return false;
|
|
1676
|
+
const serialized = serializeEntityFile(parseEntityFile(raw, this.deps.entitySchemas), this.deps.entitySchemas);
|
|
1677
|
+
if (raw.trimEnd() === serialized.trimEnd()) return false;
|
|
1678
|
+
const filePath = this.deps.resolveEntityFilePath(entityName);
|
|
1679
|
+
if (filePath === null) return false;
|
|
1680
|
+
await this.deps.writeStorageSecureFile(filePath, serialized);
|
|
1681
|
+
this.deps.bumpEntityMutationVersion();
|
|
1682
|
+
return true;
|
|
1683
|
+
});
|
|
1684
|
+
if (wrote) migrated += 1;
|
|
1787
1685
|
}
|
|
1788
|
-
if (
|
|
1686
|
+
if (migrated > 0) {
|
|
1687
|
+
this.deps.invalidateKnowledgeIndexCache();
|
|
1688
|
+
this.deps.bumpMemoryStatusVersion();
|
|
1689
|
+
}
|
|
1690
|
+
return { total: entityNames.length, migrated };
|
|
1789
1691
|
}
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1692
|
+
/**
|
|
1693
|
+
* Set or rewrite the synthesis layer of an entity file.
|
|
1694
|
+
*/
|
|
1695
|
+
async updateEntitySynthesis(name, synthesis, options = {}) {
|
|
1696
|
+
return withEntityCanonicalMutationLock(
|
|
1697
|
+
path3.join(this.deps.baseDir, "state"),
|
|
1698
|
+
() => this.updateEntitySynthesisUnlocked(name, synthesis, options)
|
|
1699
|
+
);
|
|
1798
1700
|
}
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1701
|
+
async updateEntitySynthesisUnlocked(name, synthesis, options = {}) {
|
|
1702
|
+
const filePath = path3.join(this.deps.entitiesDir, `${name}.md`);
|
|
1703
|
+
let entity;
|
|
1704
|
+
try {
|
|
1705
|
+
const content = await this.deps.readStorageSecureFile(filePath);
|
|
1706
|
+
entity = parseEntityFile(content, this.deps.entitySchemas);
|
|
1707
|
+
} catch (err) {
|
|
1708
|
+
if (err instanceof SecureStoreLockedError) throw err;
|
|
1709
|
+
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
1710
|
+
log.debug(`updateEntitySynthesis: entity file ${name}.md not found`);
|
|
1711
|
+
return;
|
|
1807
1712
|
}
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
const
|
|
1811
|
-
|
|
1812
|
-
const
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1713
|
+
const updatedAt = options.updatedAt?.trim() || entity.synthesisUpdatedAt?.trim() || void 0;
|
|
1714
|
+
const entityUpdatedAt = options.entityUpdatedAt?.trim() || updatedAt || entity.updated || (/* @__PURE__ */ new Date()).toISOString();
|
|
1715
|
+
const synthesisTimelineCount = Number.isInteger(options.synthesisTimelineCount) && (options.synthesisTimelineCount ?? 0) >= 0 ? options.synthesisTimelineCount : void 0;
|
|
1716
|
+
const synthesisStructuredFactCount = Number.isInteger(options.synthesisStructuredFactCount) && (options.synthesisStructuredFactCount ?? 0) >= 0 ? options.synthesisStructuredFactCount : countEntityStructuredFacts(entity);
|
|
1717
|
+
const synthesisStructuredFactDigest = options.synthesisStructuredFactDigest?.trim() || fingerprintEntityStructuredFacts(entity);
|
|
1718
|
+
entity.synthesis = synthesis.trim();
|
|
1719
|
+
entity.summary = entity.synthesis;
|
|
1720
|
+
entity.synthesisUpdatedAt = updatedAt;
|
|
1721
|
+
entity.synthesisTimelineCount = synthesisTimelineCount;
|
|
1722
|
+
entity.synthesisStructuredFactCount = synthesisStructuredFactCount;
|
|
1723
|
+
entity.synthesisStructuredFactDigest = synthesisStructuredFactDigest;
|
|
1724
|
+
entity.synthesisVersion = Math.max(0, entity.synthesisVersion ?? 0) + (options.incrementVersion === false ? 0 : 1);
|
|
1725
|
+
entity.updated = entityUpdatedAt;
|
|
1726
|
+
await this.deps.writeStorageSecureFile(filePath, serializeEntityFile(entity, this.deps.entitySchemas));
|
|
1727
|
+
await this.deps.removeEntitySynthesisQueueEntries([
|
|
1728
|
+
.../* @__PURE__ */ new Set([name, this.deps.normalizeEntityName(entity.name, entity.type)])
|
|
1729
|
+
]);
|
|
1730
|
+
this.deps.invalidateKnowledgeIndexCache();
|
|
1731
|
+
this.deps.bumpMemoryStatusVersion();
|
|
1732
|
+
this.deps.bumpEntityMutationVersion();
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* Read all entity files and return lightweight EntityFile objects.
|
|
1736
|
+
* Parsing is fast (~50-100ms for ~1,800 files) since entity files are small.
|
|
1737
|
+
*/
|
|
1738
|
+
async readAllEntityFiles() {
|
|
1739
|
+
const currentVersion = this.deps.getMemoryStatusVersion();
|
|
1740
|
+
const schemaCacheKey = buildEntitySchemaCacheKey(this.deps.entitySchemas);
|
|
1741
|
+
const cacheKey = `${this.deps.getEntityCacheSecureStoreKey()}\0${schemaCacheKey}`;
|
|
1742
|
+
const cached = getCachedEntities(this.deps.baseDir, currentVersion, cacheKey);
|
|
1743
|
+
if (cached) return cached;
|
|
1744
|
+
try {
|
|
1745
|
+
const entries = await readdir2(this.deps.entitiesDir);
|
|
1746
|
+
const mdFiles = entries.filter((e) => e.endsWith(".md"));
|
|
1747
|
+
if (mdFiles.length === 0) return [];
|
|
1748
|
+
const BATCH_SIZE = 100;
|
|
1749
|
+
const entities = [];
|
|
1750
|
+
for (let i = 0; i < mdFiles.length; i += BATCH_SIZE) {
|
|
1751
|
+
const batch = mdFiles.slice(i, i + BATCH_SIZE);
|
|
1752
|
+
const results = await Promise.all(
|
|
1753
|
+
batch.map(async (entry) => {
|
|
1754
|
+
try {
|
|
1755
|
+
return await this.deps.readStorageSecureFile(path3.join(this.deps.entitiesDir, entry));
|
|
1756
|
+
} catch (err) {
|
|
1757
|
+
if (err instanceof SecureStoreLockedError) throw err;
|
|
1758
|
+
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
1759
|
+
return null;
|
|
1760
|
+
}
|
|
1761
|
+
})
|
|
1762
|
+
);
|
|
1763
|
+
for (const content of results) {
|
|
1764
|
+
if (content !== null) entities.push(parseEntityFile(content, this.deps.entitySchemas));
|
|
1765
|
+
}
|
|
1819
1766
|
}
|
|
1767
|
+
setCachedEntities(this.deps.baseDir, entities, currentVersion, cacheKey);
|
|
1768
|
+
return entities;
|
|
1769
|
+
} catch (err) {
|
|
1770
|
+
if (err instanceof SecureStoreLockedError || !isErrnoCode(err, "ENOENT")) throw err;
|
|
1771
|
+
return [];
|
|
1820
1772
|
}
|
|
1821
1773
|
}
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
normalizedMappings.set(normalizedLegacyId, canonicalId);
|
|
1774
|
+
/**
|
|
1775
|
+
* Build the Knowledge Index: a compact markdown table of top-scored entities.
|
|
1776
|
+
* Respects maxEntities and maxChars limits from config.
|
|
1777
|
+
*/
|
|
1778
|
+
async buildKnowledgeIndex(config, overrides) {
|
|
1779
|
+
const useDefaultLimits = overrides?.maxEntities === void 0 && overrides?.maxChars === void 0;
|
|
1780
|
+
if (useDefaultLimits && this.deps.knowledgeIndexCache && Date.now() - this.deps.knowledgeIndexCache.builtAt < this.deps.storageManagerClass.KNOWLEDGE_INDEX_CACHE_TTL_MS) {
|
|
1781
|
+
return { result: this.deps.knowledgeIndexCache.result, cached: true };
|
|
1831
1782
|
}
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
staleMs: TOMBSTONE_LOCK_STALE_MS,
|
|
1837
|
-
maxWaitMs: TOMBSTONE_LOCK_MAX_WAIT_MS
|
|
1838
|
-
},
|
|
1839
|
-
async (acquired, tombstoneLock) => {
|
|
1840
|
-
if (!acquired) throw new Error("Timed out waiting for active tombstone migration.");
|
|
1841
|
-
if (!await assertSafeTombstoneFile(deps, tombstonePath)) return;
|
|
1842
|
-
const tombstoneEncrypted = await deps.isEncryptedStorageFile(tombstonePath);
|
|
1843
|
-
const original = await deps.readStorageSecureFile(tombstonePath);
|
|
1844
|
-
let changed = false;
|
|
1845
|
-
const rewritten = original.split("\n").map((line) => {
|
|
1846
|
-
const next = rewriteTombstoneLine(line, mappings, normalizedMappings);
|
|
1847
|
-
changed ||= next !== line;
|
|
1848
|
-
return next;
|
|
1849
|
-
}).join("\n");
|
|
1850
|
-
if (!changed) return;
|
|
1851
|
-
if (!await refreshLock()) throw new Error("Lost entity canonical-id migration lock.");
|
|
1852
|
-
if (!await tombstoneLock.refresh()) throw new Error("Lost tombstone migration lock.");
|
|
1853
|
-
await deps.snapshotBeforeWrite(tombstonePath, "write");
|
|
1854
|
-
await deps.writeStorageSecureFile(tombstonePath, rewritten, tombstoneEncrypted);
|
|
1783
|
+
const entities = await this.deps.readAllEntityFiles();
|
|
1784
|
+
if (entities.length === 0) {
|
|
1785
|
+
if (useDefaultLimits) this.deps.knowledgeIndexCache = { result: "", builtAt: Date.now() };
|
|
1786
|
+
return { result: "", cached: false };
|
|
1855
1787
|
}
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
const
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
return
|
|
1871
|
-
});
|
|
1872
|
-
if (relationships.every((relationship, index) => relationship === entity.relationships[index])) continue;
|
|
1873
|
-
const rewroteEntity = await withEntityCanonicalMutationLock(
|
|
1874
|
-
deps.stateDir,
|
|
1875
|
-
async (refreshEntityLock) => {
|
|
1876
|
-
if (!await refreshLock()) throw new Error("Lost entity canonical-id migration lock.");
|
|
1877
|
-
const latestEntity = deps.parseEntityFile(await deps.readStorageSecureFile(entityPath));
|
|
1878
|
-
if (await fingerprintPath(entityPath) !== observedEntityFingerprint) return false;
|
|
1879
|
-
const latestRelationships = latestEntity.relationships.map((relationship) => {
|
|
1880
|
-
const target = mappings[relationship.target];
|
|
1881
|
-
return target ? { ...relationship, target } : relationship;
|
|
1882
|
-
});
|
|
1883
|
-
if (latestRelationships.every((relationship, index) => relationship === latestEntity.relationships[index])) {
|
|
1884
|
-
return false;
|
|
1885
|
-
}
|
|
1886
|
-
const entityEncrypted = await deps.isEncryptedStorageFile(entityPath);
|
|
1887
|
-
if (!await refreshEntityLock()) throw new Error("Lost entity mutation lock.");
|
|
1888
|
-
if (await fingerprintPath(entityPath) !== observedEntityFingerprint) return false;
|
|
1889
|
-
await deps.snapshotBeforeWrite(entityPath, "write");
|
|
1890
|
-
if (await fingerprintPath(entityPath) !== observedEntityFingerprint) return false;
|
|
1891
|
-
await deps.writeStorageSecureFile(
|
|
1892
|
-
entityPath,
|
|
1893
|
-
deps.serializeEntityFile({ ...latestEntity, relationships: latestRelationships }),
|
|
1894
|
-
entityEncrypted
|
|
1895
|
-
);
|
|
1896
|
-
return true;
|
|
1897
|
-
}
|
|
1898
|
-
);
|
|
1899
|
-
rewritten ||= rewroteEntity;
|
|
1900
|
-
}
|
|
1901
|
-
return rewritten;
|
|
1902
|
-
}
|
|
1903
|
-
async function rewriteKnownReferences(deps, mappings, refreshLock) {
|
|
1904
|
-
await rewriteRelationshipTargets(deps, mappings, refreshLock);
|
|
1905
|
-
let rewroteColdMemory = (await rewriteReferences(deps, mappings, refreshLock)).rewroteColdMemory;
|
|
1906
|
-
await rewriteTombstoneReferences(deps, mappings, refreshLock);
|
|
1907
|
-
await deps.rewriteProjectedEntityReferences(mappings);
|
|
1908
|
-
const finalMemoryPass = await rewriteReferences(deps, mappings, refreshLock);
|
|
1909
|
-
rewroteColdMemory ||= finalMemoryPass.rewroteColdMemory;
|
|
1910
|
-
return rewroteColdMemory;
|
|
1911
|
-
}
|
|
1912
|
-
function wouldCreateMappingCycle(mappings, legacyId, canonicalId) {
|
|
1913
|
-
let current = canonicalId;
|
|
1914
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1915
|
-
while (true) {
|
|
1916
|
-
if (current === legacyId) return true;
|
|
1917
|
-
if (seen.has(current)) return false;
|
|
1918
|
-
seen.add(current);
|
|
1919
|
-
const next = mappings[current];
|
|
1920
|
-
if (!next || next === current) return false;
|
|
1921
|
-
current = next;
|
|
1922
|
-
}
|
|
1923
|
-
}
|
|
1924
|
-
function rejectMappingCycles(existing, discovered) {
|
|
1925
|
-
const accepted = {};
|
|
1926
|
-
const merged = { ...existing };
|
|
1927
|
-
for (const [legacyId, canonicalId] of Object.entries(discovered)) {
|
|
1928
|
-
if (merged[legacyId] === void 0 && wouldCreateMappingCycle(merged, legacyId, canonicalId)) continue;
|
|
1929
|
-
accepted[legacyId] = canonicalId;
|
|
1930
|
-
merged[legacyId] = canonicalId;
|
|
1931
|
-
}
|
|
1932
|
-
return accepted;
|
|
1933
|
-
}
|
|
1934
|
-
function mergeDiscoveredMappings(existing, discovered) {
|
|
1935
|
-
const merged = { ...existing };
|
|
1936
|
-
const canonicalOwners = /* @__PURE__ */ new Map();
|
|
1937
|
-
for (const [legacyId, canonicalId] of Object.entries(discovered)) {
|
|
1938
|
-
const previousMapping = merged[legacyId];
|
|
1939
|
-
if (previousMapping !== void 0 && previousMapping !== canonicalId) {
|
|
1940
|
-
let hop = canonicalId;
|
|
1941
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1942
|
-
while (hop !== void 0 && hop !== previousMapping && !seen.has(hop)) {
|
|
1943
|
-
seen.add(hop);
|
|
1944
|
-
hop = merged[hop];
|
|
1945
|
-
}
|
|
1946
|
-
if (hop !== previousMapping) {
|
|
1947
|
-
throw new Error(`Legacy entity id ${legacyId} changed canonical target during migration.`);
|
|
1948
|
-
}
|
|
1949
|
-
continue;
|
|
1788
|
+
const now = /* @__PURE__ */ new Date();
|
|
1789
|
+
const scored = entities.map((e) => ({
|
|
1790
|
+
name: e.name,
|
|
1791
|
+
type: e.type,
|
|
1792
|
+
score: this.deps.storageManagerClass.scoreEntity(e, now),
|
|
1793
|
+
factCount: e.facts.length,
|
|
1794
|
+
summary: e.synthesis ?? e.summary,
|
|
1795
|
+
topRelationships: e.relationships.slice(0, 3).map((r) => r.target)
|
|
1796
|
+
}));
|
|
1797
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1798
|
+
const maxEntities = typeof overrides?.maxEntities === "number" ? Math.max(0, Math.floor(overrides.maxEntities)) : config.knowledgeIndexMaxEntities;
|
|
1799
|
+
const topN = scored.slice(0, maxEntities);
|
|
1800
|
+
if (topN.length === 0) {
|
|
1801
|
+
if (useDefaultLimits) this.deps.knowledgeIndexCache = { result: "", builtAt: Date.now() };
|
|
1802
|
+
return { result: "", cached: false };
|
|
1950
1803
|
}
|
|
1951
|
-
|
|
1952
|
-
const
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1804
|
+
const header = "## Knowledge Index\n\n| Entity | Type | Summary | Connected to |\n|--------|------|---------|-------------|";
|
|
1805
|
+
const rows = [];
|
|
1806
|
+
let totalChars = header.length;
|
|
1807
|
+
const maxChars = typeof overrides?.maxChars === "number" ? Math.max(0, Math.floor(overrides.maxChars)) : config.knowledgeIndexMaxChars;
|
|
1808
|
+
for (const entity of topN) {
|
|
1809
|
+
const summary = entity.summary || `${entity.factCount} facts`;
|
|
1810
|
+
const connected = entity.topRelationships.length > 0 ? entity.topRelationships.join(", ") : "\u2014";
|
|
1811
|
+
const row = `| ${entity.name} | ${entity.type} | ${summary} | ${connected} |`;
|
|
1812
|
+
if (totalChars + row.length + 1 > maxChars) break;
|
|
1813
|
+
rows.push(row);
|
|
1814
|
+
totalChars += row.length + 1;
|
|
1957
1815
|
}
|
|
1958
|
-
|
|
1959
|
-
|
|
1816
|
+
const result = rows.length === 0 ? "" : `${header}
|
|
1817
|
+
${rows.join("\n")}
|
|
1818
|
+
`;
|
|
1819
|
+
if (useDefaultLimits) this.deps.knowledgeIndexCache = { result, builtAt: Date.now() };
|
|
1820
|
+
return { result, cached: false };
|
|
1960
1821
|
}
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
if (seen.has(current)) {
|
|
1972
|
-
throw new Error(`Entity canonical-id migration mapping cycle includes ${legacyId}.`);
|
|
1973
|
-
}
|
|
1974
|
-
seen.add(current);
|
|
1975
|
-
current = next;
|
|
1976
|
-
}
|
|
1977
|
-
if (current !== legacyId) collapsed[legacyId] = current;
|
|
1822
|
+
/**
|
|
1823
|
+
* Merge fragmented entity files that resolve to the same canonical name.
|
|
1824
|
+
* Preserves relationships, activity, aliases, and summary from all fragments.
|
|
1825
|
+
* Returns count of files merged.
|
|
1826
|
+
*/
|
|
1827
|
+
async mergeFragmentedEntities() {
|
|
1828
|
+
return withEntityCanonicalMutationLock(
|
|
1829
|
+
path3.join(this.deps.baseDir, "state"),
|
|
1830
|
+
() => this.mergeFragmentedEntitiesUnlocked()
|
|
1831
|
+
);
|
|
1978
1832
|
}
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
if (!legacyExists && !canonicalExists) return { deferred: true, progressed: false };
|
|
1995
|
-
if (legacyExists && canonicalExists) {
|
|
1996
|
-
if (await sameFileIdentity(legacyPath, canonicalPath)) return { deferred: false, progressed: false };
|
|
1997
|
-
const [legacyContent, canonicalContent2, legacyEncrypted, canonicalEncrypted] = await Promise.all([
|
|
1998
|
-
deps.readStorageSecureFile(legacyPath),
|
|
1999
|
-
deps.readStorageSecureFile(canonicalPath),
|
|
2000
|
-
deps.isEncryptedStorageFile(legacyPath),
|
|
2001
|
-
deps.isEncryptedStorageFile(canonicalPath)
|
|
2002
|
-
]);
|
|
2003
|
-
if (legacyContent !== canonicalContent2) {
|
|
2004
|
-
return { deferred: false, progressed: false, blocked: true };
|
|
1833
|
+
async mergeFragmentedEntitiesUnlocked() {
|
|
1834
|
+
let merged = 0;
|
|
1835
|
+
try {
|
|
1836
|
+
const entries = await readdir2(this.deps.entitiesDir);
|
|
1837
|
+
const mdFiles = entries.filter((e) => e.endsWith(".md"));
|
|
1838
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1839
|
+
for (const file of mdFiles) {
|
|
1840
|
+
const baseName = file.replace(".md", "");
|
|
1841
|
+
const dashIdx = baseName.indexOf("-");
|
|
1842
|
+
if (dashIdx === -1) continue;
|
|
1843
|
+
const type = baseName.slice(0, dashIdx);
|
|
1844
|
+
const restOfName = baseName.slice(dashIdx + 1);
|
|
1845
|
+
const canonical = this.deps.normalizeEntityName(restOfName, type);
|
|
1846
|
+
if (!groups.has(canonical)) groups.set(canonical, []);
|
|
1847
|
+
groups.get(canonical).push(file);
|
|
2005
1848
|
}
|
|
2006
|
-
|
|
2007
|
-
if (
|
|
2008
|
-
|
|
1849
|
+
for (const [canonical, files] of groups) {
|
|
1850
|
+
if (files.length <= 1) continue;
|
|
1851
|
+
const mergedEntity = {
|
|
1852
|
+
name: "",
|
|
1853
|
+
type: "other",
|
|
1854
|
+
created: "",
|
|
1855
|
+
updated: "",
|
|
1856
|
+
extraFrontmatterLines: [],
|
|
1857
|
+
preSectionLines: [],
|
|
1858
|
+
facts: [],
|
|
1859
|
+
summary: void 0,
|
|
1860
|
+
synthesis: void 0,
|
|
1861
|
+
synthesisUpdatedAt: void 0,
|
|
1862
|
+
synthesisTimelineCount: void 0,
|
|
1863
|
+
synthesisStructuredFactCount: void 0,
|
|
1864
|
+
synthesisStructuredFactDigest: void 0,
|
|
1865
|
+
synthesisVersion: void 0,
|
|
1866
|
+
timeline: [],
|
|
1867
|
+
relationships: [],
|
|
1868
|
+
activity: [],
|
|
1869
|
+
aliases: [],
|
|
1870
|
+
structuredSections: [],
|
|
1871
|
+
extraSections: []
|
|
1872
|
+
};
|
|
1873
|
+
for (const file of files) {
|
|
1874
|
+
const filePath = path3.join(this.deps.entitiesDir, file);
|
|
1875
|
+
try {
|
|
1876
|
+
const content = await this.deps.readStorageSecureFile(filePath);
|
|
1877
|
+
const parsed = parseEntityFile(content, this.deps.entitySchemas);
|
|
1878
|
+
if (!mergedEntity.type || mergedEntity.type === "other") {
|
|
1879
|
+
mergedEntity.type = parsed.type;
|
|
1880
|
+
}
|
|
1881
|
+
if (!mergedEntity.updated || compareEntityTimestamps(parsed.updated, mergedEntity.updated) > 0) {
|
|
1882
|
+
mergedEntity.updated = parsed.updated;
|
|
1883
|
+
}
|
|
1884
|
+
const parsedCreated = parsed.created || parsed.updated;
|
|
1885
|
+
const mergedCreated = mergedEntity.created?.trim() || "";
|
|
1886
|
+
const parsedCreatedMs = parsedCreated ? Date.parse(parsedCreated) : Number.NaN;
|
|
1887
|
+
const mergedCreatedMs = mergedCreated ? Date.parse(mergedCreated) : Number.NaN;
|
|
1888
|
+
const parsedCreatedIsValid = Number.isFinite(parsedCreatedMs);
|
|
1889
|
+
const mergedCreatedIsValid = Number.isFinite(mergedCreatedMs);
|
|
1890
|
+
if (parsedCreated && (!mergedCreated || parsedCreatedIsValid && !mergedCreatedIsValid || parsedCreatedIsValid && mergedCreatedIsValid && parsedCreatedMs < mergedCreatedMs || !parsedCreatedIsValid && !mergedCreatedIsValid && compareEntityTimestamps(parsedCreated, mergedCreated) < 0)) {
|
|
1891
|
+
mergedEntity.created = parsedCreated;
|
|
1892
|
+
}
|
|
1893
|
+
if (parsed.name.length > mergedEntity.name.length) {
|
|
1894
|
+
mergedEntity.name = parsed.name;
|
|
1895
|
+
}
|
|
1896
|
+
const parsedSynthesisUpdatedAt = parsed.synthesisUpdatedAt?.trim() || void 0;
|
|
1897
|
+
const mergedSynthesisUpdatedAt = mergedEntity.synthesisUpdatedAt?.trim() || void 0;
|
|
1898
|
+
if (parsed.synthesis && (!mergedEntity.synthesis || !mergedSynthesisUpdatedAt && Boolean(parsedSynthesisUpdatedAt) || Boolean(mergedSynthesisUpdatedAt) && Boolean(parsedSynthesisUpdatedAt) && compareEntityTimestamps(parsedSynthesisUpdatedAt, mergedSynthesisUpdatedAt) > 0)) {
|
|
1899
|
+
mergedEntity.synthesis = parsed.synthesis;
|
|
1900
|
+
mergedEntity.summary = parsed.synthesis;
|
|
1901
|
+
mergedEntity.synthesisUpdatedAt = parsedSynthesisUpdatedAt;
|
|
1902
|
+
mergedEntity.synthesisTimelineCount = parsed.synthesisTimelineCount;
|
|
1903
|
+
mergedEntity.synthesisStructuredFactCount = parsed.synthesisStructuredFactCount;
|
|
1904
|
+
mergedEntity.synthesisStructuredFactDigest = parsed.synthesisStructuredFactDigest;
|
|
1905
|
+
mergedEntity.synthesisVersion = parsed.synthesisVersion;
|
|
1906
|
+
} else if (!mergedEntity.summary && parsed.summary) {
|
|
1907
|
+
mergedEntity.summary = parsed.summary;
|
|
1908
|
+
mergedEntity.synthesis = parsed.summary;
|
|
1909
|
+
mergedEntity.synthesisUpdatedAt = parsedSynthesisUpdatedAt;
|
|
1910
|
+
mergedEntity.synthesisTimelineCount = parsed.synthesisTimelineCount;
|
|
1911
|
+
mergedEntity.synthesisStructuredFactCount = parsed.synthesisStructuredFactCount;
|
|
1912
|
+
mergedEntity.synthesisStructuredFactDigest = parsed.synthesisStructuredFactDigest;
|
|
1913
|
+
mergedEntity.synthesisVersion = parsed.synthesisVersion;
|
|
1914
|
+
}
|
|
1915
|
+
mergedEntity.timeline.push(...parsed.timeline);
|
|
1916
|
+
mergedEntity.relationships.push(...parsed.relationships);
|
|
1917
|
+
mergedEntity.activity.push(...parsed.activity);
|
|
1918
|
+
mergedEntity.aliases.push(...parsed.aliases);
|
|
1919
|
+
const mergedStructuredSectionMap = new Map(
|
|
1920
|
+
(mergedEntity.structuredSections ?? []).map((section) => [section.key, {
|
|
1921
|
+
...section,
|
|
1922
|
+
facts: [...section.facts]
|
|
1923
|
+
}])
|
|
1924
|
+
);
|
|
1925
|
+
for (const section of parsed.structuredSections ?? []) {
|
|
1926
|
+
const existingSection = mergedStructuredSectionMap.get(section.key);
|
|
1927
|
+
if (!existingSection) {
|
|
1928
|
+
mergedStructuredSectionMap.set(section.key, {
|
|
1929
|
+
key: section.key,
|
|
1930
|
+
title: section.title,
|
|
1931
|
+
facts: [...new Set(section.facts.map((fact) => fact.trim()).filter((fact) => fact.length > 0))]
|
|
1932
|
+
});
|
|
1933
|
+
continue;
|
|
1934
|
+
}
|
|
1935
|
+
const mergedFacts = new Set(existingSection.facts.map((fact) => fact.trim()));
|
|
1936
|
+
for (const fact of section.facts) {
|
|
1937
|
+
const trimmed = fact.trim();
|
|
1938
|
+
if (!trimmed) continue;
|
|
1939
|
+
mergedFacts.add(trimmed);
|
|
1940
|
+
}
|
|
1941
|
+
existingSection.facts = Array.from(mergedFacts);
|
|
1942
|
+
if (!existingSection.title.trim() && section.title.trim()) {
|
|
1943
|
+
existingSection.title = section.title;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
mergedEntity.structuredSections = Array.from(mergedStructuredSectionMap.values());
|
|
1947
|
+
mergedEntity.extraFrontmatterLines.push(...parsed.extraFrontmatterLines ?? []);
|
|
1948
|
+
mergedEntity.preSectionLines.push(...parsed.preSectionLines ?? []);
|
|
1949
|
+
mergedEntity.extraSections.push(...(parsed.extraSections ?? []).map((section) => ({
|
|
1950
|
+
title: section.title,
|
|
1951
|
+
lines: [...section.lines]
|
|
1952
|
+
})));
|
|
1953
|
+
} catch (err) {
|
|
1954
|
+
if (err instanceof SecureStoreLockedError) throw err;
|
|
1955
|
+
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
1956
|
+
}
|
|
2009
1957
|
}
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
1958
|
+
const timelineKeys = /* @__PURE__ */ new Set();
|
|
1959
|
+
mergedEntity.timeline = mergedEntity.timeline.filter((entry) => {
|
|
1960
|
+
const key = JSON.stringify([
|
|
1961
|
+
entry.timestamp,
|
|
1962
|
+
entry.source ?? "",
|
|
1963
|
+
entry.sessionKey ?? "",
|
|
1964
|
+
entry.principal ?? "",
|
|
1965
|
+
entry.text
|
|
1966
|
+
]);
|
|
1967
|
+
if (timelineKeys.has(key)) return false;
|
|
1968
|
+
timelineKeys.add(key);
|
|
1969
|
+
return true;
|
|
1970
|
+
});
|
|
1971
|
+
const relKeys = /* @__PURE__ */ new Set();
|
|
1972
|
+
mergedEntity.relationships = mergedEntity.relationships.filter((r) => {
|
|
1973
|
+
const key = `${r.target}::${r.label}`;
|
|
1974
|
+
if (relKeys.has(key)) return false;
|
|
1975
|
+
relKeys.add(key);
|
|
1976
|
+
return true;
|
|
1977
|
+
});
|
|
1978
|
+
const actKeys = /* @__PURE__ */ new Set();
|
|
1979
|
+
mergedEntity.activity = mergedEntity.activity.filter((a) => {
|
|
1980
|
+
const key = `${a.date}::${a.note}`;
|
|
1981
|
+
if (actKeys.has(key)) return false;
|
|
1982
|
+
actKeys.add(key);
|
|
1983
|
+
return true;
|
|
1984
|
+
}).sort((a, b) => b.date.localeCompare(a.date));
|
|
1985
|
+
mergedEntity.aliases = [...new Set(mergedEntity.aliases)];
|
|
1986
|
+
mergedEntity.structuredSections = sortStructuredSectionsBySchema(
|
|
1987
|
+
mergedEntity.type,
|
|
1988
|
+
mergedEntity.structuredSections ?? [],
|
|
1989
|
+
this.deps.entitySchemas
|
|
1990
|
+
);
|
|
1991
|
+
mergedEntity.facts = compileEntityFacts(mergedEntity.timeline, mergedEntity.structuredSections);
|
|
1992
|
+
const extraSectionKeys = /* @__PURE__ */ new Set();
|
|
1993
|
+
mergedEntity.extraSections = (mergedEntity.extraSections ?? []).filter((section) => {
|
|
1994
|
+
const key = `${section.title}::${section.lines.join("\n")}`;
|
|
1995
|
+
if (extraSectionKeys.has(key)) return false;
|
|
1996
|
+
extraSectionKeys.add(key);
|
|
1997
|
+
return true;
|
|
1998
|
+
});
|
|
1999
|
+
if (!mergedEntity.name) {
|
|
2000
|
+
const dashIdx = canonical.indexOf("-");
|
|
2001
|
+
mergedEntity.name = dashIdx !== -1 ? canonical.slice(dashIdx + 1) : canonical;
|
|
2014
2002
|
}
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
const entityEncrypted = await deps.isEncryptedStorageFile(legacyPath);
|
|
2030
|
-
const entityContent = await deps.readStorageSecureFile(legacyPath);
|
|
2031
|
-
await assertNotSymlink(legacyPath, legacyId);
|
|
2032
|
-
await assertEntityContentUnchanged(deps, legacyPath, entityContent, legacyId);
|
|
2033
|
-
let canonicalFingerprintAfterWrite = null;
|
|
2034
|
-
try {
|
|
2035
|
-
await deps.writeStorageSecureFile(canonicalPath, entityContent, entityEncrypted);
|
|
2036
|
-
canonicalFingerprintAfterWrite = await fingerprintPath(canonicalPath);
|
|
2037
|
-
if (await deps.readStorageSecureFile(canonicalPath) !== entityContent) {
|
|
2038
|
-
throw new Error(`Cannot verify migrated entity id ${canonicalId}.`);
|
|
2039
|
-
}
|
|
2040
|
-
if (!await refreshMigrationLock() || !await refreshEntityLock()) {
|
|
2041
|
-
throw new Error("Lost entity canonical-id migration lock.");
|
|
2042
|
-
}
|
|
2043
|
-
await deps.snapshotBeforeWrite(legacyPath, "write");
|
|
2044
|
-
await assertNotSymlink(legacyPath, legacyId);
|
|
2045
|
-
await assertEntityContentUnchanged(deps, legacyPath, entityContent, legacyId);
|
|
2046
|
-
await unlink(legacyPath);
|
|
2047
|
-
} catch (error) {
|
|
2048
|
-
if (!canonicalExists && canonicalFingerprintAfterWrite !== null) {
|
|
2049
|
-
try {
|
|
2050
|
-
if (await fingerprintPath(canonicalPath) === canonicalFingerprintAfterWrite) {
|
|
2051
|
-
await assertNotSymlink(canonicalPath, canonicalId);
|
|
2052
|
-
await unlink(canonicalPath);
|
|
2003
|
+
mergedEntity.created = mergedEntity.created || mergedEntity.updated || (/* @__PURE__ */ new Date()).toISOString();
|
|
2004
|
+
mergedEntity.updated = mergedEntity.updated || (/* @__PURE__ */ new Date()).toISOString();
|
|
2005
|
+
const canonicalPath = path3.join(this.deps.entitiesDir, `${canonical}.md`);
|
|
2006
|
+
await this.deps.writeStorageSecureFile(canonicalPath, serializeEntityFile(mergedEntity, this.deps.entitySchemas));
|
|
2007
|
+
this.deps.bumpEntityMutationVersion();
|
|
2008
|
+
for (const file of files) {
|
|
2009
|
+
const filePath = path3.join(this.deps.entitiesDir, file);
|
|
2010
|
+
if (filePath !== canonicalPath) {
|
|
2011
|
+
try {
|
|
2012
|
+
await unlink(filePath);
|
|
2013
|
+
merged++;
|
|
2014
|
+
log.debug(`merged entity ${file} \u2192 ${canonical}.md`);
|
|
2015
|
+
} catch {
|
|
2016
|
+
}
|
|
2053
2017
|
}
|
|
2054
|
-
} catch {
|
|
2055
2018
|
}
|
|
2056
2019
|
}
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
return { deferred: false, progressed: true };
|
|
2060
|
-
});
|
|
2061
|
-
}
|
|
2062
|
-
async function migrateLegacyEntityCanonicalIds(deps) {
|
|
2063
|
-
return withHeldFileLock(
|
|
2064
|
-
lockPath(deps),
|
|
2065
|
-
{
|
|
2066
|
-
staleMs: ENTITY_CANONICAL_ID_MIGRATION_LOCK_STALE_MS,
|
|
2067
|
-
maxWaitMs: ENTITY_CANONICAL_ID_MIGRATION_LOCK_MAX_WAIT_MS
|
|
2068
|
-
},
|
|
2069
|
-
async (acquired, lock) => {
|
|
2070
|
-
if (!acquired) throw new Error("Timed out waiting for active entity canonical-id migration.");
|
|
2071
|
-
await deps.validateMemoryScanRoots?.();
|
|
2072
|
-
await readSafeEntityEntries(deps.entitiesDir);
|
|
2073
|
-
let state = await readState(deps);
|
|
2074
|
-
if (!state) {
|
|
2075
|
-
state = {
|
|
2076
|
-
version: ENTITY_CANONICAL_ID_MIGRATION_VERSION,
|
|
2077
|
-
complete: false,
|
|
2078
|
-
mappings: {}
|
|
2079
|
-
};
|
|
2080
|
-
}
|
|
2081
|
-
const blocked = /* @__PURE__ */ new Map();
|
|
2082
|
-
const finish = () => {
|
|
2083
|
-
if (blocked.size > 0) {
|
|
2084
|
-
const pairs = [...blocked].map(([legacyId, canonicalId]) => `${legacyId} -> ${canonicalId}`).join(", ");
|
|
2085
|
-
log.warn(
|
|
2086
|
-
`entity canonical-id migration skipped ${blocked.size} unresolvable pair(s): ${pairs}. Both files were kept and the legacy id still resolves; merge or delete one side to migrate it.`
|
|
2087
|
-
);
|
|
2088
|
-
}
|
|
2089
|
-
return deps.readMigrationFingerprint?.();
|
|
2090
|
-
};
|
|
2091
|
-
const pruneBlocked = async () => {
|
|
2092
|
-
const current = state;
|
|
2093
|
-
if (!current) return false;
|
|
2094
|
-
let revived = false;
|
|
2095
|
-
const parked = { ...current.blocked ?? {} };
|
|
2096
|
-
const active = {};
|
|
2097
|
-
for (const [legacyId, canonicalId] of Object.entries(current.mappings)) {
|
|
2098
|
-
if (!blocked.has(legacyId)) active[legacyId] = canonicalId;
|
|
2099
|
-
}
|
|
2100
|
-
for (const [legacyId, canonicalId] of blocked) parked[legacyId] = canonicalId;
|
|
2101
|
-
for (const [legacyId, canonicalId] of Object.entries(parked)) {
|
|
2102
|
-
if (blocked.has(legacyId)) continue;
|
|
2103
|
-
delete parked[legacyId];
|
|
2104
|
-
if (active[legacyId] !== void 0) continue;
|
|
2105
|
-
const legacyPath = deps.resolveEntityFilePath(legacyId);
|
|
2106
|
-
if (legacyPath !== null && await fileExists(legacyPath)) continue;
|
|
2107
|
-
active[legacyId] = canonicalId;
|
|
2108
|
-
revived = true;
|
|
2109
|
-
}
|
|
2110
|
-
const collapsed = collapseMappings(active);
|
|
2111
|
-
for (const [legacyId, canonicalId] of Object.entries(parked)) {
|
|
2112
|
-
const moved = collapsed[canonicalId];
|
|
2113
|
-
if (moved !== void 0 && moved !== legacyId) parked[legacyId] = moved;
|
|
2114
|
-
}
|
|
2115
|
-
const same = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([key, value]) => b[key] === value);
|
|
2116
|
-
if (same(collapsed, current.mappings) && same(parked, current.blocked ?? {})) return revived;
|
|
2117
|
-
state = { ...current, mappings: collapsed, blocked: parked };
|
|
2118
|
-
await writeState(deps, state);
|
|
2119
|
-
return revived;
|
|
2120
|
-
};
|
|
2121
|
-
const previousMappings = state.mappings;
|
|
2122
|
-
const collapsedMappings = collapseMappings(previousMappings);
|
|
2123
|
-
const mappingsChanged = Object.keys(collapsedMappings).length !== Object.keys(previousMappings).length || Object.entries(collapsedMappings).some(
|
|
2124
|
-
([legacyId, canonicalId]) => previousMappings[legacyId] !== canonicalId
|
|
2125
|
-
);
|
|
2126
|
-
state = { ...state, mappings: collapsedMappings };
|
|
2127
|
-
for (let migrationPass = 0; migrationPass < ENTITY_MAPPING_RESCAN_MAX_PASSES; migrationPass += 1) {
|
|
2128
|
-
if (!await lock.refresh()) throw new Error("Lost entity canonical-id migration lock.");
|
|
2129
|
-
const discoveredBefore = rejectMappingCycles(
|
|
2130
|
-
state.mappings,
|
|
2131
|
-
await discoverMappings(deps, state.mappings, blocked)
|
|
2132
|
-
);
|
|
2133
|
-
await pruneBlocked();
|
|
2134
|
-
const mergedBefore = collapseMappings(mergeDiscoveredMappings(state.mappings, discoveredBefore));
|
|
2135
|
-
if (state.complete && Object.keys(discoveredBefore).length === 0 && !mappingsChanged) {
|
|
2136
|
-
if (Object.keys(state.mappings).length === 0) return finish();
|
|
2137
|
-
const rewroteColdMemory2 = await rewriteKnownReferences(
|
|
2138
|
-
deps,
|
|
2139
|
-
state.mappings,
|
|
2140
|
-
() => lock.refresh()
|
|
2141
|
-
);
|
|
2142
|
-
deps.invalidateKnowledgeIndexCache();
|
|
2143
|
-
deps.invalidateAllMemoriesCache();
|
|
2144
|
-
if (rewroteColdMemory2) deps.invalidateColdMemoriesCache();
|
|
2145
|
-
deps.bumpMemoryStatusVersion();
|
|
2146
|
-
return finish();
|
|
2147
|
-
}
|
|
2148
|
-
if (state.complete || Object.keys(discoveredBefore).length > 0 || Object.keys(mergedBefore).length !== Object.keys(state.mappings).length) {
|
|
2149
|
-
state = { ...state, complete: false, mappings: mergedBefore };
|
|
2150
|
-
await writeState(deps, state);
|
|
2151
|
-
}
|
|
2152
|
-
if (Object.keys(state.mappings).length === 0) {
|
|
2153
|
-
if (!await lock.refresh()) throw new Error("Lost entity canonical-id migration lock.");
|
|
2154
|
-
const discoveredAfter2 = rejectMappingCycles(
|
|
2155
|
-
state.mappings,
|
|
2156
|
-
await discoverMappings(deps, state.mappings, blocked)
|
|
2157
|
-
);
|
|
2158
|
-
const revivedInRescan = await pruneBlocked();
|
|
2159
|
-
if (Object.keys(discoveredAfter2).length === 0 && !revivedInRescan) {
|
|
2160
|
-
await writeState(deps, { ...state, complete: true });
|
|
2161
|
-
return finish();
|
|
2162
|
-
}
|
|
2163
|
-
state = {
|
|
2164
|
-
...state,
|
|
2165
|
-
complete: false,
|
|
2166
|
-
mappings: collapseMappings(mergeDiscoveredMappings(state.mappings, discoveredAfter2))
|
|
2167
|
-
};
|
|
2168
|
-
await writeState(deps, state);
|
|
2169
|
-
continue;
|
|
2170
|
-
}
|
|
2171
|
-
let pendingMappings = Object.entries(state.mappings);
|
|
2172
|
-
while (pendingMappings.length > 0) {
|
|
2173
|
-
const deferredMappings = [];
|
|
2174
|
-
let progressed = false;
|
|
2175
|
-
for (const [legacyId, canonicalId] of pendingMappings) {
|
|
2176
|
-
const result = await migrateEntityFilePair(
|
|
2177
|
-
deps,
|
|
2178
|
-
legacyId,
|
|
2179
|
-
canonicalId,
|
|
2180
|
-
() => lock.refresh()
|
|
2181
|
-
);
|
|
2182
|
-
if (result.blocked) blocked.set(legacyId, canonicalId);
|
|
2183
|
-
else if (result.deferred) deferredMappings.push([legacyId, canonicalId]);
|
|
2184
|
-
progressed ||= result.progressed;
|
|
2185
|
-
}
|
|
2186
|
-
if (deferredMappings.length === 0) break;
|
|
2187
|
-
if (!progressed) {
|
|
2188
|
-
const [legacyId, canonicalId] = deferredMappings[0];
|
|
2189
|
-
throw new Error(
|
|
2190
|
-
`Cannot migrate legacy entity id ${legacyId}: both files are missing and no intermediate mapping can advance migration.`
|
|
2191
|
-
);
|
|
2192
|
-
}
|
|
2193
|
-
pendingMappings = deferredMappings;
|
|
2194
|
-
}
|
|
2195
|
-
await pruneBlocked();
|
|
2196
|
-
const rewroteColdMemory = await rewriteKnownReferences(
|
|
2197
|
-
deps,
|
|
2198
|
-
state.mappings,
|
|
2199
|
-
() => lock.refresh()
|
|
2200
|
-
);
|
|
2201
|
-
deps.invalidateKnowledgeIndexCache();
|
|
2202
|
-
deps.invalidateAllMemoriesCache();
|
|
2203
|
-
if (rewroteColdMemory) deps.invalidateColdMemoriesCache();
|
|
2204
|
-
deps.bumpMemoryStatusVersion();
|
|
2205
|
-
if (!await lock.refresh()) throw new Error("Lost entity canonical-id migration lock.");
|
|
2206
|
-
const discoveredAfter = rejectMappingCycles(
|
|
2207
|
-
state.mappings,
|
|
2208
|
-
await discoverMappings(deps, state.mappings, blocked)
|
|
2209
|
-
);
|
|
2210
|
-
const revivedAfterRewrite = await pruneBlocked();
|
|
2211
|
-
if (Object.keys(discoveredAfter).length === 0 && !revivedAfterRewrite) {
|
|
2212
|
-
await writeState(deps, { ...state, complete: true });
|
|
2213
|
-
return finish();
|
|
2214
|
-
}
|
|
2215
|
-
state = {
|
|
2216
|
-
...state,
|
|
2217
|
-
complete: false,
|
|
2218
|
-
mappings: collapseMappings(mergeDiscoveredMappings(state.mappings, discoveredAfter))
|
|
2219
|
-
};
|
|
2220
|
-
await writeState(deps, state);
|
|
2221
|
-
}
|
|
2222
|
-
throw new Error("Entity canonical-id migration mappings changed while migration was running; retry migration.");
|
|
2020
|
+
} catch (err) {
|
|
2021
|
+
if (err instanceof SecureStoreLockedError || !isErrnoCode(err, "ENOENT")) throw err;
|
|
2223
2022
|
}
|
|
2224
|
-
|
|
2225
|
-
}
|
|
2023
|
+
return merged;
|
|
2024
|
+
}
|
|
2025
|
+
};
|
|
2226
2026
|
|
|
2227
|
-
// src/storage/
|
|
2228
|
-
|
|
2027
|
+
// src/storage/identity-continuity-store.ts
|
|
2028
|
+
import { readdir as readdir3 } from "fs/promises";
|
|
2029
|
+
import path4 from "path";
|
|
2030
|
+
var IdentityContinuityStore = class {
|
|
2229
2031
|
constructor(deps) {
|
|
2230
2032
|
this.deps = deps;
|
|
2231
2033
|
}
|
|
2232
2034
|
deps;
|
|
2233
|
-
|
|
2234
|
-
normalizeEntityName(raw, type) {
|
|
2235
|
-
return normalizeEntityName(raw, type, this.deps.userAliases);
|
|
2236
|
-
}
|
|
2237
|
-
async writeEntity(name, type, facts, options = {}) {
|
|
2035
|
+
async writeIdentityAnchor(content) {
|
|
2238
2036
|
await this.deps.ensureDirectories();
|
|
2239
|
-
|
|
2240
|
-
path4.join(this.deps.baseDir, "state"),
|
|
2241
|
-
() => this.writeEntityUnlocked(name, type, facts, options)
|
|
2242
|
-
);
|
|
2037
|
+
await this.deps.writeStorageSecureFile(this.deps.identityAnchorPath, content);
|
|
2243
2038
|
}
|
|
2244
|
-
async
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
return "";
|
|
2251
|
-
}
|
|
2252
|
-
const safeFacts = Array.isArray(facts) ? [...new Set(
|
|
2253
|
-
facts.filter((fact) => typeof fact === "string").map((fact) => fact.trim()).filter((fact) => fact.length > 0)
|
|
2254
|
-
)] : [];
|
|
2255
|
-
let normalized = this.deps.normalizeEntityName(name, type);
|
|
2256
|
-
const match = await this.deps.findMatchingEntity(name, type);
|
|
2257
|
-
if (match && match !== normalized) {
|
|
2258
|
-
log.debug(`fuzzy match: "${normalized}" \u2192 existing "${match}"`);
|
|
2259
|
-
normalized = match;
|
|
2039
|
+
async readIdentityAnchor() {
|
|
2040
|
+
try {
|
|
2041
|
+
return await this.deps.readStorageSecureFile(this.deps.identityAnchorPath);
|
|
2042
|
+
} catch (err) {
|
|
2043
|
+
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
2044
|
+
return null;
|
|
2260
2045
|
}
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
aliases: []
|
|
2278
|
-
};
|
|
2046
|
+
}
|
|
2047
|
+
async appendContinuityIncident(input) {
|
|
2048
|
+
await this.deps.ensureDirectories();
|
|
2049
|
+
const now = /* @__PURE__ */ new Date();
|
|
2050
|
+
const nowIso = now.toISOString();
|
|
2051
|
+
const date = nowIso.slice(0, 10);
|
|
2052
|
+
const id = this.deps.generateId("incident");
|
|
2053
|
+
const incident = createContinuityIncidentRecord(id, input, nowIso);
|
|
2054
|
+
const filePath = path4.join(this.deps.identityIncidentsDir, `${date}-${id}.md`);
|
|
2055
|
+
await this.deps.writeStorageSecureFile(filePath, serializeContinuityIncident(incident));
|
|
2056
|
+
return { ...incident, filePath };
|
|
2057
|
+
}
|
|
2058
|
+
async readContinuityIncidents(limit = 200, state = "all") {
|
|
2059
|
+
const normalizedLimit = Number.isFinite(limit) ? Math.floor(limit) : 0;
|
|
2060
|
+
const cappedLimit = Math.max(0, normalizedLimit);
|
|
2061
|
+
if (cappedLimit === 0) return [];
|
|
2279
2062
|
try {
|
|
2280
|
-
const
|
|
2281
|
-
|
|
2063
|
+
const candidates = await this.readContinuityIncidentFileNames();
|
|
2064
|
+
const incidents = [];
|
|
2065
|
+
for (const file of candidates) {
|
|
2066
|
+
if (incidents.length >= cappedLimit) break;
|
|
2067
|
+
const filePath = path4.join(this.deps.identityIncidentsDir, file);
|
|
2068
|
+
try {
|
|
2069
|
+
const raw = await this.deps.readStorageSecureFile(filePath);
|
|
2070
|
+
const parsed = parseContinuityIncident(raw);
|
|
2071
|
+
if (!parsed) continue;
|
|
2072
|
+
if (state !== "all" && parsed.state !== state) continue;
|
|
2073
|
+
incidents.push({ ...parsed, filePath });
|
|
2074
|
+
} catch (err) {
|
|
2075
|
+
if (err instanceof SecureStoreLockedError) throw err;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
return incidents;
|
|
2282
2079
|
} catch (err) {
|
|
2283
2080
|
if (err instanceof SecureStoreLockedError) throw err;
|
|
2284
2081
|
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
2082
|
+
return [];
|
|
2285
2083
|
}
|
|
2286
|
-
const timestamp = options.timestamp?.trim() || (/* @__PURE__ */ new Date()).toISOString();
|
|
2287
|
-
const source = options.source?.trim() || void 0;
|
|
2288
|
-
const sessionKey = options.sessionKey?.trim() || void 0;
|
|
2289
|
-
const principal = options.principal?.trim() || void 0;
|
|
2290
|
-
const structuredSectionMap = new Map(
|
|
2291
|
-
(entity.structuredSections ?? []).map((section) => [section.key, {
|
|
2292
|
-
...section,
|
|
2293
|
-
facts: [...section.facts]
|
|
2294
|
-
}])
|
|
2295
|
-
);
|
|
2296
|
-
for (const section of options.structuredSections ?? []) {
|
|
2297
|
-
const normalizedSection = normalizeEntityStructuredSection(type, section, this.deps.entitySchemas);
|
|
2298
|
-
const normalizedFacts = normalizeStructuredSectionFacts(section.facts);
|
|
2299
|
-
if (normalizedFacts.length === 0) continue;
|
|
2300
|
-
const existingSection = structuredSectionMap.get(normalizedSection.key);
|
|
2301
|
-
if (!existingSection) {
|
|
2302
|
-
structuredSectionMap.set(normalizedSection.key, {
|
|
2303
|
-
key: normalizedSection.key,
|
|
2304
|
-
title: normalizedSection.title,
|
|
2305
|
-
facts: normalizedFacts
|
|
2306
|
-
});
|
|
2307
|
-
continue;
|
|
2308
|
-
}
|
|
2309
|
-
existingSection.facts = normalizeStructuredSectionFacts([...existingSection.facts, ...normalizedFacts]);
|
|
2310
|
-
if (!existingSection.title.trim() && normalizedSection.title.trim()) {
|
|
2311
|
-
existingSection.title = normalizedSection.title;
|
|
2312
|
-
}
|
|
2313
|
-
}
|
|
2314
|
-
for (const fact of safeFacts) {
|
|
2315
|
-
const nextEntry = {
|
|
2316
|
-
timestamp,
|
|
2317
|
-
text: fact,
|
|
2318
|
-
...source ? { source } : {},
|
|
2319
|
-
...sessionKey ? { sessionKey } : {},
|
|
2320
|
-
...principal ? { principal } : {}
|
|
2321
|
-
};
|
|
2322
|
-
const alreadyPresent = entity.timeline.some(
|
|
2323
|
-
(entry) => entry.timestamp === nextEntry.timestamp && entry.text === nextEntry.text && entry.source === nextEntry.source && entry.sessionKey === nextEntry.sessionKey && entry.principal === nextEntry.principal
|
|
2324
|
-
);
|
|
2325
|
-
if (alreadyPresent) continue;
|
|
2326
|
-
entity.timeline.push(nextEntry);
|
|
2327
|
-
}
|
|
2328
|
-
entity.structuredSections = sortStructuredSectionsBySchema(
|
|
2329
|
-
type,
|
|
2330
|
-
Array.from(structuredSectionMap.values()).filter((section) => section.facts.length > 0),
|
|
2331
|
-
this.deps.entitySchemas
|
|
2332
|
-
);
|
|
2333
|
-
entity.facts = compileEntityFacts(entity.timeline, entity.structuredSections);
|
|
2334
|
-
entity.summary = entity.synthesis || entity.summary;
|
|
2335
|
-
entity.name = name;
|
|
2336
|
-
entity.type = type;
|
|
2337
|
-
entity.created = entity.created || timestamp;
|
|
2338
|
-
entity.updated = (/* @__PURE__ */ new Date()).toISOString();
|
|
2339
|
-
await this.deps.snapshotBeforeWrite(filePath, "write");
|
|
2340
|
-
await this.deps.writeStorageSecureFile(filePath, serializeEntityFile(entity, this.deps.entitySchemas));
|
|
2341
|
-
this.deps.invalidateKnowledgeIndexCache();
|
|
2342
|
-
this.deps.bumpMemoryStatusVersion();
|
|
2343
|
-
log.debug(`wrote entity ${normalized}`);
|
|
2344
|
-
return normalized;
|
|
2345
2084
|
}
|
|
2346
|
-
async
|
|
2347
|
-
const
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2085
|
+
async closeContinuityIncident(id, closure) {
|
|
2086
|
+
const directFilePath = await this.findContinuityIncidentFilePathById(id);
|
|
2087
|
+
const target = directFilePath ? await this.readContinuityIncidentFile(directFilePath) : null;
|
|
2088
|
+
if (!target || !directFilePath) return null;
|
|
2089
|
+
if (target.state === "closed") return target;
|
|
2090
|
+
const closed = closeContinuityIncidentRecord(target, closure, (/* @__PURE__ */ new Date()).toISOString());
|
|
2091
|
+
await this.deps.writeStorageSecureFile(directFilePath, serializeContinuityIncident(closed));
|
|
2092
|
+
return { ...closed, filePath: directFilePath };
|
|
2093
|
+
}
|
|
2094
|
+
async writeIdentityAudit(period, key, content) {
|
|
2095
|
+
await this.deps.ensureDirectories();
|
|
2096
|
+
const safeKey = this.sanitizeIdentityAuditKey(key);
|
|
2097
|
+
const dir = period === "weekly" ? this.deps.identityAuditsWeeklyDir : this.deps.identityAuditsMonthlyDir;
|
|
2098
|
+
const filePath = path4.join(dir, `${safeKey}.md`);
|
|
2099
|
+
await this.deps.writeStorageSecureFile(filePath, content);
|
|
2100
|
+
return filePath;
|
|
2101
|
+
}
|
|
2102
|
+
async readIdentityAudit(period, key) {
|
|
2351
2103
|
try {
|
|
2352
|
-
|
|
2104
|
+
const safeKey = this.sanitizeIdentityAuditKey(key);
|
|
2105
|
+
const dir = period === "weekly" ? this.deps.identityAuditsWeeklyDir : this.deps.identityAuditsMonthlyDir;
|
|
2106
|
+
return await this.deps.readStorageSecureFile(path4.join(dir, `${safeKey}.md`));
|
|
2353
2107
|
} catch (err) {
|
|
2354
|
-
if (err instanceof
|
|
2108
|
+
if (err instanceof Error && err.message === "Invalid identity audit key") return null;
|
|
2355
2109
|
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
2356
|
-
return
|
|
2357
|
-
}
|
|
2358
|
-
}
|
|
2359
|
-
/** Return sorted list of entity filenames (without .md extension) */
|
|
2360
|
-
async listEntityNames() {
|
|
2361
|
-
try {
|
|
2362
|
-
const entries = await readdir3(this.deps.entitiesDir);
|
|
2363
|
-
return entries.filter((e) => e.endsWith(".md")).map((e) => e.replace(".md", "")).sort();
|
|
2364
|
-
} catch {
|
|
2365
|
-
return [];
|
|
2110
|
+
return null;
|
|
2366
2111
|
}
|
|
2367
2112
|
}
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
async updateEntitySynthesis(name, synthesis, options = {}) {
|
|
2372
|
-
return withEntityCanonicalMutationLock(
|
|
2373
|
-
path4.join(this.deps.baseDir, "state"),
|
|
2374
|
-
() => this.updateEntitySynthesisUnlocked(name, synthesis, options)
|
|
2375
|
-
);
|
|
2113
|
+
async writeIdentityImprovementLoops(content) {
|
|
2114
|
+
await this.deps.ensureDirectories();
|
|
2115
|
+
await this.deps.writeStorageSecureFile(this.deps.identityImprovementLoopsPath, content);
|
|
2376
2116
|
}
|
|
2377
|
-
async
|
|
2378
|
-
const filePath = path4.join(this.deps.entitiesDir, `${name}.md`);
|
|
2379
|
-
let entity;
|
|
2117
|
+
async readIdentityImprovementLoops() {
|
|
2380
2118
|
try {
|
|
2381
|
-
|
|
2382
|
-
entity = parseEntityFile(content, this.deps.entitySchemas);
|
|
2119
|
+
return await this.deps.readStorageSecureFile(this.deps.identityImprovementLoopsPath);
|
|
2383
2120
|
} catch (err) {
|
|
2384
|
-
if (err instanceof SecureStoreLockedError) throw err;
|
|
2385
2121
|
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
2386
|
-
|
|
2387
|
-
return;
|
|
2122
|
+
return null;
|
|
2388
2123
|
}
|
|
2389
|
-
const updatedAt = options.updatedAt?.trim() || entity.synthesisUpdatedAt?.trim() || void 0;
|
|
2390
|
-
const entityUpdatedAt = options.entityUpdatedAt?.trim() || updatedAt || entity.updated || (/* @__PURE__ */ new Date()).toISOString();
|
|
2391
|
-
const synthesisTimelineCount = Number.isInteger(options.synthesisTimelineCount) && (options.synthesisTimelineCount ?? 0) >= 0 ? options.synthesisTimelineCount : void 0;
|
|
2392
|
-
const synthesisStructuredFactCount = Number.isInteger(options.synthesisStructuredFactCount) && (options.synthesisStructuredFactCount ?? 0) >= 0 ? options.synthesisStructuredFactCount : countEntityStructuredFacts(entity);
|
|
2393
|
-
const synthesisStructuredFactDigest = options.synthesisStructuredFactDigest?.trim() || fingerprintEntityStructuredFacts(entity);
|
|
2394
|
-
entity.synthesis = synthesis.trim();
|
|
2395
|
-
entity.summary = entity.synthesis;
|
|
2396
|
-
entity.synthesisUpdatedAt = updatedAt;
|
|
2397
|
-
entity.synthesisTimelineCount = synthesisTimelineCount;
|
|
2398
|
-
entity.synthesisStructuredFactCount = synthesisStructuredFactCount;
|
|
2399
|
-
entity.synthesisStructuredFactDigest = synthesisStructuredFactDigest;
|
|
2400
|
-
entity.synthesisVersion = Math.max(0, entity.synthesisVersion ?? 0) + (options.incrementVersion === false ? 0 : 1);
|
|
2401
|
-
entity.updated = entityUpdatedAt;
|
|
2402
|
-
await this.deps.writeStorageSecureFile(filePath, serializeEntityFile(entity, this.deps.entitySchemas));
|
|
2403
|
-
await this.deps.removeEntitySynthesisQueueEntries([
|
|
2404
|
-
.../* @__PURE__ */ new Set([name, this.deps.normalizeEntityName(entity.name, entity.type)])
|
|
2405
|
-
]);
|
|
2406
|
-
this.deps.invalidateKnowledgeIndexCache();
|
|
2407
|
-
this.deps.bumpMemoryStatusVersion();
|
|
2408
2124
|
}
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
const
|
|
2416
|
-
const
|
|
2417
|
-
const
|
|
2418
|
-
|
|
2125
|
+
async readIdentityImprovementLoopRegister() {
|
|
2126
|
+
const raw = await this.readIdentityImprovementLoops();
|
|
2127
|
+
if (!raw) return [];
|
|
2128
|
+
return parseContinuityImprovementLoops(raw);
|
|
2129
|
+
}
|
|
2130
|
+
async upsertIdentityImprovementLoop(input) {
|
|
2131
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
2132
|
+
const raw = await this.readIdentityImprovementLoops();
|
|
2133
|
+
const { markdown, loop } = upsertContinuityLoopInMarkdown(raw, input, nowIso);
|
|
2134
|
+
await this.writeIdentityImprovementLoops(markdown);
|
|
2135
|
+
return loop;
|
|
2136
|
+
}
|
|
2137
|
+
async reviewIdentityImprovementLoop(id, input) {
|
|
2138
|
+
const raw = await this.readIdentityImprovementLoops();
|
|
2139
|
+
const { markdown, loop } = reviewContinuityLoopInMarkdown(raw, id, input, (/* @__PURE__ */ new Date()).toISOString());
|
|
2140
|
+
if (!loop) return null;
|
|
2141
|
+
await this.writeIdentityImprovementLoops(markdown);
|
|
2142
|
+
return loop;
|
|
2143
|
+
}
|
|
2144
|
+
async readContinuityIncidentFileNames() {
|
|
2145
|
+
const files = await readdir3(this.deps.identityIncidentsDir);
|
|
2146
|
+
return files.filter((file) => file.endsWith(".md")).sort().reverse();
|
|
2147
|
+
}
|
|
2148
|
+
async readContinuityIncidentFile(filePath) {
|
|
2419
2149
|
try {
|
|
2420
|
-
const
|
|
2421
|
-
const
|
|
2422
|
-
|
|
2423
|
-
const BATCH_SIZE = 100;
|
|
2424
|
-
const entities = [];
|
|
2425
|
-
for (let i = 0; i < mdFiles.length; i += BATCH_SIZE) {
|
|
2426
|
-
const batch = mdFiles.slice(i, i + BATCH_SIZE);
|
|
2427
|
-
const results = await Promise.all(
|
|
2428
|
-
batch.map(async (entry) => {
|
|
2429
|
-
try {
|
|
2430
|
-
return await this.deps.readStorageSecureFile(path4.join(this.deps.entitiesDir, entry));
|
|
2431
|
-
} catch (err) {
|
|
2432
|
-
if (err instanceof SecureStoreLockedError) throw err;
|
|
2433
|
-
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
2434
|
-
return null;
|
|
2435
|
-
}
|
|
2436
|
-
})
|
|
2437
|
-
);
|
|
2438
|
-
for (const content of results) {
|
|
2439
|
-
if (content !== null) entities.push(parseEntityFile(content, this.deps.entitySchemas));
|
|
2440
|
-
}
|
|
2441
|
-
}
|
|
2442
|
-
setCachedEntities(this.deps.baseDir, entities, currentVersion, cacheKey);
|
|
2443
|
-
return entities;
|
|
2150
|
+
const raw = await this.deps.readStorageSecureFile(filePath);
|
|
2151
|
+
const parsed = parseContinuityIncident(raw);
|
|
2152
|
+
return parsed ? { ...parsed, filePath } : null;
|
|
2444
2153
|
} catch (err) {
|
|
2445
|
-
if (err instanceof SecureStoreLockedError
|
|
2446
|
-
return
|
|
2154
|
+
if (err instanceof SecureStoreLockedError) throw err;
|
|
2155
|
+
return null;
|
|
2447
2156
|
}
|
|
2448
2157
|
}
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
return { result: this.deps.knowledgeIndexCache.result, cached: true };
|
|
2457
|
-
}
|
|
2458
|
-
const entities = await this.deps.readAllEntityFiles();
|
|
2459
|
-
if (entities.length === 0) {
|
|
2460
|
-
if (useDefaultLimits) this.deps.knowledgeIndexCache = { result: "", builtAt: Date.now() };
|
|
2461
|
-
return { result: "", cached: false };
|
|
2462
|
-
}
|
|
2463
|
-
const now = /* @__PURE__ */ new Date();
|
|
2464
|
-
const scored = entities.map((e) => ({
|
|
2465
|
-
name: e.name,
|
|
2466
|
-
type: e.type,
|
|
2467
|
-
score: this.deps.storageManagerClass.scoreEntity(e, now),
|
|
2468
|
-
factCount: e.facts.length,
|
|
2469
|
-
summary: e.synthesis ?? e.summary,
|
|
2470
|
-
topRelationships: e.relationships.slice(0, 3).map((r) => r.target)
|
|
2471
|
-
}));
|
|
2472
|
-
scored.sort((a, b) => b.score - a.score);
|
|
2473
|
-
const maxEntities = typeof overrides?.maxEntities === "number" ? Math.max(0, Math.floor(overrides.maxEntities)) : config.knowledgeIndexMaxEntities;
|
|
2474
|
-
const topN = scored.slice(0, maxEntities);
|
|
2475
|
-
if (topN.length === 0) {
|
|
2476
|
-
if (useDefaultLimits) this.deps.knowledgeIndexCache = { result: "", builtAt: Date.now() };
|
|
2477
|
-
return { result: "", cached: false };
|
|
2158
|
+
async findContinuityIncidentFilePathById(id) {
|
|
2159
|
+
const fileNames = await this.readContinuityIncidentFileNames();
|
|
2160
|
+
const directMatch = fileNames.find((name) => name.endsWith(`-${id}.md`));
|
|
2161
|
+
if (directMatch) {
|
|
2162
|
+
const directPath = path4.join(this.deps.identityIncidentsDir, directMatch);
|
|
2163
|
+
const parsed = await this.readContinuityIncidentFile(directPath);
|
|
2164
|
+
if (parsed?.id === id) return directPath;
|
|
2478
2165
|
}
|
|
2479
|
-
const
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
for (const entity of topN) {
|
|
2484
|
-
const summary = entity.summary || `${entity.factCount} facts`;
|
|
2485
|
-
const connected = entity.topRelationships.length > 0 ? entity.topRelationships.join(", ") : "\u2014";
|
|
2486
|
-
const row = `| ${entity.name} | ${entity.type} | ${summary} | ${connected} |`;
|
|
2487
|
-
if (totalChars + row.length + 1 > maxChars) break;
|
|
2488
|
-
rows.push(row);
|
|
2489
|
-
totalChars += row.length + 1;
|
|
2166
|
+
for (const fileName of fileNames) {
|
|
2167
|
+
const filePath = path4.join(this.deps.identityIncidentsDir, fileName);
|
|
2168
|
+
const parsed = await this.readContinuityIncidentFile(filePath);
|
|
2169
|
+
if (parsed?.id === id) return filePath;
|
|
2490
2170
|
}
|
|
2491
|
-
|
|
2492
|
-
${rows.join("\n")}
|
|
2493
|
-
`;
|
|
2494
|
-
if (useDefaultLimits) this.deps.knowledgeIndexCache = { result, builtAt: Date.now() };
|
|
2495
|
-
return { result, cached: false };
|
|
2171
|
+
return null;
|
|
2496
2172
|
}
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
return withEntityCanonicalMutationLock(
|
|
2504
|
-
path4.join(this.deps.baseDir, "state"),
|
|
2505
|
-
() => this.mergeFragmentedEntitiesUnlocked()
|
|
2506
|
-
);
|
|
2173
|
+
sanitizeIdentityAuditKey(key) {
|
|
2174
|
+
const trimmed = key.trim();
|
|
2175
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(trimmed) || trimmed.includes("..")) {
|
|
2176
|
+
throw new Error("Invalid identity audit key");
|
|
2177
|
+
}
|
|
2178
|
+
return trimmed;
|
|
2507
2179
|
}
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2180
|
+
};
|
|
2181
|
+
|
|
2182
|
+
// src/storage/entity-canonical-id-migration.ts
|
|
2183
|
+
import { randomUUID } from "crypto";
|
|
2184
|
+
import {
|
|
2185
|
+
access,
|
|
2186
|
+
lstat as lstat2,
|
|
2187
|
+
readFile,
|
|
2188
|
+
readdir as readdir4,
|
|
2189
|
+
realpath as realpath2,
|
|
2190
|
+
rename,
|
|
2191
|
+
stat as statFile,
|
|
2192
|
+
unlink as unlink2,
|
|
2193
|
+
writeFile
|
|
2194
|
+
} from "fs/promises";
|
|
2195
|
+
import path5 from "path";
|
|
2196
|
+
import { z } from "zod";
|
|
2197
|
+
var ENTITY_CANONICAL_ID_MIGRATION_VERSION = 1;
|
|
2198
|
+
var ENTITY_CANONICAL_ID_MIGRATION_LOCK_STALE_MS = 6e4;
|
|
2199
|
+
var ENTITY_CANONICAL_ID_MIGRATION_LOCK_MAX_WAIT_MS = 3e5;
|
|
2200
|
+
var TOMBSTONE_LOCK_STALE_MS = 3e4;
|
|
2201
|
+
var TOMBSTONE_LOCK_MAX_WAIT_MS = 5e3;
|
|
2202
|
+
var MEMORY_REWRITE_MAX_PASSES = 3;
|
|
2203
|
+
var ENTITY_MAPPING_RESCAN_MAX_PASSES = 3;
|
|
2204
|
+
var EntityCanonicalIdMigrationStateSchema = z.object({
|
|
2205
|
+
version: z.literal(ENTITY_CANONICAL_ID_MIGRATION_VERSION),
|
|
2206
|
+
complete: z.boolean(),
|
|
2207
|
+
mappings: z.record(z.string().min(1)),
|
|
2208
|
+
/**
|
|
2209
|
+
* Pairs the migration refused to resolve, PARKED rather than dropped.
|
|
2210
|
+
* Deleting the record would strand the collision: once an operator removes
|
|
2211
|
+
* the legacy file, no later scan can rediscover the mapping, so references
|
|
2212
|
+
* still naming the legacy id would never be rewritten. Optional so a journal
|
|
2213
|
+
* written by an older build still parses.
|
|
2214
|
+
*/
|
|
2215
|
+
blocked: z.record(z.string().min(1)).optional()
|
|
2216
|
+
});
|
|
2217
|
+
async function fingerprintPath(filePath) {
|
|
2218
|
+
let fileStat;
|
|
2219
|
+
try {
|
|
2220
|
+
fileStat = await lstat2(filePath);
|
|
2221
|
+
} catch (error) {
|
|
2222
|
+
if (!isErrnoCode(error, "ENOENT")) throw error;
|
|
2223
|
+
}
|
|
2224
|
+
return fileStat ? `${fileStat.dev}:${fileStat.ino}:${fileStat.mtimeMs}:${fileStat.ctimeMs}:${fileStat.size}` : "missing";
|
|
2225
|
+
}
|
|
2226
|
+
async function getFingerprint(baseDir, entitiesDir, getEntityMutationVersion) {
|
|
2227
|
+
const [entityFingerprint, aliasFingerprint, reconcileFingerprint, consumingFingerprint] = await Promise.all([
|
|
2228
|
+
fingerprintPath(entitiesDir),
|
|
2229
|
+
fingerprintPath(path5.join(baseDir, "config", "aliases.json")),
|
|
2230
|
+
fingerprintPath(path5.join(baseDir, "state", ENTITY_CANONICAL_ID_RECONCILE_MARKER)),
|
|
2231
|
+
// A `.consuming` generation stranded by a crashed PEER process must also
|
|
2232
|
+
// re-invoke a live runner whose cached fingerprint predates it.
|
|
2233
|
+
fingerprintPath(path5.join(baseDir, "state", ENTITY_CANONICAL_ID_RECONCILE_CONSUMING_MARKER))
|
|
2234
|
+
]);
|
|
2235
|
+
return [entityFingerprint, aliasFingerprint, reconcileFingerprint, consumingFingerprint, getEntityMutationVersion()].join(":");
|
|
2236
|
+
}
|
|
2237
|
+
async function validateRoots(baseDir, entitiesDir, stateDir, archiveDir) {
|
|
2238
|
+
let baseStat;
|
|
2239
|
+
try {
|
|
2240
|
+
baseStat = await lstat2(baseDir);
|
|
2241
|
+
} catch (error) {
|
|
2242
|
+
if (isErrnoCode(error, "ENOENT")) return;
|
|
2243
|
+
throw error;
|
|
2244
|
+
}
|
|
2245
|
+
if (baseStat.isSymbolicLink() || !baseStat.isDirectory()) {
|
|
2246
|
+
throw new Error(`Refusing entity canonical-id migration through unsafe memory root: ${baseDir}.`);
|
|
2247
|
+
}
|
|
2248
|
+
const memoryRoot = await realpath2(baseDir);
|
|
2249
|
+
const roots = [
|
|
2250
|
+
entitiesDir,
|
|
2251
|
+
stateDir,
|
|
2252
|
+
...RECALL_FALLBACK_DIRS.map((directory) => path5.join(baseDir, directory)),
|
|
2253
|
+
path5.join(baseDir, "cold"),
|
|
2254
|
+
archiveDir
|
|
2255
|
+
];
|
|
2256
|
+
for (const root of roots) {
|
|
2257
|
+
let rootStat;
|
|
2258
|
+
try {
|
|
2259
|
+
rootStat = await lstat2(root);
|
|
2260
|
+
} catch (error) {
|
|
2261
|
+
if (isErrnoCode(error, "ENOENT")) continue;
|
|
2262
|
+
throw error;
|
|
2263
|
+
}
|
|
2264
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
2265
|
+
throw new Error(`Refusing entity canonical-id migration through unsafe memory root: ${root}.`);
|
|
2266
|
+
}
|
|
2267
|
+
assertPathInsideRoot(memoryRoot, await realpath2(root), root);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
function statePath(deps) {
|
|
2271
|
+
return path5.join(deps.stateDir, ENTITY_CANONICAL_ID_MIGRATION_FILE);
|
|
2272
|
+
}
|
|
2273
|
+
async function assertSafeMigrationStateFile(deps) {
|
|
2274
|
+
const migrationStatePath = statePath(deps);
|
|
2275
|
+
let fileStat;
|
|
2276
|
+
try {
|
|
2277
|
+
fileStat = await lstat2(migrationStatePath);
|
|
2278
|
+
} catch (error) {
|
|
2279
|
+
if (isErrnoCode(error, "ENOENT")) return;
|
|
2280
|
+
throw error;
|
|
2281
|
+
}
|
|
2282
|
+
if (fileStat.isSymbolicLink() || !fileStat.isFile()) {
|
|
2283
|
+
throw new Error(
|
|
2284
|
+
`Refusing entity canonical-id migration through unsafe migration state file: ${migrationStatePath}.`
|
|
2285
|
+
);
|
|
2286
|
+
}
|
|
2287
|
+
const [stateRoot, stateFile] = await Promise.all([
|
|
2288
|
+
realpath2(deps.stateDir),
|
|
2289
|
+
realpath2(migrationStatePath)
|
|
2290
|
+
]);
|
|
2291
|
+
assertPathInsideRoot(stateRoot, stateFile, migrationStatePath);
|
|
2292
|
+
}
|
|
2293
|
+
async function assertSafeTombstoneFile(deps, tombstonePath) {
|
|
2294
|
+
let fileStat;
|
|
2295
|
+
try {
|
|
2296
|
+
fileStat = await lstat2(tombstonePath);
|
|
2297
|
+
} catch (error) {
|
|
2298
|
+
if (isErrnoCode(error, "ENOENT")) return false;
|
|
2299
|
+
throw error;
|
|
2300
|
+
}
|
|
2301
|
+
if (fileStat.isSymbolicLink() || !fileStat.isFile()) {
|
|
2302
|
+
throw new Error(
|
|
2303
|
+
`Refusing entity canonical-id migration through unsafe tombstone ledger: ${tombstonePath}.`
|
|
2304
|
+
);
|
|
2305
|
+
}
|
|
2306
|
+
const [stateRoot, tombstoneFile] = await Promise.all([
|
|
2307
|
+
realpath2(deps.stateDir),
|
|
2308
|
+
realpath2(tombstonePath)
|
|
2309
|
+
]);
|
|
2310
|
+
assertPathInsideRoot(stateRoot, tombstoneFile, tombstonePath);
|
|
2311
|
+
return true;
|
|
2312
|
+
}
|
|
2313
|
+
function lockPath(deps) {
|
|
2314
|
+
return path5.join(deps.stateDir, "entity-canonical-id-migration.lock");
|
|
2315
|
+
}
|
|
2316
|
+
async function readState(deps) {
|
|
2317
|
+
await assertSafeMigrationStateFile(deps);
|
|
2318
|
+
let raw;
|
|
2319
|
+
try {
|
|
2320
|
+
raw = await readFile(statePath(deps), "utf-8");
|
|
2321
|
+
} catch (error) {
|
|
2322
|
+
if (isErrnoCode(error, "ENOENT")) return null;
|
|
2323
|
+
throw error;
|
|
2324
|
+
}
|
|
2325
|
+
let parsed;
|
|
2326
|
+
try {
|
|
2327
|
+
parsed = JSON.parse(raw);
|
|
2328
|
+
} catch {
|
|
2329
|
+
throw new Error("Invalid entity canonical-id migration state.");
|
|
2330
|
+
}
|
|
2331
|
+
const result = EntityCanonicalIdMigrationStateSchema.safeParse(parsed);
|
|
2332
|
+
if (!result.success) throw new Error("Invalid entity canonical-id migration state.");
|
|
2333
|
+
return result.data;
|
|
2334
|
+
}
|
|
2335
|
+
async function writeState(deps, state) {
|
|
2336
|
+
const destination = statePath(deps);
|
|
2337
|
+
const temporary = `${destination}.${process.pid}.${randomUUID()}.tmp`;
|
|
2338
|
+
try {
|
|
2339
|
+
await writeFile(temporary, `${JSON.stringify(state, null, 2)}
|
|
2340
|
+
`, "utf-8");
|
|
2341
|
+
await rename(temporary, destination);
|
|
2342
|
+
} finally {
|
|
2343
|
+
await unlink2(temporary).catch(() => void 0);
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
async function publishState(deps, state) {
|
|
2347
|
+
await withEntityCanonicalMutationLock(deps.stateDir, async () => {
|
|
2348
|
+
await writeState(deps, state);
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
async function fileExists(filePath) {
|
|
2352
|
+
try {
|
|
2353
|
+
await access(filePath);
|
|
2354
|
+
return true;
|
|
2355
|
+
} catch (error) {
|
|
2356
|
+
if (isErrnoCode(error, "ENOENT")) return false;
|
|
2357
|
+
throw error;
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
async function sameFileIdentity(leftPath, rightPath) {
|
|
2361
|
+
try {
|
|
2362
|
+
const [left, right] = await Promise.all([statFile(leftPath), statFile(rightPath)]);
|
|
2363
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
2364
|
+
} catch (error) {
|
|
2365
|
+
if (isErrnoCode(error, "ENOENT")) return false;
|
|
2366
|
+
throw error;
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
function assertSafeEntityId(entityId) {
|
|
2370
|
+
if (/[\/\\\0]/.test(entityId)) {
|
|
2371
|
+
throw new Error(`Refusing entity canonical-id migration through unsafe entity id: ${entityId}.`);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
async function assertNotSymlink(filePath, label) {
|
|
2375
|
+
let stat3;
|
|
2376
|
+
try {
|
|
2377
|
+
stat3 = await lstat2(filePath);
|
|
2378
|
+
} catch (error) {
|
|
2379
|
+
if (isErrnoCode(error, "ENOENT")) return;
|
|
2380
|
+
throw error;
|
|
2381
|
+
}
|
|
2382
|
+
if (stat3.isSymbolicLink()) {
|
|
2383
|
+
throw new Error(`Refusing entity canonical-id migration through symlink: ${label}.`);
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
async function assertNotMemorySymlink(filePath) {
|
|
2387
|
+
let stat3;
|
|
2388
|
+
try {
|
|
2389
|
+
stat3 = await lstat2(filePath);
|
|
2390
|
+
} catch (error) {
|
|
2391
|
+
if (isErrnoCode(error, "ENOENT")) return;
|
|
2392
|
+
throw error;
|
|
2393
|
+
}
|
|
2394
|
+
if (stat3.isSymbolicLink()) {
|
|
2395
|
+
throw new Error(`Refusing entity canonical-id migration through symlinked memory: ${filePath}.`);
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
async function assertEntityContentUnchanged(deps, filePath, expectedContent, legacyId) {
|
|
2399
|
+
const latestContent = await deps.readStorageSecureFile(filePath);
|
|
2400
|
+
if (latestContent !== expectedContent) {
|
|
2401
|
+
throw new Error(`Legacy entity id ${legacyId} changed while migration was running; retry migration.`);
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
async function readSafeEntityEntries(entitiesDir) {
|
|
2405
|
+
const rootStat = await lstat2(entitiesDir);
|
|
2406
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
2407
|
+
throw new Error("Refusing entity canonical-id migration through a symlinked or non-directory entities root.");
|
|
2408
|
+
}
|
|
2409
|
+
const entries = await readdir4(entitiesDir);
|
|
2410
|
+
for (const entry of entries) {
|
|
2411
|
+
await assertNotSymlink(path5.join(entitiesDir, entry), entry);
|
|
2412
|
+
}
|
|
2413
|
+
return entries;
|
|
2414
|
+
}
|
|
2415
|
+
async function discoverMappings(deps, knownMappings = {}, blocked = /* @__PURE__ */ new Map()) {
|
|
2416
|
+
const mappings = {};
|
|
2417
|
+
const canonicalOwners = /* @__PURE__ */ new Map();
|
|
2418
|
+
blocked.clear();
|
|
2419
|
+
const entityEntries = await readSafeEntityEntries(deps.entitiesDir);
|
|
2420
|
+
for (const entry of entityEntries) {
|
|
2421
|
+
if (!entry.endsWith(".md")) continue;
|
|
2422
|
+
const legacyId = entry.slice(0, -".md".length);
|
|
2423
|
+
assertSafeEntityId(legacyId);
|
|
2424
|
+
const legacyPath = deps.resolveEntityFilePath(legacyId);
|
|
2425
|
+
if (legacyPath === null) continue;
|
|
2426
|
+
const entityContent = await deps.readStorageSecureFile(legacyPath);
|
|
2427
|
+
if (!/^#[ \t]+[^\r\n \t]/m.test(entityContent) || !/^\*\*Type:\*\*[ \t]*[^\r\n \t]/m.test(entityContent)) {
|
|
2428
|
+
throw new Error(`Cannot migrate malformed entity file ${entry}: missing name heading or type.`);
|
|
2429
|
+
}
|
|
2430
|
+
const entity = deps.parseEntityFile(entityContent);
|
|
2431
|
+
if (!entity.name.trim() || !entity.type.trim()) {
|
|
2432
|
+
throw new Error(`Cannot migrate malformed entity file ${entry}: missing name or type.`);
|
|
2433
|
+
}
|
|
2434
|
+
const canonicalId = deps.normalizeEntityName(entity.name, entity.type);
|
|
2435
|
+
assertSafeEntityId(canonicalId);
|
|
2436
|
+
if (legacyId === canonicalId) continue;
|
|
2437
|
+
const canonicalPath = deps.resolveEntityFilePath(canonicalId);
|
|
2438
|
+
if (canonicalPath === null) continue;
|
|
2439
|
+
const canonicalExists = await fileExists(canonicalPath);
|
|
2440
|
+
if (canonicalExists && await sameFileIdentity(legacyPath, canonicalPath) && knownMappings[legacyId] === canonicalId) continue;
|
|
2441
|
+
if (canonicalExists) {
|
|
2442
|
+
const canonicalContent2 = await deps.readStorageSecureFile(canonicalPath);
|
|
2443
|
+
if (entityContent !== canonicalContent2) {
|
|
2444
|
+
blocked.set(legacyId, canonicalId);
|
|
2445
|
+
continue;
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
canonicalOwners.set(canonicalId, [...canonicalOwners.get(canonicalId) ?? [], legacyId]);
|
|
2449
|
+
mappings[legacyId] = canonicalId;
|
|
2450
|
+
}
|
|
2451
|
+
for (const [canonicalId, claimants] of canonicalOwners) {
|
|
2452
|
+
if (claimants.length < 2) continue;
|
|
2453
|
+
for (const legacyId of claimants) {
|
|
2454
|
+
blocked.set(legacyId, canonicalId);
|
|
2455
|
+
delete mappings[legacyId];
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
return mappings;
|
|
2459
|
+
}
|
|
2460
|
+
async function rewriteReferences(deps, mappings, refreshLock) {
|
|
2461
|
+
let rewroteColdMemory = false;
|
|
2462
|
+
for (let pass = 0; pass < MEMORY_REWRITE_MAX_PASSES; pass += 1) {
|
|
2463
|
+
deps.invalidateAllMemoriesCache();
|
|
2464
|
+
deps.invalidateColdMemoriesCache();
|
|
2465
|
+
const [hotMemories, coldMemories, archivedMemories] = await Promise.all([
|
|
2466
|
+
deps.readAllMemories(),
|
|
2467
|
+
deps.readAllColdMemories(),
|
|
2468
|
+
deps.readArchivedMemories()
|
|
2469
|
+
]);
|
|
2470
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
2471
|
+
let rewroteAny = false;
|
|
2472
|
+
for (const memory of [...hotMemories, ...coldMemories, ...archivedMemories]) {
|
|
2473
|
+
const memoryPath = path5.resolve(memory.path);
|
|
2474
|
+
if (seenPaths.has(memoryPath)) continue;
|
|
2475
|
+
seenPaths.add(memoryPath);
|
|
2476
|
+
const previousEntityRef = memory.frontmatter.entityRef;
|
|
2477
|
+
const canonicalId = previousEntityRef ? mappings[previousEntityRef] : void 0;
|
|
2478
|
+
if (!previousEntityRef || !canonicalId) continue;
|
|
2479
|
+
await assertNotMemorySymlink(memory.path);
|
|
2480
|
+
const observedMemoryFingerprint = await fingerprintPath(memory.path);
|
|
2481
|
+
if (observedMemoryFingerprint === "missing") continue;
|
|
2482
|
+
if (!await refreshLock()) throw new Error("Lost entity canonical-id migration lock.");
|
|
2483
|
+
const latestMemory = await deps.readMemoryByPath(memory.path);
|
|
2484
|
+
if (!latestMemory) continue;
|
|
2485
|
+
const latestMemoryFingerprint = await fingerprintPath(latestMemory.path);
|
|
2486
|
+
if (latestMemoryFingerprint !== observedMemoryFingerprint) continue;
|
|
2487
|
+
const latestPreviousEntityRef = latestMemory.frontmatter.entityRef;
|
|
2488
|
+
const latestCanonicalId = latestPreviousEntityRef ? mappings[latestPreviousEntityRef] : void 0;
|
|
2489
|
+
if (!latestPreviousEntityRef || !latestCanonicalId) continue;
|
|
2490
|
+
const memoryEncrypted = await deps.isEncryptedStorageFile(latestMemory.path);
|
|
2491
|
+
if (await fingerprintPath(latestMemory.path) !== latestMemoryFingerprint) continue;
|
|
2492
|
+
await deps.snapshotBeforeWrite(latestMemory.path, "write");
|
|
2493
|
+
if (await fingerprintPath(latestMemory.path) !== latestMemoryFingerprint) continue;
|
|
2494
|
+
await deps.writeStorageSecureFile(
|
|
2495
|
+
latestMemory.path,
|
|
2496
|
+
deps.serializeMemoryWithEntityRef(latestMemory, latestCanonicalId),
|
|
2497
|
+
memoryEncrypted
|
|
2498
|
+
);
|
|
2499
|
+
await deps.rewriteProjectedMemoryEntityReference(
|
|
2500
|
+
latestMemory.frontmatter.id,
|
|
2501
|
+
latestPreviousEntityRef,
|
|
2502
|
+
latestCanonicalId
|
|
2503
|
+
);
|
|
2504
|
+
rewroteAny = true;
|
|
2505
|
+
rewroteColdMemory ||= latestMemory.path.includes(`${path5.sep}cold${path5.sep}`);
|
|
2506
|
+
}
|
|
2507
|
+
if (!rewroteAny) return { rewroteColdMemory };
|
|
2508
|
+
}
|
|
2509
|
+
throw new Error("Memory references changed while entity canonical-id migration was running; retry migration.");
|
|
2510
|
+
}
|
|
2511
|
+
function rewriteTombstoneLine(line, mappings, normalizedMappings) {
|
|
2512
|
+
let parsed;
|
|
2513
|
+
try {
|
|
2514
|
+
parsed = JSON.parse(line);
|
|
2515
|
+
} catch {
|
|
2516
|
+
return line;
|
|
2517
|
+
}
|
|
2518
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return line;
|
|
2519
|
+
const tombstone = parsed;
|
|
2520
|
+
let changed = false;
|
|
2521
|
+
if (typeof tombstone.entityRef === "string") {
|
|
2522
|
+
const canonicalId = mappings[tombstone.entityRef];
|
|
2523
|
+
if (canonicalId) {
|
|
2524
|
+
tombstone.entityRef = canonicalId;
|
|
2525
|
+
changed = true;
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
if (typeof tombstone.supersessionKey === "string") {
|
|
2529
|
+
const separator = tombstone.supersessionKey.indexOf("::");
|
|
2530
|
+
if (separator < 0) return changed ? JSON.stringify(tombstone) : line;
|
|
2531
|
+
const normalizedLegacyId = normalizeSupersessionKey(tombstone.supersessionKey.slice(0, separator));
|
|
2532
|
+
const canonicalId = normalizedMappings.get(normalizedLegacyId);
|
|
2533
|
+
if (canonicalId) {
|
|
2534
|
+
const canonicalKey = computeSupersessionKey(canonicalId, tombstone.supersessionKey.slice(separator + 2));
|
|
2535
|
+
if (canonicalKey) {
|
|
2536
|
+
tombstone.supersessionKey = canonicalKey;
|
|
2537
|
+
changed = true;
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
return changed ? JSON.stringify(tombstone) : line;
|
|
2542
|
+
}
|
|
2543
|
+
async function rewriteTombstoneReferences(deps, mappings, refreshLock) {
|
|
2544
|
+
const tombstonePath = path5.join(deps.stateDir, "tombstones.jsonl");
|
|
2545
|
+
const normalizedMappings = /* @__PURE__ */ new Map();
|
|
2546
|
+
for (const [legacyId, canonicalId] of Object.entries(mappings)) {
|
|
2547
|
+
const normalizedLegacyId = normalizeSupersessionKey(legacyId);
|
|
2548
|
+
if (normalizedLegacyId && !normalizedMappings.has(normalizedLegacyId)) {
|
|
2549
|
+
normalizedMappings.set(normalizedLegacyId, canonicalId);
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
await withHeldFileLock(
|
|
2553
|
+
path5.join(deps.stateDir, "tombstones.lock"),
|
|
2554
|
+
{
|
|
2555
|
+
staleMs: TOMBSTONE_LOCK_STALE_MS,
|
|
2556
|
+
maxWaitMs: TOMBSTONE_LOCK_MAX_WAIT_MS
|
|
2557
|
+
},
|
|
2558
|
+
async (acquired, tombstoneLock) => {
|
|
2559
|
+
if (!acquired) throw new Error("Timed out waiting for active tombstone migration.");
|
|
2560
|
+
if (!await assertSafeTombstoneFile(deps, tombstonePath)) return;
|
|
2561
|
+
const tombstoneEncrypted = await deps.isEncryptedStorageFile(tombstonePath);
|
|
2562
|
+
const original = await deps.readStorageSecureFile(tombstonePath);
|
|
2563
|
+
let changed = false;
|
|
2564
|
+
const rewritten = original.split("\n").map((line) => {
|
|
2565
|
+
const next = rewriteTombstoneLine(line, mappings, normalizedMappings);
|
|
2566
|
+
changed ||= next !== line;
|
|
2567
|
+
return next;
|
|
2568
|
+
}).join("\n");
|
|
2569
|
+
if (!changed) return;
|
|
2570
|
+
if (!await refreshLock()) throw new Error("Lost entity canonical-id migration lock.");
|
|
2571
|
+
if (!await tombstoneLock.refresh()) throw new Error("Lost tombstone migration lock.");
|
|
2572
|
+
await deps.snapshotBeforeWrite(tombstonePath, "write");
|
|
2573
|
+
await deps.writeStorageSecureFile(tombstonePath, rewritten, tombstoneEncrypted);
|
|
2574
|
+
}
|
|
2575
|
+
);
|
|
2576
|
+
}
|
|
2577
|
+
async function rewriteRelationshipTargets(deps, mappings, refreshLock) {
|
|
2578
|
+
let rewritten = false;
|
|
2579
|
+
const entries = await readSafeEntityEntries(deps.entitiesDir);
|
|
2580
|
+
for (const entry of entries) {
|
|
2581
|
+
if (!entry.endsWith(".md")) continue;
|
|
2582
|
+
const entityPath = deps.resolveEntityFilePath(entry.slice(0, -".md".length));
|
|
2583
|
+
if (entityPath === null) continue;
|
|
2584
|
+
const observedEntityFingerprint = await fingerprintPath(entityPath);
|
|
2585
|
+
if (observedEntityFingerprint === "missing") continue;
|
|
2586
|
+
const entity = deps.parseEntityFile(await deps.readStorageSecureFile(entityPath));
|
|
2587
|
+
const relationships = entity.relationships.map((relationship) => {
|
|
2588
|
+
const target = mappings[relationship.target];
|
|
2589
|
+
return target ? { ...relationship, target } : relationship;
|
|
2590
|
+
});
|
|
2591
|
+
if (relationships.every((relationship, index) => relationship === entity.relationships[index])) continue;
|
|
2592
|
+
const rewroteEntity = await withEntityCanonicalMutationLock(
|
|
2593
|
+
deps.stateDir,
|
|
2594
|
+
async (refreshEntityLock) => {
|
|
2595
|
+
if (!await refreshLock()) throw new Error("Lost entity canonical-id migration lock.");
|
|
2596
|
+
const latestEntity = deps.parseEntityFile(await deps.readStorageSecureFile(entityPath));
|
|
2597
|
+
if (await fingerprintPath(entityPath) !== observedEntityFingerprint) return false;
|
|
2598
|
+
const latestRelationships = latestEntity.relationships.map((relationship) => {
|
|
2599
|
+
const target = mappings[relationship.target];
|
|
2600
|
+
return target ? { ...relationship, target } : relationship;
|
|
2601
|
+
});
|
|
2602
|
+
if (latestRelationships.every((relationship, index) => relationship === latestEntity.relationships[index])) {
|
|
2603
|
+
return false;
|
|
2604
|
+
}
|
|
2605
|
+
const entityEncrypted = await deps.isEncryptedStorageFile(entityPath);
|
|
2606
|
+
if (!await refreshEntityLock()) throw new Error("Lost entity mutation lock.");
|
|
2607
|
+
if (await fingerprintPath(entityPath) !== observedEntityFingerprint) return false;
|
|
2608
|
+
await deps.snapshotBeforeWrite(entityPath, "write");
|
|
2609
|
+
if (await fingerprintPath(entityPath) !== observedEntityFingerprint) return false;
|
|
2610
|
+
await deps.writeStorageSecureFile(
|
|
2611
|
+
entityPath,
|
|
2612
|
+
deps.serializeEntityFile({ ...latestEntity, relationships: latestRelationships }),
|
|
2613
|
+
entityEncrypted
|
|
2614
|
+
);
|
|
2615
|
+
return true;
|
|
2616
|
+
}
|
|
2617
|
+
);
|
|
2618
|
+
rewritten ||= rewroteEntity;
|
|
2619
|
+
}
|
|
2620
|
+
return rewritten;
|
|
2621
|
+
}
|
|
2622
|
+
async function rewriteKnownReferences(deps, mappings, refreshLock) {
|
|
2623
|
+
await rewriteRelationshipTargets(deps, mappings, refreshLock);
|
|
2624
|
+
let rewroteColdMemory = (await rewriteReferences(deps, mappings, refreshLock)).rewroteColdMemory;
|
|
2625
|
+
await rewriteTombstoneReferences(deps, mappings, refreshLock);
|
|
2626
|
+
await deps.rewriteProjectedEntityReferences(mappings);
|
|
2627
|
+
const finalMemoryPass = await rewriteReferences(deps, mappings, refreshLock);
|
|
2628
|
+
rewroteColdMemory ||= finalMemoryPass.rewroteColdMemory;
|
|
2629
|
+
return rewroteColdMemory;
|
|
2630
|
+
}
|
|
2631
|
+
function wouldCreateMappingCycle(mappings, legacyId, canonicalId) {
|
|
2632
|
+
let current = canonicalId;
|
|
2633
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2634
|
+
while (true) {
|
|
2635
|
+
if (current === legacyId) return true;
|
|
2636
|
+
if (seen.has(current)) return false;
|
|
2637
|
+
seen.add(current);
|
|
2638
|
+
const next = mappings[current];
|
|
2639
|
+
if (!next || next === current) return false;
|
|
2640
|
+
current = next;
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
function rejectMappingCycles(existing, discovered) {
|
|
2644
|
+
const accepted = {};
|
|
2645
|
+
const merged = { ...existing };
|
|
2646
|
+
for (const [legacyId, canonicalId] of Object.entries(discovered)) {
|
|
2647
|
+
if (merged[legacyId] === void 0 && wouldCreateMappingCycle(merged, legacyId, canonicalId)) continue;
|
|
2648
|
+
accepted[legacyId] = canonicalId;
|
|
2649
|
+
merged[legacyId] = canonicalId;
|
|
2650
|
+
}
|
|
2651
|
+
return accepted;
|
|
2652
|
+
}
|
|
2653
|
+
function mergeDiscoveredMappings(existing, discovered) {
|
|
2654
|
+
const merged = { ...existing };
|
|
2655
|
+
const canonicalOwners = /* @__PURE__ */ new Map();
|
|
2656
|
+
for (const [legacyId, canonicalId] of Object.entries(discovered)) {
|
|
2657
|
+
const previousMapping = merged[legacyId];
|
|
2658
|
+
if (previousMapping !== void 0 && previousMapping !== canonicalId) {
|
|
2659
|
+
let hop = canonicalId;
|
|
2660
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2661
|
+
while (hop !== void 0 && hop !== previousMapping && !seen.has(hop)) {
|
|
2662
|
+
seen.add(hop);
|
|
2663
|
+
hop = merged[hop];
|
|
2664
|
+
}
|
|
2665
|
+
if (hop !== previousMapping) {
|
|
2666
|
+
throw new Error(`Legacy entity id ${legacyId} changed canonical target during migration.`);
|
|
2667
|
+
}
|
|
2668
|
+
continue;
|
|
2669
|
+
}
|
|
2670
|
+
if (previousMapping === void 0 && wouldCreateMappingCycle(merged, legacyId, canonicalId)) continue;
|
|
2671
|
+
const previousOwner = canonicalOwners.get(canonicalId);
|
|
2672
|
+
if (previousOwner !== void 0 && previousOwner !== legacyId) {
|
|
2673
|
+
throw new Error(
|
|
2674
|
+
`Cannot migrate legacy entity ids ${previousOwner} and ${legacyId}: both normalize to ${canonicalId}.`
|
|
2675
|
+
);
|
|
2676
|
+
}
|
|
2677
|
+
merged[legacyId] = canonicalId;
|
|
2678
|
+
canonicalOwners.set(canonicalId, legacyId);
|
|
2679
|
+
}
|
|
2680
|
+
return merged;
|
|
2681
|
+
}
|
|
2682
|
+
function collapseMappings(mappings) {
|
|
2683
|
+
const collapsed = {};
|
|
2684
|
+
for (const legacyId of Object.keys(mappings)) {
|
|
2685
|
+
let current = legacyId;
|
|
2686
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2687
|
+
while (true) {
|
|
2688
|
+
const next = mappings[current];
|
|
2689
|
+
if (!next || next === current) break;
|
|
2690
|
+
if (seen.has(current)) {
|
|
2691
|
+
throw new Error(`Entity canonical-id migration mapping cycle includes ${legacyId}.`);
|
|
2692
|
+
}
|
|
2693
|
+
seen.add(current);
|
|
2694
|
+
current = next;
|
|
2695
|
+
}
|
|
2696
|
+
if (current !== legacyId) collapsed[legacyId] = current;
|
|
2697
|
+
}
|
|
2698
|
+
return collapsed;
|
|
2699
|
+
}
|
|
2700
|
+
async function migrateEntityFilePair(deps, legacyId, canonicalId, refreshMigrationLock) {
|
|
2701
|
+
return withEntityCanonicalMutationLock(deps.stateDir, async (refreshEntityLock) => {
|
|
2702
|
+
assertSafeEntityId(legacyId);
|
|
2703
|
+
assertSafeEntityId(canonicalId);
|
|
2704
|
+
const legacyPath = deps.resolveEntityFilePath(legacyId);
|
|
2705
|
+
const canonicalPath = deps.resolveEntityFilePath(canonicalId);
|
|
2706
|
+
if (legacyPath === null || canonicalPath === null) {
|
|
2707
|
+
throw new Error("Invalid entity canonical-id migration path.");
|
|
2708
|
+
}
|
|
2709
|
+
await assertNotSymlink(legacyPath, legacyId);
|
|
2710
|
+
await assertNotSymlink(canonicalPath, canonicalId);
|
|
2711
|
+
const legacyExists = await fileExists(legacyPath);
|
|
2712
|
+
const canonicalExists = await fileExists(canonicalPath);
|
|
2713
|
+
if (!legacyExists && !canonicalExists) return { deferred: true, progressed: false };
|
|
2714
|
+
if (legacyExists && canonicalExists) {
|
|
2715
|
+
if (await sameFileIdentity(legacyPath, canonicalPath)) return { deferred: false, progressed: false };
|
|
2716
|
+
const [legacyContent, canonicalContent2, legacyEncrypted, canonicalEncrypted] = await Promise.all([
|
|
2717
|
+
deps.readStorageSecureFile(legacyPath),
|
|
2718
|
+
deps.readStorageSecureFile(canonicalPath),
|
|
2719
|
+
deps.isEncryptedStorageFile(legacyPath),
|
|
2720
|
+
deps.isEncryptedStorageFile(canonicalPath)
|
|
2721
|
+
]);
|
|
2722
|
+
if (legacyContent !== canonicalContent2) {
|
|
2723
|
+
return { deferred: false, progressed: false, blocked: true };
|
|
2724
|
+
}
|
|
2725
|
+
if (legacyEncrypted && !canonicalEncrypted) {
|
|
2726
|
+
if (!await refreshMigrationLock() || !await refreshEntityLock()) {
|
|
2727
|
+
throw new Error("Lost entity canonical-id migration lock.");
|
|
2728
|
+
}
|
|
2729
|
+
await deps.snapshotBeforeWrite(canonicalPath, "write");
|
|
2730
|
+
await deps.writeStorageSecureFile(canonicalPath, legacyContent, true);
|
|
2731
|
+
if (await deps.readStorageSecureFile(canonicalPath) !== legacyContent) {
|
|
2732
|
+
throw new Error(`Cannot verify migrated entity id ${canonicalId}.`);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
if (!await refreshMigrationLock() || !await refreshEntityLock()) {
|
|
2736
|
+
throw new Error("Lost entity canonical-id migration lock.");
|
|
2737
|
+
}
|
|
2738
|
+
await deps.snapshotBeforeWrite(legacyPath, "write");
|
|
2739
|
+
await assertNotSymlink(legacyPath, legacyId);
|
|
2740
|
+
await assertEntityContentUnchanged(deps, legacyPath, legacyContent, legacyId);
|
|
2741
|
+
await unlink2(legacyPath);
|
|
2742
|
+
return { deferred: false, progressed: true };
|
|
2743
|
+
}
|
|
2744
|
+
if (!legacyExists) return { deferred: false, progressed: false };
|
|
2745
|
+
if (!await refreshMigrationLock() || !await refreshEntityLock()) {
|
|
2746
|
+
throw new Error("Lost entity canonical-id migration lock.");
|
|
2747
|
+
}
|
|
2748
|
+
const entityEncrypted = await deps.isEncryptedStorageFile(legacyPath);
|
|
2749
|
+
const entityContent = await deps.readStorageSecureFile(legacyPath);
|
|
2750
|
+
await assertNotSymlink(legacyPath, legacyId);
|
|
2751
|
+
await assertEntityContentUnchanged(deps, legacyPath, entityContent, legacyId);
|
|
2752
|
+
let canonicalFingerprintAfterWrite = null;
|
|
2753
|
+
try {
|
|
2754
|
+
await deps.writeStorageSecureFile(canonicalPath, entityContent, entityEncrypted);
|
|
2755
|
+
canonicalFingerprintAfterWrite = await fingerprintPath(canonicalPath);
|
|
2756
|
+
if (await deps.readStorageSecureFile(canonicalPath) !== entityContent) {
|
|
2757
|
+
throw new Error(`Cannot verify migrated entity id ${canonicalId}.`);
|
|
2758
|
+
}
|
|
2759
|
+
if (!await refreshMigrationLock() || !await refreshEntityLock()) {
|
|
2760
|
+
throw new Error("Lost entity canonical-id migration lock.");
|
|
2761
|
+
}
|
|
2762
|
+
await deps.snapshotBeforeWrite(legacyPath, "write");
|
|
2763
|
+
await assertNotSymlink(legacyPath, legacyId);
|
|
2764
|
+
await assertEntityContentUnchanged(deps, legacyPath, entityContent, legacyId);
|
|
2765
|
+
await unlink2(legacyPath);
|
|
2766
|
+
} catch (error) {
|
|
2767
|
+
if (!canonicalExists && canonicalFingerprintAfterWrite !== null) {
|
|
2768
|
+
try {
|
|
2769
|
+
if (await fingerprintPath(canonicalPath) === canonicalFingerprintAfterWrite) {
|
|
2770
|
+
await assertNotSymlink(canonicalPath, canonicalId);
|
|
2771
|
+
await unlink2(canonicalPath);
|
|
2772
|
+
}
|
|
2773
|
+
} catch {
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
throw error;
|
|
2777
|
+
}
|
|
2778
|
+
return { deferred: false, progressed: true };
|
|
2779
|
+
});
|
|
2780
|
+
}
|
|
2781
|
+
async function migrateLegacyEntityCanonicalIds(deps) {
|
|
2782
|
+
return withHeldFileLock(
|
|
2783
|
+
lockPath(deps),
|
|
2784
|
+
{
|
|
2785
|
+
staleMs: ENTITY_CANONICAL_ID_MIGRATION_LOCK_STALE_MS,
|
|
2786
|
+
maxWaitMs: ENTITY_CANONICAL_ID_MIGRATION_LOCK_MAX_WAIT_MS
|
|
2787
|
+
},
|
|
2788
|
+
async (acquired, lock) => {
|
|
2789
|
+
if (!acquired) throw new Error("Timed out waiting for active entity canonical-id migration.");
|
|
2790
|
+
await deps.validateMemoryScanRoots?.();
|
|
2791
|
+
await readSafeEntityEntries(deps.entitiesDir);
|
|
2792
|
+
const reconcileMarkerPath = path5.join(deps.stateDir, ENTITY_CANONICAL_ID_RECONCILE_MARKER);
|
|
2793
|
+
const consumingMarkerPath = path5.join(deps.stateDir, ENTITY_CANONICAL_ID_RECONCILE_CONSUMING_MARKER);
|
|
2794
|
+
let reconcileRequested = await fileExists(consumingMarkerPath);
|
|
2795
|
+
if (await fileExists(reconcileMarkerPath)) {
|
|
2796
|
+
try {
|
|
2797
|
+
await rename(reconcileMarkerPath, consumingMarkerPath);
|
|
2798
|
+
reconcileRequested = true;
|
|
2799
|
+
} catch (error) {
|
|
2800
|
+
if (!isErrnoCode(error, "ENOENT")) throw error;
|
|
2801
|
+
}
|
|
2523
2802
|
}
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
updated: "",
|
|
2531
|
-
extraFrontmatterLines: [],
|
|
2532
|
-
preSectionLines: [],
|
|
2533
|
-
facts: [],
|
|
2534
|
-
summary: void 0,
|
|
2535
|
-
synthesis: void 0,
|
|
2536
|
-
synthesisUpdatedAt: void 0,
|
|
2537
|
-
synthesisTimelineCount: void 0,
|
|
2538
|
-
synthesisStructuredFactCount: void 0,
|
|
2539
|
-
synthesisStructuredFactDigest: void 0,
|
|
2540
|
-
synthesisVersion: void 0,
|
|
2541
|
-
timeline: [],
|
|
2542
|
-
relationships: [],
|
|
2543
|
-
activity: [],
|
|
2544
|
-
aliases: [],
|
|
2545
|
-
structuredSections: [],
|
|
2546
|
-
extraSections: []
|
|
2803
|
+
let state = await readState(deps);
|
|
2804
|
+
if (!state) {
|
|
2805
|
+
state = {
|
|
2806
|
+
version: ENTITY_CANONICAL_ID_MIGRATION_VERSION,
|
|
2807
|
+
complete: false,
|
|
2808
|
+
mappings: {}
|
|
2547
2809
|
};
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2810
|
+
}
|
|
2811
|
+
const blocked = /* @__PURE__ */ new Map();
|
|
2812
|
+
const finish = async () => {
|
|
2813
|
+
if (blocked.size > 0) {
|
|
2814
|
+
const pairs = [...blocked].map(([legacyId, canonicalId]) => `${legacyId} -> ${canonicalId}`).join(", ");
|
|
2815
|
+
log.warn(
|
|
2816
|
+
`entity canonical-id migration skipped ${blocked.size} unresolvable pair(s): ${pairs}. Both files were kept and the legacy id still resolves; merge or delete one side to migrate it.`
|
|
2817
|
+
);
|
|
2818
|
+
}
|
|
2819
|
+
if (reconcileRequested) {
|
|
2820
|
+
await unlink2(consumingMarkerPath).catch((error) => {
|
|
2821
|
+
if (!isErrnoCode(error, "ENOENT")) throw error;
|
|
2822
|
+
});
|
|
2823
|
+
}
|
|
2824
|
+
const completionFingerprint = await deps.readMigrationFingerprint?.();
|
|
2825
|
+
if (await fileExists(reconcileMarkerPath)) return void 0;
|
|
2826
|
+
return completionFingerprint;
|
|
2827
|
+
};
|
|
2828
|
+
const pruneBlocked = async () => {
|
|
2829
|
+
const current = state;
|
|
2830
|
+
if (!current) return false;
|
|
2831
|
+
let revived = false;
|
|
2832
|
+
const parked = { ...current.blocked ?? {} };
|
|
2833
|
+
const active = {};
|
|
2834
|
+
for (const [legacyId, canonicalId] of Object.entries(current.mappings)) {
|
|
2835
|
+
if (!blocked.has(legacyId)) active[legacyId] = canonicalId;
|
|
2836
|
+
}
|
|
2837
|
+
for (const [legacyId, canonicalId] of blocked) parked[legacyId] = canonicalId;
|
|
2838
|
+
for (const [legacyId, canonicalId] of Object.entries(parked)) {
|
|
2839
|
+
if (blocked.has(legacyId)) continue;
|
|
2840
|
+
delete parked[legacyId];
|
|
2841
|
+
if (active[legacyId] !== void 0) continue;
|
|
2842
|
+
const legacyPath = deps.resolveEntityFilePath(legacyId);
|
|
2843
|
+
if (legacyPath !== null && await fileExists(legacyPath)) continue;
|
|
2844
|
+
active[legacyId] = canonicalId;
|
|
2845
|
+
revived = true;
|
|
2846
|
+
}
|
|
2847
|
+
const collapsed = collapseMappings(active);
|
|
2848
|
+
for (const [legacyId, canonicalId] of Object.entries(parked)) {
|
|
2849
|
+
const moved = collapsed[canonicalId];
|
|
2850
|
+
if (moved !== void 0 && moved !== legacyId) parked[legacyId] = moved;
|
|
2851
|
+
}
|
|
2852
|
+
const same = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([key, value]) => b[key] === value);
|
|
2853
|
+
if (same(collapsed, current.mappings) && same(parked, current.blocked ?? {})) return revived;
|
|
2854
|
+
state = { ...current, mappings: collapsed, blocked: parked };
|
|
2855
|
+
await publishState(deps, state);
|
|
2856
|
+
return revived;
|
|
2857
|
+
};
|
|
2858
|
+
const previousMappings = state.mappings;
|
|
2859
|
+
const collapsedMappings = collapseMappings(previousMappings);
|
|
2860
|
+
const mappingsChanged = Object.keys(collapsedMappings).length !== Object.keys(previousMappings).length || Object.entries(collapsedMappings).some(
|
|
2861
|
+
([legacyId, canonicalId]) => previousMappings[legacyId] !== canonicalId
|
|
2862
|
+
);
|
|
2863
|
+
state = { ...state, mappings: collapsedMappings };
|
|
2864
|
+
for (let migrationPass = 0; migrationPass < ENTITY_MAPPING_RESCAN_MAX_PASSES; migrationPass += 1) {
|
|
2865
|
+
if (!await lock.refresh()) throw new Error("Lost entity canonical-id migration lock.");
|
|
2866
|
+
const discoveredBefore = rejectMappingCycles(
|
|
2867
|
+
state.mappings,
|
|
2868
|
+
await discoverMappings(deps, state.mappings, blocked)
|
|
2869
|
+
);
|
|
2870
|
+
const revivedBefore = await pruneBlocked();
|
|
2871
|
+
const mergedBefore = collapseMappings(mergeDiscoveredMappings(state.mappings, discoveredBefore));
|
|
2872
|
+
if (state.complete && Object.keys(discoveredBefore).length === 0 && !mappingsChanged && !revivedBefore) {
|
|
2873
|
+
if (reconcileRequested && Object.keys(state.mappings).length > 0) {
|
|
2874
|
+
const rewroteColdMemory2 = await rewriteKnownReferences(
|
|
2875
|
+
deps,
|
|
2876
|
+
state.mappings,
|
|
2877
|
+
() => lock.refresh()
|
|
2599
2878
|
);
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
key: section.key,
|
|
2605
|
-
title: section.title,
|
|
2606
|
-
facts: [...new Set(section.facts.map((fact) => fact.trim()).filter((fact) => fact.length > 0))]
|
|
2607
|
-
});
|
|
2608
|
-
continue;
|
|
2609
|
-
}
|
|
2610
|
-
const mergedFacts = new Set(existingSection.facts.map((fact) => fact.trim()));
|
|
2611
|
-
for (const fact of section.facts) {
|
|
2612
|
-
const trimmed = fact.trim();
|
|
2613
|
-
if (!trimmed) continue;
|
|
2614
|
-
mergedFacts.add(trimmed);
|
|
2615
|
-
}
|
|
2616
|
-
existingSection.facts = Array.from(mergedFacts);
|
|
2617
|
-
if (!existingSection.title.trim() && section.title.trim()) {
|
|
2618
|
-
existingSection.title = section.title;
|
|
2619
|
-
}
|
|
2620
|
-
}
|
|
2621
|
-
mergedEntity.structuredSections = Array.from(mergedStructuredSectionMap.values());
|
|
2622
|
-
mergedEntity.extraFrontmatterLines.push(...parsed.extraFrontmatterLines ?? []);
|
|
2623
|
-
mergedEntity.preSectionLines.push(...parsed.preSectionLines ?? []);
|
|
2624
|
-
mergedEntity.extraSections.push(...(parsed.extraSections ?? []).map((section) => ({
|
|
2625
|
-
title: section.title,
|
|
2626
|
-
lines: [...section.lines]
|
|
2627
|
-
})));
|
|
2628
|
-
} catch (err) {
|
|
2629
|
-
if (err instanceof SecureStoreLockedError) throw err;
|
|
2630
|
-
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
2879
|
+
deps.invalidateKnowledgeIndexCache();
|
|
2880
|
+
deps.invalidateAllMemoriesCache();
|
|
2881
|
+
if (rewroteColdMemory2) deps.invalidateColdMemoriesCache();
|
|
2882
|
+
deps.bumpMemoryStatusVersion();
|
|
2631
2883
|
}
|
|
2884
|
+
return finish();
|
|
2632
2885
|
}
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
entry.timestamp,
|
|
2637
|
-
entry.source ?? "",
|
|
2638
|
-
entry.sessionKey ?? "",
|
|
2639
|
-
entry.principal ?? "",
|
|
2640
|
-
entry.text
|
|
2641
|
-
]);
|
|
2642
|
-
if (timelineKeys.has(key)) return false;
|
|
2643
|
-
timelineKeys.add(key);
|
|
2644
|
-
return true;
|
|
2645
|
-
});
|
|
2646
|
-
const relKeys = /* @__PURE__ */ new Set();
|
|
2647
|
-
mergedEntity.relationships = mergedEntity.relationships.filter((r) => {
|
|
2648
|
-
const key = `${r.target}::${r.label}`;
|
|
2649
|
-
if (relKeys.has(key)) return false;
|
|
2650
|
-
relKeys.add(key);
|
|
2651
|
-
return true;
|
|
2652
|
-
});
|
|
2653
|
-
const actKeys = /* @__PURE__ */ new Set();
|
|
2654
|
-
mergedEntity.activity = mergedEntity.activity.filter((a) => {
|
|
2655
|
-
const key = `${a.date}::${a.note}`;
|
|
2656
|
-
if (actKeys.has(key)) return false;
|
|
2657
|
-
actKeys.add(key);
|
|
2658
|
-
return true;
|
|
2659
|
-
}).sort((a, b) => b.date.localeCompare(a.date));
|
|
2660
|
-
mergedEntity.aliases = [...new Set(mergedEntity.aliases)];
|
|
2661
|
-
mergedEntity.structuredSections = sortStructuredSectionsBySchema(
|
|
2662
|
-
mergedEntity.type,
|
|
2663
|
-
mergedEntity.structuredSections ?? [],
|
|
2664
|
-
this.deps.entitySchemas
|
|
2665
|
-
);
|
|
2666
|
-
mergedEntity.facts = compileEntityFacts(mergedEntity.timeline, mergedEntity.structuredSections);
|
|
2667
|
-
const extraSectionKeys = /* @__PURE__ */ new Set();
|
|
2668
|
-
mergedEntity.extraSections = (mergedEntity.extraSections ?? []).filter((section) => {
|
|
2669
|
-
const key = `${section.title}::${section.lines.join("\n")}`;
|
|
2670
|
-
if (extraSectionKeys.has(key)) return false;
|
|
2671
|
-
extraSectionKeys.add(key);
|
|
2672
|
-
return true;
|
|
2673
|
-
});
|
|
2674
|
-
if (!mergedEntity.name) {
|
|
2675
|
-
const dashIdx = canonical.indexOf("-");
|
|
2676
|
-
mergedEntity.name = dashIdx !== -1 ? canonical.slice(dashIdx + 1) : canonical;
|
|
2886
|
+
if (state.complete || Object.keys(discoveredBefore).length > 0 || Object.keys(mergedBefore).length !== Object.keys(state.mappings).length) {
|
|
2887
|
+
state = { ...state, complete: false, mappings: mergedBefore };
|
|
2888
|
+
await publishState(deps, state);
|
|
2677
2889
|
}
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2890
|
+
if (Object.keys(state.mappings).length === 0) {
|
|
2891
|
+
if (!await lock.refresh()) throw new Error("Lost entity canonical-id migration lock.");
|
|
2892
|
+
const discoveredAfter2 = rejectMappingCycles(
|
|
2893
|
+
state.mappings,
|
|
2894
|
+
await discoverMappings(deps, state.mappings, blocked)
|
|
2895
|
+
);
|
|
2896
|
+
const revivedInRescan = await pruneBlocked();
|
|
2897
|
+
if (Object.keys(discoveredAfter2).length === 0 && !revivedInRescan) {
|
|
2898
|
+
await publishState(deps, { ...state, complete: true });
|
|
2899
|
+
return finish();
|
|
2900
|
+
}
|
|
2901
|
+
state = {
|
|
2902
|
+
...state,
|
|
2903
|
+
complete: false,
|
|
2904
|
+
mappings: collapseMappings(mergeDiscoveredMappings(state.mappings, discoveredAfter2))
|
|
2905
|
+
};
|
|
2906
|
+
await publishState(deps, state);
|
|
2907
|
+
continue;
|
|
2908
|
+
}
|
|
2909
|
+
let pendingMappings = Object.entries(state.mappings);
|
|
2910
|
+
while (pendingMappings.length > 0) {
|
|
2911
|
+
const deferredMappings = [];
|
|
2912
|
+
let progressed = false;
|
|
2913
|
+
for (const [legacyId, canonicalId] of pendingMappings) {
|
|
2914
|
+
const result = await migrateEntityFilePair(
|
|
2915
|
+
deps,
|
|
2916
|
+
legacyId,
|
|
2917
|
+
canonicalId,
|
|
2918
|
+
() => lock.refresh()
|
|
2919
|
+
);
|
|
2920
|
+
if (result.blocked) blocked.set(legacyId, canonicalId);
|
|
2921
|
+
else if (result.deferred) deferredMappings.push([legacyId, canonicalId]);
|
|
2922
|
+
progressed ||= result.progressed;
|
|
2923
|
+
}
|
|
2924
|
+
if (deferredMappings.length === 0) break;
|
|
2925
|
+
if (!progressed) {
|
|
2926
|
+
const [legacyId, canonicalId] = deferredMappings[0];
|
|
2927
|
+
throw new Error(
|
|
2928
|
+
`Cannot migrate legacy entity id ${legacyId}: both files are missing and no intermediate mapping can advance migration.`
|
|
2929
|
+
);
|
|
2691
2930
|
}
|
|
2931
|
+
pendingMappings = deferredMappings;
|
|
2932
|
+
}
|
|
2933
|
+
await pruneBlocked();
|
|
2934
|
+
const rewroteColdMemory = await rewriteKnownReferences(
|
|
2935
|
+
deps,
|
|
2936
|
+
state.mappings,
|
|
2937
|
+
() => lock.refresh()
|
|
2938
|
+
);
|
|
2939
|
+
deps.invalidateKnowledgeIndexCache();
|
|
2940
|
+
deps.invalidateAllMemoriesCache();
|
|
2941
|
+
if (rewroteColdMemory) deps.invalidateColdMemoriesCache();
|
|
2942
|
+
deps.bumpMemoryStatusVersion();
|
|
2943
|
+
if (!await lock.refresh()) throw new Error("Lost entity canonical-id migration lock.");
|
|
2944
|
+
const discoveredAfter = rejectMappingCycles(
|
|
2945
|
+
state.mappings,
|
|
2946
|
+
await discoverMappings(deps, state.mappings, blocked)
|
|
2947
|
+
);
|
|
2948
|
+
const revivedAfterRewrite = await pruneBlocked();
|
|
2949
|
+
if (Object.keys(discoveredAfter).length === 0 && !revivedAfterRewrite) {
|
|
2950
|
+
await publishState(deps, { ...state, complete: true });
|
|
2951
|
+
return finish();
|
|
2692
2952
|
}
|
|
2953
|
+
state = {
|
|
2954
|
+
...state,
|
|
2955
|
+
complete: false,
|
|
2956
|
+
mappings: collapseMappings(mergeDiscoveredMappings(state.mappings, discoveredAfter))
|
|
2957
|
+
};
|
|
2958
|
+
await publishState(deps, state);
|
|
2693
2959
|
}
|
|
2694
|
-
|
|
2695
|
-
if (err instanceof SecureStoreLockedError || !isErrnoCode(err, "ENOENT")) throw err;
|
|
2960
|
+
throw new Error("Entity canonical-id migration mappings changed while migration was running; retry migration.");
|
|
2696
2961
|
}
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
};
|
|
2962
|
+
);
|
|
2963
|
+
}
|
|
2700
2964
|
|
|
2701
|
-
// src/storage/
|
|
2702
|
-
import {
|
|
2703
|
-
|
|
2704
|
-
var IdentityContinuityStore = class {
|
|
2965
|
+
// src/storage/entity-ref-repair.ts
|
|
2966
|
+
import { unlink as unlink3 } from "fs/promises";
|
|
2967
|
+
var EntityRefRepair = class {
|
|
2705
2968
|
constructor(deps) {
|
|
2706
2969
|
this.deps = deps;
|
|
2707
2970
|
}
|
|
2708
2971
|
deps;
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2972
|
+
/**
|
|
2973
|
+
* Shared lookup+apply core for the write-time resurrection gate (#1579 /
|
|
2974
|
+
* #2213): fail-open, add-only; writeMemory's gate + chunk-repair re-gate
|
|
2975
|
+
* consult this one implementation (rule 43).
|
|
2976
|
+
*/
|
|
2977
|
+
async gate(fm, hashSource, structuredAttributes) {
|
|
2978
|
+
if (!this.deps.tombstonesEnabled()) return null;
|
|
2714
2979
|
try {
|
|
2715
|
-
|
|
2980
|
+
const store = await this.deps.getTombstoneStore();
|
|
2981
|
+
const supersessionKeys = fm.entityRef && structuredAttributes ? supersessionKeysForFact({ entityRef: fm.entityRef, structuredAttributes }) : [];
|
|
2982
|
+
const match = applyTombstoneResurrectionGate(store, fm, {
|
|
2983
|
+
normalizedText: ContentHashIndex.normalizeContent(hashSource),
|
|
2984
|
+
supersessionKeys,
|
|
2985
|
+
namespace: this.deps.tombstonesNamespace()
|
|
2986
|
+
});
|
|
2987
|
+
if (match) {
|
|
2988
|
+
log.info(
|
|
2989
|
+
`tombstone: blocked resurrection of fact ${fm.id} (tier=${match.matchedTier}, tombstone=${match.tombstoneId}, reason=${match.reason})`
|
|
2990
|
+
);
|
|
2991
|
+
}
|
|
2992
|
+
return match;
|
|
2716
2993
|
} catch (err) {
|
|
2717
|
-
|
|
2994
|
+
log.warn(`tombstone lookup failed for fact ${fm.id} (fail-open): ${err}`);
|
|
2718
2995
|
return null;
|
|
2719
2996
|
}
|
|
2720
2997
|
}
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
const
|
|
2724
|
-
const nowIso = now.toISOString();
|
|
2725
|
-
const date = nowIso.slice(0, 10);
|
|
2726
|
-
const id = this.deps.generateId("incident");
|
|
2727
|
-
const incident = createContinuityIncidentRecord(id, input, nowIso);
|
|
2728
|
-
const filePath = path5.join(this.deps.identityIncidentsDir, `${date}-${id}.md`);
|
|
2729
|
-
await this.deps.writeStorageSecureFile(filePath, serializeContinuityIncident(incident));
|
|
2730
|
-
return { ...incident, filePath };
|
|
2731
|
-
}
|
|
2732
|
-
async readContinuityIncidents(limit = 200, state = "all") {
|
|
2733
|
-
const normalizedLimit = Number.isFinite(limit) ? Math.floor(limit) : 0;
|
|
2734
|
-
const cappedLimit = Math.max(0, normalizedLimit);
|
|
2735
|
-
if (cappedLimit === 0) return [];
|
|
2998
|
+
/** Post-persist repair delegate — see the references module for semantics. */
|
|
2999
|
+
async repair(filePath, fm, rawRef, refIds, body, opts = {}) {
|
|
3000
|
+
const refBeforeRepair = fm.entityRef;
|
|
2736
3001
|
try {
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
3002
|
+
await repairEntityRefAfterJournalMove({
|
|
3003
|
+
stateDir: this.deps.stateDir,
|
|
3004
|
+
currentIds: () => this.deps.currentHistoricalIds(),
|
|
3005
|
+
idsAtResolve: refIds,
|
|
3006
|
+
rawRef,
|
|
3007
|
+
frontmatter: fm,
|
|
3008
|
+
rewrite: async () => {
|
|
3009
|
+
if (opts.regateFact && fm.category === "fact") {
|
|
3010
|
+
await this.gate(fm, body, void 0);
|
|
3011
|
+
}
|
|
3012
|
+
await this.deps.writeTombstoneBlockedMemory(
|
|
3013
|
+
filePath,
|
|
3014
|
+
`${this.deps.serializeFrontmatter(fm)}
|
|
3015
|
+
|
|
3016
|
+
${body}
|
|
3017
|
+
`,
|
|
3018
|
+
fm,
|
|
3019
|
+
body
|
|
3020
|
+
);
|
|
3021
|
+
this.deps.invalidateAllMemoriesCache();
|
|
2750
3022
|
}
|
|
2751
|
-
}
|
|
2752
|
-
return incidents;
|
|
2753
|
-
} catch (err) {
|
|
2754
|
-
if (err instanceof SecureStoreLockedError) throw err;
|
|
2755
|
-
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
2756
|
-
return [];
|
|
2757
|
-
}
|
|
2758
|
-
}
|
|
2759
|
-
async closeContinuityIncident(id, closure) {
|
|
2760
|
-
const directFilePath = await this.findContinuityIncidentFilePathById(id);
|
|
2761
|
-
const target = directFilePath ? await this.readContinuityIncidentFile(directFilePath) : null;
|
|
2762
|
-
if (!target || !directFilePath) return null;
|
|
2763
|
-
if (target.state === "closed") return target;
|
|
2764
|
-
const closed = closeContinuityIncidentRecord(target, closure, (/* @__PURE__ */ new Date()).toISOString());
|
|
2765
|
-
await this.deps.writeStorageSecureFile(directFilePath, serializeContinuityIncident(closed));
|
|
2766
|
-
return { ...closed, filePath: directFilePath };
|
|
2767
|
-
}
|
|
2768
|
-
async writeIdentityAudit(period, key, content) {
|
|
2769
|
-
await this.deps.ensureDirectories();
|
|
2770
|
-
const safeKey = this.sanitizeIdentityAuditKey(key);
|
|
2771
|
-
const dir = period === "weekly" ? this.deps.identityAuditsWeeklyDir : this.deps.identityAuditsMonthlyDir;
|
|
2772
|
-
const filePath = path5.join(dir, `${safeKey}.md`);
|
|
2773
|
-
await this.deps.writeStorageSecureFile(filePath, content);
|
|
2774
|
-
return filePath;
|
|
2775
|
-
}
|
|
2776
|
-
async readIdentityAudit(period, key) {
|
|
2777
|
-
try {
|
|
2778
|
-
const safeKey = this.sanitizeIdentityAuditKey(key);
|
|
2779
|
-
const dir = period === "weekly" ? this.deps.identityAuditsWeeklyDir : this.deps.identityAuditsMonthlyDir;
|
|
2780
|
-
return await this.deps.readStorageSecureFile(path5.join(dir, `${safeKey}.md`));
|
|
3023
|
+
});
|
|
2781
3024
|
} catch (err) {
|
|
2782
|
-
if (
|
|
2783
|
-
|
|
2784
|
-
|
|
3025
|
+
if (opts.onFailRestore) {
|
|
3026
|
+
await this.restore(opts.onFailRestore);
|
|
3027
|
+
} else if (opts.onFailRemove) {
|
|
3028
|
+
await unlink3(opts.onFailRemove).catch(() => void 0);
|
|
3029
|
+
this.deps.invalidateAllMemoriesCache();
|
|
3030
|
+
}
|
|
3031
|
+
throw err;
|
|
2785
3032
|
}
|
|
3033
|
+
await this.syncProjection(fm.id, refBeforeRepair, fm.entityRef);
|
|
2786
3034
|
}
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
async
|
|
3035
|
+
/**
|
|
3036
|
+
* Restore a record's pre-mutation bytes through the blocked-capture
|
|
3037
|
+
* surface when a post-persist repair fails (§14).
|
|
3038
|
+
*/
|
|
3039
|
+
async restore(before) {
|
|
2792
3040
|
try {
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
3041
|
+
await this.deps.writeTombstoneBlockedMemory(
|
|
3042
|
+
before.path,
|
|
3043
|
+
`${this.deps.serializeFrontmatter(before.frontmatter)}
|
|
3044
|
+
|
|
3045
|
+
${before.content}
|
|
3046
|
+
`,
|
|
3047
|
+
before.frontmatter,
|
|
3048
|
+
before.content
|
|
3049
|
+
);
|
|
3050
|
+
} catch (restoreErr) {
|
|
3051
|
+
log.warn(`failed to restore ${before.frontmatter.id} after repair failure: ${restoreErr}`);
|
|
2797
3052
|
}
|
|
3053
|
+
this.deps.invalidateAllMemoriesCache();
|
|
2798
3054
|
}
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
2806
|
-
const raw = await this.readIdentityImprovementLoops();
|
|
2807
|
-
const { markdown, loop } = upsertContinuityLoopInMarkdown(raw, input, nowIso);
|
|
2808
|
-
await this.writeIdentityImprovementLoops(markdown);
|
|
2809
|
-
return loop;
|
|
2810
|
-
}
|
|
2811
|
-
async reviewIdentityImprovementLoop(id, input) {
|
|
2812
|
-
const raw = await this.readIdentityImprovementLoops();
|
|
2813
|
-
const { markdown, loop } = reviewContinuityLoopInMarkdown(raw, id, input, (/* @__PURE__ */ new Date()).toISOString());
|
|
2814
|
-
if (!loop) return null;
|
|
2815
|
-
await this.writeIdentityImprovementLoops(markdown);
|
|
2816
|
-
return loop;
|
|
2817
|
-
}
|
|
2818
|
-
async readContinuityIncidentFileNames() {
|
|
2819
|
-
const files = await readdir4(this.deps.identityIncidentsDir);
|
|
2820
|
-
return files.filter((file) => file.endsWith(".md")).sort().reverse();
|
|
2821
|
-
}
|
|
2822
|
-
async readContinuityIncidentFile(filePath) {
|
|
3055
|
+
/**
|
|
3056
|
+
* Projection rows written under the pre-repair claimant cannot be restored
|
|
3057
|
+
* once the mapping is parked (Codex P1). Fail-open: projection is rebuildable.
|
|
3058
|
+
*/
|
|
3059
|
+
async syncProjection(memoryId, refBefore, refAfter) {
|
|
3060
|
+
if (typeof refBefore !== "string" || typeof refAfter !== "string" || refBefore === refAfter) return;
|
|
2823
3061
|
try {
|
|
2824
|
-
|
|
2825
|
-
const parsed = parseContinuityIncident(raw);
|
|
2826
|
-
return parsed ? { ...parsed, filePath } : null;
|
|
3062
|
+
await rewriteProjectedMemoryEntityReference(this.deps.baseDir, memoryId, refBefore, refAfter);
|
|
2827
3063
|
} catch (err) {
|
|
2828
|
-
|
|
2829
|
-
return null;
|
|
2830
|
-
}
|
|
2831
|
-
}
|
|
2832
|
-
async findContinuityIncidentFilePathById(id) {
|
|
2833
|
-
const fileNames = await this.readContinuityIncidentFileNames();
|
|
2834
|
-
const directMatch = fileNames.find((name) => name.endsWith(`-${id}.md`));
|
|
2835
|
-
if (directMatch) {
|
|
2836
|
-
const directPath = path5.join(this.deps.identityIncidentsDir, directMatch);
|
|
2837
|
-
const parsed = await this.readContinuityIncidentFile(directPath);
|
|
2838
|
-
if (parsed?.id === id) return directPath;
|
|
2839
|
-
}
|
|
2840
|
-
for (const fileName of fileNames) {
|
|
2841
|
-
const filePath = path5.join(this.deps.identityIncidentsDir, fileName);
|
|
2842
|
-
const parsed = await this.readContinuityIncidentFile(filePath);
|
|
2843
|
-
if (parsed?.id === id) return filePath;
|
|
2844
|
-
}
|
|
2845
|
-
return null;
|
|
2846
|
-
}
|
|
2847
|
-
sanitizeIdentityAuditKey(key) {
|
|
2848
|
-
const trimmed = key.trim();
|
|
2849
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(trimmed) || trimmed.includes("..")) {
|
|
2850
|
-
throw new Error("Invalid identity audit key");
|
|
3064
|
+
log.warn(`projection entityRef sync failed for ${memoryId}: ${err}`);
|
|
2851
3065
|
}
|
|
2852
|
-
return trimmed;
|
|
2853
3066
|
}
|
|
2854
3067
|
};
|
|
2855
3068
|
|
|
2856
3069
|
// src/storage/entity-canonical-id-migration-adapter.ts
|
|
2857
3070
|
async function runLegacyEntityCanonicalIdMigration(host, parseEntityFile2, serializeEntityFile2, serializeMemoryWithEntityRef2) {
|
|
2858
|
-
const readMigrationFingerprint = () => getFingerprint(host.baseDir, host.entitiesDir, () => host.
|
|
3071
|
+
const readMigrationFingerprint = () => getFingerprint(host.baseDir, host.entitiesDir, () => String(host.getEntityMutationVersion()));
|
|
2859
3072
|
const completionFingerprint = await migrateLegacyEntityCanonicalIds({
|
|
2860
3073
|
stateDir: host.stateDir,
|
|
2861
3074
|
entitiesDir: host.entitiesDir,
|
|
@@ -2881,7 +3094,7 @@ async function runLegacyEntityCanonicalIdMigration(host, parseEntityFile2, seria
|
|
|
2881
3094
|
bumpMemoryStatusVersion: host.bumpMemoryStatusVersion.bind(host),
|
|
2882
3095
|
readMigrationFingerprint
|
|
2883
3096
|
});
|
|
2884
|
-
return completionFingerprint
|
|
3097
|
+
return completionFingerprint;
|
|
2885
3098
|
}
|
|
2886
3099
|
|
|
2887
3100
|
// src/storage/entity-canonical-id-migration-runner.ts
|
|
@@ -4630,22 +4843,103 @@ var MeetingRecordStore = class {
|
|
|
4630
4843
|
`meetings ${kind} '${label}' is a symbolic link \u2014 refusing to follow it even when it resolves inside the memory dir (AGENTS.md pattern #3)`
|
|
4631
4844
|
);
|
|
4632
4845
|
}
|
|
4633
|
-
}
|
|
4634
|
-
async assertNoEscape(rootReal, candidate) {
|
|
4635
|
-
let candidateReal;
|
|
4636
|
-
try {
|
|
4637
|
-
candidateReal = await this.io.realpath(candidate);
|
|
4638
|
-
} catch {
|
|
4639
|
-
return;
|
|
4846
|
+
}
|
|
4847
|
+
async assertNoEscape(rootReal, candidate) {
|
|
4848
|
+
let candidateReal;
|
|
4849
|
+
try {
|
|
4850
|
+
candidateReal = await this.io.realpath(candidate);
|
|
4851
|
+
} catch {
|
|
4852
|
+
return;
|
|
4853
|
+
}
|
|
4854
|
+
if (!pathIsInside(rootReal, candidateReal)) {
|
|
4855
|
+
throw new Error(
|
|
4856
|
+
"meeting record path resolves outside the memory dir \u2014 refusing to follow a symlink/traversal (AGENTS.md pattern #3)"
|
|
4857
|
+
);
|
|
4858
|
+
}
|
|
4859
|
+
}
|
|
4860
|
+
};
|
|
4861
|
+
var SORT_STR = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
4862
|
+
|
|
4863
|
+
// src/storage/citation-hash-source.ts
|
|
4864
|
+
function trimTrailingSpacesAndTabs(value) {
|
|
4865
|
+
let end = value.length;
|
|
4866
|
+
while (end > 0 && (value[end - 1] === " " || value[end - 1] === " ")) {
|
|
4867
|
+
end -= 1;
|
|
4868
|
+
}
|
|
4869
|
+
return end === value.length ? value : value.slice(0, end);
|
|
4870
|
+
}
|
|
4871
|
+
function trimLeadingSpacesAndTabs(value) {
|
|
4872
|
+
let start = 0;
|
|
4873
|
+
while (start < value.length && (value[start] === " " || value[start] === " ")) {
|
|
4874
|
+
start += 1;
|
|
4875
|
+
}
|
|
4876
|
+
return start === 0 ? value : value.slice(start);
|
|
4877
|
+
}
|
|
4878
|
+
function stripDefaultCitationMarkersWithoutRegex(value) {
|
|
4879
|
+
return stripCitationMarkersForHashRemoval(value, DEFAULT_CITATION_FORMAT);
|
|
4880
|
+
}
|
|
4881
|
+
function citationTemplateLiteralParts(template) {
|
|
4882
|
+
const parts = [];
|
|
4883
|
+
let cursor = 0;
|
|
4884
|
+
while (cursor < template.length) {
|
|
4885
|
+
const open2 = template.indexOf("{", cursor);
|
|
4886
|
+
if (open2 === -1) {
|
|
4887
|
+
parts.push(template.slice(cursor));
|
|
4888
|
+
break;
|
|
4889
|
+
}
|
|
4890
|
+
parts.push(template.slice(cursor, open2));
|
|
4891
|
+
const close = template.indexOf("}", open2 + 1);
|
|
4892
|
+
if (close === -1) {
|
|
4893
|
+
cursor = open2 + 1;
|
|
4894
|
+
} else {
|
|
4895
|
+
cursor = close + 1;
|
|
4896
|
+
}
|
|
4897
|
+
}
|
|
4898
|
+
return parts.filter((part) => part.length > 0);
|
|
4899
|
+
}
|
|
4900
|
+
function stripCitationMarkersForHashRemoval(value, template) {
|
|
4901
|
+
const parts = citationTemplateLiteralParts(template);
|
|
4902
|
+
if (parts.length === 0) return value;
|
|
4903
|
+
const first = parts[0];
|
|
4904
|
+
const lowerValue = value.toLowerCase();
|
|
4905
|
+
const lowerFirst = first.toLowerCase();
|
|
4906
|
+
const lowerParts = parts.map((part) => part.toLowerCase());
|
|
4907
|
+
if (!lowerValue.includes(lowerFirst)) return value;
|
|
4908
|
+
let result = "";
|
|
4909
|
+
let cursor = 0;
|
|
4910
|
+
let removed = false;
|
|
4911
|
+
while (cursor < value.length) {
|
|
4912
|
+
const markerStart = lowerValue.indexOf(lowerFirst, cursor);
|
|
4913
|
+
if (markerStart === -1) {
|
|
4914
|
+
result += value.slice(cursor);
|
|
4915
|
+
break;
|
|
4916
|
+
}
|
|
4917
|
+
const boundedEnd = first.startsWith("[") ? value.indexOf("]", markerStart + first.length) : -1;
|
|
4918
|
+
if (first.startsWith("[") && boundedEnd === -1) {
|
|
4919
|
+
result += value.slice(cursor);
|
|
4920
|
+
break;
|
|
4921
|
+
}
|
|
4922
|
+
const searchLimit = boundedEnd === -1 ? value.length : boundedEnd + 1;
|
|
4923
|
+
let markerEnd = markerStart + first.length;
|
|
4924
|
+
let matched = true;
|
|
4925
|
+
for (let i = 1; i < lowerParts.length; i += 1) {
|
|
4926
|
+
const partIndex = lowerValue.indexOf(lowerParts[i], markerEnd);
|
|
4927
|
+
if (partIndex === -1 || partIndex + parts[i].length > searchLimit) {
|
|
4928
|
+
matched = false;
|
|
4929
|
+
break;
|
|
4930
|
+
}
|
|
4931
|
+
markerEnd = partIndex + parts[i].length;
|
|
4640
4932
|
}
|
|
4641
|
-
if (!
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
);
|
|
4933
|
+
if (!matched) {
|
|
4934
|
+
result += value.slice(cursor);
|
|
4935
|
+
break;
|
|
4645
4936
|
}
|
|
4937
|
+
result += trimTrailingSpacesAndTabs(value.slice(cursor, markerStart));
|
|
4938
|
+
cursor = markerEnd;
|
|
4939
|
+
removed = true;
|
|
4646
4940
|
}
|
|
4647
|
-
|
|
4648
|
-
|
|
4941
|
+
return removed ? trimLeadingSpacesAndTabs(result) : value;
|
|
4942
|
+
}
|
|
4649
4943
|
|
|
4650
4944
|
// src/storage/entity-timeline.ts
|
|
4651
4945
|
import { createHash as createHash4 } from "crypto";
|
|
@@ -5097,85 +5391,6 @@ function normalizeMemoryWriteTimestamp(field, value) {
|
|
|
5097
5391
|
}
|
|
5098
5392
|
return new Date(parsed).toISOString();
|
|
5099
5393
|
}
|
|
5100
|
-
function trimTrailingSpacesAndTabs(value) {
|
|
5101
|
-
let end = value.length;
|
|
5102
|
-
while (end > 0 && (value[end - 1] === " " || value[end - 1] === " ")) {
|
|
5103
|
-
end -= 1;
|
|
5104
|
-
}
|
|
5105
|
-
return end === value.length ? value : value.slice(0, end);
|
|
5106
|
-
}
|
|
5107
|
-
function trimLeadingSpacesAndTabs(value) {
|
|
5108
|
-
let start = 0;
|
|
5109
|
-
while (start < value.length && (value[start] === " " || value[start] === " ")) {
|
|
5110
|
-
start += 1;
|
|
5111
|
-
}
|
|
5112
|
-
return start === 0 ? value : value.slice(start);
|
|
5113
|
-
}
|
|
5114
|
-
function stripDefaultCitationMarkersWithoutRegex(value) {
|
|
5115
|
-
return stripCitationMarkersForHashRemoval(value, DEFAULT_CITATION_FORMAT);
|
|
5116
|
-
}
|
|
5117
|
-
function citationTemplateLiteralParts(template) {
|
|
5118
|
-
const parts = [];
|
|
5119
|
-
let cursor = 0;
|
|
5120
|
-
while (cursor < template.length) {
|
|
5121
|
-
const open2 = template.indexOf("{", cursor);
|
|
5122
|
-
if (open2 === -1) {
|
|
5123
|
-
parts.push(template.slice(cursor));
|
|
5124
|
-
break;
|
|
5125
|
-
}
|
|
5126
|
-
parts.push(template.slice(cursor, open2));
|
|
5127
|
-
const close = template.indexOf("}", open2 + 1);
|
|
5128
|
-
if (close === -1) {
|
|
5129
|
-
cursor = open2 + 1;
|
|
5130
|
-
} else {
|
|
5131
|
-
cursor = close + 1;
|
|
5132
|
-
}
|
|
5133
|
-
}
|
|
5134
|
-
return parts.filter((part) => part.length > 0);
|
|
5135
|
-
}
|
|
5136
|
-
function stripCitationMarkersForHashRemoval(value, template) {
|
|
5137
|
-
const parts = citationTemplateLiteralParts(template);
|
|
5138
|
-
if (parts.length === 0) return value;
|
|
5139
|
-
const first = parts[0];
|
|
5140
|
-
const lowerValue = value.toLowerCase();
|
|
5141
|
-
const lowerFirst = first.toLowerCase();
|
|
5142
|
-
const lowerParts = parts.map((part) => part.toLowerCase());
|
|
5143
|
-
if (!lowerValue.includes(lowerFirst)) return value;
|
|
5144
|
-
let result = "";
|
|
5145
|
-
let cursor = 0;
|
|
5146
|
-
let removed = false;
|
|
5147
|
-
while (cursor < value.length) {
|
|
5148
|
-
const markerStart = lowerValue.indexOf(lowerFirst, cursor);
|
|
5149
|
-
if (markerStart === -1) {
|
|
5150
|
-
result += value.slice(cursor);
|
|
5151
|
-
break;
|
|
5152
|
-
}
|
|
5153
|
-
const boundedEnd = first.startsWith("[") ? value.indexOf("]", markerStart + first.length) : -1;
|
|
5154
|
-
if (first.startsWith("[") && boundedEnd === -1) {
|
|
5155
|
-
result += value.slice(cursor);
|
|
5156
|
-
break;
|
|
5157
|
-
}
|
|
5158
|
-
const searchLimit = boundedEnd === -1 ? value.length : boundedEnd + 1;
|
|
5159
|
-
let markerEnd = markerStart + first.length;
|
|
5160
|
-
let matched = true;
|
|
5161
|
-
for (let i = 1; i < lowerParts.length; i += 1) {
|
|
5162
|
-
const partIndex = lowerValue.indexOf(lowerParts[i], markerEnd);
|
|
5163
|
-
if (partIndex === -1 || partIndex + parts[i].length > searchLimit) {
|
|
5164
|
-
matched = false;
|
|
5165
|
-
break;
|
|
5166
|
-
}
|
|
5167
|
-
markerEnd = partIndex + parts[i].length;
|
|
5168
|
-
}
|
|
5169
|
-
if (!matched) {
|
|
5170
|
-
result += value.slice(cursor);
|
|
5171
|
-
break;
|
|
5172
|
-
}
|
|
5173
|
-
result += trimTrailingSpacesAndTabs(value.slice(cursor, markerStart));
|
|
5174
|
-
cursor = markerEnd;
|
|
5175
|
-
removed = true;
|
|
5176
|
-
}
|
|
5177
|
-
return removed ? trimLeadingSpacesAndTabs(result) : value;
|
|
5178
|
-
}
|
|
5179
5394
|
function serializeFrontmatter(fm) {
|
|
5180
5395
|
assertMemoryFrontmatterId(fm);
|
|
5181
5396
|
const lines = [
|
|
@@ -6142,7 +6357,6 @@ var StorageManager3 = class _StorageManager extends TombstoneBlockedCaptureIndex
|
|
|
6142
6357
|
this.entitySchemas = entitySchemas;
|
|
6143
6358
|
this.hotMemoriesCacheEnabled = hotMemoriesCacheEnabledOverride ?? _StorageManager.hotMemoriesCacheDefaultByDir.get(baseDir) ?? _StorageManager.hotMemoriesCacheDefault;
|
|
6144
6359
|
this.loadAliasesSync();
|
|
6145
|
-
this.loadHistoricalEntityCanonicalIdsSync();
|
|
6146
6360
|
}
|
|
6147
6361
|
baseDir;
|
|
6148
6362
|
entitySchemas;
|
|
@@ -6162,6 +6376,8 @@ var StorageManager3 = class _StorageManager extends TombstoneBlockedCaptureIndex
|
|
|
6162
6376
|
// every plain create). In-process fallback when the on-disk sentinel is
|
|
6163
6377
|
// unavailable; canonical source is state/.memory-corpus-version.log.
|
|
6164
6378
|
static memoryCorpusVersionByDir = /* @__PURE__ */ new Map();
|
|
6379
|
+
// Entity-mutation sentinel: see getEntityMutationVersion.
|
|
6380
|
+
static entityMutationVersionByDir = /* @__PURE__ */ new Map();
|
|
6165
6381
|
static secureStoreEntityCacheKeyIds = /* @__PURE__ */ new WeakMap();
|
|
6166
6382
|
static nextSecureStoreEntityCacheKeyId = 1;
|
|
6167
6383
|
// In-process fallback for the cold-write sentinel (used when the disk file
|
|
@@ -6613,8 +6829,7 @@ var StorageManager3 = class _StorageManager extends TombstoneBlockedCaptureIndex
|
|
|
6613
6829
|
return path9.join(workspaceDir, `IDENTITY.${safeNamespace}.md`);
|
|
6614
6830
|
}
|
|
6615
6831
|
versionFilePath(kind) {
|
|
6616
|
-
|
|
6617
|
-
return path9.join(this.stateDir, fileName);
|
|
6832
|
+
return path9.join(this.stateDir, `.${kind}-version.log`);
|
|
6618
6833
|
}
|
|
6619
6834
|
bumpSharedVersion(kind, fallbackMap) {
|
|
6620
6835
|
const filePath = this.versionFilePath(kind);
|
|
@@ -6647,13 +6862,21 @@ var StorageManager3 = class _StorageManager extends TombstoneBlockedCaptureIndex
|
|
|
6647
6862
|
return this.readSharedVersion("memory-status", _StorageManager.memoryStatusVersionByDir);
|
|
6648
6863
|
}
|
|
6649
6864
|
/**
|
|
6650
|
-
*
|
|
6651
|
-
*
|
|
6652
|
-
*
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6865
|
+
* Entity-mutation sentinel: advanced only by entity-store content writes.
|
|
6866
|
+
* The migration fingerprint keys on this instead of memory-status, so
|
|
6867
|
+
* supersede/archive/delete no longer re-trigger discovery scans (#2213).
|
|
6868
|
+
*/
|
|
6869
|
+
getEntityMutationVersion() {
|
|
6870
|
+
return this.readSharedVersion("entity-mutation", _StorageManager.entityMutationVersionByDir);
|
|
6871
|
+
}
|
|
6872
|
+
bumpEntityMutationVersion() {
|
|
6873
|
+
this.bumpSharedVersion("entity-mutation", _StorageManager.entityMutationVersionByDir);
|
|
6874
|
+
}
|
|
6875
|
+
/**
|
|
6876
|
+
* Corpus version sentinel for the hot-memories result cache (#1902).
|
|
6877
|
+
* Distinct from memory-status: bumps on EVERY memory-file mutation so the
|
|
6878
|
+
* version-keyed hot cache stays coherent across processes, WITHOUT
|
|
6879
|
+
* invalidating the version-keyed entity cache on every plain fact create.
|
|
6657
6880
|
*/
|
|
6658
6881
|
getMemoryCorpusVersion() {
|
|
6659
6882
|
return this.readSharedVersion("memory-corpus", _StorageManager.memoryCorpusVersionByDir);
|
|
@@ -6663,12 +6886,10 @@ var StorageManager3 = class _StorageManager extends TombstoneBlockedCaptureIndex
|
|
|
6663
6886
|
deleteInFlightReadsForDir(this.baseDir);
|
|
6664
6887
|
}
|
|
6665
6888
|
/**
|
|
6666
|
-
* Corpus bump
|
|
6667
|
-
* since the pre-bump size (
|
|
6668
|
-
*
|
|
6669
|
-
*
|
|
6670
|
-
* (peer-incomplete) corpus at a version that already reflects a peer's
|
|
6671
|
-
* still-unread write.
|
|
6889
|
+
* Corpus bump reporting whether THIS process's append was the only one
|
|
6890
|
+
* since the pre-bump size (#1902, Codex P1): the `exclusive` flag lets
|
|
6891
|
+
* patchHotMemoriesCache refuse to re-key its locally patched corpus at a
|
|
6892
|
+
* version already reflecting a peer's still-unread concurrent append.
|
|
6672
6893
|
*/
|
|
6673
6894
|
bumpMemoryCorpusVersionExclusive() {
|
|
6674
6895
|
const filePath = this.versionFilePath("memory-corpus");
|
|
@@ -6749,7 +6970,7 @@ var StorageManager3 = class _StorageManager extends TombstoneBlockedCaptureIndex
|
|
|
6749
6970
|
writeFile: (p, c) => this.writeStorageSecureFile(p, c),
|
|
6750
6971
|
readFile: (p) => readMaybeEncryptedFile(p, this._secureStoreKey, this.baseDir),
|
|
6751
6972
|
readDir: (d) => readdir5(d),
|
|
6752
|
-
deleteFile: (p) =>
|
|
6973
|
+
deleteFile: (p) => unlink4(p),
|
|
6753
6974
|
realpath: (p) => realpath3(p),
|
|
6754
6975
|
lstat: (p) => lstat3(p).then((st) => ({ isSymbolicLink: st.isSymbolicLink() }))
|
|
6755
6976
|
});
|
|
@@ -6767,7 +6988,7 @@ var StorageManager3 = class _StorageManager extends TombstoneBlockedCaptureIndex
|
|
|
6767
6988
|
writeFile: (p, c) => this.writeStorageSecureFile(p, c),
|
|
6768
6989
|
readFile: (p) => readMaybeEncryptedFile(p, this._secureStoreKey, this.baseDir),
|
|
6769
6990
|
readDir: (d) => readdir5(d),
|
|
6770
|
-
deleteFile: (p) =>
|
|
6991
|
+
deleteFile: (p) => unlink4(p),
|
|
6771
6992
|
realpath: (p) => realpath3(p),
|
|
6772
6993
|
lstat: (p) => lstat3(p).then((st) => ({ isSymbolicLink: st.isSymbolicLink() }))
|
|
6773
6994
|
});
|
|
@@ -7204,7 +7425,14 @@ var StorageManager3 = class _StorageManager extends TombstoneBlockedCaptureIndex
|
|
|
7204
7425
|
try {
|
|
7205
7426
|
const store = await this.getTombstoneStore();
|
|
7206
7427
|
const strippedRawContent = stripCitationForTemplate(input.rawContent, this.citationTemplate);
|
|
7207
|
-
return await
|
|
7428
|
+
return await appendCanonicalizedTombstone(
|
|
7429
|
+
this.stateDir,
|
|
7430
|
+
input,
|
|
7431
|
+
() => this.currentHistoricalIds(),
|
|
7432
|
+
(identity) => store.appendTombstone({ ...input, rawContent: strippedRawContent, ...identity }),
|
|
7433
|
+
(tombstoneId) => store.revoke(tombstoneId, input.createdBy),
|
|
7434
|
+
input.sourceMemoryId
|
|
7435
|
+
);
|
|
7208
7436
|
} catch (err) {
|
|
7209
7437
|
log.warn(`tombstone append failed (memory=${input.sourceMemoryId}): ${err}`);
|
|
7210
7438
|
return null;
|
|
@@ -7500,16 +7728,37 @@ var StorageManager3 = class _StorageManager extends TombstoneBlockedCaptureIndex
|
|
|
7500
7728
|
* store loaded last rewrite every other store's canonical entity ids.
|
|
7501
7729
|
*/
|
|
7502
7730
|
userAliases = {};
|
|
7503
|
-
historicalEntityCanonicalIds =
|
|
7731
|
+
historicalEntityCanonicalIds = new HistoricalEntityCanonicalIdCache();
|
|
7732
|
+
currentHistoricalIds() {
|
|
7733
|
+
return this.historicalEntityCanonicalIds.get(this.stateDir);
|
|
7734
|
+
}
|
|
7735
|
+
/** Post-persist repair subsystem (issue #2213) — storage/entity-ref-repair.ts. */
|
|
7736
|
+
_entityRefRepair;
|
|
7737
|
+
get entityRefRepair() {
|
|
7738
|
+
if (!this._entityRefRepair) {
|
|
7739
|
+
this._entityRefRepair = new EntityRefRepair({
|
|
7740
|
+
stateDir: this.stateDir,
|
|
7741
|
+
baseDir: this.baseDir,
|
|
7742
|
+
currentHistoricalIds: () => this.currentHistoricalIds(),
|
|
7743
|
+
serializeFrontmatter,
|
|
7744
|
+
writeTombstoneBlockedMemory: (pathname, fileContent, frontmatter, content) => this.writeTombstoneBlockedMemory(pathname, fileContent, frontmatter, content),
|
|
7745
|
+
invalidateAllMemoriesCache: () => this.invalidateAllMemoriesCache(),
|
|
7746
|
+
getTombstoneStore: () => this.getTombstoneStore(),
|
|
7747
|
+
tombstonesEnabled: () => this.tombstonesConfig.enabled,
|
|
7748
|
+
tombstonesNamespace: () => this.tombstonesConfig.namespace
|
|
7749
|
+
});
|
|
7750
|
+
}
|
|
7751
|
+
return this._entityRefRepair;
|
|
7752
|
+
}
|
|
7504
7753
|
entityCanonicalIdMigration = new EntityCanonicalIdMigrationRunner(
|
|
7505
7754
|
() => !(this._secureStoreRequired && !this.isSecureStoreUnlocked()),
|
|
7506
7755
|
() => this.runLegacyEntityCanonicalIdMigration(),
|
|
7507
|
-
() => getFingerprint(this.baseDir, this.entitiesDir, () => this.
|
|
7756
|
+
() => getFingerprint(this.baseDir, this.entitiesDir, () => String(this.getEntityMutationVersion()))
|
|
7508
7757
|
);
|
|
7509
7758
|
normalizeEntityName(raw, type) {
|
|
7510
7759
|
return resolveHistoricalEntityCanonicalId(
|
|
7511
7760
|
this.entityStore.normalizeEntityName(raw, type),
|
|
7512
|
-
this.
|
|
7761
|
+
this.currentHistoricalIds()
|
|
7513
7762
|
);
|
|
7514
7763
|
}
|
|
7515
7764
|
/**
|
|
@@ -7533,7 +7782,7 @@ var StorageManager3 = class _StorageManager extends TombstoneBlockedCaptureIndex
|
|
|
7533
7782
|
const aliasPath = path9.join(this.baseDir, "config", "aliases.json");
|
|
7534
7783
|
this.userAliases = {};
|
|
7535
7784
|
try {
|
|
7536
|
-
const raw =
|
|
7785
|
+
const raw = readFileSync(aliasPath, "utf-8");
|
|
7537
7786
|
const parsed = JSON.parse(raw);
|
|
7538
7787
|
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
7539
7788
|
const cleaned = {};
|
|
@@ -7551,9 +7800,6 @@ var StorageManager3 = class _StorageManager extends TombstoneBlockedCaptureIndex
|
|
|
7551
7800
|
log.debug("no config/aliases.json found \u2014 using built-in aliases only");
|
|
7552
7801
|
}
|
|
7553
7802
|
}
|
|
7554
|
-
loadHistoricalEntityCanonicalIdsSync() {
|
|
7555
|
-
this.historicalEntityCanonicalIds = loadHistoricalEntityCanonicalIds(this.stateDir);
|
|
7556
|
-
}
|
|
7557
7803
|
async runLegacyEntityCanonicalIdMigration() {
|
|
7558
7804
|
const completionFingerprint = await runLegacyEntityCanonicalIdMigration(
|
|
7559
7805
|
this,
|
|
@@ -7561,7 +7807,6 @@ var StorageManager3 = class _StorageManager extends TombstoneBlockedCaptureIndex
|
|
|
7561
7807
|
(entity) => serializeEntityFile(entity, this.entitySchemas),
|
|
7562
7808
|
createMemoryEntityRefSerializer(serializeFrontmatter)
|
|
7563
7809
|
);
|
|
7564
|
-
this.loadHistoricalEntityCanonicalIdsSync();
|
|
7565
7810
|
return completionFingerprint;
|
|
7566
7811
|
}
|
|
7567
7812
|
async ensureDirectories() {
|
|
@@ -7622,6 +7867,9 @@ var StorageManager3 = class _StorageManager extends TombstoneBlockedCaptureIndex
|
|
|
7622
7867
|
}
|
|
7623
7868
|
async writeMemory(category, content, options = {}) {
|
|
7624
7869
|
await this.ensureDirectories();
|
|
7870
|
+
const rawEntityRef = options.entityRef;
|
|
7871
|
+
let refIds = typeof options.entityRef === "string" ? this.currentHistoricalIds() : null;
|
|
7872
|
+
if (refIds) options = canonicalizeEntityRefOption(options, refIds);
|
|
7625
7873
|
const now = /* @__PURE__ */ new Date();
|
|
7626
7874
|
const today = now.toISOString().slice(0, 10);
|
|
7627
7875
|
const id = `${category}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
@@ -7695,40 +7943,31 @@ var StorageManager3 = class _StorageManager extends TombstoneBlockedCaptureIndex
|
|
|
7695
7943
|
fm.contentHash = ContentHashIndex.computeHash(hashSource);
|
|
7696
7944
|
factHashSourceForTombstone = hashSource;
|
|
7697
7945
|
}
|
|
7946
|
+
if (refIds && typeof rawEntityRef === "string" && this.currentHistoricalIds() !== refIds) {
|
|
7947
|
+
refIds = this.currentHistoricalIds();
|
|
7948
|
+
fm.entityRef = resolveHistoricalEntityCanonicalId(rawEntityRef, refIds);
|
|
7949
|
+
}
|
|
7698
7950
|
let tombstoneBlocked = false;
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
...keyedSupersession.length > 0 ? { supersessionKeys: keyedSupersession } : {},
|
|
7717
|
-
namespace: this.tombstonesConfig.namespace
|
|
7718
|
-
});
|
|
7719
|
-
if (match) {
|
|
7720
|
-
tombstoneBlocked = true;
|
|
7721
|
-
fm.status = "pending_review";
|
|
7722
|
-
fm.blockedBy = match.tombstoneId;
|
|
7723
|
-
fm.tombstoneBlockTier = match.matchedTier;
|
|
7724
|
-
log.info(
|
|
7725
|
-
`tombstone: blocked resurrection of fact ${id} (tier=${match.matchedTier}, tombstone=${match.tombstoneId}, reason=${match.reason})`
|
|
7726
|
-
);
|
|
7727
|
-
}
|
|
7728
|
-
} catch (err) {
|
|
7729
|
-
log.warn(`tombstone lookup failed for fact ${id} (fail-open): ${err}`);
|
|
7951
|
+
let gateRef;
|
|
7952
|
+
let statusBeforeBlock;
|
|
7953
|
+
const applyTombstoneGate = async () => {
|
|
7954
|
+
if (tombstoneBlocked) {
|
|
7955
|
+
if (fm.entityRef === gateRef) return;
|
|
7956
|
+
tombstoneBlocked = false;
|
|
7957
|
+
fm.status = statusBeforeBlock;
|
|
7958
|
+
delete fm.blockedBy;
|
|
7959
|
+
delete fm.tombstoneBlockTier;
|
|
7960
|
+
}
|
|
7961
|
+
if (category !== "fact" || factHashSourceForTombstone === null) return;
|
|
7962
|
+
const statusBefore = fm.status;
|
|
7963
|
+
const match = await this.entityRefRepair.gate(fm, factHashSourceForTombstone, options.structuredAttributes);
|
|
7964
|
+
if (match) {
|
|
7965
|
+
gateRef = fm.entityRef;
|
|
7966
|
+
statusBeforeBlock = statusBefore;
|
|
7967
|
+
tombstoneBlocked = true;
|
|
7730
7968
|
}
|
|
7731
|
-
}
|
|
7969
|
+
};
|
|
7970
|
+
await applyTombstoneGate();
|
|
7732
7971
|
const fileContent = `${serializeFrontmatter(fm)}
|
|
7733
7972
|
|
|
7734
7973
|
${sanitized.text}
|
|
@@ -7757,6 +7996,37 @@ ${sanitized.text}
|
|
|
7757
7996
|
duplicateOf: duplicateBlocked.frontmatter.id
|
|
7758
7997
|
};
|
|
7759
7998
|
}
|
|
7999
|
+
if (refIds && typeof rawEntityRef === "string") {
|
|
8000
|
+
const refBeforeRepair = fm.entityRef;
|
|
8001
|
+
try {
|
|
8002
|
+
await repairEntityRefAfterJournalMove({
|
|
8003
|
+
stateDir: this.stateDir,
|
|
8004
|
+
currentIds: () => this.currentHistoricalIds(),
|
|
8005
|
+
idsAtResolve: refIds,
|
|
8006
|
+
rawRef: rawEntityRef,
|
|
8007
|
+
frontmatter: fm,
|
|
8008
|
+
rewrite: async () => {
|
|
8009
|
+
await applyTombstoneGate();
|
|
8010
|
+
await this.writeTombstoneBlockedMemory(
|
|
8011
|
+
filePath,
|
|
8012
|
+
`${serializeFrontmatter(fm)}
|
|
8013
|
+
|
|
8014
|
+
${sanitized.text}
|
|
8015
|
+
`,
|
|
8016
|
+
fm,
|
|
8017
|
+
sanitized.text,
|
|
8018
|
+
async () => this.invalidateAllMemoriesCache()
|
|
8019
|
+
);
|
|
8020
|
+
this.invalidateAllMemoriesCache();
|
|
8021
|
+
}
|
|
8022
|
+
});
|
|
8023
|
+
} catch (err) {
|
|
8024
|
+
await unlink4(filePath).catch(() => void 0);
|
|
8025
|
+
this.invalidateAllMemoriesCache();
|
|
8026
|
+
throw err;
|
|
8027
|
+
}
|
|
8028
|
+
await this.entityRefRepair.syncProjection(id, refBeforeRepair, fm.entityRef);
|
|
8029
|
+
}
|
|
7760
8030
|
await this.patchHotMemoriesCache({ addedPath: filePath }, "memory-create");
|
|
7761
8031
|
this.notifyMemoryWrite(filePath);
|
|
7762
8032
|
await this.appendGeneratedMemoryLifecycleEventFailOpen("storage.writeMemory", {
|
|
@@ -8472,11 +8742,46 @@ ${sanitized.text}
|
|
|
8472
8742
|
return path9.join(root, dir, this.resolveMemoryDateDir(memory), `${memory.frontmatter.id}.md`);
|
|
8473
8743
|
}
|
|
8474
8744
|
async writeMemoryFileAtomic(targetPath, memory) {
|
|
8475
|
-
const
|
|
8745
|
+
const refIdsAtWrite = this.currentHistoricalIds();
|
|
8746
|
+
const rawRef = memory.frontmatter.entityRef;
|
|
8747
|
+
const frontmatter = canonicalizeEntityRefOption(memory.frontmatter, refIdsAtWrite);
|
|
8748
|
+
const persist = () => writeMaybeEncryptedFile(
|
|
8749
|
+
targetPath,
|
|
8750
|
+
`${serializeFrontmatter(frontmatter)}
|
|
8476
8751
|
|
|
8477
8752
|
${memory.content}
|
|
8478
|
-
|
|
8479
|
-
|
|
8753
|
+
`,
|
|
8754
|
+
this.resolveWriteKey(),
|
|
8755
|
+
{},
|
|
8756
|
+
this.baseDir
|
|
8757
|
+
);
|
|
8758
|
+
const priorDest = await readFile2(targetPath).catch(() => null);
|
|
8759
|
+
await persist();
|
|
8760
|
+
if (typeof rawRef === "string") {
|
|
8761
|
+
const refBeforeRepair = frontmatter.entityRef;
|
|
8762
|
+
try {
|
|
8763
|
+
await repairEntityRefAfterJournalMove({
|
|
8764
|
+
stateDir: this.stateDir,
|
|
8765
|
+
currentIds: () => this.currentHistoricalIds(),
|
|
8766
|
+
idsAtResolve: refIdsAtWrite,
|
|
8767
|
+
rawRef,
|
|
8768
|
+
frontmatter,
|
|
8769
|
+
rewrite: persist
|
|
8770
|
+
});
|
|
8771
|
+
} catch (err) {
|
|
8772
|
+
if (priorDest === null) {
|
|
8773
|
+
await unlink4(targetPath).catch(() => void 0);
|
|
8774
|
+
} else {
|
|
8775
|
+
await writeFile2(targetPath, priorDest).catch(() => void 0);
|
|
8776
|
+
}
|
|
8777
|
+
this.invalidateAllMemoriesCache();
|
|
8778
|
+
throw err;
|
|
8779
|
+
}
|
|
8780
|
+
await this.entityRefRepair.syncProjection(memory.frontmatter.id, refBeforeRepair, frontmatter.entityRef);
|
|
8781
|
+
}
|
|
8782
|
+
if (memory.frontmatter.entityRef !== frontmatter.entityRef) {
|
|
8783
|
+
memory.frontmatter.entityRef = frontmatter.entityRef;
|
|
8784
|
+
}
|
|
8480
8785
|
this.invalidateAllMemoriesCache();
|
|
8481
8786
|
this.notifyCatalogWrite();
|
|
8482
8787
|
}
|
|
@@ -8486,7 +8791,7 @@ ${memory.content}
|
|
|
8486
8791
|
const destPath = path9.resolve(targetPath);
|
|
8487
8792
|
if (sourcePath !== destPath) {
|
|
8488
8793
|
try {
|
|
8489
|
-
await
|
|
8794
|
+
await unlink4(memory.path);
|
|
8490
8795
|
} catch (err) {
|
|
8491
8796
|
const message = err instanceof Error ? err.message : String(err);
|
|
8492
8797
|
if (!message.includes("ENOENT")) {
|
|
@@ -8507,7 +8812,7 @@ ${memory.content}
|
|
|
8507
8812
|
const existing = await this.readMemoryByPath(targetPath);
|
|
8508
8813
|
if (existing?.frontmatter.id === memory.frontmatter.id) {
|
|
8509
8814
|
try {
|
|
8510
|
-
await
|
|
8815
|
+
await unlink4(memory.path);
|
|
8511
8816
|
} catch (err) {
|
|
8512
8817
|
const message = err instanceof Error ? err.message : String(err);
|
|
8513
8818
|
if (!message.includes("ENOENT")) {
|
|
@@ -8541,19 +8846,42 @@ ${memory.content}
|
|
|
8541
8846
|
const today = now.toISOString().slice(0, 10);
|
|
8542
8847
|
const destDir = path9.join(this.archiveDir, today);
|
|
8543
8848
|
await mkdir(destDir, { recursive: true });
|
|
8544
|
-
const
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8849
|
+
const refIdsAtWrite = this.currentHistoricalIds();
|
|
8850
|
+
const updatedFm = canonicalizeEntityRefOption(
|
|
8851
|
+
{
|
|
8852
|
+
...current.frontmatter,
|
|
8853
|
+
status: "archived",
|
|
8854
|
+
archivedAt: now.toISOString(),
|
|
8855
|
+
updated: now.toISOString()
|
|
8856
|
+
},
|
|
8857
|
+
refIdsAtWrite
|
|
8858
|
+
);
|
|
8550
8859
|
const fileContent = `${serializeFrontmatter(updatedFm)}
|
|
8551
8860
|
|
|
8552
8861
|
${current.content}
|
|
8553
8862
|
`;
|
|
8554
8863
|
const destPath = path9.join(destDir, path9.basename(current.path));
|
|
8864
|
+
const priorDest = await this.readStorageSecureFile(destPath).catch(() => null);
|
|
8555
8865
|
await this.writeStorageSecureFile(destPath, fileContent);
|
|
8556
|
-
|
|
8866
|
+
if (typeof current.frontmatter.entityRef === "string") {
|
|
8867
|
+
try {
|
|
8868
|
+
await this.entityRefRepair.repair(
|
|
8869
|
+
destPath,
|
|
8870
|
+
updatedFm,
|
|
8871
|
+
current.frontmatter.entityRef,
|
|
8872
|
+
refIdsAtWrite,
|
|
8873
|
+
current.content
|
|
8874
|
+
);
|
|
8875
|
+
} catch (err) {
|
|
8876
|
+
if (priorDest === null) {
|
|
8877
|
+
await unlink4(destPath).catch(() => void 0);
|
|
8878
|
+
} else {
|
|
8879
|
+
await this.writeStorageSecureFile(destPath, priorDest).catch(() => void 0);
|
|
8880
|
+
}
|
|
8881
|
+
throw err;
|
|
8882
|
+
}
|
|
8883
|
+
}
|
|
8884
|
+
await unlink4(current.path);
|
|
8557
8885
|
markDurable();
|
|
8558
8886
|
markProjectedMemoryPathInvalid(this.baseDir, current.frontmatter.id);
|
|
8559
8887
|
this.invalidateAllMemoriesCache();
|
|
@@ -8582,13 +8910,9 @@ ${current.content}
|
|
|
8582
8910
|
};
|
|
8583
8911
|
return await this.runTombstoneBlockedArchive(memory, archiveCurrent);
|
|
8584
8912
|
}
|
|
8913
|
+
/** Alias of listEntityNames (kept for legacy callers; sorted). */
|
|
8585
8914
|
async readEntities() {
|
|
8586
|
-
|
|
8587
|
-
const entries = await readdir5(this.entitiesDir);
|
|
8588
|
-
return entries.filter((e) => e.endsWith(".md")).map((e) => e.replace(".md", ""));
|
|
8589
|
-
} catch {
|
|
8590
|
-
return [];
|
|
8591
|
-
}
|
|
8915
|
+
return this.entityStore.listEntityNames();
|
|
8592
8916
|
}
|
|
8593
8917
|
async readEntity(name) {
|
|
8594
8918
|
return this.entityStore.readEntity(name);
|
|
@@ -8638,7 +8962,7 @@ ${current.content}
|
|
|
8638
8962
|
return await this.runTombstoneBlockedInvalidation(
|
|
8639
8963
|
memory,
|
|
8640
8964
|
async (current, rebuildMarker, markDurable) => {
|
|
8641
|
-
await
|
|
8965
|
+
await unlink4(current.path);
|
|
8642
8966
|
markDurable();
|
|
8643
8967
|
markProjectedMemoryPathInvalid(this.baseDir, id);
|
|
8644
8968
|
this.invalidateAllMemoriesCache();
|
|
@@ -8656,13 +8980,17 @@ ${current.content}
|
|
|
8656
8980
|
const mergedLineage = [...memory.frontmatter.lineage ?? [], ...options?.lineage ?? []].filter(
|
|
8657
8981
|
(v, i, a) => a.indexOf(v) === i
|
|
8658
8982
|
);
|
|
8659
|
-
const
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
|
|
8664
|
-
|
|
8665
|
-
|
|
8983
|
+
const refIdsAtWrite = this.currentHistoricalIds();
|
|
8984
|
+
const updated = canonicalizeEntityRefOption(
|
|
8985
|
+
{
|
|
8986
|
+
...memory.frontmatter,
|
|
8987
|
+
updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8988
|
+
supersedes: options?.supersedes ?? memory.frontmatter.supersedes,
|
|
8989
|
+
lineage: mergedLineage.length > 0 ? mergedLineage : void 0,
|
|
8990
|
+
...memory.frontmatter.sourceConnector === void 0 && options?.sourceConnector ? { sourceConnector: options.sourceConnector } : {}
|
|
8991
|
+
},
|
|
8992
|
+
refIdsAtWrite
|
|
8993
|
+
);
|
|
8666
8994
|
const sanitized = sanitizeMemoryContent(newContent);
|
|
8667
8995
|
if (!sanitized.clean) {
|
|
8668
8996
|
log.warn(`updated memory content sanitized for ${id}; violations=${sanitized.violations.join(", ")}`);
|
|
@@ -8674,6 +9002,16 @@ ${sanitized.text}
|
|
|
8674
9002
|
await this.writeTombstoneBlockedUpdate(memory, fileContent, updated, sanitized.text, async () => {
|
|
8675
9003
|
this.invalidateAllMemoriesCache();
|
|
8676
9004
|
});
|
|
9005
|
+
if (typeof memory.frontmatter.entityRef === "string") {
|
|
9006
|
+
await this.entityRefRepair.repair(
|
|
9007
|
+
memory.path,
|
|
9008
|
+
updated,
|
|
9009
|
+
memory.frontmatter.entityRef,
|
|
9010
|
+
refIdsAtWrite,
|
|
9011
|
+
sanitized.text,
|
|
9012
|
+
{ onFailRestore: memory }
|
|
9013
|
+
);
|
|
9014
|
+
}
|
|
8677
9015
|
await this.patchHotMemoriesCache({ addedPath: memory.path });
|
|
8678
9016
|
await this.appendGeneratedMemoryLifecycleEventFailOpen("storage.updateMemory", {
|
|
8679
9017
|
memoryId: id,
|
|
@@ -8696,10 +9034,12 @@ ${sanitized.text}
|
|
|
8696
9034
|
*/
|
|
8697
9035
|
async writeMemoryFrontmatter(memory, patch, lifecycle) {
|
|
8698
9036
|
const beforeStatus = memory.frontmatter.status ?? "active";
|
|
8699
|
-
const
|
|
8700
|
-
|
|
8701
|
-
...patch
|
|
8702
|
-
|
|
9037
|
+
const resolveIds = this.currentHistoricalIds();
|
|
9038
|
+
const updated = canonicalizeEntityRefOption(
|
|
9039
|
+
{ ...memory.frontmatter, ...patch },
|
|
9040
|
+
resolveIds
|
|
9041
|
+
);
|
|
9042
|
+
const refIds = typeof updated.entityRef === "string" ? resolveIds : null;
|
|
8703
9043
|
const afterStatus = updated.status ?? "active";
|
|
8704
9044
|
const fileContent = `${serializeFrontmatter(updated)}
|
|
8705
9045
|
|
|
@@ -8711,6 +9051,17 @@ ${memory.content}
|
|
|
8711
9051
|
this.invalidateColdMemoriesCache();
|
|
8712
9052
|
}
|
|
8713
9053
|
});
|
|
9054
|
+
const rawMergedRef = typeof patch.entityRef === "string" ? patch.entityRef : memory.frontmatter.entityRef;
|
|
9055
|
+
if (refIds && typeof rawMergedRef === "string") {
|
|
9056
|
+
await this.entityRefRepair.repair(
|
|
9057
|
+
memory.path,
|
|
9058
|
+
updated,
|
|
9059
|
+
rawMergedRef,
|
|
9060
|
+
refIds,
|
|
9061
|
+
memory.content,
|
|
9062
|
+
{ onFailRestore: memory }
|
|
9063
|
+
);
|
|
9064
|
+
}
|
|
8714
9065
|
await this.patchHotMemoriesCache({ addedPath: memory.path });
|
|
8715
9066
|
if (memory.path.includes(`${path9.sep}cold${path9.sep}`)) {
|
|
8716
9067
|
this.invalidateColdMemoriesCache();
|
|
@@ -9164,8 +9515,8 @@ ${memory.content}
|
|
|
9164
9515
|
activationState: "active"
|
|
9165
9516
|
});
|
|
9166
9517
|
await Promise.all([
|
|
9167
|
-
|
|
9168
|
-
|
|
9518
|
+
unlink4(this.compressionGuidelineDraftPath).catch(() => void 0),
|
|
9519
|
+
unlink4(this.compressionGuidelineDraftStatePath).catch(() => void 0)
|
|
9169
9520
|
]);
|
|
9170
9521
|
return true;
|
|
9171
9522
|
}
|
|
@@ -9399,96 +9750,27 @@ ${reflection}
|
|
|
9399
9750
|
await this.writeStorageSecureFile(this.identityReflectionsPath, `${existing.trimEnd()}${section}`);
|
|
9400
9751
|
log.debug(`appended namespace-local reflection to ${this.identityReflectionsPath}`);
|
|
9401
9752
|
}
|
|
9402
|
-
//
|
|
9403
|
-
//
|
|
9404
|
-
// ---------------------------------------------------------------------------
|
|
9405
|
-
/**
|
|
9406
|
-
* Add a relationship to an entity file.
|
|
9407
|
-
* Deduplicates by target+label.
|
|
9408
|
-
*/
|
|
9753
|
+
// Entity-file mutators live on EntityStore (issue #2213): journal-resolved
|
|
9754
|
+
// with legacy-file fallback, under the entity-mutation lock.
|
|
9409
9755
|
async addEntityRelationship(name, rel) {
|
|
9410
|
-
|
|
9411
|
-
let entity;
|
|
9412
|
-
try {
|
|
9413
|
-
const content = await this.readStorageSecureFile(filePath);
|
|
9414
|
-
entity = parseEntityFile(content, this.entitySchemas);
|
|
9415
|
-
} catch (err) {
|
|
9416
|
-
if (err instanceof SecureStoreLockedError) throw err;
|
|
9417
|
-
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
9418
|
-
log.debug(`addEntityRelationship: entity file ${name}.md not found`);
|
|
9419
|
-
return;
|
|
9420
|
-
}
|
|
9421
|
-
const exists = entity.relationships.some((r) => r.target === rel.target && r.label === rel.label);
|
|
9422
|
-
if (exists) return;
|
|
9423
|
-
entity.relationships.push(rel);
|
|
9424
|
-
entity.updated = (/* @__PURE__ */ new Date()).toISOString();
|
|
9425
|
-
await this.writeStorageSecureFile(filePath, serializeEntityFile(entity, this.entitySchemas));
|
|
9426
|
-
this.invalidateKnowledgeIndexCache();
|
|
9756
|
+
return this.entityStore.addEntityRelationship(name, rel);
|
|
9427
9757
|
}
|
|
9428
|
-
/**
|
|
9429
|
-
* Add an activity entry to an entity file.
|
|
9430
|
-
* Prepends to the beginning, prunes oldest entries beyond maxEntries.
|
|
9431
|
-
*/
|
|
9432
9758
|
async addEntityActivity(name, entry, maxEntries) {
|
|
9433
|
-
|
|
9434
|
-
let entity;
|
|
9435
|
-
try {
|
|
9436
|
-
const content = await this.readStorageSecureFile(filePath);
|
|
9437
|
-
entity = parseEntityFile(content, this.entitySchemas);
|
|
9438
|
-
} catch (err) {
|
|
9439
|
-
if (err instanceof SecureStoreLockedError) throw err;
|
|
9440
|
-
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
9441
|
-
log.debug(`addEntityActivity: entity file ${name}.md not found`);
|
|
9442
|
-
return;
|
|
9443
|
-
}
|
|
9444
|
-
entity.activity.unshift(entry);
|
|
9445
|
-
if (entity.activity.length > maxEntries) {
|
|
9446
|
-
entity.activity = entity.activity.slice(0, maxEntries);
|
|
9447
|
-
}
|
|
9448
|
-
entity.updated = (/* @__PURE__ */ new Date()).toISOString();
|
|
9449
|
-
await this.writeStorageSecureFile(filePath, serializeEntityFile(entity, this.entitySchemas));
|
|
9450
|
-
this.invalidateKnowledgeIndexCache();
|
|
9759
|
+
return this.entityStore.addEntityActivity(name, entry, maxEntries);
|
|
9451
9760
|
}
|
|
9452
9761
|
async addEntityAlias(name, alias) {
|
|
9453
|
-
|
|
9454
|
-
let entity;
|
|
9455
|
-
try {
|
|
9456
|
-
const content = await this.readStorageSecureFile(filePath);
|
|
9457
|
-
entity = parseEntityFile(content, this.entitySchemas);
|
|
9458
|
-
} catch (err) {
|
|
9459
|
-
if (err instanceof SecureStoreLockedError) throw err;
|
|
9460
|
-
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
9461
|
-
log.debug(`addEntityAlias: entity file ${name}.md not found`);
|
|
9462
|
-
return;
|
|
9463
|
-
}
|
|
9464
|
-
if (entity.aliases.includes(alias)) return;
|
|
9465
|
-
entity.aliases.push(alias);
|
|
9466
|
-
entity.updated = (/* @__PURE__ */ new Date()).toISOString();
|
|
9467
|
-
await this.writeStorageSecureFile(filePath, serializeEntityFile(entity, this.entitySchemas));
|
|
9468
|
-
this.invalidateKnowledgeIndexCache();
|
|
9469
|
-
this.bumpMemoryStatusVersion();
|
|
9762
|
+
return this.entityStore.addEntityAlias(name, alias);
|
|
9470
9763
|
}
|
|
9471
9764
|
async updateEntitySynthesis(name, synthesis, options = {}) {
|
|
9472
9765
|
return this.entityStore.updateEntitySynthesis(name, synthesis, options);
|
|
9473
9766
|
}
|
|
9474
|
-
/**
|
|
9475
|
-
* Backward-compatible alias for legacy callers/tests.
|
|
9476
|
-
*/
|
|
9767
|
+
/** Backward-compatible alias for legacy callers/tests. */
|
|
9477
9768
|
async updateEntitySummary(name, summary) {
|
|
9478
9769
|
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9479
|
-
|
|
9480
|
-
try {
|
|
9481
|
-
const filePath = path9.join(this.entitiesDir, `${name}.md`);
|
|
9482
|
-
const content = await this.readStorageSecureFile(filePath);
|
|
9483
|
-
synthesisTimelineCount = parseEntityFile(content, this.entitySchemas).timeline.length;
|
|
9484
|
-
} catch (err) {
|
|
9485
|
-
if (err instanceof SecureStoreLockedError) throw err;
|
|
9486
|
-
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
9487
|
-
synthesisTimelineCount = void 0;
|
|
9488
|
-
}
|
|
9770
|
+
const raw = await this.readEntity(name);
|
|
9489
9771
|
await this.updateEntitySynthesis(name, summary, {
|
|
9490
9772
|
entityUpdatedAt: updatedAt,
|
|
9491
|
-
synthesisTimelineCount,
|
|
9773
|
+
synthesisTimelineCount: raw ? parseEntityFile(raw, this.entitySchemas).timeline.length : void 0,
|
|
9492
9774
|
updatedAt
|
|
9493
9775
|
});
|
|
9494
9776
|
}
|
|
@@ -9554,24 +9836,7 @@ ${reflection}
|
|
|
9554
9836
|
);
|
|
9555
9837
|
}
|
|
9556
9838
|
async migrateEntityFilesToCompiledTruthTimeline() {
|
|
9557
|
-
|
|
9558
|
-
let migrated = 0;
|
|
9559
|
-
for (const entityName of entityNames) {
|
|
9560
|
-
const raw = await this.readEntity(entityName);
|
|
9561
|
-
if (!raw) continue;
|
|
9562
|
-
const serialized = serializeEntityFile(parseEntityFile(raw, this.entitySchemas), this.entitySchemas);
|
|
9563
|
-
if (raw.trimEnd() === serialized.trimEnd()) continue;
|
|
9564
|
-
await this.writeStorageSecureFile(path9.join(this.entitiesDir, `${entityName}.md`), serialized);
|
|
9565
|
-
migrated += 1;
|
|
9566
|
-
}
|
|
9567
|
-
if (migrated > 0) {
|
|
9568
|
-
this.invalidateKnowledgeIndexCache();
|
|
9569
|
-
this.bumpMemoryStatusVersion();
|
|
9570
|
-
}
|
|
9571
|
-
return {
|
|
9572
|
-
total: entityNames.length,
|
|
9573
|
-
migrated
|
|
9574
|
-
};
|
|
9839
|
+
return this.entityStore.migrateEntityFilesToCompiledTruthTimeline();
|
|
9575
9840
|
}
|
|
9576
9841
|
// ---------------------------------------------------------------------------
|
|
9577
9842
|
// Scoring + Knowledge Index (Knowledge Graph v7.0)
|
|
@@ -9666,17 +9931,27 @@ ${reflection}
|
|
|
9666
9931
|
for (const entry of entries) {
|
|
9667
9932
|
const memory = entry.memoryPath ? memoryPathMap.get(path9.resolve(entry.memoryPath)) : memoryMap.get(entry.memoryId);
|
|
9668
9933
|
if (!memory) continue;
|
|
9669
|
-
const
|
|
9670
|
-
|
|
9671
|
-
accessCount: entry.newCount,
|
|
9672
|
-
|
|
9673
|
-
|
|
9934
|
+
const rowIds = this.currentHistoricalIds();
|
|
9935
|
+
const newFm = canonicalizeEntityRefOption(
|
|
9936
|
+
{ ...memory.frontmatter, accessCount: entry.newCount, lastAccessed: entry.lastAccessed },
|
|
9937
|
+
rowIds
|
|
9938
|
+
);
|
|
9674
9939
|
try {
|
|
9675
9940
|
const fileContent = `${serializeFrontmatter(newFm)}
|
|
9676
9941
|
|
|
9677
9942
|
${memory.content}
|
|
9678
9943
|
`;
|
|
9679
9944
|
await this.writeStorageSecureFile(memory.path, fileContent);
|
|
9945
|
+
if (typeof memory.frontmatter.entityRef === "string") {
|
|
9946
|
+
await this.entityRefRepair.repair(
|
|
9947
|
+
memory.path,
|
|
9948
|
+
newFm,
|
|
9949
|
+
memory.frontmatter.entityRef,
|
|
9950
|
+
rowIds,
|
|
9951
|
+
memory.content,
|
|
9952
|
+
{ onFailRestore: memory }
|
|
9953
|
+
);
|
|
9954
|
+
}
|
|
9680
9955
|
if (warm) {
|
|
9681
9956
|
updateCacheOnWrite(this.baseDir, { ...memory, frontmatter: newFm }, keyId);
|
|
9682
9957
|
}
|
|
@@ -9842,6 +10117,9 @@ ${memory.content}
|
|
|
9842
10117
|
*/
|
|
9843
10118
|
async writeChunk(parentId, chunkIndex, chunkTotal, category, content, options = {}) {
|
|
9844
10119
|
await this.ensureDirectories();
|
|
10120
|
+
const rawEntityRef = options.entityRef;
|
|
10121
|
+
let refIds = typeof options.entityRef === "string" ? this.currentHistoricalIds() : null;
|
|
10122
|
+
if (refIds) options = canonicalizeEntityRefOption(options, refIds);
|
|
9845
10123
|
const now = /* @__PURE__ */ new Date();
|
|
9846
10124
|
const today = now.toISOString().slice(0, 10);
|
|
9847
10125
|
const id = `${parentId}-chunk-${chunkIndex}`;
|
|
@@ -9883,12 +10161,22 @@ ${memory.content}
|
|
|
9883
10161
|
if (!sanitized.clean) {
|
|
9884
10162
|
log.warn(`chunk content sanitized for ${id}; violations=${sanitized.violations.join(", ")}`);
|
|
9885
10163
|
}
|
|
10164
|
+
const filePath = await this.resolveCategoryWritePath(category, id, today);
|
|
9886
10165
|
const fileContent = `${serializeFrontmatter(fm)}
|
|
9887
10166
|
|
|
9888
10167
|
${sanitized.text}
|
|
9889
10168
|
`;
|
|
9890
|
-
|
|
9891
|
-
|
|
10169
|
+
let priorChunk = null;
|
|
10170
|
+
try {
|
|
10171
|
+
const priorRaw = await readMaybeEncryptedFile(filePath, this._secureStoreKey, this.baseDir);
|
|
10172
|
+
const parsedPrior = parseFrontmatter(priorRaw);
|
|
10173
|
+
if (parsedPrior) {
|
|
10174
|
+
priorChunk = { path: filePath, frontmatter: parsedPrior.frontmatter, content: parsedPrior.content };
|
|
10175
|
+
}
|
|
10176
|
+
} catch {
|
|
10177
|
+
priorChunk = null;
|
|
10178
|
+
}
|
|
10179
|
+
const written = await this.writeTombstoneBlockedChunk(
|
|
9892
10180
|
filePath,
|
|
9893
10181
|
fileContent,
|
|
9894
10182
|
fm,
|
|
@@ -9899,6 +10187,13 @@ ${sanitized.text}
|
|
|
9899
10187
|
log.debug(`wrote chunk ${id} (${chunkIndex + 1}/${chunkTotal}) to ${filePath}`);
|
|
9900
10188
|
}
|
|
9901
10189
|
);
|
|
10190
|
+
if (written === id && refIds && typeof rawEntityRef === "string") {
|
|
10191
|
+
await this.entityRefRepair.repair(filePath, fm, rawEntityRef, refIds, sanitized.text, {
|
|
10192
|
+
regateFact: true,
|
|
10193
|
+
...priorChunk ? { onFailRestore: priorChunk } : { onFailRemove: filePath }
|
|
10194
|
+
});
|
|
10195
|
+
}
|
|
10196
|
+
return written;
|
|
9902
10197
|
}
|
|
9903
10198
|
/**
|
|
9904
10199
|
* Get all chunks for a given parent memory ID.
|
|
@@ -9920,19 +10215,27 @@ ${sanitized.text}
|
|
|
9920
10215
|
const oldMemory = memories.find((m) => m.frontmatter.id === oldMemoryId);
|
|
9921
10216
|
if (!oldMemory) return false;
|
|
9922
10217
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9923
|
-
const
|
|
9924
|
-
|
|
9925
|
-
status: "superseded",
|
|
9926
|
-
|
|
9927
|
-
|
|
9928
|
-
updated: now
|
|
9929
|
-
};
|
|
10218
|
+
const refIdsAtWrite = this.currentHistoricalIds();
|
|
10219
|
+
const updatedFm = canonicalizeEntityRefOption(
|
|
10220
|
+
{ ...oldMemory.frontmatter, status: "superseded", supersededBy: newMemoryId, supersededAt: now, updated: now },
|
|
10221
|
+
refIdsAtWrite
|
|
10222
|
+
);
|
|
9930
10223
|
const fileContent = `${serializeFrontmatter(updatedFm)}
|
|
9931
10224
|
|
|
9932
10225
|
${oldMemory.content}
|
|
9933
10226
|
`;
|
|
9934
10227
|
try {
|
|
9935
10228
|
await this.writeStorageSecureFile(oldMemory.path, fileContent);
|
|
10229
|
+
if (typeof oldMemory.frontmatter.entityRef === "string") {
|
|
10230
|
+
await this.entityRefRepair.repair(
|
|
10231
|
+
oldMemory.path,
|
|
10232
|
+
updatedFm,
|
|
10233
|
+
oldMemory.frontmatter.entityRef,
|
|
10234
|
+
refIdsAtWrite,
|
|
10235
|
+
oldMemory.content,
|
|
10236
|
+
{ onFailRestore: oldMemory }
|
|
10237
|
+
);
|
|
10238
|
+
}
|
|
9936
10239
|
this.bumpMemoryCorpusVersion();
|
|
9937
10240
|
await this.appendGeneratedMemoryLifecycleEventFailOpen("storage.supersedeMemory", {
|
|
9938
10241
|
memoryId: oldMemoryId,
|
|
@@ -9952,7 +10255,7 @@ ${oldMemory.content}
|
|
|
9952
10255
|
id: oldMemoryId,
|
|
9953
10256
|
content: stripCitationForTemplate(oldMemory.content, this.citationTemplate),
|
|
9954
10257
|
contentHash: oldMemory.frontmatter.contentHash,
|
|
9955
|
-
entityRef:
|
|
10258
|
+
entityRef: updatedFm.entityRef,
|
|
9956
10259
|
structuredAttributes: oldMemory.frontmatter.structuredAttributes
|
|
9957
10260
|
},
|
|
9958
10261
|
{
|
|
@@ -10028,18 +10331,27 @@ Reason: ${reason}`;
|
|
|
10028
10331
|
const memory = memoryMap.get(id);
|
|
10029
10332
|
if (!memory) continue;
|
|
10030
10333
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10031
|
-
const
|
|
10032
|
-
|
|
10033
|
-
status: "archived",
|
|
10034
|
-
|
|
10035
|
-
|
|
10036
|
-
};
|
|
10334
|
+
const rowIds = this.currentHistoricalIds();
|
|
10335
|
+
const updatedFm = canonicalizeEntityRefOption(
|
|
10336
|
+
{ ...memory.frontmatter, status: "archived", archivedAt: now, updated: now },
|
|
10337
|
+
rowIds
|
|
10338
|
+
);
|
|
10037
10339
|
try {
|
|
10038
10340
|
const fileContent = `${serializeFrontmatter(updatedFm)}
|
|
10039
10341
|
|
|
10040
10342
|
${memory.content}
|
|
10041
10343
|
`;
|
|
10042
10344
|
await this.writeStorageSecureFile(memory.path, fileContent);
|
|
10345
|
+
if (typeof memory.frontmatter.entityRef === "string") {
|
|
10346
|
+
await this.entityRefRepair.repair(
|
|
10347
|
+
memory.path,
|
|
10348
|
+
updatedFm,
|
|
10349
|
+
memory.frontmatter.entityRef,
|
|
10350
|
+
rowIds,
|
|
10351
|
+
memory.content,
|
|
10352
|
+
{ onFailRestore: memory }
|
|
10353
|
+
);
|
|
10354
|
+
}
|
|
10043
10355
|
this.bumpMemoryCorpusVersion();
|
|
10044
10356
|
await this.appendGeneratedMemoryLifecycleEventFailOpen("storage.archiveMemories", {
|
|
10045
10357
|
memoryId: id,
|
|
@@ -10200,4 +10512,4 @@ export {
|
|
|
10200
10512
|
isValidBufferSurpriseEvent,
|
|
10201
10513
|
StorageManager3 as StorageManager
|
|
10202
10514
|
};
|
|
10203
|
-
//# sourceMappingURL=chunk-
|
|
10515
|
+
//# sourceMappingURL=chunk-Q3CVY7LV.js.map
|