@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.
Files changed (224) hide show
  1. package/dist/access-admin-ops-surface.d.ts +5 -5
  2. package/dist/access-admin-ops-surface.js +19 -18
  3. package/dist/access-authorization-probe.d.ts +5 -5
  4. package/dist/access-authorization-probe.js +20 -19
  5. package/dist/access-boundary.d.ts +5 -5
  6. package/dist/access-boundary.js +19 -18
  7. package/dist/access-cli.js +43 -42
  8. package/dist/access-cli.js.map +1 -1
  9. package/dist/access-extraction-force-flush.d.ts +5 -5
  10. package/dist/access-extraction-force-flush.js +19 -18
  11. package/dist/access-http-lcm-compaction.d.ts +5 -5
  12. package/dist/access-http-lifecycle-flush.d.ts +5 -5
  13. package/dist/access-http.d.ts +5 -5
  14. package/dist/access-http.js +24 -23
  15. package/dist/access-identity-continuity-surface.d.ts +4 -4
  16. package/dist/access-identity-continuity-surface.js +19 -18
  17. package/dist/access-lcm-surface.d.ts +5 -5
  18. package/dist/access-lcm-surface.js +19 -18
  19. package/dist/access-mcp.d.ts +5 -5
  20. package/dist/access-mcp.js +22 -21
  21. package/dist/access-namespace-preflight.js +19 -18
  22. package/dist/access-observe-write-surface.d.ts +5 -5
  23. package/dist/access-observe-write-surface.js +19 -18
  24. package/dist/access-operations-batch.js +20 -19
  25. package/dist/access-operations.d.ts +8 -8
  26. package/dist/access-operations.js +21 -20
  27. package/dist/access-recall-concurrency.d.ts +5 -5
  28. package/dist/access-recall-concurrency.js +19 -18
  29. package/dist/access-recall-response.d.ts +5 -5
  30. package/dist/access-recall-response.js +19 -18
  31. package/dist/access-recall-surface.d.ts +5 -5
  32. package/dist/access-recall-surface.js +19 -18
  33. package/dist/access-schema.d.ts +88 -88
  34. package/dist/{access-service-D9zIBlpp.d.ts → access-service-C2WYT1pO.d.ts} +3 -3
  35. package/dist/access-service-helpers.d.ts +5 -5
  36. package/dist/access-service.d.ts +5 -5
  37. package/dist/access-service.js +19 -18
  38. package/dist/access-surface-catalog.d.ts +5 -5
  39. package/dist/access-wearables-meetings-surface.d.ts +2 -2
  40. package/dist/bootstrap.d.ts +4 -4
  41. package/dist/briefing.d.ts +1 -1
  42. package/dist/briefing.js +10 -9
  43. package/dist/buffer.d.ts +1 -1
  44. package/dist/{capsule-merge-2H2IUTBT.js → capsule-merge-IDWJPCWK.js} +77 -8
  45. package/dist/capsule-merge-IDWJPCWK.js.map +1 -0
  46. package/dist/causal-consolidation.js +11 -10
  47. package/dist/causal-consolidation.js.map +1 -1
  48. package/dist/{chunk-65ZPVYTA.js → chunk-3CE77VAP.js} +55 -5
  49. package/dist/chunk-3CE77VAP.js.map +1 -0
  50. package/dist/chunk-4J7G5SY4.js +338 -0
  51. package/dist/chunk-4J7G5SY4.js.map +1 -0
  52. package/dist/{chunk-EO7X2LDK.js → chunk-4TAD3B56.js} +2 -2
  53. package/dist/{chunk-QHYB4MJV.js → chunk-5ZGHUWTT.js} +2 -2
  54. package/dist/{chunk-TCIWHK3N.js → chunk-6D5D5A4M.js} +5 -5
  55. package/dist/{chunk-BIXWNHIL.js → chunk-6TF4M2LB.js} +5 -5
  56. package/dist/{chunk-2NUC473W.js → chunk-AKELMJO2.js} +220 -48
  57. package/dist/{chunk-2NUC473W.js.map → chunk-AKELMJO2.js.map} +1 -1
  58. package/dist/{chunk-FBUE6PD3.js → chunk-BEQOZOYZ.js} +21 -4
  59. package/dist/chunk-BEQOZOYZ.js.map +1 -0
  60. package/dist/{chunk-SA3WJJF2.js → chunk-BGLDKAYY.js} +2 -2
  61. package/dist/{chunk-OPRP6Q4Z.js → chunk-DKKZ6SO2.js} +3 -3
  62. package/dist/{chunk-6TAUHL33.js → chunk-DLVPUX7U.js} +10 -2
  63. package/dist/chunk-DLVPUX7U.js.map +1 -0
  64. package/dist/{chunk-QNWXCJ76.js → chunk-ENTCVTQR.js} +18 -1
  65. package/dist/{chunk-QNWXCJ76.js.map → chunk-ENTCVTQR.js.map} +1 -1
  66. package/dist/{chunk-AEPYA55F.js → chunk-FE7MUX7M.js} +2 -2
  67. package/dist/{chunk-NQUF7XKR.js → chunk-FT4RF53O.js} +2 -2
  68. package/dist/{chunk-GHR7WHZX.js → chunk-GORHSN5C.js} +2 -2
  69. package/dist/{chunk-7TEZUUGM.js → chunk-HUS2XN3T.js} +2 -2
  70. package/dist/{chunk-XWDZQIYU.js → chunk-IJAQCBGV.js} +2 -2
  71. package/dist/{chunk-TJLLYV2X.js → chunk-IMEXE4XS.js} +7 -7
  72. package/dist/{chunk-ZCPP5OW6.js → chunk-JQSBRLJ6.js} +31 -27
  73. package/dist/chunk-JQSBRLJ6.js.map +1 -0
  74. package/dist/{chunk-IZXTZ7BF.js → chunk-JYLN6KHH.js} +6 -6
  75. package/dist/{chunk-HEVWWPTL.js → chunk-LH3KXIBJ.js} +17 -17
  76. package/dist/{chunk-EH5U2YCX.js → chunk-M3DBB2ND.js} +2 -2
  77. package/dist/{chunk-XABS455Y.js → chunk-NBAEGFXT.js} +5 -5
  78. package/dist/{chunk-DP7ZV2II.js → chunk-OA6PIPDE.js} +2 -2
  79. package/dist/{chunk-T4HH5Q3U.js → chunk-OJRLTZQM.js} +3 -3
  80. package/dist/{chunk-7ZHGPX56.js → chunk-PRP3M5Y3.js} +2 -2
  81. package/dist/{chunk-Y2ZD2RVG.js → chunk-PYTUBWAS.js} +2 -2
  82. package/dist/{chunk-4TGZCYHH.js → chunk-Q3CVY7LV.js} +1954 -1642
  83. package/dist/chunk-Q3CVY7LV.js.map +1 -0
  84. package/dist/{chunk-PH3ZWL3W.js → chunk-Q53RLEW7.js} +2 -2
  85. package/dist/{chunk-Y6OWJ3V5.js → chunk-RY2XZTFN.js} +2 -2
  86. package/dist/{chunk-2SHIWGY7.js → chunk-RY7CHVGE.js} +2 -2
  87. package/dist/{chunk-7VUAG236.js → chunk-T3FVAHM7.js} +4 -4
  88. package/dist/{chunk-2LTDWSR6.js → chunk-TSH56VPU.js} +2 -2
  89. package/dist/{chunk-J6SLNQRO.js → chunk-UBWOCQ3I.js} +6 -2
  90. package/dist/chunk-UBWOCQ3I.js.map +1 -0
  91. package/dist/{chunk-5FHE4JI7.js → chunk-VEUVON5I.js} +2 -2
  92. package/dist/{chunk-KEWUNL7P.js → chunk-WET4M3R6.js} +2 -2
  93. package/dist/chunk-WKDFJXW5.js +158 -0
  94. package/dist/chunk-WKDFJXW5.js.map +1 -0
  95. package/dist/{chunk-SLPLPGJQ.js → chunk-WL2S2UBB.js} +5 -5
  96. package/dist/{chunk-AHZYNPLX.js → chunk-YVMVVE4B.js} +2 -2
  97. package/dist/{chunk-5HTAJCTO.js → chunk-ZB3YODID.js} +15 -15
  98. package/dist/{cli-mFX-QQvx.d.ts → cli-jkmzdT8P.d.ts} +3 -3
  99. package/dist/cli.d.ts +6 -6
  100. package/dist/cli.js +34 -33
  101. package/dist/compounding/engine.d.ts +1 -1
  102. package/dist/compounding/engine.js +10 -9
  103. package/dist/connectors/codex-materialize-runner.js +10 -9
  104. package/dist/connectors/index.js +10 -9
  105. package/dist/consolidation-provenance-check.d.ts +1 -1
  106. package/dist/consolidation-provenance-check.js +8 -2
  107. package/dist/consolidation-undo.d.ts +1 -1
  108. package/dist/consolidation-undo.js +10 -1
  109. package/dist/consolidation-undo.js.map +1 -1
  110. package/dist/contradiction/index.d.ts +1 -1
  111. package/dist/corpus-watermark.js +12 -11
  112. package/dist/entity-retrieval.d.ts +1 -1
  113. package/dist/entity-retrieval.js +10 -9
  114. package/dist/explicit-capture.d.ts +4 -4
  115. package/dist/explicit-capture.js +6 -3
  116. package/dist/extraction.js +13 -12
  117. package/dist/{forget-4UY2EOKH.js → forget-DNAJ66ZD.js} +2 -2
  118. package/dist/index.d.ts +398 -398
  119. package/dist/index.js +43 -42
  120. package/dist/local-llm-helpers.d.ts +63 -1
  121. package/dist/local-llm-helpers.js +10 -1
  122. package/dist/local-llm.d.ts +4 -1
  123. package/dist/local-llm.js +2 -2
  124. package/dist/maintenance/memory-governance.js +10 -9
  125. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +1 -1
  126. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -9
  127. package/dist/maintenance/rebuild-memory-projection.d.ts +1 -1
  128. package/dist/maintenance/rebuild-memory-projection.js +11 -10
  129. package/dist/{maintenance-70Wq118b.d.ts → maintenance-Ha9Dg93M.d.ts} +1 -1
  130. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  131. package/dist/memory-worth-outcomes.d.ts +1 -1
  132. package/dist/namespaces/migrate.d.ts +1 -1
  133. package/dist/namespaces/migrate.js +11 -10
  134. package/dist/namespaces/storage.d.ts +1 -1
  135. package/dist/namespaces/storage.js +10 -9
  136. package/dist/offline-sync-impression-drain.js +3 -3
  137. package/dist/operator-doctor-corpus.js +13 -12
  138. package/dist/operator-toolkit.d.ts +1 -1
  139. package/dist/operator-toolkit.js +17 -16
  140. package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
  141. package/dist/orchestration/maintenance.d.ts +2 -2
  142. package/dist/orchestration/maintenance.js +15 -14
  143. package/dist/{orchestrator-BH8LT-NX.d.ts → orchestrator-DV_u9bfx.d.ts} +3 -3
  144. package/dist/orchestrator.d.ts +4 -4
  145. package/dist/orchestrator.js +43 -42
  146. package/dist/page-versioning.js +7 -1
  147. package/dist/schemas.d.ts +76 -76
  148. package/dist/semantic-consolidation.js +11 -10
  149. package/dist/semantic-rule-promotion.js +10 -9
  150. package/dist/semantic-rule-verifier.js +10 -9
  151. package/dist/{service-CrxXHz5T.d.ts → service-JxXXY-Eh.d.ts} +1 -1
  152. package/dist/shared-context/manager.d.ts +8 -8
  153. package/dist/source-agent-qualifier.js +10 -9
  154. package/dist/{storage-CyWa1Jm6.d.ts → storage-BbH4HDUS.d.ts} +30 -26
  155. package/dist/storage.d.ts +1 -1
  156. package/dist/storage.js +9 -8
  157. package/dist/summarizer.js +3 -3
  158. package/dist/temporal-supersession.d.ts +1 -1
  159. package/dist/tier-migration.d.ts +1 -1
  160. package/dist/transfer/capsule-import.js +8 -2
  161. package/dist/transfer/types.d.ts +66 -66
  162. package/dist/verified-recall.js +10 -9
  163. package/package.json +2 -2
  164. package/src/binary-lifecycle/pipeline.ts +52 -1
  165. package/src/consolidation-undo.ts +4 -0
  166. package/src/curation/index.ts +41 -5
  167. package/src/lifecycle/tombstones.ts +40 -0
  168. package/src/local-llm-helpers.ts +171 -0
  169. package/src/local-llm.ts +34 -30
  170. package/src/maintenance/memory-governance.ts +4 -0
  171. package/src/page-versioning.ts +16 -1
  172. package/src/review/index.ts +97 -13
  173. package/src/spaces/index.ts +127 -6
  174. package/src/storage/citation-hash-source.ts +97 -0
  175. package/src/storage/entity-canonical-id-lock.ts +33 -0
  176. package/src/storage/entity-canonical-id-migration-adapter.ts +8 -4
  177. package/src/storage/entity-canonical-id-migration.ts +129 -77
  178. package/src/storage/entity-canonical-id-references.ts +557 -0
  179. package/src/storage/entity-ref-repair.ts +171 -0
  180. package/src/storage/entity-store.ts +163 -2
  181. package/src/storage/tombstone-blocked-capture-sync.ts +18 -0
  182. package/src/storage.ts +363 -396
  183. package/src/transfer/capsule-import.ts +91 -3
  184. package/src/transfer/capsule-merge.ts +128 -10
  185. package/dist/capsule-merge-2H2IUTBT.js.map +0 -1
  186. package/dist/chunk-4RNZMRAV.js +0 -60
  187. package/dist/chunk-4RNZMRAV.js.map +0 -1
  188. package/dist/chunk-4TGZCYHH.js.map +0 -1
  189. package/dist/chunk-65ZPVYTA.js.map +0 -1
  190. package/dist/chunk-6TAUHL33.js.map +0 -1
  191. package/dist/chunk-FBUE6PD3.js.map +0 -1
  192. package/dist/chunk-J6SLNQRO.js.map +0 -1
  193. package/dist/chunk-ZCPP5OW6.js.map +0 -1
  194. /package/dist/{chunk-EO7X2LDK.js.map → chunk-4TAD3B56.js.map} +0 -0
  195. /package/dist/{chunk-QHYB4MJV.js.map → chunk-5ZGHUWTT.js.map} +0 -0
  196. /package/dist/{chunk-TCIWHK3N.js.map → chunk-6D5D5A4M.js.map} +0 -0
  197. /package/dist/{chunk-BIXWNHIL.js.map → chunk-6TF4M2LB.js.map} +0 -0
  198. /package/dist/{chunk-SA3WJJF2.js.map → chunk-BGLDKAYY.js.map} +0 -0
  199. /package/dist/{chunk-OPRP6Q4Z.js.map → chunk-DKKZ6SO2.js.map} +0 -0
  200. /package/dist/{chunk-AEPYA55F.js.map → chunk-FE7MUX7M.js.map} +0 -0
  201. /package/dist/{chunk-NQUF7XKR.js.map → chunk-FT4RF53O.js.map} +0 -0
  202. /package/dist/{chunk-GHR7WHZX.js.map → chunk-GORHSN5C.js.map} +0 -0
  203. /package/dist/{chunk-7TEZUUGM.js.map → chunk-HUS2XN3T.js.map} +0 -0
  204. /package/dist/{chunk-XWDZQIYU.js.map → chunk-IJAQCBGV.js.map} +0 -0
  205. /package/dist/{chunk-TJLLYV2X.js.map → chunk-IMEXE4XS.js.map} +0 -0
  206. /package/dist/{chunk-IZXTZ7BF.js.map → chunk-JYLN6KHH.js.map} +0 -0
  207. /package/dist/{chunk-HEVWWPTL.js.map → chunk-LH3KXIBJ.js.map} +0 -0
  208. /package/dist/{chunk-EH5U2YCX.js.map → chunk-M3DBB2ND.js.map} +0 -0
  209. /package/dist/{chunk-XABS455Y.js.map → chunk-NBAEGFXT.js.map} +0 -0
  210. /package/dist/{chunk-DP7ZV2II.js.map → chunk-OA6PIPDE.js.map} +0 -0
  211. /package/dist/{chunk-T4HH5Q3U.js.map → chunk-OJRLTZQM.js.map} +0 -0
  212. /package/dist/{chunk-7ZHGPX56.js.map → chunk-PRP3M5Y3.js.map} +0 -0
  213. /package/dist/{chunk-Y2ZD2RVG.js.map → chunk-PYTUBWAS.js.map} +0 -0
  214. /package/dist/{chunk-PH3ZWL3W.js.map → chunk-Q53RLEW7.js.map} +0 -0
  215. /package/dist/{chunk-Y6OWJ3V5.js.map → chunk-RY2XZTFN.js.map} +0 -0
  216. /package/dist/{chunk-2SHIWGY7.js.map → chunk-RY7CHVGE.js.map} +0 -0
  217. /package/dist/{chunk-7VUAG236.js.map → chunk-T3FVAHM7.js.map} +0 -0
  218. /package/dist/{chunk-2LTDWSR6.js.map → chunk-TSH56VPU.js.map} +0 -0
  219. /package/dist/{chunk-5FHE4JI7.js.map → chunk-VEUVON5I.js.map} +0 -0
  220. /package/dist/{chunk-KEWUNL7P.js.map → chunk-WET4M3R6.js.map} +0 -0
  221. /package/dist/{chunk-SLPLPGJQ.js.map → chunk-WL2S2UBB.js.map} +0 -0
  222. /package/dist/{chunk-AHZYNPLX.js.map → chunk-YVMVVE4B.js.map} +0 -0
  223. /package/dist/{chunk-5HTAJCTO.js.map → chunk-ZB3YODID.js.map} +0 -0
  224. /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-migration.js";
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 { EntityFile, EntityStructuredSection, PluginConfig, ScoredEntity } from "../types.js";
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> {