@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.
Files changed (208) 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 +39 -38
  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 +11 -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-FBUE6PD3.js → chunk-BEQOZOYZ.js} +21 -4
  57. package/dist/chunk-BEQOZOYZ.js.map +1 -0
  58. package/dist/{chunk-SA3WJJF2.js → chunk-BGLDKAYY.js} +2 -2
  59. package/dist/{chunk-OPRP6Q4Z.js → chunk-DKKZ6SO2.js} +3 -3
  60. package/dist/{chunk-6TAUHL33.js → chunk-DLVPUX7U.js} +10 -2
  61. package/dist/chunk-DLVPUX7U.js.map +1 -0
  62. package/dist/{chunk-QNWXCJ76.js → chunk-ENTCVTQR.js} +18 -1
  63. package/dist/{chunk-QNWXCJ76.js.map → chunk-ENTCVTQR.js.map} +1 -1
  64. package/dist/{chunk-AEPYA55F.js → chunk-FE7MUX7M.js} +2 -2
  65. package/dist/{chunk-GHR7WHZX.js → chunk-GORHSN5C.js} +2 -2
  66. package/dist/{chunk-7TEZUUGM.js → chunk-HUS2XN3T.js} +2 -2
  67. package/dist/{chunk-SLPLPGJQ.js → chunk-IE2CS5BM.js} +17 -10
  68. package/dist/chunk-IE2CS5BM.js.map +1 -0
  69. package/dist/{chunk-XWDZQIYU.js → chunk-IJAQCBGV.js} +2 -2
  70. package/dist/{chunk-TJLLYV2X.js → chunk-IMEXE4XS.js} +7 -7
  71. package/dist/{chunk-IZXTZ7BF.js → chunk-JYLN6KHH.js} +6 -6
  72. package/dist/{chunk-EH5U2YCX.js → chunk-M3DBB2ND.js} +2 -2
  73. package/dist/{chunk-XABS455Y.js → chunk-NBAEGFXT.js} +5 -5
  74. package/dist/{chunk-T4HH5Q3U.js → chunk-OJRLTZQM.js} +3 -3
  75. package/dist/{chunk-7ZHGPX56.js → chunk-PRP3M5Y3.js} +2 -2
  76. package/dist/{chunk-Y2ZD2RVG.js → chunk-PYTUBWAS.js} +2 -2
  77. package/dist/{chunk-4TGZCYHH.js → chunk-Q3CVY7LV.js} +1954 -1642
  78. package/dist/chunk-Q3CVY7LV.js.map +1 -0
  79. package/dist/{chunk-PH3ZWL3W.js → chunk-Q53RLEW7.js} +2 -2
  80. package/dist/{chunk-Y6OWJ3V5.js → chunk-RY2XZTFN.js} +2 -2
  81. package/dist/{chunk-2SHIWGY7.js → chunk-RY7CHVGE.js} +2 -2
  82. package/dist/{chunk-7VUAG236.js → chunk-T3FVAHM7.js} +4 -4
  83. package/dist/{chunk-2LTDWSR6.js → chunk-TSH56VPU.js} +2 -2
  84. package/dist/{chunk-J6SLNQRO.js → chunk-UBWOCQ3I.js} +6 -2
  85. package/dist/chunk-UBWOCQ3I.js.map +1 -0
  86. package/dist/{chunk-5FHE4JI7.js → chunk-VEUVON5I.js} +2 -2
  87. package/dist/{chunk-KEWUNL7P.js → chunk-WET4M3R6.js} +2 -2
  88. package/dist/{chunk-HEVWWPTL.js → chunk-Y2UUEB4I.js} +17 -17
  89. package/dist/{chunk-AHZYNPLX.js → chunk-YVMVVE4B.js} +2 -2
  90. package/dist/{chunk-5HTAJCTO.js → chunk-ZB3YODID.js} +15 -15
  91. package/dist/{chunk-FHYGLITW.js → chunk-ZJMVZLPK.js} +217 -45
  92. package/dist/{chunk-FHYGLITW.js.map → chunk-ZJMVZLPK.js.map} +1 -1
  93. package/dist/{cli-mFX-QQvx.d.ts → cli-jkmzdT8P.d.ts} +3 -3
  94. package/dist/cli.d.ts +6 -6
  95. package/dist/cli.js +34 -33
  96. package/dist/compounding/engine.d.ts +1 -1
  97. package/dist/compounding/engine.js +10 -9
  98. package/dist/connectors/codex-materialize-runner.js +10 -9
  99. package/dist/connectors/index.js +10 -9
  100. package/dist/consolidation-provenance-check.d.ts +1 -1
  101. package/dist/consolidation-provenance-check.js +8 -2
  102. package/dist/consolidation-undo.d.ts +1 -1
  103. package/dist/consolidation-undo.js +10 -1
  104. package/dist/consolidation-undo.js.map +1 -1
  105. package/dist/contradiction/index.d.ts +1 -1
  106. package/dist/corpus-watermark.js +12 -11
  107. package/dist/entity-retrieval.d.ts +1 -1
  108. package/dist/entity-retrieval.js +10 -9
  109. package/dist/explicit-capture.d.ts +4 -4
  110. package/dist/explicit-capture.js +6 -3
  111. package/dist/extraction.js +10 -9
  112. package/dist/{forget-4UY2EOKH.js → forget-DNAJ66ZD.js} +2 -2
  113. package/dist/index.d.ts +396 -396
  114. package/dist/index.js +39 -38
  115. package/dist/maintenance/memory-governance.js +10 -9
  116. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +1 -1
  117. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -9
  118. package/dist/maintenance/rebuild-memory-projection.d.ts +1 -1
  119. package/dist/maintenance/rebuild-memory-projection.js +11 -10
  120. package/dist/{maintenance-70Wq118b.d.ts → maintenance-Ha9Dg93M.d.ts} +1 -1
  121. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  122. package/dist/memory-worth-outcomes.d.ts +1 -1
  123. package/dist/namespaces/migrate.d.ts +1 -1
  124. package/dist/namespaces/migrate.js +11 -10
  125. package/dist/namespaces/storage.d.ts +1 -1
  126. package/dist/namespaces/storage.js +10 -9
  127. package/dist/offline-sync-impression-drain.js +3 -3
  128. package/dist/operator-doctor-corpus.js +13 -12
  129. package/dist/operator-toolkit.d.ts +1 -1
  130. package/dist/operator-toolkit.js +17 -16
  131. package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
  132. package/dist/orchestration/maintenance.d.ts +2 -2
  133. package/dist/orchestration/maintenance.js +15 -14
  134. package/dist/{orchestrator-BH8LT-NX.d.ts → orchestrator-DV_u9bfx.d.ts} +3 -3
  135. package/dist/orchestrator.d.ts +4 -4
  136. package/dist/orchestrator.js +39 -38
  137. package/dist/page-versioning.js +7 -1
  138. package/dist/schemas.d.ts +54 -54
  139. package/dist/semantic-consolidation.js +11 -10
  140. package/dist/semantic-rule-promotion.js +10 -9
  141. package/dist/semantic-rule-verifier.js +10 -9
  142. package/dist/{service-CrxXHz5T.d.ts → service-JxXXY-Eh.d.ts} +1 -1
  143. package/dist/shared-context/manager.d.ts +8 -8
  144. package/dist/source-agent-qualifier.js +10 -9
  145. package/dist/{storage-CyWa1Jm6.d.ts → storage-BbH4HDUS.d.ts} +30 -26
  146. package/dist/storage.d.ts +1 -1
  147. package/dist/storage.js +9 -8
  148. package/dist/temporal-supersession.d.ts +1 -1
  149. package/dist/tier-migration.d.ts +1 -1
  150. package/dist/transfer/capsule-import.js +8 -2
  151. package/dist/transfer/types.d.ts +66 -66
  152. package/dist/verified-recall.js +10 -9
  153. package/package.json +2 -2
  154. package/src/access-http.ts +14 -9
  155. package/src/binary-lifecycle/pipeline.ts +52 -1
  156. package/src/consolidation-undo.ts +4 -0
  157. package/src/curation/index.ts +41 -5
  158. package/src/lifecycle/tombstones.ts +40 -0
  159. package/src/maintenance/memory-governance.ts +4 -0
  160. package/src/page-versioning.ts +16 -1
  161. package/src/review/index.ts +97 -13
  162. package/src/spaces/index.ts +127 -6
  163. package/src/storage/citation-hash-source.ts +97 -0
  164. package/src/storage/entity-canonical-id-lock.ts +33 -0
  165. package/src/storage/entity-canonical-id-migration-adapter.ts +8 -4
  166. package/src/storage/entity-canonical-id-migration.ts +129 -77
  167. package/src/storage/entity-canonical-id-references.ts +557 -0
  168. package/src/storage/entity-ref-repair.ts +171 -0
  169. package/src/storage/entity-store.ts +163 -2
  170. package/src/storage/tombstone-blocked-capture-sync.ts +18 -0
  171. package/src/storage.ts +363 -396
  172. package/src/transfer/capsule-import.ts +91 -3
  173. package/src/transfer/capsule-merge.ts +128 -10
  174. package/dist/capsule-merge-2H2IUTBT.js.map +0 -1
  175. package/dist/chunk-4TGZCYHH.js.map +0 -1
  176. package/dist/chunk-65ZPVYTA.js.map +0 -1
  177. package/dist/chunk-6TAUHL33.js.map +0 -1
  178. package/dist/chunk-FBUE6PD3.js.map +0 -1
  179. package/dist/chunk-J6SLNQRO.js.map +0 -1
  180. package/dist/chunk-SLPLPGJQ.js.map +0 -1
  181. /package/dist/{chunk-EO7X2LDK.js.map → chunk-4TAD3B56.js.map} +0 -0
  182. /package/dist/{chunk-QHYB4MJV.js.map → chunk-5ZGHUWTT.js.map} +0 -0
  183. /package/dist/{chunk-TCIWHK3N.js.map → chunk-6D5D5A4M.js.map} +0 -0
  184. /package/dist/{chunk-BIXWNHIL.js.map → chunk-6TF4M2LB.js.map} +0 -0
  185. /package/dist/{chunk-SA3WJJF2.js.map → chunk-BGLDKAYY.js.map} +0 -0
  186. /package/dist/{chunk-OPRP6Q4Z.js.map → chunk-DKKZ6SO2.js.map} +0 -0
  187. /package/dist/{chunk-AEPYA55F.js.map → chunk-FE7MUX7M.js.map} +0 -0
  188. /package/dist/{chunk-GHR7WHZX.js.map → chunk-GORHSN5C.js.map} +0 -0
  189. /package/dist/{chunk-7TEZUUGM.js.map → chunk-HUS2XN3T.js.map} +0 -0
  190. /package/dist/{chunk-XWDZQIYU.js.map → chunk-IJAQCBGV.js.map} +0 -0
  191. /package/dist/{chunk-TJLLYV2X.js.map → chunk-IMEXE4XS.js.map} +0 -0
  192. /package/dist/{chunk-IZXTZ7BF.js.map → chunk-JYLN6KHH.js.map} +0 -0
  193. /package/dist/{chunk-EH5U2YCX.js.map → chunk-M3DBB2ND.js.map} +0 -0
  194. /package/dist/{chunk-XABS455Y.js.map → chunk-NBAEGFXT.js.map} +0 -0
  195. /package/dist/{chunk-T4HH5Q3U.js.map → chunk-OJRLTZQM.js.map} +0 -0
  196. /package/dist/{chunk-7ZHGPX56.js.map → chunk-PRP3M5Y3.js.map} +0 -0
  197. /package/dist/{chunk-Y2ZD2RVG.js.map → chunk-PYTUBWAS.js.map} +0 -0
  198. /package/dist/{chunk-PH3ZWL3W.js.map → chunk-Q53RLEW7.js.map} +0 -0
  199. /package/dist/{chunk-Y6OWJ3V5.js.map → chunk-RY2XZTFN.js.map} +0 -0
  200. /package/dist/{chunk-2SHIWGY7.js.map → chunk-RY7CHVGE.js.map} +0 -0
  201. /package/dist/{chunk-7VUAG236.js.map → chunk-T3FVAHM7.js.map} +0 -0
  202. /package/dist/{chunk-2LTDWSR6.js.map → chunk-TSH56VPU.js.map} +0 -0
  203. /package/dist/{chunk-5FHE4JI7.js.map → chunk-VEUVON5I.js.map} +0 -0
  204. /package/dist/{chunk-KEWUNL7P.js.map → chunk-WET4M3R6.js.map} +0 -0
  205. /package/dist/{chunk-HEVWWPTL.js.map → chunk-Y2UUEB4I.js.map} +0 -0
  206. /package/dist/{chunk-AHZYNPLX.js.map → chunk-YVMVVE4B.js.map} +0 -0
  207. /package/dist/{chunk-5HTAJCTO.js.map → chunk-ZB3YODID.js.map} +0 -0
  208. /package/dist/{forget-4UY2EOKH.js.map → forget-DNAJ66ZD.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.35.2",
3
+ "version": "9.35.4",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3086,7 +3086,7 @@
3086
3086
  "core"
3087
3087
  ],
