@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
package/src/storage.ts
CHANGED
|
@@ -34,6 +34,8 @@ import { selfDeps } from "./orchestration/self-deps.js";
|
|
|
34
34
|
import { EntityStore } from "./storage/entity-store.js";
|
|
35
35
|
import { IdentityContinuityStore } from "./storage/identity-continuity-store.js";
|
|
36
36
|
import * as entityMigration from "./storage/entity-canonical-id-migration.js";
|
|
37
|
+
import * as entityRefs from "./storage/entity-canonical-id-references.js";
|
|
38
|
+
import { EntityRefRepair } from "./storage/entity-ref-repair.js";
|
|
37
39
|
import {
|
|
38
40
|
runLegacyEntityCanonicalIdMigration,
|
|
39
41
|
type EntityCanonicalIdMigrationHost,
|
|
@@ -104,6 +106,10 @@ import {
|
|
|
104
106
|
stripCitationForTemplate,
|
|
105
107
|
DEFAULT_CITATION_FORMAT,
|
|
106
108
|
} from "./source-attribution.js";
|
|
109
|
+
import {
|
|
110
|
+
stripCitationMarkersForHashRemoval,
|
|
111
|
+
stripDefaultCitationMarkersWithoutRegex,
|
|
112
|
+
} from "./storage/citation-hash-source.js";
|
|
107
113
|
import {
|
|
108
114
|
TombstoneStore,
|
|
109
115
|
collectRetiredMemoriesForRebuild,
|
|
@@ -245,6 +251,8 @@ const ARTIFACT_SEARCH_STOPWORDS = new Set([
|
|
|
245
251
|
"with",
|
|
246
252
|
]);
|
|
247
253
|
|
|
254
|
+
type SharedVersionKind = "memory-status" | "artifact-write" | "cold-write" | "memory-corpus" | "entity-mutation";
|
|
255
|
+
|
|
248
256
|
type OfflineSyncDigestCacheEntry = {
|
|
249
257
|
statBytes: number;
|
|
250
258
|
mtimeMs: number;
|
|
@@ -311,92 +319,6 @@ function normalizeMemoryWriteTimestamp(field: string, value: string | undefined)
|
|
|
311
319
|
return new Date(parsed).toISOString();
|
|
312
320
|
}
|
|
313
321
|
|
|
314
|
-
function trimTrailingSpacesAndTabs(value: string): string {
|
|
315
|
-
let end = value.length;
|
|
316
|
-
while (end > 0 && (value[end - 1] === " " || value[end - 1] === "\t")) {
|
|
317
|
-
end -= 1;
|
|
318
|
-
}
|
|
319
|
-
return end === value.length ? value : value.slice(0, end);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function trimLeadingSpacesAndTabs(value: string): string {
|
|
323
|
-
let start = 0;
|
|
324
|
-
while (start < value.length && (value[start] === " " || value[start] === "\t")) {
|
|
325
|
-
start += 1;
|
|
326
|
-
}
|
|
327
|
-
return start === 0 ? value : value.slice(start);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function stripDefaultCitationMarkersWithoutRegex(value: string): string {
|
|
331
|
-
return stripCitationMarkersForHashRemoval(value, DEFAULT_CITATION_FORMAT);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function citationTemplateLiteralParts(template: string): string[] {
|
|
335
|
-
const parts: string[] = [];
|
|
336
|
-
let cursor = 0;
|
|
337
|
-
while (cursor < template.length) {
|
|
338
|
-
const open = template.indexOf("{", cursor);
|
|
339
|
-
if (open === -1) {
|
|
340
|
-
parts.push(template.slice(cursor));
|
|
341
|
-
break;
|
|
342
|
-
}
|
|
343
|
-
parts.push(template.slice(cursor, open));
|
|
344
|
-
const close = template.indexOf("}", open + 1);
|
|
345
|
-
if (close === -1) {
|
|
346
|
-
cursor = open + 1;
|
|
347
|
-
} else {
|
|
348
|
-
cursor = close + 1;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
return parts.filter((part) => part.length > 0);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function stripCitationMarkersForHashRemoval(value: string, template: string): string {
|
|
355
|
-
const parts = citationTemplateLiteralParts(template);
|
|
356
|
-
if (parts.length === 0) return value;
|
|
357
|
-
const first = parts[0]!;
|
|
358
|
-
const lowerValue = value.toLowerCase();
|
|
359
|
-
const lowerFirst = first.toLowerCase();
|
|
360
|
-
const lowerParts = parts.map((part) => part.toLowerCase());
|
|
361
|
-
if (!lowerValue.includes(lowerFirst)) return value;
|
|
362
|
-
|
|
363
|
-
let result = "";
|
|
364
|
-
let cursor = 0;
|
|
365
|
-
let removed = false;
|
|
366
|
-
while (cursor < value.length) {
|
|
367
|
-
const markerStart = lowerValue.indexOf(lowerFirst, cursor);
|
|
368
|
-
if (markerStart === -1) {
|
|
369
|
-
result += value.slice(cursor);
|
|
370
|
-
break;
|
|
371
|
-
}
|
|
372
|
-
const boundedEnd = first.startsWith("[") ? value.indexOf("]", markerStart + first.length) : -1;
|
|
373
|
-
if (first.startsWith("[") && boundedEnd === -1) {
|
|
374
|
-
result += value.slice(cursor);
|
|
375
|
-
break;
|
|
376
|
-
}
|
|
377
|
-
const searchLimit = boundedEnd === -1 ? value.length : boundedEnd + 1;
|
|
378
|
-
let markerEnd = markerStart + first.length;
|
|
379
|
-
let matched = true;
|
|
380
|
-
for (let i = 1; i < lowerParts.length; i += 1) {
|
|
381
|
-
const partIndex = lowerValue.indexOf(lowerParts[i]!, markerEnd);
|
|
382
|
-
if (partIndex === -1 || partIndex + parts[i]!.length > searchLimit) {
|
|
383
|
-
matched = false;
|
|
384
|
-
break;
|
|
385
|
-
}
|
|
386
|
-
markerEnd = partIndex + parts[i]!.length;
|
|
387
|
-
}
|
|
388
|
-
if (!matched) {
|
|
389
|
-
result += value.slice(cursor);
|
|
390
|
-
break;
|
|
391
|
-
}
|
|
392
|
-
result += trimTrailingSpacesAndTabs(value.slice(cursor, markerStart));
|
|
393
|
-
cursor = markerEnd;
|
|
394
|
-
removed = true;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
return removed ? trimLeadingSpacesAndTabs(result) : value;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
322
|
function serializeFrontmatter(fm: MemoryFrontmatter): string {
|
|
401
323
|
assertMemoryFrontmatterId(fm);
|
|
402
324
|
const lines = [
|
|
@@ -1136,23 +1058,6 @@ const FACT_HASH_INDEX_REBUILD_RETRY_BASE_MS = 50;
|
|
|
1136
1058
|
// Attribute normalization helper
|
|
1137
1059
|
// ---------------------------------------------------------------------------
|
|
1138
1060
|
|
|
1139
|
-
/**
|
|
1140
|
-
* Render a structured-attributes map into a stable, canonical string fragment
|
|
1141
|
-
* suitable for appending to enriched memory content before hashing.
|
|
1142
|
-
*
|
|
1143
|
-
* Normalization rules:
|
|
1144
|
-
* - Keys are trimmed and lowercased (values are trimmed but preserve case)
|
|
1145
|
-
* - Key-value pairs are sorted alphabetically by normalized key
|
|
1146
|
-
* - Pairs are joined with "; " and rendered as "key: value"
|
|
1147
|
-
*
|
|
1148
|
-
* Using this helper at BOTH the write path (enrichedContent) and the
|
|
1149
|
-
* dedup-lookup path (dedupContent) guarantees identical output regardless of
|
|
1150
|
-
* the insertion order or casing used by the caller.
|
|
1151
|
-
*
|
|
1152
|
-
* @example
|
|
1153
|
-
* normalizeAttributePairs({ foo: "bar", BAZ: "qux" })
|
|
1154
|
-
* // → "baz: qux; foo: bar"
|
|
1155
|
-
*/
|
|
1156
1061
|
// `stripAttributesSuffix` now lives in ./structured-attributes.ts (shared with
|
|
1157
1062
|
// the coding surfaces + wearable service). Imported here so internal callers
|
|
1158
1063
|
// (snapshotBeforeWrite, snapshotForProvenance) resolve, and re-exported to
|
|
@@ -1839,6 +1744,8 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
1839
1744
|
// every plain create). In-process fallback when the on-disk sentinel is
|
|
1840
1745
|
// unavailable; canonical source is state/.memory-corpus-version.log.
|
|
1841
1746
|
private static readonly memoryCorpusVersionByDir = new Map<string, number>();
|
|
1747
|
+
// Entity-mutation sentinel: see getEntityMutationVersion.
|
|
1748
|
+
private static readonly entityMutationVersionByDir = new Map<string, number>();
|
|
1842
1749
|
private static readonly secureStoreEntityCacheKeyIds = new WeakMap<Buffer, number>();
|
|
1843
1750
|
private static nextSecureStoreEntityCacheKeyId = 1;
|
|
1844
1751
|
// In-process fallback for the cold-write sentinel (used when the disk file
|
|
@@ -2305,7 +2212,6 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
2305
2212
|
// loadAliases() explicitly — every creation path must still get the
|
|
2306
2213
|
// store's own aliases, never an empty or foreign table.
|
|
2307
2214
|
this.loadAliasesSync();
|
|
2308
|
-
this.loadHistoricalEntityCanonicalIdsSync();
|
|
2309
2215
|
}
|
|
2310
2216
|
|
|
2311
2217
|
/** Set the process-wide hot-memories cache default (issue #1902). The
|
|
@@ -2404,20 +2310,12 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
2404
2310
|
return path.join(workspaceDir, `IDENTITY.${safeNamespace}.md`);
|
|
2405
2311
|
}
|
|
2406
2312
|
|
|
2407
|
-
private versionFilePath(kind:
|
|
2408
|
-
|
|
2409
|
-
kind === "memory-status"
|
|
2410
|
-
? ".memory-status-version.log"
|
|
2411
|
-
: kind === "artifact-write"
|
|
2412
|
-
? ".artifact-write-version.log"
|
|
2413
|
-
: kind === "cold-write"
|
|
2414
|
-
? ".cold-write-version.log"
|
|
2415
|
-
: ".memory-corpus-version.log";
|
|
2416
|
-
return path.join(this.stateDir, fileName);
|
|
2313
|
+
private versionFilePath(kind: SharedVersionKind): string {
|
|
2314
|
+
return path.join(this.stateDir, `.${kind}-version.log`);
|
|
2417
2315
|
}
|
|
2418
2316
|
|
|
2419
2317
|
private bumpSharedVersion(
|
|
2420
|
-
kind:
|
|
2318
|
+
kind: SharedVersionKind,
|
|
2421
2319
|
fallbackMap: Map<string, number>
|
|
2422
2320
|
): number {
|
|
2423
2321
|
const filePath = this.versionFilePath(kind);
|
|
@@ -2435,7 +2333,7 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
2435
2333
|
}
|
|
2436
2334
|
|
|
2437
2335
|
private readSharedVersion(
|
|
2438
|
-
kind:
|
|
2336
|
+
kind: SharedVersionKind,
|
|
2439
2337
|
fallbackMap: Map<string, number>
|
|
2440
2338
|
): number {
|
|
2441
2339
|
const filePath = this.versionFilePath(kind);
|
|
@@ -2448,16 +2346,14 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
2448
2346
|
|
|
2449
2347
|
protected bumpMemoryStatusVersion(): void {
|
|
2450
2348
|
this.bumpSharedVersion("memory-status", StorageManager.memoryStatusVersionByDir);
|
|
2451
|
-
//
|
|
2452
|
-
//
|
|
2453
|
-
// below drops the hot layer locally
|
|
2349
|
+
// Corpus sentinel too: peer processes rescan the hot-memories cache
|
|
2350
|
+
// after status/lifecycle/bulk mutations (#1902); invalidateAllForDir
|
|
2351
|
+
// below drops the hot layer locally.
|
|
2454
2352
|
this.bumpSharedVersion("memory-corpus", StorageManager.memoryCorpusVersionByDir);
|
|
2455
|
-
// Invalidation chokepoint (
|
|
2456
|
-
//
|
|
2457
|
-
//
|
|
2458
|
-
//
|
|
2459
|
-
// Version bumps only invalidate the version-checked layers lazily; the
|
|
2460
|
-
// TTL-based QMD caches need this eager clear.
|
|
2353
|
+
// Invalidation chokepoint (#1535): several status/entity mutations
|
|
2354
|
+
// (supersede, archiveMemories, writeEntity, commitment cleanup) never
|
|
2355
|
+
// call invalidateAllMemoriesCache; the TTL-based QMD caches need this
|
|
2356
|
+
// eager clear.
|
|
2461
2357
|
invalidateAllForDir(this.baseDir);
|
|
2462
2358
|
}
|
|
2463
2359
|
|
|
@@ -2466,38 +2362,43 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
2466
2362
|
}
|
|
2467
2363
|
|
|
2468
2364
|
/**
|
|
2469
|
-
*
|
|
2470
|
-
*
|
|
2471
|
-
*
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2365
|
+
* Entity-mutation sentinel: advanced only by entity-store content writes.
|
|
2366
|
+
* The migration fingerprint keys on this instead of memory-status, so
|
|
2367
|
+
* supersede/archive/delete no longer re-trigger discovery scans (#2213).
|
|
2368
|
+
*/
|
|
2369
|
+
getEntityMutationVersion(): number {
|
|
2370
|
+
return this.readSharedVersion("entity-mutation", StorageManager.entityMutationVersionByDir);
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
protected bumpEntityMutationVersion(): void {
|
|
2374
|
+
this.bumpSharedVersion("entity-mutation", StorageManager.entityMutationVersionByDir);
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
/**
|
|
2378
|
+
* Corpus version sentinel for the hot-memories result cache (#1902).
|
|
2379
|
+
* Distinct from memory-status: bumps on EVERY memory-file mutation so the
|
|
2380
|
+
* version-keyed hot cache stays coherent across processes, WITHOUT
|
|
2381
|
+
* invalidating the version-keyed entity cache on every plain fact create.
|
|
2476
2382
|
*/
|
|
2477
2383
|
getMemoryCorpusVersion(): number {
|
|
2478
2384
|
return this.readSharedVersion("memory-corpus", StorageManager.memoryCorpusVersionByDir);
|
|
2479
2385
|
}
|
|
2480
2386
|
|
|
2481
2387
|
protected bumpMemoryCorpusVersion(): void {
|
|
2482
|
-
// Bump only —
|
|
2483
|
-
//
|
|
2388
|
+
// Bump only — NOT invalidateAllForDir (that would drop the very hot
|
|
2389
|
+
// entry the write path just patched).
|
|
2484
2390
|
this.bumpSharedVersion("memory-corpus", StorageManager.memoryCorpusVersionByDir);
|
|
2485
|
-
//
|
|
2486
|
-
//
|
|
2487
|
-
//
|
|
2488
|
-
// slot forces such a read to start a fresh scan of current disk state. (The
|
|
2489
|
-
// patch path uses bumpMemoryCorpusVersionExclusive and clears the slot
|
|
2490
|
-
// itself; this covers the per-item supersede/archive/TTL-cleanup bumps.)
|
|
2391
|
+
// Drop the in-flight readAllMemories slot (#1902): a concurrent read
|
|
2392
|
+
// must not attach to a scan that began BEFORE this mutation. (The patch
|
|
2393
|
+
// path uses bumpMemoryCorpusVersionExclusive and clears the slot itself.)
|
|
2491
2394
|
deleteInFlightReadsForDir(this.baseDir);
|
|
2492
2395
|
}
|
|
2493
2396
|
|
|
2494
2397
|
/**
|
|
2495
|
-
* Corpus bump
|
|
2496
|
-
* since the pre-bump size (
|
|
2497
|
-
*
|
|
2498
|
-
*
|
|
2499
|
-
* (peer-incomplete) corpus at a version that already reflects a peer's
|
|
2500
|
-
* still-unread write.
|
|
2398
|
+
* Corpus bump reporting whether THIS process's append was the only one
|
|
2399
|
+
* since the pre-bump size (#1902, Codex P1): the `exclusive` flag lets
|
|
2400
|
+
* patchHotMemoriesCache refuse to re-key its locally patched corpus at a
|
|
2401
|
+
* version already reflecting a peer's still-unread concurrent append.
|
|
2501
2402
|
*/
|
|
2502
2403
|
private bumpMemoryCorpusVersionExclusive(): { produced: number; exclusive: boolean } {
|
|
2503
2404
|
const filePath = this.versionFilePath("memory-corpus");
|
|
@@ -3147,16 +3048,20 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
3147
3048
|
if (!this.tombstonesConfig.enabled) return null;
|
|
3148
3049
|
try {
|
|
3149
3050
|
const store = await this.getTombstoneStore();
|
|
3150
|
-
// Chokepoint citation strip (#1579 review):
|
|
3151
|
-
//
|
|
3152
|
-
//
|
|
3153
|
-
//
|
|
3154
|
-
// citation-stripped contentHashSource. Idempotent on already-stripped
|
|
3155
|
-
// text, so callers that pre-strip (e.g. recordSupersession) are unaffected.
|
|
3051
|
+
// Chokepoint citation strip (#1579 review): both the exact-tier hash
|
|
3052
|
+
// and the normalized-text tier must match re-extraction, which hashes
|
|
3053
|
+
// the citation-stripped contentHashSource. Idempotent on pre-stripped
|
|
3054
|
+
// text (e.g. recordSupersession).
|
|
3156
3055
|
const strippedRawContent = stripCitationForTemplate(input.rawContent, this.citationTemplate);
|
|
3157
|
-
//
|
|
3158
|
-
|
|
3159
|
-
|
|
3056
|
+
// Canonicalization + post-append recheck (#2213) live in the helper.
|
|
3057
|
+
return await entityRefs.appendCanonicalizedTombstone(
|
|
3058
|
+
this.stateDir,
|
|
3059
|
+
input,
|
|
3060
|
+
() => this.currentHistoricalIds(),
|
|
3061
|
+
(identity) => store.appendTombstone({ ...input, rawContent: strippedRawContent, ...identity }),
|
|
3062
|
+
(tombstoneId) => store.revoke(tombstoneId, input.createdBy),
|
|
3063
|
+
input.sourceMemoryId,
|
|
3064
|
+
);
|
|
3160
3065
|
} catch (err) {
|
|
3161
3066
|
log.warn(`tombstone append failed (memory=${input.sourceMemoryId}): ${err}`);
|
|
3162
3067
|
return null;
|
|
@@ -3522,16 +3427,38 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
3522
3427
|
* store loaded last rewrite every other store's canonical entity ids.
|
|
3523
3428
|
*/
|
|
3524
3429
|
private userAliases: Record<string, string> = {};
|
|
3525
|
-
private historicalEntityCanonicalIds
|
|
3430
|
+
private readonly historicalEntityCanonicalIds = new entityRefs.HistoricalEntityCanonicalIdCache();
|
|
3431
|
+
private currentHistoricalIds(): Readonly<Record<string, string>> {
|
|
3432
|
+
return this.historicalEntityCanonicalIds.get(this.stateDir);
|
|
3433
|
+
}
|
|
3434
|
+
/** Post-persist repair subsystem (issue #2213) — storage/entity-ref-repair.ts. */
|
|
3435
|
+
private _entityRefRepair: EntityRefRepair | undefined;
|
|
3436
|
+
private get entityRefRepair(): EntityRefRepair {
|
|
3437
|
+
if (!this._entityRefRepair) {
|
|
3438
|
+
this._entityRefRepair = new EntityRefRepair({
|
|
3439
|
+
stateDir: this.stateDir,
|
|
3440
|
+
baseDir: this.baseDir,
|
|
3441
|
+
currentHistoricalIds: () => this.currentHistoricalIds(),
|
|
3442
|
+
serializeFrontmatter,
|
|
3443
|
+
writeTombstoneBlockedMemory: (pathname, fileContent, frontmatter, content) =>
|
|
3444
|
+
this.writeTombstoneBlockedMemory(pathname, fileContent, frontmatter, content),
|
|
3445
|
+
invalidateAllMemoriesCache: () => this.invalidateAllMemoriesCache(),
|
|
3446
|
+
getTombstoneStore: () => this.getTombstoneStore(),
|
|
3447
|
+
tombstonesEnabled: () => this.tombstonesConfig.enabled,
|
|
3448
|
+
tombstonesNamespace: () => this.tombstonesConfig.namespace,
|
|
3449
|
+
});
|
|
3450
|
+
}
|
|
3451
|
+
return this._entityRefRepair;
|
|
3452
|
+
}
|
|
3526
3453
|
private readonly entityCanonicalIdMigration = new EntityCanonicalIdMigrationRunner(
|
|
3527
3454
|
() => !(this._secureStoreRequired && !this.isSecureStoreUnlocked()),
|
|
3528
3455
|
() => this.runLegacyEntityCanonicalIdMigration(),
|
|
3529
|
-
() => entityMigration.getFingerprint(this.baseDir, this.entitiesDir, () => this.
|
|
3456
|
+
() => entityMigration.getFingerprint(this.baseDir, this.entitiesDir, () => String(this.getEntityMutationVersion())),
|
|
3530
3457
|
);
|
|
3531
3458
|
normalizeEntityName(raw: string, type: string): string {
|
|
3532
|
-
return
|
|
3459
|
+
return entityRefs.resolveHistoricalEntityCanonicalId(
|
|
3533
3460
|
this.entityStore.normalizeEntityName(raw, type),
|
|
3534
|
-
this.
|
|
3461
|
+
this.currentHistoricalIds(),
|
|
3535
3462
|
);
|
|
3536
3463
|
}
|
|
3537
3464
|
|
|
@@ -3579,18 +3506,13 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
3579
3506
|
log.debug("no config/aliases.json found — using built-in aliases only");
|
|
3580
3507
|
}
|
|
3581
3508
|
}
|
|
3582
|
-
private
|
|
3583
|
-
this.historicalEntityCanonicalIds = entityMigration.loadHistoricalEntityCanonicalIds(this.stateDir);
|
|
3584
|
-
}
|
|
3585
|
-
|
|
3586
|
-
private async runLegacyEntityCanonicalIdMigration(): Promise<string> {
|
|
3509
|
+
private async runLegacyEntityCanonicalIdMigration(): Promise<string | undefined> {
|
|
3587
3510
|
const completionFingerprint = await runLegacyEntityCanonicalIdMigration(
|
|
3588
3511
|
this as unknown as EntityCanonicalIdMigrationHost,
|
|
3589
3512
|
(content) => parseEntityFile(content, this.entitySchemas),
|
|
3590
3513
|
(entity) => serializeEntityFile(entity, this.entitySchemas),
|
|
3591
3514
|
createMemoryEntityRefSerializer(serializeFrontmatter),
|
|
3592
3515
|
);
|
|
3593
|
-
this.loadHistoricalEntityCanonicalIdsSync();
|
|
3594
3516
|
return completionFingerprint;
|
|
3595
3517
|
}
|
|
3596
3518
|
|
|
@@ -3676,6 +3598,9 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
3676
3598
|
options: WriteMemoryOptions = {}
|
|
3677
3599
|
): Promise<MemoryWriteResult> {
|
|
3678
3600
|
await this.ensureDirectories();
|
|
3601
|
+
const rawEntityRef = options.entityRef;
|
|
3602
|
+
let refIds = typeof options.entityRef === "string" ? this.currentHistoricalIds() : null;
|
|
3603
|
+
if (refIds) options = entityRefs.canonicalizeEntityRefOption(options, refIds);
|
|
3679
3604
|
const now = new Date();
|
|
3680
3605
|
const today = now.toISOString().slice(0, 10);
|
|
3681
3606
|
const id = `${category}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
@@ -3779,62 +3704,41 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
3779
3704
|
}
|
|
3780
3705
|
|
|
3781
3706
|
// ── Non-resurrection chokepoint (issue #1579) ────────────────────────
|
|
3782
|
-
//
|
|
3783
|
-
//
|
|
3784
|
-
//
|
|
3785
|
-
//
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3707
|
+
// A match persists the fact as pending_review + blockedBy — VISIBLE,
|
|
3708
|
+
// never a silent drop (rule 34) — and skips active dedup registration
|
|
3709
|
+
// (rule 44). Pre-gate revalidation (#2213): re-resolve from the caller's
|
|
3710
|
+
// ORIGINAL ref so the lookup and the persisted ref share one id space.
|
|
3711
|
+
if (refIds && typeof rawEntityRef === "string" && this.currentHistoricalIds() !== refIds) {
|
|
3712
|
+
refIds = this.currentHistoricalIds();
|
|
3713
|
+
fm.entityRef = entityRefs.resolveHistoricalEntityCanonicalId(rawEntityRef, refIds);
|
|
3714
|
+
}
|
|
3790
3715
|
let tombstoneBlocked = false;
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
const tombstoneStore = await this.getTombstoneStore();
|
|
3805
|
-
// Derive the keyed-tier supersession key from structured attributes
|
|
3806
|
-
// using the SAME helper write-time supersession uses (one helper).
|
|
3807
|
-
const keyedSupersession =
|
|
3808
|
-
options.entityRef && options.structuredAttributes
|
|
3809
|
-
? supersessionKeysForFact({
|
|
3810
|
-
entityRef: options.entityRef,
|
|
3811
|
-
structuredAttributes: options.structuredAttributes,
|
|
3812
|
-
})
|
|
3813
|
-
: [];
|
|
3814
|
-
// Issue #1579 thread Ociag/Oci-W: pass EVERY key — emitters register one tombstone per key, so the block can be on any later key.
|
|
3815
|
-
const match = tombstoneStore.lookup({
|
|
3816
|
-
contentHash: fm.contentHash,
|
|
3817
|
-
normalizedText: ContentHashIndex.normalizeContent(factHashSourceForTombstone),
|
|
3818
|
-
...(options.entityRef ? { entityRef: options.entityRef } : {}),
|
|
3819
|
-
...(keyedSupersession.length > 0 ? { supersessionKeys: keyedSupersession } : {}),
|
|
3820
|
-
namespace: this.tombstonesConfig.namespace,
|
|
3821
|
-
});
|
|
3822
|
-
if (match) {
|
|
3823
|
-
tombstoneBlocked = true;
|
|
3824
|
-
fm.status = "pending_review";
|
|
3825
|
-
fm.blockedBy = match.tombstoneId;
|
|
3826
|
-
fm.tombstoneBlockTier = match.matchedTier;
|
|
3827
|
-
log.info(
|
|
3828
|
-
`tombstone: blocked resurrection of fact ${id} (tier=${match.matchedTier}, tombstone=${match.tombstoneId}, reason=${match.reason})`
|
|
3829
|
-
);
|
|
3830
|
-
}
|
|
3831
|
-
} catch (err) {
|
|
3832
|
-
// Fail-open (rule 34 spirit): a tombstone lookup error must not block
|
|
3833
|
-
// the write. The fact persists as active; the next lookup after the
|
|
3834
|
-
// store recovers will catch a subsequent re-extraction.
|
|
3835
|
-
log.warn(`tombstone lookup failed for fact ${id} (fail-open): ${err}`);
|
|
3716
|
+
let gateRef: string | undefined;
|
|
3717
|
+
let statusBeforeBlock: MemoryFrontmatter["status"];
|
|
3718
|
+
// Closure so the post-persist repair can RE-RUN it when a journal move
|
|
3719
|
+
// changed the final entityRef: a verdict is only valid for the ref it was
|
|
3720
|
+
// computed under, so reset and re-evaluate (parked mappings fall BACK to
|
|
3721
|
+
// the legacy claimant; entity-independent tiers re-block in the lookup).
|
|
3722
|
+
const applyTombstoneGate = async (): Promise<void> => {
|
|
3723
|
+
if (tombstoneBlocked) {
|
|
3724
|
+
if (fm.entityRef === gateRef) return;
|
|
3725
|
+
tombstoneBlocked = false;
|
|
3726
|
+
fm.status = statusBeforeBlock;
|
|
3727
|
+
delete fm.blockedBy;
|
|
3728
|
+
delete fm.tombstoneBlockTier;
|
|
3836
3729
|
}
|
|
3837
|
-
|
|
3730
|
+
// Status semantics (thread ObteQ: pending_review candidates included,
|
|
3731
|
+
// terminal statuses respected) live in applyTombstoneResurrectionGate.
|
|
3732
|
+
if (category !== "fact" || factHashSourceForTombstone === null) return;
|
|
3733
|
+
const statusBefore = fm.status;
|
|
3734
|
+
const match = await this.entityRefRepair.gate(fm, factHashSourceForTombstone, options.structuredAttributes);
|
|
3735
|
+
if (match) {
|
|
3736
|
+
gateRef = fm.entityRef;
|
|
3737
|
+
statusBeforeBlock = statusBefore;
|
|
3738
|
+
tombstoneBlocked = true;
|
|
3739
|
+
}
|
|
3740
|
+
};
|
|
3741
|
+
await applyTombstoneGate();
|
|
3838
3742
|
|
|
3839
3743
|
const fileContent = `${serializeFrontmatter(fm)}\n\n${sanitized.text}\n`;
|
|
3840
3744
|
|
|
@@ -3864,6 +3768,38 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
3864
3768
|
duplicateOf: duplicateBlocked.frontmatter.id,
|
|
3865
3769
|
};
|
|
3866
3770
|
}
|
|
3771
|
+
if (refIds && typeof rawEntityRef === "string") {
|
|
3772
|
+
const refBeforeRepair = fm.entityRef;
|
|
3773
|
+
try {
|
|
3774
|
+
await entityRefs.repairEntityRefAfterJournalMove({
|
|
3775
|
+
stateDir: this.stateDir,
|
|
3776
|
+
currentIds: () => this.currentHistoricalIds(),
|
|
3777
|
+
idsAtResolve: refIds,
|
|
3778
|
+
rawRef: rawEntityRef,
|
|
3779
|
+
frontmatter: fm,
|
|
3780
|
+
rewrite: async () => {
|
|
3781
|
+
// Re-gate under the final ref, then rewrite THROUGH the blocked-
|
|
3782
|
+
// capture surface so any verdict change keeps the index consistent.
|
|
3783
|
+
await applyTombstoneGate();
|
|
3784
|
+
await this.writeTombstoneBlockedMemory(
|
|
3785
|
+
filePath,
|
|
3786
|
+
`${serializeFrontmatter(fm)}\n\n${sanitized.text}\n`,
|
|
3787
|
+
fm,
|
|
3788
|
+
sanitized.text,
|
|
3789
|
+
async () => this.invalidateAllMemoriesCache(),
|
|
3790
|
+
);
|
|
3791
|
+
this.invalidateAllMemoriesCache();
|
|
3792
|
+
},
|
|
3793
|
+
});
|
|
3794
|
+
} catch (err) {
|
|
3795
|
+
// Remove the just-created file before propagating (§14): it would
|
|
3796
|
+
// linger un-bumped/un-indexed and a retry would duplicate it.
|
|
3797
|
+
await unlink(filePath).catch(() => undefined);
|
|
3798
|
+
this.invalidateAllMemoriesCache();
|
|
3799
|
+
throw err;
|
|
3800
|
+
}
|
|
3801
|
+
await this.entityRefRepair.syncProjection(id, refBeforeRepair, fm.entityRef);
|
|
3802
|
+
}
|
|
3867
3803
|
await this.patchHotMemoriesCache({ addedPath: filePath }, "memory-create");
|
|
3868
3804
|
this.notifyMemoryWrite(filePath);
|
|
3869
3805
|
await this.appendGeneratedMemoryLifecycleEventFailOpen("storage.writeMemory", {
|
|
@@ -3878,16 +3814,14 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
3878
3814
|
],
|
|
3879
3815
|
});
|
|
3880
3816
|
if (category === "fact" && !tombstoneBlocked) {
|
|
3881
|
-
// Rule 44 (#1579):
|
|
3882
|
-
//
|
|
3883
|
-
//
|
|
3884
|
-
// (un-blocked) facts enter the hash index.
|
|
3817
|
+
// Rule 44 (#1579): only active (un-blocked) facts enter the hash
|
|
3818
|
+
// index — a blocked entry would silently ban the content on the next
|
|
3819
|
+
// extraction.
|
|
3885
3820
|
try {
|
|
3886
3821
|
const factHashIndex = await this.getFactHashIndex();
|
|
3887
|
-
//
|
|
3888
|
-
//
|
|
3889
|
-
//
|
|
3890
|
-
// Otherwise fall back to the sanitized persisted body as before.
|
|
3822
|
+
// Index the caller's contentHashSource (raw fact text before
|
|
3823
|
+
// citation annotation) when provided, so hasFactContentHash(rawFact)
|
|
3824
|
+
// matches subsequent extractions; else the sanitized persisted body.
|
|
3891
3825
|
const hashText =
|
|
3892
3826
|
options.contentHashSource !== undefined && options.contentHashSource.length > 0
|
|
3893
3827
|
? sanitizeMemoryContent(options.contentHashSource).text
|
|
@@ -4890,8 +4824,53 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
4890
4824
|
return path.join(root, dir, this.resolveMemoryDateDir(memory), `${memory.frontmatter.id}.md`);
|
|
4891
4825
|
}
|
|
4892
4826
|
private async writeMemoryFileAtomic(targetPath: string, memory: MemoryFile): Promise<void> {
|
|
4893
|
-
|
|
4894
|
-
|
|
4827
|
+
// Whole-record rewrite (tier moves) — #2213. Repair, not just detect:
|
|
4828
|
+
// the source is unlinked after the move, so a mapping parked across this
|
|
4829
|
+
// write would strand the moved copy with nothing left to reconcile from.
|
|
4830
|
+
const refIdsAtWrite = this.currentHistoricalIds();
|
|
4831
|
+
const rawRef = memory.frontmatter.entityRef;
|
|
4832
|
+
const frontmatter = entityRefs.canonicalizeEntityRefOption(memory.frontmatter, refIdsAtWrite);
|
|
4833
|
+
const persist = (): Promise<void> =>
|
|
4834
|
+
writeMaybeEncryptedFile(
|
|
4835
|
+
targetPath,
|
|
4836
|
+
`${serializeFrontmatter(frontmatter)}\n\n${memory.content}\n`,
|
|
4837
|
+
this.resolveWriteKey(),
|
|
4838
|
+
{},
|
|
4839
|
+
this.baseDir,
|
|
4840
|
+
);
|
|
4841
|
+
// Snapshot pre-write destination bytes (raw — encryption-agnostic) so a
|
|
4842
|
+
// repair failure restores them or removes a fresh copy (§14): a retained
|
|
4843
|
+
// destination would duplicate the record across tiers under a stale ref.
|
|
4844
|
+
const priorDest = await readFile(targetPath).catch(() => null);
|
|
4845
|
+
await persist();
|
|
4846
|
+
if (typeof rawRef === "string") {
|
|
4847
|
+
const refBeforeRepair = frontmatter.entityRef;
|
|
4848
|
+
try {
|
|
4849
|
+
await entityRefs.repairEntityRefAfterJournalMove({
|
|
4850
|
+
stateDir: this.stateDir,
|
|
4851
|
+
currentIds: () => this.currentHistoricalIds(),
|
|
4852
|
+
idsAtResolve: refIdsAtWrite,
|
|
4853
|
+
rawRef,
|
|
4854
|
+
frontmatter,
|
|
4855
|
+
rewrite: persist,
|
|
4856
|
+
});
|
|
4857
|
+
} catch (err) {
|
|
4858
|
+
if (priorDest === null) {
|
|
4859
|
+
await unlink(targetPath).catch(() => undefined);
|
|
4860
|
+
} else {
|
|
4861
|
+
await writeFile(targetPath, priorDest).catch(() => undefined);
|
|
4862
|
+
}
|
|
4863
|
+
this.invalidateAllMemoriesCache();
|
|
4864
|
+
throw err;
|
|
4865
|
+
}
|
|
4866
|
+
await this.entityRefRepair.syncProjection(memory.frontmatter.id, refBeforeRepair, frontmatter.entityRef);
|
|
4867
|
+
}
|
|
4868
|
+
// Sync the caller's record with the ref the file now carries: tier-move
|
|
4869
|
+
// callers keep using the passed MemoryFile, and a stale legacy ref would
|
|
4870
|
+
// mis-route later attribution/projection work (Cursor Medium, round 21).
|
|
4871
|
+
if (memory.frontmatter.entityRef !== frontmatter.entityRef) {
|
|
4872
|
+
memory.frontmatter.entityRef = frontmatter.entityRef;
|
|
4873
|
+
}
|
|
4895
4874
|
this.invalidateAllMemoriesCache();
|
|
4896
4875
|
this.notifyCatalogWrite();
|
|
4897
4876
|
}
|
|
@@ -4969,15 +4948,39 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
4969
4948
|
const today = now.toISOString().slice(0, 10);
|
|
4970
4949
|
const destDir = path.join(this.archiveDir, today);
|
|
4971
4950
|
await mkdir(destDir, { recursive: true });
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4951
|
+
// Whole-record rewrite — inherited-entityRef rule (issue #2213).
|
|
4952
|
+
const refIdsAtWrite = this.currentHistoricalIds();
|
|
4953
|
+
const updatedFm: MemoryFrontmatter = entityRefs.canonicalizeEntityRefOption(
|
|
4954
|
+
{
|
|
4955
|
+
...current.frontmatter,
|
|
4956
|
+
status: "archived",
|
|
4957
|
+
archivedAt: now.toISOString(),
|
|
4958
|
+
updated: now.toISOString(),
|
|
4959
|
+
},
|
|
4960
|
+
refIdsAtWrite,
|
|
4961
|
+
);
|
|
4978
4962
|
const fileContent = `${serializeFrontmatter(updatedFm)}\n\n${current.content}\n`;
|
|
4979
4963
|
const destPath = path.join(destDir, path.basename(current.path));
|
|
4964
|
+
// Snapshot a pre-existing destination (retried archive) so a repair
|
|
4965
|
+
// failure restores it instead of deleting the only archived copy (§14).
|
|
4966
|
+
const priorDest = await this.readStorageSecureFile(destPath).catch(() => null);
|
|
4980
4967
|
await this.writeStorageSecureFile(destPath, fileContent);
|
|
4968
|
+
if (typeof current.frontmatter.entityRef === "string") {
|
|
4969
|
+
try {
|
|
4970
|
+
await this.entityRefRepair.repair(
|
|
4971
|
+
destPath, updatedFm, current.frontmatter.entityRef, refIdsAtWrite, current.content,
|
|
4972
|
+
);
|
|
4973
|
+
} catch (err) {
|
|
4974
|
+
// Restore/remove the destination before reporting failure (§14):
|
|
4975
|
+
// a retained fresh copy would surface the memory in BOTH scans.
|
|
4976
|
+
if (priorDest === null) {
|
|
4977
|
+
await unlink(destPath).catch(() => undefined);
|
|
4978
|
+
} else {
|
|
4979
|
+
await this.writeStorageSecureFile(destPath, priorDest).catch(() => undefined);
|
|
4980
|
+
}
|
|
4981
|
+
throw err;
|
|
4982
|
+
}
|
|
4983
|
+
}
|
|
4981
4984
|
await unlink(current.path);
|
|
4982
4985
|
markDurable();
|
|
4983
4986
|
markProjectedMemoryPathInvalid(this.baseDir, current.frontmatter.id);
|
|
@@ -5008,13 +5011,9 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
5008
5011
|
return await this.runTombstoneBlockedArchive(memory, archiveCurrent);
|
|
5009
5012
|
}
|
|
5010
5013
|
|
|
5014
|
+
/** Alias of listEntityNames (kept for legacy callers; sorted). */
|
|
5011
5015
|
async readEntities(): Promise<string[]> {
|
|
5012
|
-
|
|
5013
|
-
const entries = await readdir(this.entitiesDir);
|
|
5014
|
-
return entries.filter((e) => e.endsWith(".md")).map((e) => e.replace(".md", ""));
|
|
5015
|
-
} catch {
|
|
5016
|
-
return [];
|
|
5017
|
-
}
|
|
5016
|
+
return this.entityStore.listEntityNames();
|
|
5018
5017
|
}
|
|
5019
5018
|
|
|
5020
5019
|
async readEntity(name: string): Promise<string> {
|
|
@@ -5109,15 +5108,20 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
5109
5108
|
(v, i, a) => a.indexOf(v) === i
|
|
5110
5109
|
); // dedupe
|
|
5111
5110
|
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
:
|
|
5120
|
-
|
|
5111
|
+
// Whole-record rewrite — inherited-entityRef rule (issue #2213).
|
|
5112
|
+
const refIdsAtWrite = this.currentHistoricalIds();
|
|
5113
|
+
const updated: MemoryFrontmatter = entityRefs.canonicalizeEntityRefOption(
|
|
5114
|
+
{
|
|
5115
|
+
...memory.frontmatter,
|
|
5116
|
+
updated: new Date().toISOString(),
|
|
5117
|
+
supersedes: options?.supersedes ?? memory.frontmatter.supersedes,
|
|
5118
|
+
lineage: mergedLineage.length > 0 ? mergedLineage : undefined,
|
|
5119
|
+
...(memory.frontmatter.sourceConnector === undefined && options?.sourceConnector
|
|
5120
|
+
? { sourceConnector: options.sourceConnector }
|
|
5121
|
+
: {}),
|
|
5122
|
+
},
|
|
5123
|
+
refIdsAtWrite,
|
|
5124
|
+
);
|
|
5121
5125
|
const sanitized = sanitizeMemoryContent(newContent);
|
|
5122
5126
|
if (!sanitized.clean) {
|
|
5123
5127
|
log.warn(`updated memory content sanitized for ${id}; violations=${sanitized.violations.join(", ")}`);
|
|
@@ -5126,6 +5130,12 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
5126
5130
|
await this.writeTombstoneBlockedUpdate(memory, fileContent, updated, sanitized.text, async () => {
|
|
5127
5131
|
this.invalidateAllMemoriesCache();
|
|
5128
5132
|
});
|
|
5133
|
+
if (typeof memory.frontmatter.entityRef === "string") {
|
|
5134
|
+
await this.entityRefRepair.repair(
|
|
5135
|
+
memory.path, updated, memory.frontmatter.entityRef, refIdsAtWrite, sanitized.text,
|
|
5136
|
+
{ onFailRestore: memory },
|
|
5137
|
+
);
|
|
5138
|
+
}
|
|
5129
5139
|
await this.patchHotMemoriesCache({ addedPath: memory.path });
|
|
5130
5140
|
await this.appendGeneratedMemoryLifecycleEventFailOpen("storage.updateMemory", {
|
|
5131
5141
|
memoryId: id,
|
|
@@ -5153,10 +5163,14 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
5153
5163
|
lifecycle?: MemoryLifecycleEventWriteOptions
|
|
5154
5164
|
): Promise<boolean> {
|
|
5155
5165
|
const beforeStatus = memory.frontmatter.status ?? "active";
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5166
|
+
// Canonicalize the EFFECTIVE merged entityRef (issue #2213) — an
|
|
5167
|
+
// unrelated patch must not rewrite an inherited legacy ref back out.
|
|
5168
|
+
const resolveIds = this.currentHistoricalIds();
|
|
5169
|
+
const updated: MemoryFrontmatter = entityRefs.canonicalizeEntityRefOption(
|
|
5170
|
+
{ ...memory.frontmatter, ...patch },
|
|
5171
|
+
resolveIds,
|
|
5172
|
+
);
|
|
5173
|
+
const refIds = typeof updated.entityRef === "string" ? resolveIds : null;
|
|
5160
5174
|
const afterStatus = updated.status ?? "active";
|
|
5161
5175
|
|
|
5162
5176
|
const fileContent = `${serializeFrontmatter(updated)}\n\n${memory.content}\n`;
|
|
@@ -5167,6 +5181,13 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
5167
5181
|
this.invalidateColdMemoriesCache();
|
|
5168
5182
|
}
|
|
5169
5183
|
});
|
|
5184
|
+
const rawMergedRef = typeof patch.entityRef === "string" ? patch.entityRef : memory.frontmatter.entityRef;
|
|
5185
|
+
if (refIds && typeof rawMergedRef === "string") {
|
|
5186
|
+
await this.entityRefRepair.repair(
|
|
5187
|
+
memory.path, updated, rawMergedRef, refIds, memory.content,
|
|
5188
|
+
{ onFailRestore: memory },
|
|
5189
|
+
);
|
|
5190
|
+
}
|
|
5170
5191
|
await this.patchHotMemoriesCache({ addedPath: memory.path });
|
|
5171
5192
|
if (memory.path.includes(`${path.sep}cold${path.sep}`)) {
|
|
5172
5193
|
this.invalidateColdMemoriesCache();
|
|
@@ -6057,82 +6078,18 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
6057
6078
|
log.debug(`appended namespace-local reflection to ${this.identityReflectionsPath}`);
|
|
6058
6079
|
}
|
|
6059
6080
|
|
|
6060
|
-
//
|
|
6061
|
-
//
|
|
6062
|
-
// ---------------------------------------------------------------------------
|
|
6063
|
-
|
|
6064
|
-
/**
|
|
6065
|
-
* Add a relationship to an entity file.
|
|
6066
|
-
* Deduplicates by target+label.
|
|
6067
|
-
*/
|
|
6081
|
+
// Entity-file mutators live on EntityStore (issue #2213): journal-resolved
|
|
6082
|
+
// with legacy-file fallback, under the entity-mutation lock.
|
|
6068
6083
|
async addEntityRelationship(name: string, rel: EntityRelationship): Promise<void> {
|
|
6069
|
-
|
|
6070
|
-
let entity: EntityFile;
|
|
6071
|
-
try {
|
|
6072
|
-
const content = await this.readStorageSecureFile(filePath);
|
|
6073
|
-
entity = parseEntityFile(content, this.entitySchemas);
|
|
6074
|
-
} catch (err) {
|
|
6075
|
-
if (err instanceof SecureStoreLockedError) throw err;
|
|
6076
|
-
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
6077
|
-
log.debug(`addEntityRelationship: entity file ${name}.md not found`);
|
|
6078
|
-
return;
|
|
6079
|
-
}
|
|
6080
|
-
|
|
6081
|
-
// Dedupe by target+label
|
|
6082
|
-
const exists = entity.relationships.some((r) => r.target === rel.target && r.label === rel.label);
|
|
6083
|
-
if (exists) return;
|
|
6084
|
-
|
|
6085
|
-
entity.relationships.push(rel);
|
|
6086
|
-
entity.updated = new Date().toISOString();
|
|
6087
|
-
await this.writeStorageSecureFile(filePath, serializeEntityFile(entity, this.entitySchemas));
|
|
6088
|
-
this.invalidateKnowledgeIndexCache();
|
|
6084
|
+
return this.entityStore.addEntityRelationship(name, rel);
|
|
6089
6085
|
}
|
|
6090
6086
|
|
|
6091
|
-
/**
|
|
6092
|
-
* Add an activity entry to an entity file.
|
|
6093
|
-
* Prepends to the beginning, prunes oldest entries beyond maxEntries.
|
|
6094
|
-
*/
|
|
6095
6087
|
async addEntityActivity(name: string, entry: EntityActivityEntry, maxEntries: number): Promise<void> {
|
|
6096
|
-
|
|
6097
|
-
let entity: EntityFile;
|
|
6098
|
-
try {
|
|
6099
|
-
const content = await this.readStorageSecureFile(filePath);
|
|
6100
|
-
entity = parseEntityFile(content, this.entitySchemas);
|
|
6101
|
-
} catch (err) {
|
|
6102
|
-
if (err instanceof SecureStoreLockedError) throw err;
|
|
6103
|
-
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
6104
|
-
log.debug(`addEntityActivity: entity file ${name}.md not found`);
|
|
6105
|
-
return;
|
|
6106
|
-
}
|
|
6107
|
-
|
|
6108
|
-
entity.activity.unshift(entry);
|
|
6109
|
-
if (entity.activity.length > maxEntries) {
|
|
6110
|
-
entity.activity = entity.activity.slice(0, maxEntries);
|
|
6111
|
-
}
|
|
6112
|
-
entity.updated = new Date().toISOString();
|
|
6113
|
-
await this.writeStorageSecureFile(filePath, serializeEntityFile(entity, this.entitySchemas));
|
|
6114
|
-
this.invalidateKnowledgeIndexCache();
|
|
6088
|
+
return this.entityStore.addEntityActivity(name, entry, maxEntries);
|
|
6115
6089
|
}
|
|
6116
6090
|
|
|
6117
6091
|
async addEntityAlias(name: string, alias: string): Promise<void> {
|
|
6118
|
-
|
|
6119
|
-
let entity: EntityFile;
|
|
6120
|
-
try {
|
|
6121
|
-
const content = await this.readStorageSecureFile(filePath);
|
|
6122
|
-
entity = parseEntityFile(content, this.entitySchemas);
|
|
6123
|
-
} catch (err) {
|
|
6124
|
-
if (err instanceof SecureStoreLockedError) throw err;
|
|
6125
|
-
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
6126
|
-
log.debug(`addEntityAlias: entity file ${name}.md not found`);
|
|
6127
|
-
return;
|
|
6128
|
-
}
|
|
6129
|
-
|
|
6130
|
-
if (entity.aliases.includes(alias)) return;
|
|
6131
|
-
entity.aliases.push(alias);
|
|
6132
|
-
entity.updated = new Date().toISOString();
|
|
6133
|
-
await this.writeStorageSecureFile(filePath, serializeEntityFile(entity, this.entitySchemas));
|
|
6134
|
-
this.invalidateKnowledgeIndexCache();
|
|
6135
|
-
this.bumpMemoryStatusVersion();
|
|
6092
|
+
return this.entityStore.addEntityAlias(name, alias);
|
|
6136
6093
|
}
|
|
6137
6094
|
|
|
6138
6095
|
async updateEntitySynthesis(
|
|
@@ -6150,24 +6107,13 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
6150
6107
|
return this.entityStore.updateEntitySynthesis(name, synthesis, options);
|
|
6151
6108
|
}
|
|
6152
6109
|
|
|
6153
|
-
/**
|
|
6154
|
-
* Backward-compatible alias for legacy callers/tests.
|
|
6155
|
-
*/
|
|
6110
|
+
/** Backward-compatible alias for legacy callers/tests. */
|
|
6156
6111
|
async updateEntitySummary(name: string, summary: string): Promise<void> {
|
|
6157
6112
|
const updatedAt = new Date().toISOString();
|
|
6158
|
-
|
|
6159
|
-
try {
|
|
6160
|
-
const filePath = path.join(this.entitiesDir, `${name}.md`);
|
|
6161
|
-
const content = await this.readStorageSecureFile(filePath);
|
|
6162
|
-
synthesisTimelineCount = parseEntityFile(content, this.entitySchemas).timeline.length;
|
|
6163
|
-
} catch (err) {
|
|
6164
|
-
if (err instanceof SecureStoreLockedError) throw err;
|
|
6165
|
-
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
6166
|
-
synthesisTimelineCount = undefined;
|
|
6167
|
-
}
|
|
6113
|
+
const raw = await this.readEntity(name);
|
|
6168
6114
|
await this.updateEntitySynthesis(name, summary, {
|
|
6169
6115
|
entityUpdatedAt: updatedAt,
|
|
6170
|
-
synthesisTimelineCount,
|
|
6116
|
+
synthesisTimelineCount: raw ? parseEntityFile(raw, this.entitySchemas).timeline.length : undefined,
|
|
6171
6117
|
updatedAt,
|
|
6172
6118
|
});
|
|
6173
6119
|
}
|
|
@@ -6243,28 +6189,8 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
6243
6189
|
);
|
|
6244
6190
|
}
|
|
6245
6191
|
|
|
6246
|
-
async migrateEntityFilesToCompiledTruthTimeline(): Promise<{
|
|
6247
|
-
|
|
6248
|
-
migrated: number;
|
|
6249
|
-
}> {
|
|
6250
|
-
const entityNames = await this.listEntityNames();
|
|
6251
|
-
let migrated = 0;
|
|
6252
|
-
for (const entityName of entityNames) {
|
|
6253
|
-
const raw = await this.readEntity(entityName);
|
|
6254
|
-
if (!raw) continue;
|
|
6255
|
-
const serialized = serializeEntityFile(parseEntityFile(raw, this.entitySchemas), this.entitySchemas);
|
|
6256
|
-
if (raw.trimEnd() === serialized.trimEnd()) continue;
|
|
6257
|
-
await this.writeStorageSecureFile(path.join(this.entitiesDir, `${entityName}.md`), serialized);
|
|
6258
|
-
migrated += 1;
|
|
6259
|
-
}
|
|
6260
|
-
if (migrated > 0) {
|
|
6261
|
-
this.invalidateKnowledgeIndexCache();
|
|
6262
|
-
this.bumpMemoryStatusVersion();
|
|
6263
|
-
}
|
|
6264
|
-
return {
|
|
6265
|
-
total: entityNames.length,
|
|
6266
|
-
migrated,
|
|
6267
|
-
};
|
|
6192
|
+
async migrateEntityFilesToCompiledTruthTimeline(): Promise<{ total: number; migrated: number }> {
|
|
6193
|
+
return this.entityStore.migrateEntityFilesToCompiledTruthTimeline();
|
|
6268
6194
|
}
|
|
6269
6195
|
|
|
6270
6196
|
// ---------------------------------------------------------------------------
|
|
@@ -6408,19 +6334,24 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
6408
6334
|
: memoryMap.get(entry.memoryId);
|
|
6409
6335
|
if (!memory) continue;
|
|
6410
6336
|
|
|
6411
|
-
const
|
|
6412
|
-
|
|
6413
|
-
accessCount: entry.newCount,
|
|
6414
|
-
|
|
6415
|
-
|
|
6337
|
+
const rowIds = this.currentHistoricalIds();
|
|
6338
|
+
const newFm: MemoryFrontmatter = entityRefs.canonicalizeEntityRefOption(
|
|
6339
|
+
{ ...memory.frontmatter, accessCount: entry.newCount, lastAccessed: entry.lastAccessed },
|
|
6340
|
+
rowIds,
|
|
6341
|
+
);
|
|
6416
6342
|
|
|
6417
6343
|
try {
|
|
6418
6344
|
const fileContent = `${serializeFrontmatter(newFm)}\n\n${memory.content}\n`;
|
|
6419
6345
|
await this.writeStorageSecureFile(memory.path, fileContent);
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6346
|
+
if (typeof memory.frontmatter.entityRef === "string") {
|
|
6347
|
+
// onFailRestore (§14): the count patch was never reported applied.
|
|
6348
|
+
await this.entityRefRepair.repair(
|
|
6349
|
+
memory.path, newFm, memory.frontmatter.entityRef, rowIds, memory.content,
|
|
6350
|
+
{ onFailRestore: memory },
|
|
6351
|
+
);
|
|
6352
|
+
}
|
|
6353
|
+
// Patch the hot corpus cache entry in place (#1902); the shared
|
|
6354
|
+
// sentinel is bumped once after the loop for cross-process coherence.
|
|
6424
6355
|
if (warm) {
|
|
6425
6356
|
updateCacheOnWrite(this.baseDir, { ...memory, frontmatter: newFm }, keyId);
|
|
6426
6357
|
}
|
|
@@ -6684,6 +6615,9 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
6684
6615
|
} = {}
|
|
6685
6616
|
): Promise<string> {
|
|
6686
6617
|
await this.ensureDirectories();
|
|
6618
|
+
const rawEntityRef = options.entityRef;
|
|
6619
|
+
let refIds = typeof options.entityRef === "string" ? this.currentHistoricalIds() : null;
|
|
6620
|
+
if (refIds) options = entityRefs.canonicalizeEntityRefOption(options, refIds);
|
|
6687
6621
|
const now = new Date();
|
|
6688
6622
|
const today = now.toISOString().slice(0, 10);
|
|
6689
6623
|
const id = `${parentId}-chunk-${chunkIndex}`;
|
|
@@ -6727,11 +6661,24 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
6727
6661
|
if (!sanitized.clean) {
|
|
6728
6662
|
log.warn(`chunk content sanitized for ${id}; violations=${sanitized.violations.join(", ")}`);
|
|
6729
6663
|
}
|
|
6730
|
-
const fileContent = `${serializeFrontmatter(fm)}\n\n${sanitized.text}\n`;
|
|
6731
|
-
|
|
6732
6664
|
const filePath = await this.resolveCategoryWritePath(category, id, today);
|
|
6665
|
+
const fileContent = `${serializeFrontmatter(fm)}\n\n${sanitized.text}\n`;
|
|
6666
|
+
// A retried chunk write lands on the SAME deterministic path, so the
|
|
6667
|
+
// target can already hold a valid prior chunk: snapshot it, and a repair
|
|
6668
|
+
// failure restores it — unlink only a file THIS invocation created
|
|
6669
|
+
// (Codex P2, round 22).
|
|
6670
|
+
let priorChunk: MemoryFile | null = null;
|
|
6671
|
+
try {
|
|
6672
|
+
const priorRaw = await readMaybeEncryptedFile(filePath, this._secureStoreKey, this.baseDir);
|
|
6673
|
+
const parsedPrior = parseFrontmatter(priorRaw);
|
|
6674
|
+
if (parsedPrior) {
|
|
6675
|
+
priorChunk = { path: filePath, frontmatter: parsedPrior.frontmatter, content: parsedPrior.content };
|
|
6676
|
+
}
|
|
6677
|
+
} catch {
|
|
6678
|
+
priorChunk = null;
|
|
6679
|
+
}
|
|
6733
6680
|
|
|
6734
|
-
|
|
6681
|
+
const written = await this.writeTombstoneBlockedChunk(
|
|
6735
6682
|
filePath,
|
|
6736
6683
|
fileContent,
|
|
6737
6684
|
fm,
|
|
@@ -6744,6 +6691,18 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
6744
6691
|
log.debug(`wrote chunk ${id} (${chunkIndex + 1}/${chunkTotal}) to ${filePath}`);
|
|
6745
6692
|
}
|
|
6746
6693
|
);
|
|
6694
|
+
// Repair only when THIS chunk persisted: a tombstone-dedupe result
|
|
6695
|
+
// returns an existing id without writing filePath, and an unconditional
|
|
6696
|
+
// repair rewrite would create a stray second chunk from scratch. The
|
|
6697
|
+
// rewrite RE-GATES under the final ref (Bugbot): a journal move into
|
|
6698
|
+
// tombstone-blocked identity space must not leave a fact chunk active.
|
|
6699
|
+
if (written === id && refIds && typeof rawEntityRef === "string") {
|
|
6700
|
+
await this.entityRefRepair.repair(filePath, fm, rawEntityRef, refIds, sanitized.text, {
|
|
6701
|
+
regateFact: true,
|
|
6702
|
+
...(priorChunk ? { onFailRestore: priorChunk } : { onFailRemove: filePath }),
|
|
6703
|
+
});
|
|
6704
|
+
}
|
|
6705
|
+
return written;
|
|
6747
6706
|
}
|
|
6748
6707
|
|
|
6749
6708
|
/**
|
|
@@ -6771,18 +6730,25 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
6771
6730
|
if (!oldMemory) return false;
|
|
6772
6731
|
|
|
6773
6732
|
const now = new Date().toISOString();
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
supersededBy: newMemoryId,
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
};
|
|
6733
|
+
// Whole-record rewrite — inherited-entityRef rule (issue #2213).
|
|
6734
|
+
const refIdsAtWrite = this.currentHistoricalIds();
|
|
6735
|
+
const updatedFm: MemoryFrontmatter = entityRefs.canonicalizeEntityRefOption(
|
|
6736
|
+
{ ...oldMemory.frontmatter, status: "superseded", supersededBy: newMemoryId, supersededAt: now, updated: now },
|
|
6737
|
+
refIdsAtWrite,
|
|
6738
|
+
);
|
|
6781
6739
|
|
|
6782
6740
|
const fileContent = `${serializeFrontmatter(updatedFm)}\n\n${oldMemory.content}\n`;
|
|
6783
6741
|
|
|
6784
6742
|
try {
|
|
6785
6743
|
await this.writeStorageSecureFile(oldMemory.path, fileContent);
|
|
6744
|
+
if (typeof oldMemory.frontmatter.entityRef === "string") {
|
|
6745
|
+
// onFailRestore (§14): a record left superseded on disk would skip
|
|
6746
|
+
// its non-resurrection tombstone while caches expose the old state.
|
|
6747
|
+
await this.entityRefRepair.repair(
|
|
6748
|
+
oldMemory.path, updatedFm, oldMemory.frontmatter.entityRef, refIdsAtWrite, oldMemory.content,
|
|
6749
|
+
{ onFailRestore: oldMemory },
|
|
6750
|
+
);
|
|
6751
|
+
}
|
|
6786
6752
|
// Advance the corpus sentinel immediately after the on-disk write, BEFORE
|
|
6787
6753
|
// the awaited lifecycle append (Cursor Medium, #1902). Otherwise a warm
|
|
6788
6754
|
// hot-memories cache keeps serving the pre-supersede snapshot during the
|
|
@@ -6802,20 +6768,17 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
6802
6768
|
this.bumpMemoryStatusVersion();
|
|
6803
6769
|
log.debug(`superseded memory ${oldMemoryId} by ${newMemoryId}: ${reason}`);
|
|
6804
6770
|
|
|
6805
|
-
//
|
|
6806
|
-
//
|
|
6807
|
-
//
|
|
6808
|
-
//
|
|
6809
|
-
// one tombstone PER derived supersession key (buildRetiredFactTombstoneInputs)
|
|
6810
|
-
// so a paraphrased re-write is caught on the keyed tier — a single
|
|
6811
|
-
// entityRef-only record missed and the fact resurrected until rebuild.
|
|
6771
|
+
// #1579 — every contradiction verb (keep-a/keep-b/merge) funnels here,
|
|
6772
|
+
// so emitting covers each exactly once (rule 22); facts only. One
|
|
6773
|
+
// tombstone PER derived supersession key (thread Oci-Y) so a
|
|
6774
|
+
// paraphrased re-write is caught on the keyed tier.
|
|
6812
6775
|
if (oldMemory.frontmatter.category === "fact") {
|
|
6813
6776
|
for (const input of buildRetiredFactTombstoneInputs(
|
|
6814
6777
|
{
|
|
6815
6778
|
id: oldMemoryId,
|
|
6816
6779
|
content: stripCitationForTemplate(oldMemory.content, this.citationTemplate),
|
|
6817
6780
|
contentHash: oldMemory.frontmatter.contentHash,
|
|
6818
|
-
entityRef:
|
|
6781
|
+
entityRef: updatedFm.entityRef,
|
|
6819
6782
|
structuredAttributes: oldMemory.frontmatter.structuredAttributes,
|
|
6820
6783
|
},
|
|
6821
6784
|
{
|
|
@@ -6901,20 +6864,24 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
6901
6864
|
if (!memory) continue;
|
|
6902
6865
|
|
|
6903
6866
|
const now = new Date().toISOString();
|
|
6904
|
-
const
|
|
6905
|
-
|
|
6906
|
-
status: "archived",
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
};
|
|
6867
|
+
const rowIds = this.currentHistoricalIds();
|
|
6868
|
+
const updatedFm: MemoryFrontmatter = entityRefs.canonicalizeEntityRefOption(
|
|
6869
|
+
{ ...memory.frontmatter, status: "archived", archivedAt: now, updated: now },
|
|
6870
|
+
rowIds,
|
|
6871
|
+
);
|
|
6910
6872
|
|
|
6911
6873
|
try {
|
|
6912
6874
|
const fileContent = `${serializeFrontmatter(updatedFm)}\n\n${memory.content}\n`;
|
|
6913
6875
|
await this.writeStorageSecureFile(memory.path, fileContent);
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6876
|
+
if (typeof memory.frontmatter.entityRef === "string") {
|
|
6877
|
+
// onFailRestore (§14): the method reports the row NOT archived.
|
|
6878
|
+
await this.entityRefRepair.repair(
|
|
6879
|
+
memory.path, updatedFm, memory.frontmatter.entityRef, rowIds, memory.content,
|
|
6880
|
+
{ onFailRestore: memory },
|
|
6881
|
+
);
|
|
6882
|
+
}
|
|
6883
|
+
// Per-file corpus bump BEFORE the awaited lifecycle append (#1902):
|
|
6884
|
+
// the end-of-loop status bump fires only after the whole batch.
|
|
6918
6885
|
this.bumpMemoryCorpusVersion();
|
|
6919
6886
|
await this.appendGeneratedMemoryLifecycleEventFailOpen("storage.archiveMemories", {
|
|
6920
6887
|
memoryId: id,
|