@remnic/core 9.3.698 → 9.3.700

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 (271) hide show
  1. package/dist/access-boundary.d.ts +5 -5
  2. package/dist/access-boundary.js +14 -10
  3. package/dist/access-cli.js +28 -26
  4. package/dist/access-cli.js.map +1 -1
  5. package/dist/access-http.d.ts +5 -5
  6. package/dist/access-http.js +18 -14
  7. package/dist/access-mcp.d.ts +5 -5
  8. package/dist/access-mcp.js +17 -13
  9. package/dist/access-operations.d.ts +5 -5
  10. package/dist/access-operations.js +16 -12
  11. package/dist/access-schema.js +3 -3
  12. package/dist/{access-service-DftqtNUy.d.ts → access-service-Cte3ol0W.d.ts} +2 -2
  13. package/dist/access-service.d.ts +5 -5
  14. package/dist/access-service.js +13 -9
  15. package/dist/access-surface-catalog.d.ts +5 -5
  16. package/dist/action-confidence.d.ts +1 -1
  17. package/dist/action-confidence.js +2 -2
  18. package/dist/active-memory-bridge.d.ts +1 -1
  19. package/dist/active-recall.d.ts +1 -1
  20. package/dist/active-recall.js +1 -1
  21. package/dist/behavior-learner.d.ts +1 -1
  22. package/dist/behavior-signals.d.ts +1 -1
  23. package/dist/bootstrap.d.ts +4 -4
  24. package/dist/briefing.d.ts +1 -1
  25. package/dist/briefing.js +7 -3
  26. package/dist/buffer-surprise-report.d.ts +1 -1
  27. package/dist/buffer.d.ts +1 -1
  28. package/dist/calibration.d.ts +1 -1
  29. package/dist/capabilities.d.ts +1 -1
  30. package/dist/{catalog-D7YDNNF7.d.ts → catalog-DN1PzThs.d.ts} +1 -1
  31. package/dist/causal-behavior.d.ts +1 -1
  32. package/dist/causal-consolidation.d.ts +1 -1
  33. package/dist/causal-consolidation.js +8 -4
  34. package/dist/causal-consolidation.js.map +1 -1
  35. package/dist/{chunk-SJQ4HY3E.js → chunk-2NWHLAXX.js} +2 -2
  36. package/dist/{chunk-UDDSC6PO.js → chunk-33L6XHU2.js} +54 -6
  37. package/dist/chunk-33L6XHU2.js.map +1 -0
  38. package/dist/{chunk-XFG3PVZE.js → chunk-3E5WRQNQ.js} +8 -8
  39. package/dist/{chunk-EZWQZYLK.js → chunk-6JDGADXK.js} +2 -2
  40. package/dist/{chunk-IZ5E6ZTK.js → chunk-CCOXIDRM.js} +6 -6
  41. package/dist/{chunk-COEZR6F5.js → chunk-CHM274U6.js} +2 -2
  42. package/dist/{chunk-TIVZ4MCG.js → chunk-D75JXBV4.js} +3 -3
  43. package/dist/{chunk-M4DQWUKX.js → chunk-DR2JTSLZ.js} +2 -2
  44. package/dist/{chunk-XL5RSHZP.js → chunk-EOBJRBLC.js} +2 -2
  45. package/dist/{chunk-YY2HGKWR.js → chunk-G5PKTQ5J.js} +2 -2
  46. package/dist/{chunk-JYVD2XZ4.js → chunk-GA5A6MJH.js} +57 -24
  47. package/dist/chunk-GA5A6MJH.js.map +1 -0
  48. package/dist/{chunk-E6GOVHHJ.js → chunk-GYVVQYA3.js} +3 -3
  49. package/dist/{chunk-2KJG6ZZK.js → chunk-HDLC75NX.js} +2 -2
  50. package/dist/{chunk-AC5LO7IU.js → chunk-I4WBNM3M.js} +7 -1
  51. package/dist/chunk-I4WBNM3M.js.map +1 -0
  52. package/dist/{chunk-QVTSLRKT.js → chunk-IJEZMWKA.js} +3 -3
  53. package/dist/{chunk-ZXUOAFUG.js → chunk-IKNQAGBV.js} +1 -1
  54. package/dist/chunk-IKNQAGBV.js.map +1 -0
  55. package/dist/{chunk-N55RJT4N.js → chunk-JKW5XSWC.js} +2 -2
  56. package/dist/{chunk-CQ4PGFMC.js → chunk-JX3YZVII.js} +4 -4
  57. package/dist/{chunk-3LSSFCB4.js → chunk-K4DWSPMW.js} +11 -1
  58. package/dist/chunk-K4DWSPMW.js.map +1 -0
  59. package/dist/{chunk-HZ5DRA4Q.js → chunk-NHBEO3F3.js} +20 -20
  60. package/dist/{chunk-FSEQXHEZ.js → chunk-O54DY26V.js} +2 -2
  61. package/dist/{chunk-MRFVMSTR.js → chunk-OMNTQPZO.js} +20 -1
  62. package/dist/{chunk-MRFVMSTR.js.map → chunk-OMNTQPZO.js.map} +1 -1
  63. package/dist/chunk-PCZR32VL.js +446 -0
  64. package/dist/chunk-PCZR32VL.js.map +1 -0
  65. package/dist/{chunk-2K64VH66.js → chunk-PQG4T5V3.js} +22 -2
  66. package/dist/chunk-PQG4T5V3.js.map +1 -0
  67. package/dist/{chunk-JI6HWBYL.js → chunk-Q4O3ET6F.js} +2 -2
  68. package/dist/{chunk-ANZLT74L.js → chunk-RJ2THZ4H.js} +2 -2
  69. package/dist/{chunk-LPIWEKZ3.js → chunk-SDPDU2PM.js} +3 -3
  70. package/dist/{chunk-A62RAIBN.js → chunk-SEWF2O74.js} +2 -2
  71. package/dist/{chunk-2ULWWAQH.js → chunk-SMIVW7XC.js} +2 -2
  72. package/dist/{chunk-RTN2BLZM.js → chunk-T5QAZIBO.js} +2 -2
  73. package/dist/{chunk-RIC5U67B.js → chunk-X74FJSW7.js} +232 -4
  74. package/dist/chunk-X74FJSW7.js.map +1 -0
  75. package/dist/{chunk-SIDSEXUG.js → chunk-XY4WJTEX.js} +2 -2
  76. package/dist/{chunk-LIWU4QI6.js → chunk-YMTGXDN6.js} +5 -5
  77. package/dist/chunk-YPR7DOPD.js +286 -0
  78. package/dist/chunk-YPR7DOPD.js.map +1 -0
  79. package/dist/{chunk-MRX6S22R.js → chunk-YXIFA36P.js} +2 -2
  80. package/dist/{chunk-AH2JUU6X.js → chunk-Z2YFLXHU.js} +2 -2
  81. package/dist/{chunk-PJIHCOGV.js → chunk-ZDK2IW5F.js} +2 -2
  82. package/dist/{cli-DUMkkdLl.d.ts → cli--yVN9yEV.d.ts} +3 -3
  83. package/dist/cli.d.ts +6 -6
  84. package/dist/cli.js +32 -28
  85. package/dist/compounding/engine.d.ts +1 -1
  86. package/dist/compounding/engine.js +7 -3
  87. package/dist/compounding/preference-consolidator.d.ts +1 -1
  88. package/dist/compression-optimizer.d.ts +1 -1
  89. package/dist/config.d.ts +1 -1
  90. package/dist/config.js +1 -1
  91. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  92. package/dist/connectors/codex-materialize-runner.js +7 -3
  93. package/dist/connectors/codex-materialize.d.ts +1 -1
  94. package/dist/connectors/index.d.ts +1 -1
  95. package/dist/connectors/index.js +7 -3
  96. package/dist/consolidation-provenance-check.d.ts +1 -1
  97. package/dist/consolidation-undo.d.ts +1 -1
  98. package/dist/contradiction/index.d.ts +1 -1
  99. package/dist/conversation-index/backend.d.ts +1 -1
  100. package/dist/conversation-index/chunker.d.ts +1 -1
  101. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  102. package/dist/conversation-index/indexer.d.ts +1 -1
  103. package/dist/conversation-index/search.d.ts +1 -1
  104. package/dist/day-summary.d.ts +1 -1
  105. package/dist/delinearize.d.ts +1 -1
  106. package/dist/direct-answer-wiring.d.ts +1 -1
  107. package/dist/direct-answer.d.ts +1 -1
  108. package/dist/embedding-fallback.d.ts +1 -1
  109. package/dist/enrichment/index.d.ts +1 -1
  110. package/dist/entity-retrieval.d.ts +1 -1
  111. package/dist/entity-retrieval.js +7 -3
  112. package/dist/entity-schema.d.ts +1 -1
  113. package/dist/explicit-capture.d.ts +6 -6
  114. package/dist/extraction-faithfulness.d.ts +1 -1
  115. package/dist/extraction-judge-telemetry.d.ts +1 -1
  116. package/dist/extraction-judge-training.d.ts +1 -1
  117. package/dist/extraction-judge.d.ts +1 -1
  118. package/dist/extraction.d.ts +1 -1
  119. package/dist/fallback-llm.d.ts +1 -1
  120. package/dist/{forget-PLR6J5DN.js → forget-BEXG5PQC.js} +32 -1
  121. package/dist/forget-BEXG5PQC.js.map +1 -0
  122. package/dist/identity-continuity.d.ts +1 -1
  123. package/dist/importance.d.ts +1 -1
  124. package/dist/index.d.ts +21 -10
  125. package/dist/index.js +63 -41
  126. package/dist/index.js.map +1 -1
  127. package/dist/intent.d.ts +1 -1
  128. package/dist/lcm/engine.d.ts +1 -1
  129. package/dist/lcm/index.d.ts +1 -1
  130. package/dist/lcm/tools.d.ts +1 -1
  131. package/dist/lifecycle.d.ts +1 -1
  132. package/dist/live-connectors-runner.d.ts +1 -1
  133. package/dist/local-llm.d.ts +1 -1
  134. package/dist/maintenance/memory-governance.d.ts +1 -1
  135. package/dist/maintenance/memory-governance.js +7 -3
  136. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -3
  137. package/dist/maintenance/rebuild-memory-projection.js +8 -4
  138. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  139. package/dist/memory-action-policy.d.ts +1 -1
  140. package/dist/memory-cache.d.ts +1 -1
  141. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  142. package/dist/memory-projection-store.d.ts +1 -1
  143. package/dist/memory-provenance.d.ts +1 -1
  144. package/dist/memory-provenance.js +1 -1
  145. package/dist/memory-worth-outcomes.d.ts +1 -1
  146. package/dist/models-json.d.ts +1 -1
  147. package/dist/namespaces/migrate.d.ts +2 -2
  148. package/dist/namespaces/migrate.js +8 -4
  149. package/dist/namespaces/principal.d.ts +1 -1
  150. package/dist/namespaces/search.d.ts +1 -1
  151. package/dist/namespaces/storage.d.ts +20 -2
  152. package/dist/namespaces/storage.js +7 -3
  153. package/dist/native-knowledge.d.ts +1 -1
  154. package/dist/operator-toolkit.d.ts +19 -2
  155. package/dist/operator-toolkit.js +17 -11
  156. package/dist/orchestration/maintenance.d.ts +2 -2
  157. package/dist/orchestration/maintenance.js +9 -5
  158. package/dist/{orchestrator-Dv8JQu3-.d.ts → orchestrator-CJI4xdqV.d.ts} +3 -3
  159. package/dist/orchestrator.d.ts +4 -4
  160. package/dist/orchestrator.js +20 -18
  161. package/dist/patterns-cli.d.ts +1 -1
  162. package/dist/policy-runtime.d.ts +1 -1
  163. package/dist/provenance.d.ts +1 -1
  164. package/dist/qmd-recall-cache.d.ts +1 -1
  165. package/dist/qmd.d.ts +1 -1
  166. package/dist/recall-disclosure-escalation.d.ts +1 -1
  167. package/dist/recall-explain-renderer.d.ts +1 -1
  168. package/dist/recall-explain-renderer.js +4 -4
  169. package/dist/recall-planner-llm.d.ts +1 -1
  170. package/dist/recall-state.d.ts +1 -1
  171. package/dist/recall-tag-filter.d.ts +1 -1
  172. package/dist/recall-xray-cli.d.ts +1 -1
  173. package/dist/recall-xray-cli.js +5 -5
  174. package/dist/recall-xray-renderer.d.ts +1 -1
  175. package/dist/recall-xray-renderer.js +4 -4
  176. package/dist/recall-xray.d.ts +1 -1
  177. package/dist/recall-xray.js +3 -3
  178. package/dist/resolve-auth-token.d.ts +1 -1
  179. package/dist/resume-bundles.js +2 -2
  180. package/dist/retrieval-agents.d.ts +1 -1
  181. package/dist/retrieval-tiers.d.ts +1 -1
  182. package/dist/routing/engine.d.ts +1 -1
  183. package/dist/routing/store.d.ts +1 -1
  184. package/dist/schemas.d.ts +6 -6
  185. package/dist/search/embed-helper.d.ts +1 -1
  186. package/dist/search/factory.d.ts +1 -1
  187. package/dist/search/index.d.ts +1 -1
  188. package/dist/search/lancedb-backend.d.ts +1 -1
  189. package/dist/search/meilisearch-backend.d.ts +1 -1
  190. package/dist/search/noop-backend.d.ts +1 -1
  191. package/dist/search/orama-backend.d.ts +1 -1
  192. package/dist/search/port.d.ts +1 -1
  193. package/dist/search/remote-backend.d.ts +1 -1
  194. package/dist/{semantic-consolidation-CgREi9E0.d.ts → semantic-consolidation-BtUfv-AL.d.ts} +1 -1
  195. package/dist/semantic-consolidation.d.ts +2 -2
  196. package/dist/semantic-consolidation.js +8 -4
  197. package/dist/semantic-rule-promotion.js +7 -3
  198. package/dist/semantic-rule-verifier.d.ts +1 -1
  199. package/dist/semantic-rule-verifier.js +7 -3
  200. package/dist/session-observer-bands.d.ts +1 -1
  201. package/dist/session-observer-state.d.ts +1 -1
  202. package/dist/shared-context/manager.d.ts +1 -1
  203. package/dist/signal.d.ts +1 -1
  204. package/dist/storage.d.ts +306 -2
  205. package/dist/storage.js +6 -2
  206. package/dist/summarizer.d.ts +1 -1
  207. package/dist/summary-snapshot.d.ts +1 -1
  208. package/dist/temporal-supersession.d.ts +1 -1
  209. package/dist/temporal-supersession.js +1 -1
  210. package/dist/temporal-validity.d.ts +1 -1
  211. package/dist/threading.d.ts +1 -1
  212. package/dist/tier-migration.d.ts +1 -1
  213. package/dist/tier-routing.d.ts +1 -1
  214. package/dist/topics.d.ts +1 -1
  215. package/dist/transcript.d.ts +1 -1
  216. package/dist/{types-CeZYSC-E.d.ts → types-PuiPZ9iE.d.ts} +34 -1
  217. package/dist/types.d.ts +1 -1
  218. package/dist/types.js +1 -1
  219. package/dist/utility-runtime.d.ts +1 -1
  220. package/dist/utils/serialize-mutations.js +5 -280
  221. package/dist/utils/serialize-mutations.js.map +1 -1
  222. package/dist/verified-recall.js +7 -3
  223. package/package.json +2 -2
  224. package/src/config.ts +10 -0
  225. package/src/lifecycle/tombstones.test.ts +355 -0
  226. package/src/lifecycle/tombstones.ts +807 -0
  227. package/src/maintenance/forget.ts +41 -0
  228. package/src/maintenance/pattern-reinforcement.ts +47 -0
  229. package/src/memory-provenance.ts +12 -0
  230. package/src/namespaces/storage.ts +33 -0
  231. package/src/operator-toolkit.ts +77 -0
  232. package/src/orchestrator.ts +16 -0
  233. package/src/review/index.ts +48 -3
  234. package/src/storage.ts +329 -2
  235. package/src/temporal-supersession.ts +38 -0
  236. package/src/types.ts +34 -0
  237. package/dist/chunk-2K64VH66.js.map +0 -1
  238. package/dist/chunk-3LSSFCB4.js.map +0 -1
  239. package/dist/chunk-AC5LO7IU.js.map +0 -1
  240. package/dist/chunk-JYVD2XZ4.js.map +0 -1
  241. package/dist/chunk-RIC5U67B.js.map +0 -1
  242. package/dist/chunk-UDDSC6PO.js.map +0 -1
  243. package/dist/chunk-ZXUOAFUG.js.map +0 -1
  244. package/dist/forget-PLR6J5DN.js.map +0 -1
  245. /package/dist/{chunk-SJQ4HY3E.js.map → chunk-2NWHLAXX.js.map} +0 -0
  246. /package/dist/{chunk-XFG3PVZE.js.map → chunk-3E5WRQNQ.js.map} +0 -0
  247. /package/dist/{chunk-EZWQZYLK.js.map → chunk-6JDGADXK.js.map} +0 -0
  248. /package/dist/{chunk-IZ5E6ZTK.js.map → chunk-CCOXIDRM.js.map} +0 -0
  249. /package/dist/{chunk-COEZR6F5.js.map → chunk-CHM274U6.js.map} +0 -0
  250. /package/dist/{chunk-TIVZ4MCG.js.map → chunk-D75JXBV4.js.map} +0 -0
  251. /package/dist/{chunk-M4DQWUKX.js.map → chunk-DR2JTSLZ.js.map} +0 -0
  252. /package/dist/{chunk-XL5RSHZP.js.map → chunk-EOBJRBLC.js.map} +0 -0
  253. /package/dist/{chunk-YY2HGKWR.js.map → chunk-G5PKTQ5J.js.map} +0 -0
  254. /package/dist/{chunk-E6GOVHHJ.js.map → chunk-GYVVQYA3.js.map} +0 -0
  255. /package/dist/{chunk-2KJG6ZZK.js.map → chunk-HDLC75NX.js.map} +0 -0
  256. /package/dist/{chunk-QVTSLRKT.js.map → chunk-IJEZMWKA.js.map} +0 -0
  257. /package/dist/{chunk-N55RJT4N.js.map → chunk-JKW5XSWC.js.map} +0 -0
  258. /package/dist/{chunk-CQ4PGFMC.js.map → chunk-JX3YZVII.js.map} +0 -0
  259. /package/dist/{chunk-HZ5DRA4Q.js.map → chunk-NHBEO3F3.js.map} +0 -0
  260. /package/dist/{chunk-FSEQXHEZ.js.map → chunk-O54DY26V.js.map} +0 -0
  261. /package/dist/{chunk-JI6HWBYL.js.map → chunk-Q4O3ET6F.js.map} +0 -0
  262. /package/dist/{chunk-ANZLT74L.js.map → chunk-RJ2THZ4H.js.map} +0 -0
  263. /package/dist/{chunk-LPIWEKZ3.js.map → chunk-SDPDU2PM.js.map} +0 -0
  264. /package/dist/{chunk-A62RAIBN.js.map → chunk-SEWF2O74.js.map} +0 -0
  265. /package/dist/{chunk-2ULWWAQH.js.map → chunk-SMIVW7XC.js.map} +0 -0
  266. /package/dist/{chunk-RTN2BLZM.js.map → chunk-T5QAZIBO.js.map} +0 -0
  267. /package/dist/{chunk-SIDSEXUG.js.map → chunk-XY4WJTEX.js.map} +0 -0
  268. /package/dist/{chunk-LIWU4QI6.js.map → chunk-YMTGXDN6.js.map} +0 -0
  269. /package/dist/{chunk-MRX6S22R.js.map → chunk-YXIFA36P.js.map} +0 -0
  270. /package/dist/{chunk-AH2JUU6X.js.map → chunk-Z2YFLXHU.js.map} +0 -0
  271. /package/dist/{chunk-PJIHCOGV.js.map → chunk-ZDK2IW5F.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/namespaces/storage.ts"],"sourcesContent":["import path from \"node:path\";\nimport { access, lstat, readdir } from \"node:fs/promises\";\nimport { isSafeRouteNamespace } from \"../routing/engine.js\";\nimport { StorageManager } from \"../storage.js\";\nimport type { PluginConfig } from \"../types.js\";\nimport { ALL_CATEGORY_DIRS } from \"../utils/category-dir.js\";\nimport { namespaceIdentityToken, normalizeNamespaceIdentity } from \"./identity.js\";\nimport type { NamespaceCatalog } from \"./catalog.js\";\n\nasync function exists(p: string): Promise<boolean> {\n try {\n await access(p);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function hasStoredEntries(p: string): Promise<boolean> {\n try {\n const entry = await lstat(p);\n if (entry.isSymbolicLink()) return true;\n if (!entry.isDirectory()) return true;\n const children = await readdir(p, { withFileTypes: true });\n for (const child of children) {\n const childPath = path.join(p, child.name);\n if (child.isSymbolicLink() || child.isFile()) return true;\n if (child.isDirectory() && (await hasStoredEntries(childPath))) return true;\n }\n return false;\n } catch {\n return false;\n }\n}\n\n// Build a per-namespace directory under `<memoryDir>/namespaces` and assert the\n// resolved path stays inside that base. Namespace identifiers can originate from\n// operator config (config.defaultNamespace) and request-derived routing, so this\n// containment check prevents directory traversal (CodeQL js/path-injection).\n// For safe segments this returns exactly `path.join(base, segment)`, so there is\n// no behavioral change for valid namespaces.\nfunction resolveNamespaceDir(memoryDir: string, segment: string): string {\n // Mirror isSafeRouteNamespace's separator/parent-ref rejection (without its\n // 64-char cap, so identity tokens still pass). Rejecting separators and \"..\"\n // up front keeps the value a single contained child of <memoryDir>/namespaces.\n if (\n segment.length === 0 ||\n segment.includes(\"/\") ||\n segment.includes(\"\\\\\") ||\n segment.includes(\"..\") ||\n path.isAbsolute(segment)\n ) {\n throw new Error(`unsafe namespace path segment: ${segment}`);\n }\n return path.join(memoryDir, \"namespaces\", segment);\n}\n\nconst LEGACY_NAMESPACE_CONTENT_CHILDREN = [\n ...ALL_CATEGORY_DIRS,\n \"entities\",\n \"artifacts\",\n \"identity\",\n \"config\",\n \"summaries\",\n \"profile.md\",\n] as const;\n\nconst LEGACY_NAMESPACE_RUNTIME_CHILDREN = [\"state\"] as const;\n\nasync function hasAnyLegacyData(\n rootDir: string,\n options: { includeRuntimeState?: boolean } = {},\n): Promise<boolean> {\n const children = options.includeRuntimeState === true\n ? [...LEGACY_NAMESPACE_CONTENT_CHILDREN, ...LEGACY_NAMESPACE_RUNTIME_CHILDREN]\n : LEGACY_NAMESPACE_CONTENT_CHILDREN;\n for (const child of children) {\n if (await hasStoredEntries(path.join(rootDir, child))) return true;\n }\n return false;\n}\n\nasync function hasAnyNamespaceStorageMarker(\n rootDir: string,\n options: { includeRuntimeState?: boolean } = {},\n): Promise<boolean> {\n const children = options.includeRuntimeState === true\n ? [...LEGACY_NAMESPACE_CONTENT_CHILDREN, ...LEGACY_NAMESPACE_RUNTIME_CHILDREN]\n : LEGACY_NAMESPACE_CONTENT_CHILDREN;\n for (const child of children) {\n if (await exists(path.join(rootDir, child))) return true;\n }\n return false;\n}\n\n/**\n * Storage routing for namespaces.\n *\n * Compatibility note:\n * - When namespaces are enabled, existing raw namespace roots are preserved.\n * New namespace roots use tokenized names under `memoryDir/namespaces/<token>`.\n * - The default namespace continues to use the legacy `memoryDir` root unless the caller\n * has created `memoryDir/namespaces/<defaultNamespace>` (in which case we use that).\n *\n * This avoids surprising \"lost memories\" when an install flips namespaces on without\n * migrating existing data.\n */\n/**\n * Optional hooks for the storage router. `onResolve` fires whenever a namespace's\n * storage is resolved/created, so a downstream consumer (e.g. the namespace\n * catalog, issue #1499) can register the namespace. The hook MUST NOT throw into\n * the router; the router invokes it defensively and a hook failure never affects\n * storage resolution.\n *\n * The hook MAY return (or resolve to) a boolean indicating whether the\n * registration actually PERSISTED (round 6, codex P2 — NEFoX). When it resolves\n * to `false` (a dropped/no-op registration), the router does NOT mark the\n * (namespace, storageDir) pair as notified, so the next resolve RETRIES it\n * instead of suppressing it forever. A `void`/`undefined` result is treated as\n * success (legacy hooks).\n */\nexport interface NamespaceStorageRouterHooks {\n onResolve?: (\n namespace: string,\n storageDir: string,\n ) => void | boolean | Promise<void | boolean>;\n}\n\n/**\n * Resolve the runtime storage root for the configured DEFAULT namespace.\n *\n * Shared between the live router (`NamespaceStorageRouter.defaultNamespaceRoot`)\n * and the rebuildable catalog (`NamespaceCatalog.rebuildFromDisk`) so the two\n * can never diverge (CLAUDE.md rule #22/#42 — read & write paths resolve through\n * the same logic). The contract is: while legacy memory data still lives\n * directly under `memoryDir`, the default root stays `memoryDir`; only once the\n * legacy root is empty and a `namespaces/<default|token>` dir holds data does\n * the default migrate into that tokenized/legacy-named dir.\n */\nexport async function resolveDefaultNamespaceRoot(config: PluginConfig): Promise<string> {\n if (!config.namespacesEnabled) {\n return config.memoryDir;\n }\n\n // Build the legacy default root from the NORMALIZED (trimmed) name so a\n // whitespace-padded `defaultNamespace` still finds the live `namespaces/default`\n // root (NIabe). `storageFor()` classifies the trimmed value as the default, and\n // the on-disk legacy dir is created under the trimmed name; using the raw spaced\n // name here would look for `namespaces/<spaced>` and miss the real root, falling\n // back to memoryDir/tokenized. `namespaceIdentityToken` already normalizes\n // internally, so the tokenized path is unaffected.\n const defaultIdentity = normalizeNamespaceIdentity(config.defaultNamespace);\n const legacyNsDir = resolveNamespaceDir(config.memoryDir, defaultIdentity);\n const tokenizedNsDir = resolveNamespaceDir(\n config.memoryDir,\n namespaceIdentityToken(config.defaultNamespace),\n );\n const tokenizedHasData =\n (await exists(tokenizedNsDir)) &&\n (await hasAnyNamespaceStorageMarker(tokenizedNsDir, { includeRuntimeState: true }));\n const nsDir = tokenizedHasData\n ? tokenizedNsDir\n : (await exists(legacyNsDir))\n ? legacyNsDir\n : tokenizedNsDir;\n return (await exists(nsDir)) && !(await hasAnyLegacyData(config.memoryDir))\n ? nsDir\n : config.memoryDir;\n}\n\n/**\n * Resolve the runtime storage root for ANY namespace exactly as the live router\n * would (`NamespaceStorageRouter.namespaceRoot`). Shared so the rebuildable\n * catalog records the SAME on-disk root the router routes to — a recall/read\n * touch must not guess `namespaces/<token>` when the router actually serves a\n * legacy raw-name dir or a migrated default root (CLAUDE.md rule #22/#42; round\n * 4, cursor Medium). The default namespace delegates to `resolveDefaultNamespaceRoot`;\n * every other namespace prefers the tokenized root when it has a storage marker,\n * else a legacy raw-name dir when present, else the tokenized root.\n */\nexport async function resolveNamespaceStorageRoot(\n config: PluginConfig,\n namespace: string,\n): Promise<string> {\n if (!config.namespacesEnabled) return config.memoryDir;\n // Compare on NORMALIZED identity so a whitespace-padded configured default name\n // still routes to the default root rather than a tokenized non-default dir\n // (NH-FH). The catalog keys records by the same normalized identity.\n if (normalizeNamespaceIdentity(namespace) === normalizeNamespaceIdentity(config.defaultNamespace)) {\n return resolveDefaultNamespaceRoot(config);\n }\n const legacyRoot = resolveNamespaceDir(config.memoryDir, namespace);\n const tokenizedRoot = resolveNamespaceDir(config.memoryDir, namespaceIdentityToken(namespace));\n if (\n (await exists(tokenizedRoot)) &&\n (await hasAnyNamespaceStorageMarker(tokenizedRoot, { includeRuntimeState: true }))\n ) {\n return tokenizedRoot;\n }\n return (await exists(legacyRoot)) ? legacyRoot : tokenizedRoot;\n}\n\nexport class NamespaceStorageRouter {\n private readonly cache = new Map<string, StorageManager>();\n private defaultNsRootResolved: string | null = null;\n // Dedup the resolve hook (round 6, cursor Medium — NCNL2). Recall/extraction\n // call `storageFor` repeatedly; firing `onResolve` (→ catalog loadCompacted +\n // append) on every cache hit grows `namespaces.jsonl` without bound between\n // rebuilds. We fire the hook only when the (namespace, storageDir) pair is new\n // or its dir changed, so a steady-state cache hit is a no-op for the catalog.\n private readonly notifiedResolved = new Map<string, string>();\n // In-flight resolve-hook dedup (NFJV-, codex P2). The catalog's `onResolve`\n // hook is ASYNC (it returns `registerResolved(...)`), so `notifiedResolved` is\n // only set after the hook's promise SETTLES. Without tracking the in-flight\n // window, a burst of `storageFor()` cache hits for the SAME namespace before\n // the first registration finishes would each pass the `notifiedResolved` guard\n // and fire their OWN `onResolve` — queueing N duplicate catalog touches + lock\n // acquisitions despite the once-per-namespace intent. We therefore record the\n // (namespace → storageDir) being registered BEFORE awaiting the hook so a\n // concurrent call for the same pair skips firing. On SUCCESS the pair is\n // promoted to `notifiedResolved` (future calls skip permanently); on `false`\n // (dropped touch — e.g. rebuild-lock timeout) OR rejection the in-flight marker\n // is CLEARED so a later `storageFor()` can RETRY the dropped registration. The\n // entry is always removed when the promise settles, so the map cannot grow\n // unbounded (one transient entry per concurrently-resolving namespace).\n private readonly inFlightResolved = new Map<string, string>();\n // Tracks every in-flight resolve-hook promise so callers can deterministically\n // await the fire-and-forget registrations that `storageFor()` kicks off (see\n // `whenResolveHooksSettled`). Entries are removed as each hook settles, so the\n // set holds at most one promise per concurrently-resolving namespace.\n private readonly pendingResolveHooks = new Set<Promise<unknown>>();\n // Pending post-write catalog touch promises (#1522). Like pendingResolveHooks,\n // lets tests await fire-and-forget write touches deterministically.\n private readonly pendingWriteTouches = new Set<Promise<unknown>>();\n\n // Normalized (trimmed) default namespace identity (NH-FH). `storageFor`\n // normalizes its input, so default-namespace branches must compare against the\n // normalized config default too — otherwise a whitespace-padded configured\n // default name routes the default namespace to a tokenized non-default root.\n private readonly defaultNamespaceIdentity: string;\n\n constructor(\n private readonly config: PluginConfig,\n private readonly hooks: NamespaceStorageRouterHooks = {},\n /** Catalog reference for post-write/read touches (issue #1522 chokepoint). */\n private readonly catalog?: NamespaceCatalog,\n ) {\n this.defaultNamespaceIdentity = normalizeNamespaceIdentity(config.defaultNamespace);\n }\n\n private async defaultNamespaceRoot(): Promise<string> {\n this.defaultNsRootResolved = await resolveDefaultNamespaceRoot(this.config);\n return this.defaultNsRootResolved;\n }\n\n private async namespaceRoot(namespace: string): Promise<string> {\n // NOTE: only used after defaultNamespaceRoot() resolution.\n if (!this.config.namespacesEnabled) return this.config.memoryDir;\n if (normalizeNamespaceIdentity(namespace) === this.defaultNamespaceIdentity) {\n return this.defaultNsRootResolved ?? this.config.memoryDir;\n }\n return resolveNamespaceStorageRoot(this.config, namespace);\n }\n\n async storageFor(namespace: string): Promise<StorageManager> {\n const ns = normalizeNamespaceIdentity(namespace || this.config.defaultNamespace);\n if (ns !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(ns)) {\n throw new Error(`unsafe namespace: ${ns}`);\n }\n // Even when the default namespace is exempt from the check above, every\n // on-disk path is built through resolveNamespaceDir(), which rejects\n // traversal segments — so an unsafe configured default still cannot escape\n // <memoryDir>/namespaces (CodeQL js/path-injection).\n\n let root: string;\n if (ns === this.defaultNamespaceIdentity) {\n root = await this.defaultNamespaceRoot();\n const cached = this.cache.get(ns);\n if (cached && cached.dir === root) {\n this.notifyResolved(ns, root);\n return cached;\n }\n } else {\n const cached = this.cache.get(ns);\n root = await this.namespaceRoot(ns);\n if (cached && cached.dir === root) {\n this.notifyResolved(ns, root);\n return cached;\n }\n }\n\n const sm = new StorageManager(root, this.config.entitySchemas);\n // Propagate the inline-attribution template so that router-created storages\n // (used by extraction and shared-promotion paths) strip citations consistently,\n // matching the behaviour of the primary this.storage instance in the orchestrator.\n sm.citationTemplate = this.config.inlineSourceAttributionFormat;\n // #1522: install the post-write catalog touch at the chokepoint — every\n // successful write on this StorageManager records the namespace touch.\n this.bindCatalogWriteHook(sm, ns);\n // #1579: apply the tombstone non-resurrection config so every namespace\n // storage enforces the invariant at its own writeMemory chokepoint.\n this.applyTombstonesConfig(sm, ns);\n this.cache.set(ns, sm);\n this.notifyResolved(ns, root);\n return sm;\n }\n\n /**\n * Fire the resolve hook defensively. A hook failure (e.g. a catalog write\n * error) MUST NOT crash storage resolution — see CLAUDE.md gotcha #13.\n */\n private notifyResolved(namespace: string, storageDir: string): void {\n const hook = this.hooks.onResolve;\n if (!hook) return;\n // Skip when we've already SUCCESSFULLY notified this exact (namespace,\n // storageDir) — a steady-state cache hit must not re-append to the catalog\n // log (NCNL2). A changed dir (rare: migration/realignment) still re-fires\n // once. We mark the pair as notified ONLY AFTER the hook succeeds, and CLEAR\n // it on failure, so a dropped registration (e.g. rebuild-lock timeout) is\n // RETRIED on the next cache hit instead of being suppressed forever (round 6,\n // cursor Medium — ND3EJ).\n if (this.notifiedResolved.get(namespace) === storageDir) return;\n // In-flight dedup (NFJV-, codex P2): if a registration for this exact\n // (namespace, storageDir) is already AWAITING its async hook, do not fire a\n // second one. Without this, concurrent cache-hit bursts before the first\n // append settles each pass the `notifiedResolved` guard above and queue\n // duplicate catalog touches/lock acquisitions. A pair with a DIFFERENT\n // in-flight dir (rare mid-migration realignment) still fires once.\n if (this.inFlightResolved.get(namespace) === storageDir) return;\n try {\n // Handle BOTH synchronous throws and asynchronous rejections (round 6,\n // codex P2 — NDo8C). The hook may be `async`; its rejected promise would\n // bypass this try/catch and, where unhandled rejections are fatal, crash\n // storage resolution. Mark the dedup pair as notified ONLY when the hook\n // resolves to a PERSISTED result (round 6, codex P2 — NEFoX): a result of\n // `false` means the registration was dropped/no-op (e.g. rebuild-lock\n // timeout), so we must NOT suppress its retry. `void`/`undefined` is treated\n // as success for legacy hooks. On rejection we leave it un-notified to retry.\n //\n // Record the in-flight marker BEFORE awaiting so concurrent calls for the\n // same pair skip (NFJV-). It is always cleared once the promise settles, so\n // the map holds at most one transient entry per concurrently-resolving\n // namespace and cannot grow unbounded.\n this.inFlightResolved.set(namespace, storageDir);\n const hookResult = Promise.resolve(hook(namespace, storageDir));\n // Track the in-flight promise so `whenResolveHooksSettled()` can await it.\n this.pendingResolveHooks.add(hookResult);\n hookResult.then(\n (persisted) => {\n // Clear the in-flight marker ONLY if it is still ours (a newer resolve\n // for a different dir may have replaced it).\n if (this.inFlightResolved.get(namespace) === storageDir) {\n this.inFlightResolved.delete(namespace);\n }\n if (persisted !== false) {\n this.notifiedResolved.set(namespace, storageDir);\n }\n // On `false` (dropped touch) we intentionally do NOT mark notified, so\n // a later `storageFor()` retries the registration. Clearing the\n // in-flight marker above is what re-enables that retry.\n this.pendingResolveHooks.delete(hookResult);\n },\n () => {\n // Registration failed — clear in-flight AND do NOT mark as notified, so\n // it is retried on the next cache hit.\n if (this.inFlightResolved.get(namespace) === storageDir) {\n this.inFlightResolved.delete(namespace);\n }\n if (this.notifiedResolved.get(namespace) === storageDir) {\n this.notifiedResolved.delete(namespace);\n }\n this.pendingResolveHooks.delete(hookResult);\n },\n );\n } catch {\n // Synchronous throw: clear any in-flight marker we just set and leave the\n // pair un-notified so a later resolve retries.\n if (this.inFlightResolved.get(namespace) === storageDir) {\n this.inFlightResolved.delete(namespace);\n }\n }\n }\n\n /**\n * Install the post-write catalog touch hook on an externally-constructed\n * StorageManager (issue #1522). Used by the orchestrator for the legacy\n * default-namespace storage (`this.storage`) that bypasses the router.\n */\n bindCatalogWriteHook(sm: StorageManager, namespace: string): void {\n sm.onCatalogWrite = () => this.touchCatalogWrite(namespace, sm.dir);\n }\n\n /**\n * Install the tombstone config on an externally-constructed StorageManager\n * (issue #1579). Mirrors `bindCatalogWriteHook` — used for the legacy\n * default-namespace storage that bypasses the router. Router-created\n * storages are wired inline in `storageFor()` via `applyTombstonesConfig`.\n */\n bindTombstonesConfig(\n sm: StorageManager,\n namespace: string,\n config: { enabled: boolean; semanticMatch: boolean; semanticThreshold: number },\n ): void {\n this.tombstonesGlobalConfig = { ...config };\n sm.setTombstonesConfig({ ...config, namespace });\n }\n\n private tombstonesGlobalConfig: {\n enabled: boolean;\n semanticMatch: boolean;\n semanticThreshold: number;\n } = { enabled: false, semanticMatch: false, semanticThreshold: 0.9 };\n\n /**\n * Apply the tombstone config to a router-created StorageManager. Called\n * inline in `storageFor()` so every namespace storage enforces the\n * non-resurrection invariant, namespace-scoped (rule 42).\n */\n private applyTombstonesConfig(sm: StorageManager, namespace: string): void {\n sm.setTombstonesConfig({ ...this.tombstonesGlobalConfig, namespace });\n }\n\n /**\n * Post-write catalog touch (issue #1522 chokepoint). Called by every\n * StorageManager's post-write hook AFTER a successful write. Best-effort\n * and failure-tolerant — a catalog error MUST NOT affect the primary write\n * (gotcha #13, rule #40). Fire-and-forget by design.\n */\n private touchCatalogWrite(namespace: string, storageDir: string): void {\n if (!this.catalog) return;\n const touch = this.catalog\n .markWrite(namespace, { discoveredBy: \"write\", storageDir })\n .catch(() => undefined);\n this.pendingWriteTouches.add(touch);\n void touch.then(\n () => { this.pendingWriteTouches.delete(touch); },\n () => { this.pendingWriteTouches.delete(touch); },\n );\n }\n\n /**\n * Record a namespace read in the catalog (issue #1522 chokepoint move).\n * Best-effort and failure-tolerant. Used by recall paths so the read touch\n * lives in the storage layer, not at the caller.\n */\n recordRead(namespace: string, storageDir?: string): void {\n if (!this.catalog) return;\n const ns = normalizeNamespaceIdentity(namespace || this.config.defaultNamespace);\n void this.catalog\n .markRead(ns, { discoveredBy: \"read\", storageDir })\n .catch(() => undefined);\n }\n /**\n * Record a namespace write touch in the catalog (issue #1522). Best-effort\n * and failure-tolerant. Used by consolidation/cleanup passes that may mutate\n * the store via delete-only paths (e.g. entity-file merges, TTL cleanup) so\n * the namespace's lastWriteAt stays fresh even when no explicit write went\n * through the storage chokepoint. This is NOT a substitute for the chokepoint\n * — it's a belt-and-suspenders for paths the chokepoint doesn't cover.\n */\n recordWrite(namespace: string, storageDir?: string): void {\n if (!this.catalog) return;\n const ns = normalizeNamespaceIdentity(namespace || this.config.defaultNamespace);\n const touch = this.catalog\n .markWrite(ns, { discoveredBy: \"write\", storageDir })\n .catch(() => undefined);\n this.pendingWriteTouches.add(touch);\n void touch.then(\n () => { this.pendingWriteTouches.delete(touch); },\n () => { this.pendingWriteTouches.delete(touch); },\n );\n }\n\n /**\n * Resolve once every in-flight post-write catalog touch has settled (#1522).\n * Mirrors `whenResolveHooksSettled()`: the StorageManager's post-write hook\n * fires the catalog touch fire-and-forget, so tests asserting lastWriteAt\n * moved should await this instead of racing a timer.\n */\n async whenWriteTouchesSettled(): Promise<void> {\n while (this.pendingWriteTouches.size > 0) {\n await Promise.allSettled([...this.pendingWriteTouches]);\n }\n }\n\n /**\n * Resolve once every in-flight `onResolve` registration has settled.\n *\n * `storageFor()` fires the resolve hook fire-and-forget, so its catalog side\n * effect (e.g. `registerResolved(...)`) is not observable the moment\n * `storageFor()` returns. Callers that must act on that side effect — notably\n * tests asserting the catalog was updated — should await this instead of\n * racing a timer. Resolves immediately when no hook is registered or nothing\n * is in flight. The loop re-checks because a settling hook could, in\n * principle, trigger a follow-on resolution.\n */\n async whenResolveHooksSettled(): Promise<void> {\n while (this.pendingResolveHooks.size > 0) {\n await Promise.allSettled([...this.pendingResolveHooks]);\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -15,7 +15,7 @@ import {
15
15
  ACTION_CONFIDENCE_CONTEXT_READINESS,
16
16
  ACTION_CONFIDENCE_RISK_CATEGORIES,
17
17
  ACTION_CONFIDENCE_RULE_KINDS
18
- } from "./chunk-AH2JUU6X.js";
18
+ } from "./chunk-Z2YFLXHU.js";
19
19
 
