@remnic/core 9.35.1 → 9.35.3
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 +43 -42
- 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 +5 -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-2NUC473W.js → chunk-AKELMJO2.js} +220 -48
- package/dist/{chunk-2NUC473W.js.map → chunk-AKELMJO2.js.map} +1 -1
- 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-NQUF7XKR.js → chunk-FT4RF53O.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-XWDZQIYU.js → chunk-IJAQCBGV.js} +2 -2
- package/dist/{chunk-TJLLYV2X.js → chunk-IMEXE4XS.js} +7 -7
- package/dist/{chunk-ZCPP5OW6.js → chunk-JQSBRLJ6.js} +31 -27
- package/dist/chunk-JQSBRLJ6.js.map +1 -0
- package/dist/{chunk-IZXTZ7BF.js → chunk-JYLN6KHH.js} +6 -6
- package/dist/{chunk-HEVWWPTL.js → chunk-LH3KXIBJ.js} +17 -17
- package/dist/{chunk-EH5U2YCX.js → chunk-M3DBB2ND.js} +2 -2
- package/dist/{chunk-XABS455Y.js → chunk-NBAEGFXT.js} +5 -5
- package/dist/{chunk-DP7ZV2II.js → chunk-OA6PIPDE.js} +2 -2
- 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-WKDFJXW5.js +158 -0
- package/dist/chunk-WKDFJXW5.js.map +1 -0
- package/dist/{chunk-SLPLPGJQ.js → chunk-WL2S2UBB.js} +5 -5
- package/dist/{chunk-AHZYNPLX.js → chunk-YVMVVE4B.js} +2 -2
- package/dist/{chunk-5HTAJCTO.js → chunk-ZB3YODID.js} +15 -15
- 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 +13 -12
- package/dist/{forget-4UY2EOKH.js → forget-DNAJ66ZD.js} +2 -2
- package/dist/index.d.ts +398 -398
- package/dist/index.js +43 -42
- package/dist/local-llm-helpers.d.ts +63 -1
- package/dist/local-llm-helpers.js +10 -1
- package/dist/local-llm.d.ts +4 -1
- package/dist/local-llm.js +2 -2
- 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 +43 -42
- package/dist/page-versioning.js +7 -1
- package/dist/schemas.d.ts +76 -76
- 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/summarizer.js +3 -3
- 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/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/local-llm-helpers.ts +171 -0
- package/src/local-llm.ts +34 -30
- 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-4RNZMRAV.js +0 -60
- package/dist/chunk-4RNZMRAV.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-ZCPP5OW6.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-NQUF7XKR.js.map → chunk-FT4RF53O.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-HEVWWPTL.js.map → chunk-LH3KXIBJ.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-DP7ZV2II.js.map → chunk-OA6PIPDE.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-SLPLPGJQ.js.map → chunk-WL2S2UBB.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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-persist entity-reference repair (issue #2213), extracted from
|
|
3
|
+
* storage.ts (#1520 god-file ratchet).
|
|
4
|
+
*
|
|
5
|
+
* Every store-mediated writer that persists an `entityRef` resolves it at
|
|
6
|
+
* the write; when the canonical-id journal moves ACROSS the write (a peer
|
|
7
|
+
* migration publishing between resolve and persist), the repair re-resolves
|
|
8
|
+
* from the caller's ORIGINAL ref and rewrites through the blocked-capture
|
|
9
|
+
* surface. On repair failure the caller-declared rollback runs BEFORE the
|
|
10
|
+
* error propagates (AGENTS.md §14): a caller that reports failure must
|
|
11
|
+
* leave disk in the state peers/caches were told about.
|
|
12
|
+
*/
|
|
13
|
+
import { unlink } from "node:fs/promises";
|
|
14
|
+
import { log } from "../logger.js";
|
|
15
|
+
import type { MemoryFile, MemoryFrontmatter } from "../types.js";
|
|
16
|
+
import * as entityRefs from "./entity-canonical-id-references.js";
|
|
17
|
+
import {
|
|
18
|
+
applyTombstoneResurrectionGate,
|
|
19
|
+
type TombstoneMatch,
|
|
20
|
+
type TombstoneStore,
|
|
21
|
+
} from "../lifecycle/tombstones.js";
|
|
22
|
+
import { supersessionKeysForFact } from "../temporal-supersession.js";
|
|
23
|
+
import { ContentHashIndex } from "./content-hash-index.js";
|
|
24
|
+
import { rewriteProjectedMemoryEntityReference } from "../memory-projection-mutations.js";
|
|
25
|
+
|
|
26
|
+
export interface EntityRefRepairDeps {
|
|
27
|
+
readonly stateDir: string;
|
|
28
|
+
readonly baseDir: string;
|
|
29
|
+
currentHistoricalIds(): Readonly<Record<string, string>>;
|
|
30
|
+
serializeFrontmatter(fm: MemoryFrontmatter): string;
|
|
31
|
+
writeTombstoneBlockedMemory(
|
|
32
|
+
pathname: string,
|
|
33
|
+
fileContent: string,
|
|
34
|
+
frontmatter: MemoryFrontmatter,
|
|
35
|
+
content: string,
|
|
36
|
+
): Promise<void>;
|
|
37
|
+
invalidateAllMemoriesCache(): void;
|
|
38
|
+
getTombstoneStore(): Promise<TombstoneStore>;
|
|
39
|
+
tombstonesEnabled(): boolean;
|
|
40
|
+
tombstonesNamespace(): string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface EntityRefRepairOptions {
|
|
44
|
+
/** Re-run the add-only resurrection gate under the FINAL ref before the
|
|
45
|
+
* rewrite (chunk writes, where `body` IS the hash source). */
|
|
46
|
+
regateFact?: boolean;
|
|
47
|
+
/** Rollback: rewrite this pre-mutation record on repair failure. */
|
|
48
|
+
onFailRestore?: MemoryFile;
|
|
49
|
+
/** Rollback: unlink a file this mutation created on repair failure. */
|
|
50
|
+
onFailRemove?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class EntityRefRepair {
|
|
54
|
+
constructor(private readonly deps: EntityRefRepairDeps) {}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Shared lookup+apply core for the write-time resurrection gate (#1579 /
|
|
58
|
+
* #2213): fail-open, add-only; writeMemory's gate + chunk-repair re-gate
|
|
59
|
+
* consult this one implementation (rule 43).
|
|
60
|
+
*/
|
|
61
|
+
async gate(
|
|
62
|
+
fm: MemoryFrontmatter,
|
|
63
|
+
hashSource: string,
|
|
64
|
+
structuredAttributes?: Record<string, string>,
|
|
65
|
+
): Promise<TombstoneMatch | null> {
|
|
66
|
+
if (!this.deps.tombstonesEnabled()) return null;
|
|
67
|
+
try {
|
|
68
|
+
const store = await this.deps.getTombstoneStore();
|
|
69
|
+
// Pass EVERY derived key (thread Ociag/Oci-W): emitters register one
|
|
70
|
+
// tombstone per key, so the block can be on any of them.
|
|
71
|
+
const supersessionKeys =
|
|
72
|
+
fm.entityRef && structuredAttributes
|
|
73
|
+
? supersessionKeysForFact({ entityRef: fm.entityRef, structuredAttributes })
|
|
74
|
+
: [];
|
|
75
|
+
const match = applyTombstoneResurrectionGate(store, fm, {
|
|
76
|
+
normalizedText: ContentHashIndex.normalizeContent(hashSource),
|
|
77
|
+
supersessionKeys,
|
|
78
|
+
namespace: this.deps.tombstonesNamespace(),
|
|
79
|
+
});
|
|
80
|
+
if (match) {
|
|
81
|
+
log.info(
|
|
82
|
+
`tombstone: blocked resurrection of fact ${fm.id} (tier=${match.matchedTier}, tombstone=${match.tombstoneId}, reason=${match.reason})`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return match;
|
|
86
|
+
} catch (err) {
|
|
87
|
+
// Fail-open (rule 34): a lookup error must not block the write.
|
|
88
|
+
log.warn(`tombstone lookup failed for fact ${fm.id} (fail-open): ${err}`);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Post-persist repair delegate — see the references module for semantics. */
|
|
94
|
+
async repair(
|
|
95
|
+
filePath: string,
|
|
96
|
+
fm: MemoryFrontmatter,
|
|
97
|
+
rawRef: string,
|
|
98
|
+
refIds: Readonly<Record<string, string>>,
|
|
99
|
+
body: string,
|
|
100
|
+
opts: EntityRefRepairOptions = {},
|
|
101
|
+
): Promise<void> {
|
|
102
|
+
const refBeforeRepair = fm.entityRef;
|
|
103
|
+
try {
|
|
104
|
+
await entityRefs.repairEntityRefAfterJournalMove({
|
|
105
|
+
stateDir: this.deps.stateDir,
|
|
106
|
+
currentIds: () => this.deps.currentHistoricalIds(),
|
|
107
|
+
idsAtResolve: refIds,
|
|
108
|
+
rawRef,
|
|
109
|
+
frontmatter: fm,
|
|
110
|
+
rewrite: async () => {
|
|
111
|
+
if (opts.regateFact && fm.category === "fact") {
|
|
112
|
+
await this.gate(fm, body, undefined);
|
|
113
|
+
}
|
|
114
|
+
// Blocked-capture surface: a repair rewrite of a tombstone-blocked
|
|
115
|
+
// record must keep TombstoneBlockedCaptureIndex consistent.
|
|
116
|
+
await this.deps.writeTombstoneBlockedMemory(
|
|
117
|
+
filePath,
|
|
118
|
+
`${this.deps.serializeFrontmatter(fm)}\n\n${body}\n`,
|
|
119
|
+
fm,
|
|
120
|
+
body,
|
|
121
|
+
);
|
|
122
|
+
this.deps.invalidateAllMemoriesCache();
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
} catch (err) {
|
|
126
|
+
if (opts.onFailRestore) {
|
|
127
|
+
await this.restore(opts.onFailRestore);
|
|
128
|
+
} else if (opts.onFailRemove) {
|
|
129
|
+
await unlink(opts.onFailRemove).catch(() => undefined);
|
|
130
|
+
this.deps.invalidateAllMemoriesCache();
|
|
131
|
+
}
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
await this.syncProjection(fm.id, refBeforeRepair, fm.entityRef);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Restore a record's pre-mutation bytes through the blocked-capture
|
|
139
|
+
* surface when a post-persist repair fails (§14).
|
|
140
|
+
*/
|
|
141
|
+
async restore(before: MemoryFile): Promise<void> {
|
|
142
|
+
try {
|
|
143
|
+
await this.deps.writeTombstoneBlockedMemory(
|
|
144
|
+
before.path,
|
|
145
|
+
`${this.deps.serializeFrontmatter(before.frontmatter)}\n\n${before.content}\n`,
|
|
146
|
+
before.frontmatter,
|
|
147
|
+
before.content,
|
|
148
|
+
);
|
|
149
|
+
} catch (restoreErr) {
|
|
150
|
+
log.warn(`failed to restore ${before.frontmatter.id} after repair failure: ${restoreErr}`);
|
|
151
|
+
}
|
|
152
|
+
this.deps.invalidateAllMemoriesCache();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Projection rows written under the pre-repair claimant cannot be restored
|
|
157
|
+
* once the mapping is parked (Codex P1). Fail-open: projection is rebuildable.
|
|
158
|
+
*/
|
|
159
|
+
async syncProjection(
|
|
160
|
+
memoryId: string,
|
|
161
|
+
refBefore: string | undefined,
|
|
162
|
+
refAfter: string | undefined,
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
if (typeof refBefore !== "string" || typeof refAfter !== "string" || refBefore === refAfter) return;
|
|
165
|
+
try {
|
|
166
|
+
await rewriteProjectedMemoryEntityReference(this.deps.baseDir, memoryId, refBefore, refAfter);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
log.warn(`projection entityRef sync failed for ${memoryId}: ${err}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -13,13 +13,25 @@
|
|
|
13
13
|
import { readdir, unlink } from "node:fs/promises";
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import { normalizeEntityStructuredSection, sortStructuredSectionsBySchema } from "../entity-schema.js";
|
|
16
|
-
import { withEntityCanonicalMutationLock } from "./entity-canonical-id-
|
|
16
|
+
import { withEntityCanonicalMutationLock } from "./entity-canonical-id-lock.js";
|
|
17
17
|
import { log } from "../logger.js";
|
|
18
18
|
import { getCachedEntities, setCachedEntities } from "../memory-cache.js";
|
|
19
19
|
import type { VersionTrigger } from "../page-versioning.js";
|
|
20
20
|
import { SecureStoreLockedError } from "../secure-store/secure-fs.js";
|
|
21
|
-
import type {
|
|
21
|
+
import type {
|
|
22
|
+
EntityActivityEntry,
|
|
23
|
+
EntityFile,
|
|
24
|
+
EntityRelationship,
|
|
25
|
+
EntityStructuredSection,
|
|
26
|
+
PluginConfig,
|
|
27
|
+
ScoredEntity,
|
|
28
|
+
} from "../types.js";
|
|
22
29
|
import { isErrnoCode } from "../utils/errno.js";
|
|
30
|
+
import {
|
|
31
|
+
reconcileIfJournalMovedSync,
|
|
32
|
+
requestEntityCanonicalIdReconcileSync,
|
|
33
|
+
resolveHistoricalEntityCanonicalId,
|
|
34
|
+
} from "./entity-canonical-id-references.js";
|
|
23
35
|
import {
|
|
24
36
|
buildEntitySchemaCacheKey,
|
|
25
37
|
compareEntityTimestamps,
|
|
@@ -38,9 +50,12 @@ export interface EntityStoreDeps {
|
|
|
38
50
|
readonly storageManagerClass: typeof StorageManager;
|
|
39
51
|
readonly baseDir: string;
|
|
40
52
|
bumpMemoryStatusVersion(): void;
|
|
53
|
+
/** Entity-mutation sentinel for the migration fingerprint (issue #2213). */
|
|
54
|
+
bumpEntityMutationVersion(): void;
|
|
41
55
|
ensureDirectories(): Promise<void>;
|
|
42
56
|
readonly entitiesDir: string;
|
|
43
57
|
readonly entitySchemas: PluginConfig["entitySchemas"] | undefined;
|
|
58
|
+
currentHistoricalIds(): Readonly<Record<string, string>>;
|
|
44
59
|
findMatchingEntity(proposedName: string, type: string): Promise<string | null>;
|
|
45
60
|
getEntityCacheSecureStoreKey(): string;
|
|
46
61
|
getMemoryStatusVersion(): number;
|
|
@@ -66,6 +81,118 @@ export class EntityStore {
|
|
|
66
81
|
return normalizeEntityNameShared(raw, type, this.deps.userAliases);
|
|
67
82
|
}
|
|
68
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Read an entity file by id, resolving through the migration journal with a
|
|
86
|
+
* fallback to the caller's original id (issue #2213): mid-migration, the
|
|
87
|
+
* journal maps legacy → canonical BEFORE the file moves — writing the
|
|
88
|
+
* still-existing legacy file is safe (the rename retry carries it), while
|
|
89
|
+
* resolving only the canonical path silently drops the mutation.
|
|
90
|
+
*/
|
|
91
|
+
private async readEntityForMutation(
|
|
92
|
+
name: string,
|
|
93
|
+
method: string,
|
|
94
|
+
historicalIds: Readonly<Record<string, string>>,
|
|
95
|
+
): Promise<{ filePath: string; entity: EntityFile } | null> {
|
|
96
|
+
const canonical = resolveHistoricalEntityCanonicalId(name, historicalIds);
|
|
97
|
+
for (const candidate of canonical === name ? [name] : [canonical, name]) {
|
|
98
|
+
const filePath = path.join(this.deps.entitiesDir, `${candidate}.md`);
|
|
99
|
+
try {
|
|
100
|
+
const entity = parseEntityFile(await this.deps.readStorageSecureFile(filePath), this.deps.entitySchemas);
|
|
101
|
+
return { filePath, entity };
|
|
102
|
+
} catch (err) {
|
|
103
|
+
if (err instanceof SecureStoreLockedError) throw err;
|
|
104
|
+
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
log.debug(`${method}: entity file ${canonical}.md not found`);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Locked read-modify-write for the entity-file mutators (issue #2213):
|
|
113
|
+
* holding the entity-mutation lock keeps `migrateEntityFilePair` (which
|
|
114
|
+
* takes the same lock) from moving the file between the fallback read and
|
|
115
|
+
* this write — an unlocked write could recreate a just-renamed legacy file.
|
|
116
|
+
* The journal itself is NOT serialized by this lock (`pruneBlocked` can
|
|
117
|
+
* park a mapping mid-mutation), so the journal identity is REVALIDATED
|
|
118
|
+
* just before committing — a moved journal restarts resolution against the
|
|
119
|
+
* fresh table instead of writing into a since-contested claimant — and
|
|
120
|
+
* checked once more after the write for a move across the write itself.
|
|
121
|
+
* `mutate` returns false to skip the write. Resolves true when written.
|
|
122
|
+
*/
|
|
123
|
+
private async mutateEntityFile(
|
|
124
|
+
name: string,
|
|
125
|
+
method: string,
|
|
126
|
+
mutate: (entity: EntityFile, historicalIds: Readonly<Record<string, string>>) => boolean,
|
|
127
|
+
): Promise<boolean> {
|
|
128
|
+
const stateDir = path.join(this.deps.baseDir, "state");
|
|
129
|
+
return withEntityCanonicalMutationLock(stateDir, async () => {
|
|
130
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
131
|
+
const idsAtResolve = this.deps.currentHistoricalIds();
|
|
132
|
+
const located = await this.readEntityForMutation(name, method, idsAtResolve);
|
|
133
|
+
const mutated = located !== null && mutate(located.entity, idsAtResolve);
|
|
134
|
+
// Revalidate BEFORE honoring either verdict: a journal that moved
|
|
135
|
+
// makes both the resolution AND a dedupe-based no-op stale (a parked
|
|
136
|
+
// mapping can make "already present on the claimant" wrong).
|
|
137
|
+
if (this.deps.currentHistoricalIds() !== idsAtResolve) continue;
|
|
138
|
+
if (!located || !mutated) return false;
|
|
139
|
+
located.entity.updated = new Date().toISOString();
|
|
140
|
+
await this.deps.writeStorageSecureFile(
|
|
141
|
+
located.filePath,
|
|
142
|
+
serializeEntityFile(located.entity, this.deps.entitySchemas),
|
|
143
|
+
);
|
|
144
|
+
reconcileIfJournalMovedSync(stateDir, idsAtResolve, this.deps.currentHistoricalIds());
|
|
145
|
+
this.deps.invalidateKnowledgeIndexCache();
|
|
146
|
+
this.deps.bumpEntityMutationVersion();
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
// The journal kept moving under us. A silent false would DROP the
|
|
150
|
+
// requested mutation (the reconcile pass rewrites existing references;
|
|
151
|
+
// it cannot replay a mutation that was never written) — surface a
|
|
152
|
+
// retryable error so the caller's persist machinery logs/retries it.
|
|
153
|
+
requestEntityCanonicalIdReconcileSync(stateDir);
|
|
154
|
+
throw new Error(
|
|
155
|
+
`${method}: entity canonical-id journal kept changing; mutation not applied — retry`,
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Add a relationship to an entity file. Deduplicates by target+label. */
|
|
161
|
+
async addEntityRelationship(name: string, rel: EntityRelationship): Promise<void> {
|
|
162
|
+
await this.mutateEntityFile(name, "addEntityRelationship", (entity, historicalIds) => {
|
|
163
|
+
// Resolve the target with the SAME snapshot the locked read used, and
|
|
164
|
+
// dedupe against CANONICALIZED stored targets — an existing edge the
|
|
165
|
+
// migration has not rewritten yet still names the legacy id, and a
|
|
166
|
+
// plain equality check would miss it and leave duplicates after the
|
|
167
|
+
// rewrite (issue #2213).
|
|
168
|
+
const target = resolveHistoricalEntityCanonicalId(rel.target, historicalIds);
|
|
169
|
+
const duplicate = entity.relationships.some(
|
|
170
|
+
(r) => resolveHistoricalEntityCanonicalId(r.target, historicalIds) === target && r.label === rel.label,
|
|
171
|
+
);
|
|
172
|
+
if (duplicate) return false;
|
|
173
|
+
entity.relationships.push({ ...rel, target });
|
|
174
|
+
return true;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Add an activity entry to an entity file; prunes oldest beyond maxEntries. */
|
|
179
|
+
async addEntityActivity(name: string, entry: EntityActivityEntry, maxEntries: number): Promise<void> {
|
|
180
|
+
await this.mutateEntityFile(name, "addEntityActivity", (entity) => {
|
|
181
|
+
entity.activity.unshift(entry);
|
|
182
|
+
if (entity.activity.length > maxEntries) entity.activity = entity.activity.slice(0, maxEntries);
|
|
183
|
+
return true;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async addEntityAlias(name: string, alias: string): Promise<void> {
|
|
188
|
+
const wrote = await this.mutateEntityFile(name, "addEntityAlias", (entity) => {
|
|
189
|
+
if (entity.aliases.includes(alias)) return false;
|
|
190
|
+
entity.aliases.push(alias);
|
|
191
|
+
return true;
|
|
192
|
+
});
|
|
193
|
+
if (wrote) this.deps.bumpMemoryStatusVersion();
|
|
194
|
+
}
|
|
195
|
+
|
|
69
196
|
async writeEntity(
|
|
70
197
|
name: string,
|
|
71
198
|
type: string,
|
|
@@ -212,6 +339,7 @@ export class EntityStore {
|
|
|
212
339
|
await this.deps.writeStorageSecureFile(filePath, serializeEntityFile(entity, this.deps.entitySchemas));
|
|
213
340
|
this.deps.invalidateKnowledgeIndexCache();
|
|
214
341
|
this.deps.bumpMemoryStatusVersion(); // invalidate entity cache
|
|
342
|
+
this.deps.bumpEntityMutationVersion();
|
|
215
343
|
log.debug(`wrote entity ${normalized}`);
|
|
216
344
|
return normalized;
|
|
217
345
|
}
|
|
@@ -243,6 +371,37 @@ export class EntityStore {
|
|
|
243
371
|
}
|
|
244
372
|
}
|
|
245
373
|
|
|
374
|
+
/** Re-serialize every entity file into the compiled-truth timeline shape. */
|
|
375
|
+
async migrateEntityFilesToCompiledTruthTimeline(): Promise<{ total: number; migrated: number }> {
|
|
376
|
+
const entityNames = await this.listEntityNames();
|
|
377
|
+
const stateDir = path.join(this.deps.baseDir, "state");
|
|
378
|
+
let migrated = 0;
|
|
379
|
+
for (const entityName of entityNames) {
|
|
380
|
+
// Read+rewrite under the entity MUTATION lock (issue #2213): an
|
|
381
|
+
// unlocked whole-record rewrite could restore relationship targets a
|
|
382
|
+
// peer migration rewrote between the read and the write, and this
|
|
383
|
+
// path bumps only the status version, which never re-triggers the
|
|
384
|
+
// completed migration's reference scan.
|
|
385
|
+
const wrote = await withEntityCanonicalMutationLock(stateDir, async () => {
|
|
386
|
+
const raw = await this.readEntity(entityName);
|
|
387
|
+
if (!raw) return false;
|
|
388
|
+
const serialized = serializeEntityFile(parseEntityFile(raw, this.deps.entitySchemas), this.deps.entitySchemas);
|
|
389
|
+
if (raw.trimEnd() === serialized.trimEnd()) return false;
|
|
390
|
+
const filePath = this.deps.resolveEntityFilePath(entityName);
|
|
391
|
+
if (filePath === null) return false;
|
|
392
|
+
await this.deps.writeStorageSecureFile(filePath, serialized);
|
|
393
|
+
this.deps.bumpEntityMutationVersion();
|
|
394
|
+
return true;
|
|
395
|
+
});
|
|
396
|
+
if (wrote) migrated += 1;
|
|
397
|
+
}
|
|
398
|
+
if (migrated > 0) {
|
|
399
|
+
this.deps.invalidateKnowledgeIndexCache();
|
|
400
|
+
this.deps.bumpMemoryStatusVersion();
|
|
401
|
+
}
|
|
402
|
+
return { total: entityNames.length, migrated };
|
|
403
|
+
}
|
|
404
|
+
|
|
246
405
|
/**
|
|
247
406
|
* Set or rewrite the synthesis layer of an entity file.
|
|
248
407
|
*/
|
|
@@ -315,6 +474,7 @@ export class EntityStore {
|
|
|
315
474
|
]);
|
|
316
475
|
this.deps.invalidateKnowledgeIndexCache();
|
|
317
476
|
this.deps.bumpMemoryStatusVersion(); // invalidate entity cache
|
|
477
|
+
this.deps.bumpEntityMutationVersion();
|
|
318
478
|
}
|
|
319
479
|
|
|
320
480
|
/**
|
|
@@ -693,6 +853,7 @@ export class EntityStore {
|
|
|
693
853
|
|
|
694
854
|
const canonicalPath = path.join(this.deps.entitiesDir, `${canonical}.md`);
|
|
695
855
|
await this.deps.writeStorageSecureFile(canonicalPath, serializeEntityFile(mergedEntity, this.deps.entitySchemas));
|
|
856
|
+
this.deps.bumpEntityMutationVersion();
|
|
696
857
|
|
|
697
858
|
// Remove non-canonical files
|
|
698
859
|
for (const file of files) {
|
|
@@ -7,6 +7,10 @@ import type { MemoryFile, MemoryFrontmatter } from "../types.js";
|
|
|
7
7
|
import { markProjectedMemoryPathInvalid } from "../memory-projection-store.js";
|
|
8
8
|
import { RECALL_FALLBACK_DIRS } from "../utils/category-dir.js";
|
|
9
9
|
import { isErrnoCode } from "../utils/errno.js";
|
|
10
|
+
import {
|
|
11
|
+
pathMayCarryEntityRefs,
|
|
12
|
+
requestEntityCanonicalIdReconcile,
|
|
13
|
+
} from "./entity-canonical-id-references.js";
|
|
10
14
|
import {
|
|
11
15
|
readMaybeEncryptedFileFromChunks,
|
|
12
16
|
writeMaybeEncryptedFileFromChunks,
|
|
@@ -822,11 +826,25 @@ export abstract class TombstoneBlockedCaptureIndexHost {
|
|
|
822
826
|
async writeOfflineSyncFile(filePath: string, content: Buffer): Promise<void> {
|
|
823
827
|
const target = this.assertManagedStoragePath(filePath, "storage.writeOfflineSyncFile");
|
|
824
828
|
await this.writeTombstoneBlockedOfflineSyncFile(target, content);
|
|
829
|
+
await this.requestSyncReconcileIfMemoryPath(target);
|
|
825
830
|
}
|
|
826
831
|
|
|
827
832
|
async writeOfflineSyncFileChunks(filePath: string, chunks: AsyncIterable<Buffer>): Promise<void> {
|
|
828
833
|
const target = this.assertManagedStoragePath(filePath, "storage.writeOfflineSyncFileChunks");
|
|
829
834
|
await this.writeTombstoneBlockedOfflineSyncFileChunks(target, chunks);
|
|
835
|
+
await this.requestSyncReconcileIfMemoryPath(target);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Replicated bytes are opaque (possibly encrypted) and can carry legacy
|
|
840
|
+
* entity references a completed migration already renamed (issue #2213) —
|
|
841
|
+
* but only memory-tier markdown can, so transcript/state/other sync traffic
|
|
842
|
+
* must not trigger the full-corpus reconcile pass.
|
|
843
|
+
*/
|
|
844
|
+
private async requestSyncReconcileIfMemoryPath(target: string): Promise<void> {
|
|
845
|
+
const stateDir = this.tombstoneBlockedCaptureIndexOptions().stateDir;
|
|
846
|
+
if (!pathMayCarryEntityRefs(path.dirname(stateDir), target)) return;
|
|
847
|
+
await requestEntityCanonicalIdReconcile(stateDir);
|
|
830
848
|
}
|
|
831
849
|
|
|
832
850
|
async deleteOfflineSyncFile(filePath: string): Promise<void> {
|