3088
3088
  "peerDependencies": {
3089
- "@remnic/coding-graph": "^9.35.2"
3089
+ "@remnic/coding-graph": "^9.35.4"
3090
3090
  },
3091
3091
  "peerDependenciesMeta": {
3092
3092
  "@remnic/coding-graph": {
@@ -66,6 +66,12 @@ export interface AccessHttpReadinessState {
66
66
  ready: boolean;
67
67
  warmupAttempts: number;
68
68
  lastError?: string | null;
69
+ /**
70
+ * True when the standalone init gate opened before search warm-up completed
71
+ * (issue #2215): the service is functional (recall serves via fallback
72
+ * retrieval), so health answers 200 with degraded info instead of 503.
73
+ */
74
+ degraded?: boolean;
69
75
  }
70
76
 
71
77
  export interface EngramAccessHttpServerOptions {
@@ -801,18 +807,13 @@ export class EngramAccessHttpServer {
801
807
  }
802
808
 
803
809
  if (req.method === "GET" && pathname === "/engram/v1/health") {
804
- const readiness = this.readiness();
805
- if (!readiness.ready) {
810
+ const { ready, warmupAttempts, lastError } = this.readiness();
811
+ if (!ready) {
806
812
  this.respondJson(res, 503, {
807
- ok: false,
808
- ready: false,
809
- warmupAttempts: readiness.warmupAttempts,
810
- lastError: readiness.lastError ?? null,
811
- code: "not_ready",
813
+ ok: false, ready: false, warmupAttempts, lastError: lastError ?? null, code: "not_ready",
812
814
  });
813
815
  return;
814
816
  }
815
-
816
817
  }
817
818
 
818
819
  // Run any host-supplied pre-auth request handler. It runs AFTER the
@@ -888,7 +889,11 @@ export class EngramAccessHttpServer {
888
889
  }
889
890
 
890
891
  if (req.method === "GET" && pathname === "/engram/v1/health") {
891
- this.respondJson(res, 200, await this.service.health());
892
+ const { degraded, warmupAttempts, lastError } = this.readiness();
893
+ const health = await this.service.health();
894
+ this.respondJson(res, 200, degraded !== true ? health : {
895
+ ...health, degraded: true, warmupAttempts, lastError: lastError ?? null,
896
+ });
892
897
  return;
893
898
  }
894
899
  if (req.method === "GET" && pathname === "/engram/v1/capabilities") return respondLcmCompactionCapabilitiesHttp(res);
@@ -20,6 +20,16 @@ import { readManifest, writeManifest } from "./manifest.js";
20
20
  import { scanForBinaries } from "./scanner.js";
21
21
  import { bumpMemoryCorpusVersionForDir } from "../memory-corpus-version.js";
22
22
  import { RECALL_FALLBACK_DIRS } from "../utils/category-dir.js";
23
+ import {
24
+ HistoricalEntityCanonicalIdCache,
25
+ canonicalizeEntityRefFrontmatter,
26
+ classifyEntityRefWritePath,
27
+ repairContentAfterJournalMove,
28
+ requestEntityCanonicalIdReconcile,
29
+ } from "../storage/entity-canonical-id-references.js";
30
+
31
+ // Write-boundary journal cache for redirect rewrites (issue #2213).
32
+ const historicalIdCache = new HistoricalEntityCanonicalIdCache();
23
33
 
24
34
  /**
25
35
  * True when `mdPath` is an active recall-corpus memory file under `memoryDir`
@@ -319,7 +329,48 @@ async function stageRedirect(
319
329
  let writeFailCount = 0;
320
330
  for (const update of updates) {
321
331
  try {
322
- await writeMarkdownFile(update.mdPath, update.content);
332
+ // Whole-record rewrite of bytes read earlier (issue #2213): for
333
+ // memory-tier records, resolve the frontmatter entityRef through the
334
+ // CURRENT journal at the write and REPAIR afterwards — writing back
335
+ // the as-read bytes could revert a rewrite a peer migration performed
336
+ // since the read. findMarkdownFiles also surfaces transcripts,
337
+ // profiles, and other Markdown outside the migration's scope: those
338
+ // keep their bytes (plus the redirect) untouched, and entities/ pages
339
+ // get the bounded reconcile marker instead — their migrated surface
340
+ // is relationship targets, not an entityRef frontmatter field.
341
+ const stateDir = path.join(memoryDir, "state");
342
+ const kind = classifyEntityRefWritePath(memoryDir, update.mdPath);
343
+ if (kind === "memory") {
344
+ const idsAtWrite = historicalIdCache.get(stateDir);
345
+ const written = canonicalizeEntityRefFrontmatter(update.content, idsAtWrite);
346
+ // Snapshot the ORIGINAL bytes: if repair fails after the write, the
347
+ // catch below reports a failed redirect, so the file must return to
348
+ // its pre-redirect state — a retry needs the original reference,
349
+ // and the skipped corpus bump means a warm cache would otherwise
350
+ // keep serving bytes that no longer match disk (AGENTS.md §14).
351
+ const originalBytes = await readMarkdownFile(update.mdPath).catch(() => null);
352
+ await writeMarkdownFile(update.mdPath, written);
353
+ try {
354
+ await repairContentAfterJournalMove({
355
+ stateDir,
356
+ cache: historicalIdCache,
357
+ idsAtWrite,
358
+ rawContent: update.content,
359
+ lastWritten: written,
360
+ rewrite: (next) => writeMarkdownFile(update.mdPath, next),
361
+ });
362
+ } catch (err) {
363
+ if (originalBytes !== null) {
364
+ await writeMarkdownFile(update.mdPath, originalBytes).catch(() => undefined);
365
+ }
366
+ throw err;
367
+ }
368
+ } else {
369
+ await writeMarkdownFile(update.mdPath, update.content);
370
+ if (kind === "entity") {
371
+ await requestEntityCanonicalIdReconcile(stateDir);
372
+ }
373
+ }
323
374
  // The redirect rewrote an on-disk memory file out-of-band (not through a
324
375
  // StorageManager mutation). Bump the corpus sentinel per write so a warm
325
376
  // hot-memories cache rescans — mid-batch too, never serving pre-redirect
@@ -26,6 +26,7 @@ import { constants as fsConstants } from "node:fs";
26
26
  import type { StorageManager } from "./storage.js";
27
27
  import type { VersioningConfig } from "./page-versioning.js";
28
28
  import { bumpMemoryCorpusVersionForDir } from "./memory-corpus-version.js";
29
+ import { requestEntityCanonicalIdReconcile } from "./storage/entity-canonical-id-references.js";
29
30
  import { getVersion } from "./page-versioning.js";
30
31
 
31
32
  /**
@@ -654,6 +655,9 @@ export async function runConsolidationUndo(options: {
654
655
  // restores would stay invisible to recall until an unrelated mutation or
655
656
  // restart (esp. with hotMemoriesCacheTtlMs: 0). Issue #1902, Codex Medium.
656
657
  bumpMemoryCorpusVersionForDir(memoryDir);
658
+ // Restored bytes predate the current migration journal and can carry
659
+ // legacy entity references (issue #2213) — request one reconcile pass.
660
+ await requestEntityCanonicalIdReconcile(path.join(memoryDir, "state"));
657
661
  result.restores.push({
658
662
  entry: p.entry,
659
663
  sourcePath: p.sourcePath,
@@ -10,6 +10,11 @@ import path from "node:path";
10
10
  import crypto from "node:crypto";
11
11
  import { getCategoryDir, ALL_CATEGORY_DIRS } from "../utils/category-dir.js";
12
12
  import { bumpMemoryCorpusVersionForDir } from "../memory-corpus-version.js";
13
+ import {
14
+ HistoricalEntityCanonicalIdCache,
15
+ canonicalizeEntityRefFrontmatter,
16
+ repairContentAfterJournalMoveSync,
17
+ } from "../storage/entity-canonical-id-references.js";
13
18
 
14
19
  // ── Types ────────────────────────────────────────────────────────────────────
15
20
 
@@ -444,6 +449,10 @@ function loadExistingMemories(memoryDir: string): Map<string, ExistingMemory> {
444
449
 
445
450
  // ── Writing ──────────────────────────────────────────────────────────────────
446
451
 
452
+ // Curation writes raw frontmatter directly, so it is a write boundary for
453
+ // entity references too (issue #2213): resolve through the target's journal.
454
+ const historicalIdCache = new HistoricalEntityCanonicalIdCache();
455
+
447
456
  function writeStatement(stmt: CuratedStatement, memoryDir: string): string {
448
457
  const now = new Date();
449
458
  const dateDir = now.toISOString().split("T")[0];
@@ -454,8 +463,12 @@ function writeStatement(stmt: CuratedStatement, memoryDir: string): string {
454
463
 
455
464
  const fileName = `${stmt.category}-${Date.now()}-${stmt.id.slice(0, 8)}.md`;
456
465
  const filePath = path.join(dir, fileName);
466
+ const historicalIds = historicalIdCache.get(path.join(memoryDir, "state"));
457
467
 
458
- const frontmatter = [
468
+ // Keep the caller's ORIGINAL ref in the raw form: repair-after-write must
469
+ // re-resolve from it, including the direction where a mapping was parked
470
+ // and the resolution falls back to the legacy claimant (issue #2213).
471
+ const rawBody = `${[
459
472
  "---",
460
473
  `id: ${stmt.id}`,
461
474
  `category: ${stmt.category}`,
@@ -471,14 +484,37 @@ function writeStatement(stmt: CuratedStatement, memoryDir: string): string {
471
484
  "---",
472
485
  ]
473
486
  .filter(Boolean)
474
- .join("\n");
475
-
476
- const body = `${frontmatter}\n\n${stmt.content}\n`;
487
+ .join("\n")}\n\n${stmt.content}\n`;
488
+ const body = canonicalizeEntityRefFrontmatter(rawBody, historicalIds);
477
489
 
478
490
  fs.writeFileSync(filePath, body);
491
+ // Post-write REPAIR (issue #2213): a peer process publishing the journal
492
+ // across this write would otherwise strand the reference — the corpus bump
493
+ // below no longer re-triggers the migration, and a parked mapping cannot be
494
+ // fixed after the fact by the reconcile pass. On repair exhaustion, remove
495
+ // the just-created file so the retryable error leaves no untracked partial
496
+ // statement (a retry would otherwise duplicate it).
497
+ try {
498
+ repairContentAfterJournalMoveSync({
499
+ stateDir: path.join(memoryDir, "state"),
500
+ cache: historicalIdCache,
501
+ idsAtWrite: historicalIds,
502
+ rawContent: rawBody,
503
+ lastWritten: body,
504
+ rewrite: (next) => fs.writeFileSync(filePath, next),
505
+ });
506
+ } catch (err) {
507
+ try {
508
+ fs.unlinkSync(filePath);
509
+ } catch {
510
+ // Best effort — the retryable repair error is the primary signal.
511
+ }
512
+ throw err;
513
+ }
479
514
  // Bump the corpus sentinel per write (out-of-band create) so a concurrent
480
515
  // same-process readAllMemories rescans and sees this statement immediately,
481
- // never a stale/partial corpus mid-batch (Cursor Medium, #1902).
516
+ // never a stale/partial corpus mid-batch (Cursor Medium, #1902). AFTER the
517
+ // repair, so a rescan never captures pre-repair bytes.
482
518
  bumpMemoryCorpusVersionForDir(memoryDir);
483
519
  return filePath;
484
520
  }
@@ -835,6 +835,46 @@ export function collectRetiredMemoriesForRebuild(
835
835
  return retired;
836
836
  }
837
837
 
838
+ /**
839
+ * One-shot write-time resurrection gate (issue #1579 / #2213). Looks up the
840
+ * fact's identity in the tombstone store and, on a match, downgrades the
841
+ * frontmatter to `pending_review` + `blockedBy` IN PLACE. ADD-ONLY: an
842
+ * already-blocked or terminal-status record is left untouched. Pure over its
843
+ * inputs (no I/O beyond the injected store) so both `writeMemory`'s gate and
844
+ * the chunk-repair re-gate share one lookup + apply.
845
+ */
846
+ export function applyTombstoneResurrectionGate(
847
+ store: Pick<TombstoneStore, "lookup">,
848
+ fm: {
849
+ id: string;
850
+ status?: string;
851
+ blockedBy?: string;
852
+ tombstoneBlockTier?: TombstoneMatchTier;
853
+ entityRef?: string;
854
+ contentHash?: string;
855
+ },
856
+ query: {
857
+ normalizedText: string;
858
+ supersessionKeys: string[];
859
+ namespace: string;
860
+ },
861
+ ): TombstoneMatch | null {
862
+ if (fm.status === "pending_review" && fm.blockedBy) return null;
863
+ if (!(fm.status === undefined || fm.status === "active" || fm.status === "pending_review")) return null;
864
+ const match = store.lookup({
865
+ contentHash: fm.contentHash,
866
+ normalizedText: query.normalizedText,
867
+ ...(fm.entityRef ? { entityRef: fm.entityRef } : {}),
868
+ ...(query.supersessionKeys.length > 0 ? { supersessionKeys: query.supersessionKeys } : {}),
869
+ namespace: query.namespace,
870
+ });
871
+ if (!match) return null;
872
+ fm.status = "pending_review";
873
+ fm.blockedBy = match.tombstoneId;
874
+ fm.tombstoneBlockTier = match.matchedTier;
875
+ return match;
876
+ }
877
+
838
878
  /**
839
879
  * Build the live-emission tombstone inputs for a single retired FACT — one
840
880
  * input per derived supersession key (or a single keyless record when no
@@ -6,6 +6,7 @@ import type { MemoryFile, MemoryStatus } from "../types.js";
6
6
  import { RECALL_FALLBACK_DIRS } from "../utils/category-dir.js";
7
7
  import { bumpMemoryCorpusVersionForDir } from "../memory-corpus-version.js";
8
8
  import { assertPathInsideRoot } from "../utils/path-containment.js";
9
+ import { requestEntityCanonicalIdReconcile } from "../storage/entity-canonical-id-references.js";
9
10
 
10
11
  export type MemoryGovernanceMode = "shadow" | "apply";
11
12
  export type MemoryGovernanceReasonCode =
@@ -1021,6 +1022,9 @@ export async function restoreMemoryGovernanceRun(
1021
1022
  // same-process or peer readAllMemories rescans mid-batch and never serves a
1022
1023
  // partially-restored corpus (Cursor Medium, #1902).
1023
1024
  bumpMemoryCorpusVersionForDir(options.memoryDir);
1025
+ // Restored bytes predate the current migration journal and can carry
1026
+ // legacy entity references (issue #2213) — request one reconcile pass.
1027
+ await requestEntityCanonicalIdReconcile(path.join(options.memoryDir, "state"));
1024
1028
  }
1025
1029
 
1026
1030
  return {
@@ -27,6 +27,10 @@ import {
27
27
  } from "node:fs/promises";
28
28
  import { ALL_CATEGORY_DIRS, RECALL_FALLBACK_DIRS } from "./utils/category-dir.js";
29
29
  import { bumpMemoryCorpusVersionForDir } from "./memory-corpus-version.js";
30
+ import {
31
+ pathMayCarryEntityRefs,
32
+ requestEntityCanonicalIdReconcile,
33
+ } from "./storage/entity-canonical-id-references.js";
30
34
 
31
35
  // ---------------------------------------------------------------------------
32
36
  // Public interfaces
@@ -342,9 +346,20 @@ export async function revertToVersion(
342
346
  // non-memory queue dirs (questions/). Use RECALL_FALLBACK_DIRS — a revert of a
343
347
  // questions/ page must NOT bump the recall corpus sentinel and force a rescan.
344
348
  const revertedTop = relPath(pagePath, resolvedMemoryDir).split(path.sep)[0];
345
- if ((RECALL_FALLBACK_DIRS as readonly string[]).includes(revertedTop)) {
349
+ const inRecallTier = (RECALL_FALLBACK_DIRS as readonly string[]).includes(revertedTop);
350
+ if (inRecallTier) {
346
351
  bumpMemoryCorpusVersionForDir(resolvedMemoryDir);
347
352
  }
353
+ // A pre-migration snapshot can reintroduce a legacy entityRef — or, under
354
+ // entities/, a legacy relationship target rewriteRelationshipTargets had
355
+ // migrated — that the target's completed journal already renamed (issue
356
+ // #2213); the revert keeps the snapshot bytes faithful, so request one
357
+ // bounded reconcile pass instead. pathMayCarryEntityRefs is the migration's
358
+ // own scan scope (hot recall + cold + archive + entities), which is wider
359
+ // than the corpus bump above on purpose.
360
+ if (pathMayCarryEntityRefs(resolvedMemoryDir, pagePath)) {
361
+ await requestEntityCanonicalIdReconcile(path.join(resolvedMemoryDir, "state"));
362
+ }
348
363
  log.debug(`page-versioning: reverted ${pagePath} to version ${versionId}`);
349
364
 
350
365
  return version;
@@ -9,6 +9,58 @@ import fs from "node:fs";
9
9
  import path from "node:path";
10
10
  import { getCategoryDir, ALL_CATEGORY_DIRS } from "../utils/category-dir.js";
11
11
  import { bumpMemoryCorpusVersionForDir } from "../memory-corpus-version.js";
12
+ import {
13
+ HistoricalEntityCanonicalIdCache,
14
+ canonicalizeEntityRefFrontmatter,
15
+ repairContentAfterJournalMoveSync,
16
+ } from "../storage/entity-canonical-id-references.js";
17
+
18
+ // Write-boundary journal cache for review-queue promotions (issue #2213).
19
+ const historicalIdCache = new HistoricalEntityCanonicalIdCache();
20
+
21
+ /**
22
+ * In-place raw rewrite under the uniform write-boundary rule (issue #2213):
23
+ * canonicalize the effective entityRef at the write and REPAIR when the
24
+ * journal moved across it — these rewrites re-serialize whole records, so an
25
+ * unrelated approve/dismiss/flag must not write a legacy ref back out. On
26
+ * repair exhaustion the ORIGINAL bytes are restored before the retryable
27
+ * error propagates: performReview bumps the corpus sentinel only after the
28
+ * action returns, so a half-applied rewrite would be invisible to a warm
29
+ * cache while the caller is told the action failed.
30
+ */
31
+ function writeReviewFileCanonicalized(memoryDir: string, filePath: string, content: string): void {
32
+ const stateDir = path.join(memoryDir, "state");
33
+ const idsAtWrite = historicalIdCache.get(stateDir);
34
+ let originalBytes: string | null = null;
35
+ try {
36
+ originalBytes = fs.readFileSync(filePath, "utf8");
37
+ } catch {
38
+ originalBytes = null; // new file — rollback is removal
39
+ }
40
+ const written = canonicalizeEntityRefFrontmatter(content, idsAtWrite);
41
+ fs.writeFileSync(filePath, written, "utf8");
42
+ try {
43
+ repairContentAfterJournalMoveSync({
44
+ stateDir,
45
+ cache: historicalIdCache,
46
+ idsAtWrite,
47
+ rawContent: content,
48
+ lastWritten: written,
49
+ rewrite: (next) => fs.writeFileSync(filePath, next, "utf8"),
50
+ });
51
+ } catch (err) {
52
+ try {
53
+ if (originalBytes === null) {
54
+ fs.unlinkSync(filePath);
55
+ } else {
56
+ fs.writeFileSync(filePath, originalBytes, "utf8");
57
+ }
58
+ } catch {
59
+ // Best effort — the retryable repair error is the primary signal.
60
+ }
61
+ throw err;
62
+ }
63
+ }
12
64
 
13
65
  // ── Types ────────────────────────────────────────────────────────────────────
14
66
 
@@ -295,13 +347,16 @@ function approveItem(
295
347
  const fm = parseFrontmatter(content);
296
348
  if (!fm) return { itemId, action: "approve", message: "Could not parse frontmatter" };
297
349
 
298
- // Issue #1579 — when approving a tombstone-blocked memory, fire the
299
- // revocation hook so the content is re-allowed (append-only log: a
300
- // `kind: "revocation"` entry supersedes the tombstone at lookup). Clear
301
- // blockedBy/tombstoneBlockTier on the promoted memory so it is fully
302
- // active. Fire-and-forget: a hook failure never fails the approval.
350
+ // Issue #1579 — approving a tombstone-blocked memory fires the revocation
351
+ // hook so the content is re-allowed (append-only log: a `kind: "revocation"`
352
+ // entry supersedes the tombstone at lookup). Deferred until the approval's
353
+ // writes commit (Codex P2, round 19): revoking before the promote left a
354
+ // failed repair with the non-resurrection guard already destroyed, letting
355
+ // a later extraction resurrect the content. Fire-and-forget once invoked:
356
+ // a hook failure never fails the approval.
303
357
  const blockedBy = typeof fm.blockedBy === "string" ? fm.blockedBy : null;
304
- if (blockedBy && options.onApproveBlockedMemory) {
358
+ const fireBlockedRevocation = (): void => {
359
+ if (!blockedBy || !options.onApproveBlockedMemory) return;
305
360
  try {
306
361
  const maybe = options.onApproveBlockedMemory(blockedBy, itemId);
307
362
  if (maybe && typeof (maybe as Promise<void>).then === "function") {
@@ -310,7 +365,7 @@ function approveItem(
310
365
  } catch {
311
366
  /* fire-and-forget — never fail the approval (gotcha #13) */
312
367
  }
313
- }
368
+ };
314
369
 
315
370
  const updatedContent = updateFrontmatterFields(content, {
316
371
  confidence: "0.9",
@@ -324,7 +379,8 @@ function approveItem(
324
379
  });
325
380
 
326
381
  if (found.location === "category") {
327
- fs.writeFileSync(found.filePath, updatedContent, "utf8");
382
+ writeReviewFileCanonicalized(memoryDir, found.filePath, updatedContent);
383
+ fireBlockedRevocation();
328
384
  return {
329
385
  itemId,
330
386
  action: "approve",
@@ -342,10 +398,38 @@ function approveItem(
342
398
  const outputPath = path.join(targetDir, dateDir, path.basename(found.filePath));
343
399
 
344
400
  ensureSafeDirectory(rootReal, path.dirname(outputPath));
345
- const promotedPath = writeFileWithoutClobber(outputPath, updatedContent, itemId);
401
+ // The suggestions/review queue dirs sit OUTSIDE the migration's reference
402
+ // scan, so a queued pre-migration item can still carry a legacy entityRef
403
+ // (issue #2213): promote it canonicalized against the target journal, and
404
+ // REPAIR when the journal moved across the write.
405
+ const reviewStateDir = path.join(memoryDir, "state");
406
+ const idsAtWrite = historicalIdCache.get(reviewStateDir);
407
+ const promotedWritten = canonicalizeEntityRefFrontmatter(updatedContent, idsAtWrite);
408
+ const promotedPath = writeFileWithoutClobber(outputPath, promotedWritten, itemId);
409
+ try {
410
+ repairContentAfterJournalMoveSync({
411
+ stateDir: reviewStateDir,
412
+ cache: historicalIdCache,
413
+ idsAtWrite,
414
+ rawContent: updatedContent,
415
+ lastWritten: promotedWritten,
416
+ rewrite: (next) => fs.writeFileSync(promotedPath, next, "utf8"),
417
+ });
418
+ } catch (err) {
419
+ // Roll back so a retried approve does not clobber-fail against a partial
420
+ // promotion; the queue entry survives untouched (Bugbot).
421
+ try {
422
+ fs.unlinkSync(promotedPath);
423
+ } catch {
424
+ // Best effort — the retryable repair error below is the primary signal.
425
+ }
426
+ throw err;
427
+ }
346
428
 
347
- // Remove from review
429
+ // Remove from review, THEN revoke: the guard only falls once the approval
430
+ // has fully committed (promoted file repaired + queue entry gone).
348
431
  fs.unlinkSync(found.filePath);
432
+ fireBlockedRevocation();
349
433
 
350
434
  return {
351
435
  itemId,
@@ -372,13 +456,13 @@ function dismissItem(
372
456
  }
373
457
 
374
458
  const content = fs.readFileSync(found.filePath, "utf8");
375
- fs.writeFileSync(
459
+ writeReviewFileCanonicalized(
460
+ memoryDir,
376
461
  found.filePath,
377
462
  updateFrontmatterFields(content, {
378
463
  reviewDismissed: "true",
379
464
  reviewDismissedAt: new Date().toISOString(),
380
465
  }),
381
- "utf8",
382
466
  );
383
467
  return {
384
468
  itemId,
@@ -403,7 +487,7 @@ function flagItem(
403
487
  flagged: "true",
404
488
  flaggedAt: new Date().toISOString(),
405
489
  });
406
- fs.writeFileSync(found.filePath, fixed);
490
+ writeReviewFileCanonicalized(memoryDir, found.filePath, fixed);
407
491
  return {
408
492
  itemId,
409
493
  action: "flag",