20
20
  // src/access-schema.ts
21
21
  import { z } from "zod";
@@ -433,4 +433,4 @@ export {
433
433
  actionConfidenceRequestSchema,
434
434
  validateRequest
435
435
  };
436
- //# sourceMappingURL=chunk-JI6HWBYL.js.map
436
+ //# sourceMappingURL=chunk-Q4O3ET6F.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  StorageManager
3
- } from "./chunk-RIC5U67B.js";
3
+ } from "./chunk-X74FJSW7.js";
4
4
  import {
5
5
  assertPathInsideRoot
6
6
  } from "./chunk-5GPPACXK.js";
@@ -751,4 +751,4 @@ export {
751
751
  listMemoryGovernanceRuns,
752
752
  readMemoryGovernanceRunArtifact
753
753
  };
754
- //# sourceMappingURL=chunk-ANZLT74L.js.map
754
+ //# sourceMappingURL=chunk-RJ2THZ4H.js.map
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  isRecallDisclosure
3
- } from "./chunk-ZXUOAFUG.js";
3
+ } from "./chunk-IKNQAGBV.js";
4
4
  import {
5
5
  normalizeRetrievedMemoryProvenance
6
- } from "./chunk-AC5LO7IU.js";
6
+ } from "./chunk-I4WBNM3M.js";
7
7
 
8
8
  // src/recall-xray.ts
9
9
  import { randomUUID } from "crypto";
@@ -269,4 +269,4 @@ export {
269
269
  RecallXrayBuilder,
270
270
  summarizeDisclosureTokens
271
271
  };
272
- //# sourceMappingURL=chunk-LPIWEKZ3.js.map
272
+ //# sourceMappingURL=chunk-SDPDU2PM.js.map
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-SFQ6QNL7.js";
7
7
  import {
8
8
  StorageManager
9
- } from "./chunk-RIC5U67B.js";
9
+ } from "./chunk-X74FJSW7.js";
10
10
  import {
11
11
  buildLifecycleEventsForMemory,
12
12
  sortMemoryLifecycleEvents
@@ -74,4 +74,4 @@ export {
74
74
  backupExistingLedger,
75
75
  rebuildMemoryLifecycleLedger
76
76
  };
77
- //# sourceMappingURL=chunk-A62RAIBN.js.map
77
+ //# sourceMappingURL=chunk-SEWF2O74.js.map
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-3UXOZBHV.js";
8
8
  import {
9
9
  StorageManager
10
- } from "./chunk-RIC5U67B.js";
10
+ } from "./chunk-X74FJSW7.js";
11
11
  import {
12
12
  isSafeRouteNamespace
13
13
  } from "./chunk-U3PN77QT.js";
@@ -118,4 +118,4 @@ export {
118
118
  runCodexMaterialize,
119
119
  runPostConsolidationMaterialize
120
120
  };
121
- //# sourceMappingURL=chunk-2ULWWAQH.js.map
121
+ //# sourceMappingURL=chunk-SMIVW7XC.js.map
@@ -11,7 +11,7 @@ import {
11
11
  } from "./chunk-LMDRGRJ2.js";
12
12
  import {
13
13
  parseConfig
14
- } from "./chunk-3LSSFCB4.js";
14
+ } from "./chunk-K4DWSPMW.js";
15
15
  import {
16
16
  resolveObjectiveStateStoreDir,
17
17
  validateObjectiveStateSnapshot
@@ -268,4 +268,4 @@ export {
268
268
  recordResumeBundle,
269
269
  getResumeBundleStatus
270
270
  };
271
- //# sourceMappingURL=chunk-RTN2BLZM.js.map
271
+ //# sourceMappingURL=chunk-T5QAZIBO.js.map
@@ -1,3 +1,8 @@
1
+ import {
2
+ TombstoneStore,
3
+ buildRetiredFactTombstoneInputs,
4
+ collectRetiredMemoriesForRebuild
5
+ } from "./chunk-PCZR32VL.js";
1
6
  import {
2
7
  WEARABLES_DIR_NAME,
3
8
  isValidTranscriptDate
@@ -8,6 +13,9 @@ import {
8
13
  import {
9
14
  assertPathInsideRoot
10
15
  } from "./chunk-5GPPACXK.js";
16
+ import {
17
+ supersessionKeysForFact
18
+ } from "./chunk-OMNTQPZO.js";
11
19
  import {
12
20
  stripAttributesSuffix
13
21
  } from "./chunk-SVOZFLIQ.js";
@@ -19,7 +27,7 @@ import {
19
27
  import {
20
28
  SPECULATIVE_TTL_DAYS,
21
29
  confidenceTier
22
- } from "./chunk-ZXUOAFUG.js";
30
+ } from "./chunk-IKNQAGBV.js";
23
31
  import {
24
32
  normalizeProjectionPreview,
25
33
  normalizeProjectionTags
@@ -257,6 +265,8 @@ function serializeFrontmatter(fm) {
257
265
  lines.push(`lineage: [${fm.lineage.map((l) => `"${l}"`).join(", ")}]`);
258
266
  }
259
267
  if (fm.status && fm.status !== "active") lines.push(`status: ${fm.status}`);
268
+ if (fm.blockedBy) lines.push(`blockedBy: ${fm.blockedBy}`);
269
+ if (fm.tombstoneBlockTier) lines.push(`tombstoneBlockTier: ${fm.tombstoneBlockTier}`);
260
270
  if (fm.supersededBy) lines.push(`supersededBy: ${fm.supersededBy}`);
261
271
  if (fm.supersededAt) lines.push(`supersededAt: ${fm.supersededAt}`);
262
272
  if (fm.archivedAt) lines.push(`archivedAt: ${fm.archivedAt}`);
@@ -615,6 +625,9 @@ function parseFrontmatter(raw) {
615
625
  lineage: lineage && lineage.length > 0 ? lineage : void 0,
616
626
  // Status management
617
627
  status: fm.status || "active",
628
+ // Tombstone block visibility (issue #1579) — read-through.
629
+ blockedBy: fm.blockedBy || void 0,
630
+ tombstoneBlockTier: fm.tombstoneBlockTier === "exact" || fm.tombstoneBlockTier === "normalized" || fm.tombstoneBlockTier === "keyed" || fm.tombstoneBlockTier === "semantic" ? fm.tombstoneBlockTier : void 0,
618
631
  supersededBy: fm.supersededBy || void 0,
619
632
  supersededAt: fm.supersededAt || void 0,
620
633
  archivedAt: fm.archivedAt || void 0,
@@ -1751,6 +1764,13 @@ var StorageManager = class _StorageManager {
1751
1764
  } catch {
1752
1765
  }
1753
1766
  }
1767
+ // ── Tombstone store (issue #1579) ───────────────────────────────────────
1768
+ // Instance-scoped (rule 11) + namespace-scoped (rule 42) tombstone index.
1769
+ // Lazily loaded from <stateDir>/tombstones.jsonl; invalidated together with
1770
+ // every other cache layer via invalidateAllMemoriesCache (rule 25).
1771
+ tombstoneStore = null;
1772
+ tombstoneStoreLoadPromise = null;
1773
+ tombstonesConfig = { enabled: false, semanticMatch: false, semanticThreshold: 0.9, namespace: "default" };
1754
1774
  /** Page-versioning configuration. Set by the orchestrator after construction. */
1755
1775
  _versioningConfig = null;
1756
1776
  /** Set the page-versioning configuration. When `enabled` is false (default), all versioning calls are no-ops. */
@@ -2048,13 +2068,17 @@ var StorageManager = class _StorageManager {
2048
2068
  * Promote a pending_review wearable memory to active in place,
2049
2069
  * merging updated trust evidence into structuredAttributes. Returns
2050
2070
  * false when the memory is missing or no longer pending_review (a
2051
- * concurrent review decision wins).
2071
+ * concurrent review decision wins), or when the row is tombstone-blocked
2072
+ * (`blockedBy` — issue #1579 threads OcuDx/Ocu1l): writeMemoryFrontmatter
2073
+ * bypasses the writeMemory chokepoint, so a blocked row must first be
2074
+ * cleared via revokeTombstone before promotion can proceed.
2052
2075
  */
2053
2076
  async promoteWearableMemory(id, attributeUpdates, confidence) {
2054
2077
  const memories = await this.readAllMemories();
2055
2078
  const memory = memories.find((entry) => entry.frontmatter.id === id);
2056
2079
  if (!memory) return false;
2057
2080
  if (memory.frontmatter.status !== "pending_review") return false;
2081
+ if (memory.frontmatter.blockedBy) return false;
2058
2082
  return this.writeMemoryFrontmatter(memory, {
2059
2083
  status: "active",
2060
2084
  // Keep frontmatter confidence in step with the re-scored trust —
@@ -2361,6 +2385,139 @@ var StorageManager = class _StorageManager {
2361
2385
  get factHashIndexReadyPath() {
2362
2386
  return path.join(this.stateDir, "fact-hashes.ready");
2363
2387
  }
2388
+ // ── Tombstone store access (issue #1579) ─────────────────────────────────
2389
+ /**
2390
+ * The on-disk tombstone log path. Lives under `<stateDir>/tombstones.jsonl`
2391
+ * so it is co-located with the fact-hash index and encrypted at rest when
2392
+ * the secure-store is enabled (the StorageManager's secure read/write
2393
+ * helpers are injected into the store).
2394
+ */
2395
+ get tombstonesPath() {
2396
+ return path.join(this.stateDir, "tombstones.jsonl");
2397
+ }
2398
+ /**
2399
+ * Configure the tombstone invariant for this storage instance. Installed
2400
+ * by the orchestrator after construction (same pattern as
2401
+ * `setVersioningConfig` / `citationTemplate`). When `enabled` is false
2402
+ * (default until the orchestrator wires config), the chokepoint check is a
2403
+ * no-op — pre-feature behavior for rollback safety (rule 30).
2404
+ */
2405
+ setTombstonesConfig(config) {
2406
+ this.tombstonesConfig = { ...config };
2407
+ if (this.tombstoneStore) {
2408
+ this.tombstoneStore.invalidate();
2409
+ this.tombstoneStore = null;
2410
+ this.tombstoneStoreLoadPromise = null;
2411
+ }
2412
+ }
2413
+ buildTombstoneStore() {
2414
+ const options = {
2415
+ enabled: this.tombstonesConfig.enabled,
2416
+ semanticMatch: this.tombstonesConfig.semanticMatch,
2417
+ semanticThreshold: this.tombstonesConfig.semanticThreshold,
2418
+ // Wire the SAME helpers the dedup index uses (rule 23 / checklist §13):
2419
+ // hashContent = ContentHashIndex.computeHash, normalizeText =
2420
+ // ContentHashIndex.normalizeContent. Importing them here (not copying)
2421
+ // guarantees the tombstone tiers can never drift from dedup.
2422
+ hashContent: ContentHashIndex.computeHash,
2423
+ normalizeText: ContentHashIndex.normalizeContent
2424
+ };
2425
+ const io = {
2426
+ read: (filePath) => this.readStorageSecureFile(filePath),
2427
+ append: (filePath, content) => this.appendStorageSecureFile(filePath, content),
2428
+ write: (filePath, content) => this.writeStorageSecureFile(filePath, content),
2429
+ // stat lets the store own its cross-process staleness probe (#1579).
2430
+ stat: (filePath) => statSync(filePath)
2431
+ };
2432
+ return new TombstoneStore(
2433
+ this.tombstonesPath,
2434
+ this.tombstonesConfig.namespace,
2435
+ options,
2436
+ io
2437
+ );
2438
+ }
2439
+ /**
2440
+ * Lazily load the tombstone store. Mirrors the fact-hash-index lazy-load
2441
+ * pattern: a single in-flight load promise dedups concurrent first-access.
2442
+ * The cross-process staleness probe lives inside the store now (#1579): on
2443
+ * each cached access it stats its own file and invalidates if a peer process
2444
+ * appended, so the next lookup reloads from disk. If the probe just
2445
+ * invalidated the store, fall through to the reload branch — returning null
2446
+ * here was the original regression (the chokepoint threw and failed OPEN).
2447
+ */
2448
+ getTombstoneStore() {
2449
+ if (this.tombstoneStore) {
2450
+ const cached = this.tombstoneStore;
2451
+ return cached.ensureFreshAgainstDisk().then(() => cached);
2452
+ }
2453
+ if (!this.tombstoneStoreLoadPromise) {
2454
+ const store = this.buildTombstoneStore();
2455
+ this.tombstoneStoreLoadPromise = store.load().then(() => {
2456
+ this.tombstoneStore = store;
2457
+ return store;
2458
+ }).catch((err) => {
2459
+ this.tombstoneStoreLoadPromise = null;
2460
+ throw err;
2461
+ });
2462
+ }
2463
+ return this.tombstoneStoreLoadPromise;
2464
+ }
2465
+ /**
2466
+ * Append a tombstone for a retired memory (issue #1579 emitters).
2467
+ * Best-effort: a tombstone append failure MUST NOT fail the supersession /
2468
+ * correction that triggered it (gotcha #13 / rule 34). The memory is
2469
+ * already retired on disk; the tombstone is the non-resurrection guard.
2470
+ */
2471
+ async appendTombstone(input) {
2472
+ if (!this.tombstonesConfig.enabled) return null;
2473
+ try {
2474
+ const store = await this.getTombstoneStore();
2475
+ const strippedRawContent = stripCitationForTemplate(input.rawContent, this.citationTemplate);
2476
+ return await store.appendTombstone({ ...input, rawContent: strippedRawContent });
2477
+ } catch (err) {
2478
+ log.warn(`tombstone append failed (memory=${input.sourceMemoryId}): ${err}`);
2479
+ return null;
2480
+ }
2481
+ }
2482
+ /**
2483
+ * Revoke a tombstone (re-allow the content). Used by the review-queue
2484
+ * approval path when an operator approves a blocked memory.
2485
+ */
2486
+ async revokeTombstone(tombstoneId, createdBy) {
2487
+ if (!this.tombstonesConfig.enabled) return null;
2488
+ try {
2489
+ const store = await this.getTombstoneStore();
2490
+ return await store.revoke(tombstoneId, createdBy);
2491
+ } catch (err) {
2492
+ log.warn(`tombstone revoke failed (id=${tombstoneId}): ${err}`);
2493
+ return null;
2494
+ }
2495
+ }
2496
+ /** Aggregate tombstone stats for `remnic doctor`. */
2497
+ async getTombstoneStats() {
2498
+ if (!this.tombstonesConfig.enabled) return null;
2499
+ try {
2500
+ const store = await this.getTombstoneStore();
2501
+ return store.stats();
2502
+ } catch (err) {
2503
+ log.warn(`tombstone stats failed: ${err}`);
2504
+ return null;
2505
+ }
2506
+ }
2507
+ /**
2508
+ * Rebuild the tombstone log from retired memories on disk. Exposed for
2509
+ * `remnic doctor --rebuild-tombstones` and tests.
2510
+ */
2511
+ async rebuildTombstonesFromFiles() {
2512
+ if (!this.tombstonesConfig.enabled) return 0;
2513
+ const store = await this.getTombstoneStore();
2514
+ const all = [...await this.readAllMemories(), ...await this.readAllColdMemories()];
2515
+ const retired = collectRetiredMemoriesForRebuild(all, {
2516
+ stripCitation: (text) => stripCitationForTemplate(text, this.citationTemplate),
2517
+ supersessionKeysForFact
2518
+ });
2519
+ return await store.rebuild(retired);
2520
+ }
2364
2521
  async getFactHashIndex() {
2365
2522
  if (this.factHashIndex) {
2366
2523
  return this.factHashIndex;
@@ -2655,9 +2812,45 @@ var StorageManager = class _StorageManager {
2655
2812
  if (!sanitized.clean) {
2656
2813
  log.warn(`memory content sanitized for ${id}; violations=${sanitized.violations.join(", ")}`);
2657
2814
  }
2815
+ let factHashSourceForTombstone = null;
2658
2816
  if (category === "fact") {
2659
2817
  const hashSource = options.contentHashSource !== void 0 && options.contentHashSource.length > 0 ? sanitizeMemoryContent(options.contentHashSource).text : sanitized.text;
2660
2818
  fm.contentHash = ContentHashIndex.computeHash(hashSource);
2819
+ factHashSourceForTombstone = hashSource;
2820
+ }
2821
+ let tombstoneBlocked = false;
2822
+ if (category === "fact" && this.tombstonesConfig.enabled && factHashSourceForTombstone !== null && // Block facts that would become ACTIVE or are already pending_review
2823
+ // (issue #1579 thread ObteQ: wearable/native imports write fact
2824
+ // candidates with status: pending_review; without checking them here,
2825
+ // promoteWearableMemory could later flip a retired fact active without
2826
+ // a tombstone check). A caller that explicitly requests a terminal
2827
+ // non-active status (rejected/retracted/superseded) is respected.
2828
+ (fm.status === void 0 || fm.status === "active" || fm.status === "pending_review")) {
2829
+ try {
2830
+ const tombstoneStore = await this.getTombstoneStore();
2831
+ const keyedSupersession = options.entityRef && options.structuredAttributes ? supersessionKeysForFact({
2832
+ entityRef: options.entityRef,
2833
+ structuredAttributes: options.structuredAttributes
2834
+ }) : [];
2835
+ const match = tombstoneStore.lookup({
2836
+ contentHash: fm.contentHash,
2837
+ normalizedText: ContentHashIndex.normalizeContent(factHashSourceForTombstone),
2838
+ ...options.entityRef ? { entityRef: options.entityRef } : {},
2839
+ ...keyedSupersession.length > 0 ? { supersessionKeys: keyedSupersession } : {},
2840
+ namespace: this.tombstonesConfig.namespace
2841
+ });
2842
+ if (match) {
2843
+ tombstoneBlocked = true;
2844
+ fm.status = "pending_review";
2845
+ fm.blockedBy = match.tombstoneId;
2846
+ fm.tombstoneBlockTier = match.matchedTier;
2847
+ log.info(
2848
+ `tombstone: blocked resurrection of fact ${id} (tier=${match.matchedTier}, tombstone=${match.tombstoneId}, reason=${match.reason})`
2849
+ );
2850
+ }
2851
+ } catch (err) {
2852
+ log.warn(`tombstone lookup failed for fact ${id} (fail-open): ${err}`);
2853
+ }
2661
2854
  }
2662
2855
  const fileContent = `${serializeFrontmatter(fm)}
2663
2856
 
@@ -2678,7 +2871,7 @@ ${sanitized.text}
2678
2871
  ...(options.lineage ?? []).filter(Boolean)
2679
2872
  ]
2680
2873
  });
2681
- if (category === "fact") {
2874
+ if (category === "fact" && !tombstoneBlocked) {
2682
2875
  try {
2683
2876
  const factHashIndex = await this.getFactHashIndex();
2684
2877
  if (options.contentHashSource !== void 0 && options.contentHashSource.length > 0) {
@@ -2720,6 +2913,22 @@ ${sanitized.text}
2720
2913
  factHashIndex.addByHash(hash);
2721
2914
  await factHashIndex.save();
2722
2915
  }
2916
+ /**
2917
+ * Re-register a fact's contentHash in the dedup index after a tombstone
2918
+ * block is lifted on approval (issue #1579 thread ObnTy). `writeMemory`
2919
+ * skips hash-index registration for tombstone-blocked facts (rule 44); when
2920
+ * the review queue later promotes such a fact back to `status: active`, the
2921
+ * hash must enter the index or the next extraction of the same content
2922
+ * creates a second active fact. Reads the memory by id so the caller (the
2923
+ * review CLI) does not need to re-parse the file. No-op for non-facts or
2924
+ * facts that are not active.
2925
+ */
2926
+ async restoreFactHashAfterApproval(memoryId) {
2927
+ const all = await this.readAllMemories();
2928
+ const memory = all.find((m) => m.frontmatter.id === memoryId);
2929
+ if (!memory) return;
2930
+ await this.addActiveFactContentHash(memory);
2931
+ }
2723
2932
  async syncFactHashIndexAfterRewrite(before, after) {
2724
2933
  if (before.frontmatter.category !== "fact" && after.frontmatter.category !== "fact") return;
2725
2934
  const beforeHash = this.factContentHashForRemoval(before);
@@ -3117,6 +3326,11 @@ ${sanitized.text}
3117
3326
  invalidateAllMemoriesCache() {
3118
3327
  _StorageManager.allMemoriesInFlight.delete(this.baseDir);
3119
3328
  invalidateAllForDir(this.baseDir);
3329
+ if (this.tombstoneStore) {
3330
+ this.tombstoneStore.invalidate();
3331
+ this.tombstoneStore = null;
3332
+ this.tombstoneStoreLoadPromise = null;
3333
+ }
3120
3334
  }
3121
3335
  /**
3122
3336
  * Invalidate the cold-scan cache for this storage root and bump the
@@ -5416,6 +5630,20 @@ ${oldMemory.content}
5416
5630
  });
5417
5631
  this.bumpMemoryStatusVersion();
5418
5632
  log.debug(`superseded memory ${oldMemoryId} by ${newMemoryId}: ${reason}`);
5633
+ if (oldMemory.frontmatter.category === "fact") {
5634
+ for (const input of buildRetiredFactTombstoneInputs(
5635
+ {
5636
+ id: oldMemoryId,
5637
+ content: stripCitationForTemplate(oldMemory.content, this.citationTemplate),
5638
+ contentHash: oldMemory.frontmatter.contentHash,
5639
+ entityRef: oldMemory.frontmatter.entityRef,
5640
+ structuredAttributes: oldMemory.frontmatter.structuredAttributes
5641
+ },
5642
+ { reason: "contradiction_resolution", createdBy: "contradiction_resolution", createdAt: now, supersessionKeysForFact }
5643
+ )) {
5644
+ await this.appendTombstone(input);
5645
+ }
5646
+ }
5419
5647
  await this.writeMemory("correction", `Superseded: ${oldMemory.content}
5420
5648
 
5421
5649
  Reason: ${reason}`, {
@@ -5612,4 +5840,4 @@ export {
5612
5840
  serializeEntityFile,
5613
5841
  StorageManager
5614
5842
  };
5615
- //# sourceMappingURL=chunk-RIC5U67B.js.map
5843
+ //# sourceMappingURL=chunk-X74FJSW7.js.map