@remnic/core 9.3.699 → 9.3.701

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 (285) 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 +30 -28
  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-CGVWK6lZ.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-DBIghceA.d.ts} +27 -53
  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-E6GOVHHJ.js → chunk-27LQPUMZ.js} +3 -3
  36. package/dist/{chunk-XJNBEDFE.js → chunk-3FAMU5TX.js} +31 -74
  37. package/dist/chunk-3FAMU5TX.js.map +1 -0
  38. package/dist/chunk-3JJWNZTT.js +446 -0
  39. package/dist/chunk-3JJWNZTT.js.map +1 -0
  40. package/dist/{chunk-JYVD2XZ4.js → chunk-4HIAWLA2.js} +58 -25
  41. package/dist/chunk-4HIAWLA2.js.map +1 -0
  42. package/dist/{chunk-A62RAIBN.js → chunk-6W2D6FGG.js} +2 -2
  43. package/dist/{chunk-COEZR6F5.js → chunk-7TAQEPLE.js} +2 -2
  44. package/dist/{chunk-TIVZ4MCG.js → chunk-D75JXBV4.js} +3 -3
  45. package/dist/{chunk-YY2HGKWR.js → chunk-DEDQXIDL.js} +2 -2
  46. package/dist/{chunk-XFG3PVZE.js → chunk-FUCJAZ25.js} +8 -8
  47. package/dist/{chunk-EZWQZYLK.js → chunk-HF4N43Q7.js} +2 -2
  48. package/dist/{chunk-HZ5DRA4Q.js → chunk-HXHKLVAS.js} +20 -20
  49. package/dist/{chunk-AC5LO7IU.js → chunk-I4WBNM3M.js} +7 -1
  50. package/dist/chunk-I4WBNM3M.js.map +1 -0
  51. package/dist/{chunk-QVTSLRKT.js → chunk-IJEZMWKA.js} +3 -3
  52. package/dist/{chunk-ZXUOAFUG.js → chunk-IKNQAGBV.js} +1 -1
  53. package/dist/chunk-IKNQAGBV.js.map +1 -0
  54. package/dist/{chunk-RC3CNIPK.js → chunk-IO5NQEGZ.js} +2 -2
  55. package/dist/{chunk-2ULWWAQH.js → chunk-ISLJ5WIM.js} +2 -2
  56. package/dist/{chunk-2KJG6ZZK.js → chunk-IYOPIG3E.js} +2 -2
  57. package/dist/{chunk-M4DQWUKX.js → chunk-JKOKX3PS.js} +62 -163
  58. package/dist/chunk-JKOKX3PS.js.map +1 -0
  59. package/dist/{chunk-SJQ4HY3E.js → chunk-JO3E5VGS.js} +2 -2
  60. package/dist/{chunk-3LSSFCB4.js → chunk-K4DWSPMW.js} +11 -1
  61. package/dist/chunk-K4DWSPMW.js.map +1 -0
  62. package/dist/{chunk-IZ5E6ZTK.js → chunk-KF4TXW7Z.js} +6 -6
  63. package/dist/{chunk-CQ4PGFMC.js → chunk-KS7WQ4BZ.js} +4 -4
  64. package/dist/chunk-LTJAMRGI.js +286 -0
  65. package/dist/chunk-LTJAMRGI.js.map +1 -0
  66. package/dist/{chunk-PJIHCOGV.js → chunk-MNU5G4TK.js} +2 -2
  67. package/dist/{chunk-2K64VH66.js → chunk-ODTWHSY2.js} +69 -45
  68. package/dist/chunk-ODTWHSY2.js.map +1 -0
  69. package/dist/{chunk-LIWU4QI6.js → chunk-OLOYQZFB.js} +5 -5
  70. package/dist/{chunk-MRFVMSTR.js → chunk-OMNTQPZO.js} +20 -1
  71. package/dist/{chunk-MRFVMSTR.js.map → chunk-OMNTQPZO.js.map} +1 -1
  72. package/dist/{chunk-JI6HWBYL.js → chunk-Q4O3ET6F.js} +2 -2
  73. package/dist/{chunk-N55RJT4N.js → chunk-QP37KL5H.js} +2 -2
  74. package/dist/{chunk-LPIWEKZ3.js → chunk-SDPDU2PM.js} +3 -3
  75. package/dist/{chunk-RIC5U67B.js → chunk-SFMRLXIV.js} +232 -4
  76. package/dist/chunk-SFMRLXIV.js.map +1 -0
  77. package/dist/{chunk-RTN2BLZM.js → chunk-T5QAZIBO.js} +2 -2
  78. package/dist/{chunk-ANZLT74L.js → chunk-TFVVONWD.js} +2 -2
  79. package/dist/{chunk-SIDSEXUG.js → chunk-WFEZUGU5.js} +2 -2
  80. package/dist/{chunk-FSEQXHEZ.js → chunk-XTIRCSIH.js} +2 -2
  81. package/dist/{chunk-XL5RSHZP.js → chunk-YO4MBK3I.js} +2 -2
  82. package/dist/{chunk-AH2JUU6X.js → chunk-Z2YFLXHU.js} +2 -2
  83. package/dist/{chunk-MRX6S22R.js → chunk-ZT7B64BE.js} +2 -2
  84. package/dist/{chunk-UDDSC6PO.js → chunk-ZYNMX6IU.js} +54 -6
  85. package/dist/chunk-ZYNMX6IU.js.map +1 -0
  86. package/dist/{cli-DUMkkdLl.d.ts → cli-D3XeenwN.d.ts} +3 -3
  87. package/dist/cli.d.ts +6 -6
  88. package/dist/cli.js +32 -28
  89. package/dist/compounding/engine.d.ts +1 -1
  90. package/dist/compounding/engine.js +7 -3
  91. package/dist/compounding/preference-consolidator.d.ts +1 -1
  92. package/dist/compression-optimizer.d.ts +1 -1
  93. package/dist/config.d.ts +1 -1
  94. package/dist/config.js +1 -1
  95. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  96. package/dist/connectors/codex-materialize-runner.js +7 -3
  97. package/dist/connectors/codex-materialize.d.ts +1 -1
  98. package/dist/connectors/index.d.ts +1 -1
  99. package/dist/connectors/index.js +7 -3
  100. package/dist/consolidation-provenance-check.d.ts +1 -1
  101. package/dist/consolidation-undo.d.ts +1 -1
  102. package/dist/contradiction/index.d.ts +1 -1
  103. package/dist/conversation-index/backend.d.ts +1 -1
  104. package/dist/conversation-index/chunker.d.ts +1 -1
  105. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  106. package/dist/conversation-index/indexer.d.ts +1 -1
  107. package/dist/conversation-index/search.d.ts +1 -1
  108. package/dist/day-summary.d.ts +1 -1
  109. package/dist/delinearize.d.ts +1 -1
  110. package/dist/direct-answer-wiring.d.ts +1 -1
  111. package/dist/direct-answer.d.ts +1 -1
  112. package/dist/embedding-fallback.d.ts +1 -1
  113. package/dist/enrichment/index.d.ts +1 -1
  114. package/dist/entity-retrieval.d.ts +1 -1
  115. package/dist/entity-retrieval.js +7 -3
  116. package/dist/entity-schema.d.ts +1 -1
  117. package/dist/explicit-capture.d.ts +6 -6
  118. package/dist/extraction-faithfulness.d.ts +1 -1
  119. package/dist/extraction-judge-telemetry.d.ts +1 -1
  120. package/dist/extraction-judge-training.d.ts +1 -1
  121. package/dist/extraction-judge.d.ts +1 -1
  122. package/dist/extraction.d.ts +1 -1
  123. package/dist/fallback-llm.d.ts +1 -1
  124. package/dist/{forget-PLR6J5DN.js → forget-6SOIPUMQ.js} +32 -1
  125. package/dist/forget-6SOIPUMQ.js.map +1 -0
  126. package/dist/identity-continuity.d.ts +1 -1
  127. package/dist/importance.d.ts +1 -1
  128. package/dist/index.d.ts +21 -10
  129. package/dist/index.js +65 -43
  130. package/dist/index.js.map +1 -1
  131. package/dist/intent.d.ts +1 -1
  132. package/dist/lcm/engine.d.ts +1 -1
  133. package/dist/lcm/index.d.ts +1 -1
  134. package/dist/lcm/tools.d.ts +1 -1
  135. package/dist/lifecycle.d.ts +1 -1
  136. package/dist/live-connectors-runner.d.ts +1 -1
  137. package/dist/local-llm.d.ts +1 -1
  138. package/dist/maintenance/memory-governance.d.ts +1 -1
  139. package/dist/maintenance/memory-governance.js +7 -3
  140. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -3
  141. package/dist/maintenance/rebuild-memory-projection.js +8 -4
  142. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  143. package/dist/memory-action-policy.d.ts +1 -1
  144. package/dist/memory-cache.d.ts +1 -1
  145. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  146. package/dist/memory-projection-store.d.ts +1 -1
  147. package/dist/memory-provenance.d.ts +1 -1
  148. package/dist/memory-provenance.js +1 -1
  149. package/dist/memory-worth-outcomes.d.ts +1 -1
  150. package/dist/models-json.d.ts +1 -1
  151. package/dist/namespaces/migrate.d.ts +2 -2
  152. package/dist/namespaces/migrate.js +8 -4
  153. package/dist/namespaces/principal.d.ts +1 -1
  154. package/dist/namespaces/search.d.ts +1 -1
  155. package/dist/namespaces/storage.d.ts +32 -3
  156. package/dist/namespaces/storage.js +7 -3
  157. package/dist/native-knowledge.d.ts +1 -1
  158. package/dist/operator-toolkit.d.ts +19 -2
  159. package/dist/operator-toolkit.js +17 -11
  160. package/dist/orchestration/maintenance.d.ts +2 -2
  161. package/dist/orchestration/maintenance.js +9 -5
  162. package/dist/{orchestrator-Dv8JQu3-.d.ts → orchestrator-BzMCZlKn.d.ts} +3 -3
  163. package/dist/orchestrator.d.ts +4 -4
  164. package/dist/orchestrator.js +22 -20
  165. package/dist/patterns-cli.d.ts +1 -1
  166. package/dist/policy-runtime.d.ts +1 -1
  167. package/dist/provenance.d.ts +1 -1
  168. package/dist/qmd-recall-cache.d.ts +1 -1
  169. package/dist/qmd.d.ts +1 -1
  170. package/dist/recall-disclosure-escalation.d.ts +1 -1
  171. package/dist/recall-explain-renderer.d.ts +1 -1
  172. package/dist/recall-explain-renderer.js +4 -4
  173. package/dist/recall-planner-llm.d.ts +1 -1
  174. package/dist/recall-state.d.ts +1 -1
  175. package/dist/recall-tag-filter.d.ts +1 -1
  176. package/dist/recall-xray-cli.d.ts +1 -1
  177. package/dist/recall-xray-cli.js +5 -5
  178. package/dist/recall-xray-renderer.d.ts +1 -1
  179. package/dist/recall-xray-renderer.js +4 -4
  180. package/dist/recall-xray.d.ts +1 -1
  181. package/dist/recall-xray.js +3 -3
  182. package/dist/resolve-auth-token.d.ts +1 -1
  183. package/dist/resume-bundles.js +2 -2
  184. package/dist/retrieval-agents.d.ts +1 -1
  185. package/dist/retrieval-tiers.d.ts +1 -1
  186. package/dist/routing/engine.d.ts +1 -1
  187. package/dist/routing/store.d.ts +1 -1
  188. package/dist/schemas.d.ts +28 -28
  189. package/dist/search/embed-helper.d.ts +1 -1
  190. package/dist/search/factory.d.ts +1 -1
  191. package/dist/search/index.d.ts +1 -1
  192. package/dist/search/lancedb-backend.d.ts +1 -1
  193. package/dist/search/meilisearch-backend.d.ts +1 -1
  194. package/dist/search/noop-backend.d.ts +1 -1
  195. package/dist/search/orama-backend.d.ts +1 -1
  196. package/dist/search/port.d.ts +1 -1
  197. package/dist/search/remote-backend.d.ts +1 -1
  198. package/dist/{semantic-consolidation-CgREi9E0.d.ts → semantic-consolidation-BtUfv-AL.d.ts} +1 -1
  199. package/dist/semantic-consolidation.d.ts +2 -2
  200. package/dist/semantic-consolidation.js +8 -4
  201. package/dist/semantic-rule-promotion.js +7 -3
  202. package/dist/semantic-rule-verifier.d.ts +1 -1
  203. package/dist/semantic-rule-verifier.js +7 -3
  204. package/dist/session-observer-bands.d.ts +1 -1
  205. package/dist/session-observer-state.d.ts +1 -1
  206. package/dist/shared-context/manager.d.ts +1 -1
  207. package/dist/signal.d.ts +1 -1
  208. package/dist/storage.d.ts +306 -2
  209. package/dist/storage.js +6 -2
  210. package/dist/summarizer.d.ts +1 -1
  211. package/dist/summarizer.js +3 -2
  212. package/dist/summary-snapshot.d.ts +1 -1
  213. package/dist/summary-snapshot.js +2 -1
  214. package/dist/temporal-supersession.d.ts +1 -1
  215. package/dist/temporal-supersession.js +1 -1
  216. package/dist/temporal-validity.d.ts +1 -1
  217. package/dist/threading.d.ts +1 -1
  218. package/dist/tier-migration.d.ts +1 -1
  219. package/dist/tier-routing.d.ts +1 -1
  220. package/dist/topics.d.ts +1 -1
  221. package/dist/transcript.d.ts +1 -1
  222. package/dist/transfer/types.d.ts +12 -12
  223. package/dist/{types-CeZYSC-E.d.ts → types-PuiPZ9iE.d.ts} +34 -1
  224. package/dist/types.d.ts +1 -1
  225. package/dist/types.js +1 -1
  226. package/dist/utility-runtime.d.ts +1 -1
  227. package/dist/utils/serialize-mutations.js +5 -280
  228. package/dist/utils/serialize-mutations.js.map +1 -1
  229. package/dist/verified-recall.js +7 -3
  230. package/package.json +2 -2
  231. package/src/config.ts +10 -0
  232. package/src/lifecycle/tombstones.test.ts +355 -0
  233. package/src/lifecycle/tombstones.ts +807 -0
  234. package/src/maintenance/forget.ts +41 -0
  235. package/src/maintenance/pattern-reinforcement.ts +47 -0
  236. package/src/memory-provenance.ts +12 -0
  237. package/src/namespaces/catalog.test.ts +222 -14
  238. package/src/namespaces/catalog.ts +54 -187
  239. package/src/namespaces/storage.ts +120 -80
  240. package/src/operator-toolkit.ts +77 -0
  241. package/src/orchestrator.ts +16 -0
  242. package/src/review/index.ts +48 -3
  243. package/src/storage.ts +329 -2
  244. package/src/summary-snapshot.test.ts +63 -1
  245. package/src/summary-snapshot.ts +61 -80
  246. package/src/temporal-supersession.ts +38 -0
  247. package/src/types.ts +34 -0
  248. package/src/utils/serialize-mutations.ts +10 -6
  249. package/dist/chunk-2K64VH66.js.map +0 -1
  250. package/dist/chunk-3LSSFCB4.js.map +0 -1
  251. package/dist/chunk-AC5LO7IU.js.map +0 -1
  252. package/dist/chunk-JYVD2XZ4.js.map +0 -1
  253. package/dist/chunk-M4DQWUKX.js.map +0 -1
  254. package/dist/chunk-RIC5U67B.js.map +0 -1
  255. package/dist/chunk-UDDSC6PO.js.map +0 -1
  256. package/dist/chunk-XJNBEDFE.js.map +0 -1
  257. package/dist/chunk-ZXUOAFUG.js.map +0 -1
  258. package/dist/forget-PLR6J5DN.js.map +0 -1
  259. /package/dist/{chunk-E6GOVHHJ.js.map → chunk-27LQPUMZ.js.map} +0 -0
  260. /package/dist/{chunk-A62RAIBN.js.map → chunk-6W2D6FGG.js.map} +0 -0
  261. /package/dist/{chunk-COEZR6F5.js.map → chunk-7TAQEPLE.js.map} +0 -0
  262. /package/dist/{chunk-TIVZ4MCG.js.map → chunk-D75JXBV4.js.map} +0 -0
  263. /package/dist/{chunk-YY2HGKWR.js.map → chunk-DEDQXIDL.js.map} +0 -0
  264. /package/dist/{chunk-XFG3PVZE.js.map → chunk-FUCJAZ25.js.map} +0 -0
  265. /package/dist/{chunk-EZWQZYLK.js.map → chunk-HF4N43Q7.js.map} +0 -0
  266. /package/dist/{chunk-HZ5DRA4Q.js.map → chunk-HXHKLVAS.js.map} +0 -0
  267. /package/dist/{chunk-QVTSLRKT.js.map → chunk-IJEZMWKA.js.map} +0 -0
  268. /package/dist/{chunk-RC3CNIPK.js.map → chunk-IO5NQEGZ.js.map} +0 -0
  269. /package/dist/{chunk-2ULWWAQH.js.map → chunk-ISLJ5WIM.js.map} +0 -0
  270. /package/dist/{chunk-2KJG6ZZK.js.map → chunk-IYOPIG3E.js.map} +0 -0
  271. /package/dist/{chunk-SJQ4HY3E.js.map → chunk-JO3E5VGS.js.map} +0 -0
  272. /package/dist/{chunk-IZ5E6ZTK.js.map → chunk-KF4TXW7Z.js.map} +0 -0
  273. /package/dist/{chunk-CQ4PGFMC.js.map → chunk-KS7WQ4BZ.js.map} +0 -0
  274. /package/dist/{chunk-PJIHCOGV.js.map → chunk-MNU5G4TK.js.map} +0 -0
  275. /package/dist/{chunk-LIWU4QI6.js.map → chunk-OLOYQZFB.js.map} +0 -0
  276. /package/dist/{chunk-JI6HWBYL.js.map → chunk-Q4O3ET6F.js.map} +0 -0
  277. /package/dist/{chunk-N55RJT4N.js.map → chunk-QP37KL5H.js.map} +0 -0
  278. /package/dist/{chunk-LPIWEKZ3.js.map → chunk-SDPDU2PM.js.map} +0 -0
  279. /package/dist/{chunk-RTN2BLZM.js.map → chunk-T5QAZIBO.js.map} +0 -0
  280. /package/dist/{chunk-ANZLT74L.js.map → chunk-TFVVONWD.js.map} +0 -0
  281. /package/dist/{chunk-SIDSEXUG.js.map → chunk-WFEZUGU5.js.map} +0 -0
  282. /package/dist/{chunk-FSEQXHEZ.js.map → chunk-XTIRCSIH.js.map} +0 -0
  283. /package/dist/{chunk-XL5RSHZP.js.map → chunk-YO4MBK3I.js.map} +0 -0
  284. /package/dist/{chunk-AH2JUU6X.js.map → chunk-Z2YFLXHU.js.map} +0 -0
  285. /package/dist/{chunk-MRX6S22R.js.map → chunk-ZT7B64BE.js.map} +0 -0
@@ -22,6 +22,8 @@
22
22
  */
23
23
 
24
24
  import type { StorageManager } from "../storage.js";
25
+ import { buildRetiredFactTombstoneInputs } from "../lifecycle/tombstones.js";
26
+ import { supersessionKeysForFact } from "../temporal-supersession.js";
25
27
  import type { MemoryFile } from "../types.js";
26
28
 
27
29
  export interface ForgetMemoryRequest {
@@ -100,6 +102,45 @@ export async function forgetMemory(
100
102
  actor: "remnic-forget",
101
103
  reasonCode: "operator_forget",
102
104
  });
105
+ // Issue #1579 thread OchiF: emit a LIVE tombstone at forget time so a
106
+ // forgotten FACT cannot resurrect via re-extraction/import before an
107
+ // operator runs `remnic doctor --rebuild-tombstones`. Without this, the
108
+ // non-resurrection chokepoint in writeMemory has nothing to match — the
109
+ // forgotten fact's content hash is gone from the active index but no
110
+ // tombstone exists until a manual rebuild derives one — so the same fact
111
+ // written again through writeMemory becomes active. Mirrors supersedeMemory
112
+ // (contradiction) and applyTemporalSupersession via the shared
113
+ // buildRetiredFactTombstoneInputs helper. Best-effort (rule 34): a tombstone
114
+ // append failure must not fail the forget, which already succeeded on disk.
115
+ if (memory.frontmatter.category === "fact") {
116
+ for (const input of buildRetiredFactTombstoneInputs(
117
+ {
118
+ id,
119
+ content: memory.content,
120
+ ...(memory.frontmatter.contentHash
121
+ ? { contentHash: memory.frontmatter.contentHash }
122
+ : {}),
123
+ ...(memory.frontmatter.entityRef
124
+ ? { entityRef: memory.frontmatter.entityRef }
125
+ : {}),
126
+ ...(memory.frontmatter.structuredAttributes
127
+ ? { structuredAttributes: memory.frontmatter.structuredAttributes }
128
+ : {}),
129
+ },
130
+ {
131
+ reason: "retraction",
132
+ createdBy: "user_correction",
133
+ createdAt: forgottenAt,
134
+ supersessionKeysForFact,
135
+ },
136
+ )) {
137
+ try {
138
+ await storage.appendTombstone(input);
139
+ } catch {
140
+ // Best-effort — the forget already succeeded on disk.
141
+ }
142
+ }
143
+ }
103
144
  return {
104
145
  id,
105
146
  path: memory.path,
@@ -33,6 +33,8 @@
33
33
  */
34
34
 
35
35
  import { clusterByKey } from "../procedural/reinforcement-core.js";
36
+ import { buildRetiredFactTombstoneInputs, type TombstoneReason, type TombstoneCreatedBy } from "../lifecycle/tombstones.js";
37
+ import { supersessionKeysForFact } from "../temporal-supersession.js";
36
38
  import type {
37
39
  MemoryFile,
38
40
  MemoryFrontmatter,
@@ -50,6 +52,21 @@ export interface PatternReinforcementStorage {
50
52
  memory: MemoryFile,
51
53
  patch: Partial<MemoryFrontmatter>,
52
54
  ): Promise<boolean>;
55
+ /**
56
+ * Best-effort tombstone append for retired duplicates (issue #1579 thread
57
+ * Oblq9). Optional so unit-test stubs need not implement it; the production
58
+ * StorageManager satisfies it via its appendTombstone chokepoint.
59
+ */
60
+ appendTombstone?(input: {
61
+ reason: TombstoneReason;
62
+ createdBy: TombstoneCreatedBy;
63
+ sourceMemoryId: string;
64
+ contentHash?: string;
65
+ rawContent: string;
66
+ entityRef?: string;
67
+ supersessionKey?: string;
68
+ createdAt: string;
69
+ }): Promise<string | null>;
53
70
  }
54
71
 
55
72
  export interface PatternReinforcementOptions {
@@ -345,6 +362,36 @@ export async function runPatternReinforcement(
345
362
  await storage.writeMemoryFrontmatter(dup, patch);
346
363
  supersededIds.push(dup.frontmatter.id);
347
364
  result.duplicatesSuperseded += 1;
365
+ // Issue #1579 thread Oblq9 / Ocn0C / OcoPs: emit a tombstone so the
366
+ // retired duplicate cannot resurrect through re-extraction / import.
367
+ // Pattern reinforcement retires via writeMemoryFrontmatter which
368
+ // bypasses the writeMemory chokepoint; without this emit, the same
369
+ // content re-extracted before a manual rebuild would be stored active.
370
+ // Threads Ocn0C/OcoPs: emit one tombstone PER derived supersession key
371
+ // via buildRetiredFactTombstoneInputs (same helper as supersedeMemory +
372
+ // forgetMemory), not a single entityRef-only record — a paraphrased
373
+ // re-extraction with the same entity/attribute can only be caught by
374
+ // the keyed tier. Best-effort (the interface method is optional; a
375
+ // failure must not fail the reinforcement job).
376
+ if (storage.appendTombstone) {
377
+ for (const input of buildRetiredFactTombstoneInputs(
378
+ {
379
+ id: dup.frontmatter.id,
380
+ content: dup.content,
381
+ contentHash: dup.frontmatter.contentHash,
382
+ entityRef: dup.frontmatter.entityRef,
383
+ structuredAttributes: dup.frontmatter.structuredAttributes,
384
+ },
385
+ { reason: "supersession", createdBy: "supersession", createdAt: nowIso, supersessionKeysForFact },
386
+ )) {
387
+ try {
388
+ await storage.appendTombstone(input);
389
+ } catch {
390
+ // Best-effort: a tombstone append failure must not fail the
391
+ // reinforcement job (gotcha #13 / rule 34 spirit).
392
+ }
393
+ }
394
+ }
348
395
  }
349
396
 
350
397
  result.clusters.push({
@@ -328,6 +328,18 @@ function inferSafetyReasons(input: {
328
328
  if (frontmatter.verificationState === "disputed") {
329
329
  reasons.push("verification=disputed");
330
330
  }
331
+ // Issue #1579 — x-ray visibility (rule 34: never a silent drop). When a
332
+ // memory carries `blockedBy`, it was tombstone-blocked at the storage
333
+ // chokepoint. Surface the tombstone id (and the matched tier when known)
334
+ // in the safety reasons so every recall x-ray consumer (CLI / HTTP / MCP)
335
+ // can attribute the pending_review state to a tombstone rather than a
336
+ // generic low-confidence review.
337
+ if (typeof frontmatter.blockedBy === "string" && frontmatter.blockedBy.length > 0) {
338
+ reasons.push(`tombstone-blocked=${frontmatter.blockedBy}`);
339
+ if (frontmatter.tombstoneBlockTier) {
340
+ reasons.push(`tombstone-tier=${frontmatter.tombstoneBlockTier}`);
341
+ }
342
+ }
331
343
  if (stale) reasons.push("stale=true");
332
344
 
333
345
  const boundaryScopes = userContextScopes.filter(isUserBoundaryScope);
@@ -11,6 +11,7 @@ import {
11
11
  resolveDefaultNamespaceRoot,
12
12
  resolveNamespaceStorageRoot,
13
13
  } from "./storage.js";
14
+ import { withHeldFileLock } from "../utils/serialize-mutations.js";
14
15
 
15
16
  function makeConfig(memoryDir: string, overrides: Partial<PluginConfig> = {}): PluginConfig {
16
17
  return {
@@ -2171,8 +2172,16 @@ test("registerConfiguredNamespaces skips an unsafe configured name without abort
2171
2172
 
2172
2173
  // ── Round 7 (codex P2 — NBsGP): two catalog instances in the SAME process
2173
2174
  // sharing a memoryDir must not treat each other's rebuild lock as self-held. A
2174
- // touch on instance B must DROP its append while instance A holds the lock,
2175
- // instead of skipping the wait (same PID) and appending into A's window.
2175
+ // touch on instance B must DROP its append while a foreign lock is held,
2176
+ // instead of skipping the wait (same PID) and appending into the holder's
2177
+ // window.
2178
+ //
2179
+ // Issue #1524 note: the catalog now delegates to the shared withHeldFileLock
2180
+ // utility, which generates a per-CALL owner uuid (stronger than the previous
2181
+ // per-instance lockOwnerId). ANY foreign lock — including one written by
2182
+ // another call on the SAME instance — is therefore not self-held. The test
2183
+ // seeds a foreign lock with an arbitrary uuid + a heartbeat that keeps it
2184
+ // fresh, then asserts a touch on a fresh instance waits then DROPS.
2176
2185
  test("a same-process second instance does not treat another instance's lock as self-held", async () => {
2177
2186
  const memoryDir = await mkMemoryDir();
2178
2187
  try {
@@ -2183,10 +2192,12 @@ test("a same-process second instance does not treat another instance's lock as s
2183
2192
  await mkdir(stateDir, { recursive: true });
2184
2193
  const lockPath = path.join(stateDir, "namespaces.rebuild.lock");
2185
2194
 
2186
- // Instance A writes a lock with ITS OWN owner id (a UUID) same PID as B.
2187
- const instanceA = new NamespaceCatalog(makeConfig(memoryDir));
2188
- const aOwnerId = (instanceA as unknown as { lockOwnerId: string }).lockOwnerId;
2189
- await writeFile(lockPath, `${process.pid} ${aOwnerId} ${new Date().toISOString()}\n`, "utf8");
2195
+ // A foreign held lock: a different PID + a real UUID owner id (so it is
2196
+ // NEVER mistaken for self by any catalog instance in any process) + a fresh
2197
+ // mtime. A heartbeat keeps it fresh so it is never broken as stale during
2198
+ // the touch's bounded wait.
2199
+ const foreignOwner = "00000000-0000-4000-8000-000000000000";
2200
+ await writeFile(lockPath, `999999 ${foreignOwner} ${new Date().toISOString()}\n`, "utf8");
2190
2201
  const hb = setInterval(() => {
2191
2202
  const now = new Date();
2192
2203
  utimes(lockPath, now, now).catch(() => undefined);
@@ -2194,17 +2205,17 @@ test("a same-process second instance does not treat another instance's lock as s
2194
2205
  hb.unref?.();
2195
2206
 
2196
2207
  try {
2197
- // Instance B (different owner id, same PID) must NOT consider A's lock
2198
- // self-held; its touch waits then DROPS on timeout (no append).
2199
- const instanceB = new NamespaceCatalog(makeConfig(memoryDir));
2208
+ // The catalog instance must NOT consider the foreign lock self-held; its
2209
+ // touch waits then DROPS on timeout (no append).
2210
+ const instance = new NamespaceCatalog(makeConfig(memoryDir));
2200
2211
  const started = Date.now();
2201
- await instanceB.markWrite(ns, { discoveredBy: "write", storageDir: tokenDir });
2212
+ await instance.markWrite(ns, { discoveredBy: "write", storageDir: tokenDir });
2202
2213
  const waited = Date.now() - started;
2203
- assert.ok(waited >= 4_000, "instance B must wait on instance A's lock, not skip it as self");
2214
+ assert.ok(waited >= 4_000, "the catalog must wait on the foreign lock, not skip it as self");
2204
2215
  assert.equal(
2205
- await instanceB.getNamespaceRecord(ns),
2216
+ await instance.getNamespaceRecord(ns),
2206
2217
  null,
2207
- "instance B's touch must DROP while instance A's lock is held (no overwrite race)",
2218
+ "the touch must DROP while the foreign lock is held (no overwrite race)",
2208
2219
  );
2209
2220
  } finally {
2210
2221
  clearInterval(hb);
@@ -2609,6 +2620,98 @@ test("a dropped resolve registration (hook returns false) is retried on a later
2609
2620
  }
2610
2621
  });
2611
2622
 
2623
+ // ── In-flight dedup under a DROPPED hook (cursor Medium 06f58a7c, codex P2).
2624
+ // The serializer strictly orders queued tasks, but ordering alone does not
2625
+ // collapse a burst when the hook returns `false` (dropped registration, e.g. a
2626
+ // rebuild-lock timeout): `notifiedResolved` stays unset, so every queued sibling
2627
+ // task passes its re-check and re-invokes the hook — N serial lock waits. The
2628
+ // `inFlightResolveHooks` marker (set synchronously before queueing) collapses the
2629
+ // burst to a single hook invocation; the drop stays retryable on a LATER
2630
+ // storageFor(). This test PROVE-FAILS without the marker (calls === N) and PASSES
2631
+ // with it (calls === 1).
2632
+ test("a burst of concurrent storageFor() with a DROPPED hook fires it ONCE (not once per queued task)", async () => {
2633
+ const memoryDir = await mkMemoryDir();
2634
+ try {
2635
+ let calls = 0;
2636
+ // Gated hook that DROPS (returns false) once released. This is the real
2637
+ // scenario the reviewers flagged: the catalog's onResolve waits on the
2638
+ // rebuild lock (slow), times out, and returns false. While that hook is
2639
+ // in-flight, a burst of cache hits must collapse to the one in-flight
2640
+ // registration instead of each queueing its own serial lock wait.
2641
+ let release!: () => void;
2642
+ const gate = new Promise<void>((r) => {
2643
+ release = r;
2644
+ });
2645
+ const router = new NamespaceStorageRouter(makeConfig(memoryDir), {
2646
+ onResolve: async () => {
2647
+ calls += 1;
2648
+ await gate;
2649
+ return false; // dropped (rebuild-lock timeout analogue)
2650
+ },
2651
+ });
2652
+
2653
+ // A burst of concurrent cache hits while the hook is IN-FLIGHT. Without the
2654
+ // in-flight dedup marker each enqueued task would re-run the dropped hook
2655
+ // once the first settles (N serial lock waits); with it they collapse.
2656
+ const N = 8;
2657
+ await Promise.all(Array.from({ length: N }, () => router.storageFor("project-origin-burst-drop")));
2658
+ // Let the in-flight hook settle as DROPPED.
2659
+ release();
2660
+ await router.whenResolveHooksSettled();
2661
+ assert.equal(calls, 1, "a dropped hook fires ONCE under a burst, not once per queued task");
2662
+
2663
+ // The drop must remain retryable: a later storageFor() re-fires the hook.
2664
+ await router.storageFor("project-origin-burst-drop");
2665
+ await router.whenResolveHooksSettled();
2666
+ assert.equal(calls, 2, "a dropped registration is retried on the next storageFor()");
2667
+ } finally {
2668
+ await rm(memoryDir, { recursive: true, force: true });
2669
+ }
2670
+ });
2671
+
2672
+ // ── Composite-key in-flight dedup (cursor Medium, codex P2): the in-flight
2673
+ // marker must be keyed by (namespace, storageDir), NOT namespace alone. A
2674
+ // CHANGED storageDir (migration/realignment) for the same namespace while
2675
+ // another dir's hook is pending must still get its OWN hook invocation — it is
2676
+ // not collapsed onto the old dir's pending registration. A namespace-only key
2677
+ // would silently drop the new-dir notification.
2678
+ test("a CHANGED storageDir for the same namespace is not collapsed onto a pending hook", async () => {
2679
+ const memoryDir = await mkMemoryDir();
2680
+ try {
2681
+ const seen: string[] = [];
2682
+ let release!: () => void;
2683
+ const gate = new Promise<void>((r) => {
2684
+ release = r;
2685
+ });
2686
+ const router = new NamespaceStorageRouter(makeConfig(memoryDir), {
2687
+ onResolve: async (_ns, dir) => {
2688
+ seen.push(dir);
2689
+ await gate;
2690
+ },
2691
+ });
2692
+ // notifyResolved is private; reach it via the same cast pattern other tests
2693
+ // use for router internals, so we can drive two distinct dirs directly.
2694
+ const internals = router as unknown as {
2695
+ notifyResolved(namespace: string, storageDir: string): void;
2696
+ };
2697
+ const dirA = path.join(memoryDir, "dir-a");
2698
+ const dirB = path.join(memoryDir, "dir-b");
2699
+ // dirA's hook is IN-FLIGHT (gated). dirB is a DIFFERENT dir for the same
2700
+ // namespace — it must NOT be collapsed onto dirA's pending registration.
2701
+ internals.notifyResolved("project-origin-dir-change", dirA);
2702
+ internals.notifyResolved("project-origin-dir-change", dirB);
2703
+ release();
2704
+ await router.whenResolveHooksSettled();
2705
+ assert.deepEqual(
2706
+ seen.sort(),
2707
+ [dirA, dirB].sort(),
2708
+ "both distinct dirs fire their own hook; the new dir is not collapsed onto the pending one",
2709
+ );
2710
+ } finally {
2711
+ await rm(memoryDir, { recursive: true, force: true });
2712
+ }
2713
+ });
2714
+
2612
2715
  // ── Round 7 (codex P2 — NDxiS): a configured non-default namespace must be seeded
2613
2716
  // with the ROUTER-resolved root, not a blanket tokenized dir. When a legacy raw
2614
2717
  // root (`namespaces/<rawname>`) already exists, the router serves it, so the
@@ -2777,8 +2880,35 @@ class SeamCatalog extends NamespaceCatalog {
2777
2880
  (this as unknown as { onBeforeBreakStaleUnlinkForTest?: () => Promise<void> }).onBeforeBreakStaleUnlinkForTest =
2778
2881
  fn;
2779
2882
  }
2883
+ /**
2884
+ * Drive the catalog's break-stale path through the shared util (issue #1524
2885
+ * adoption). The catalog no longer owns a private breakStaleRebuildLock; the
2886
+ * util's breakStaleLock fires inside its acquire loop, which is what
2887
+ * withHeldCatalogLock now invokes. We trigger that path with a SHORT maxWaitMs
2888
+ * so a surviving replacement lock is observed quickly (the production
2889
+ * REBUILD_LOCK_MAX_WAIT_MS would force a 5s wait on the NG7Bg-replacement
2890
+ * case). The seam is forwarded exactly as production does.
2891
+ */
2780
2892
  async callBreakStaleRebuildLock(): Promise<void> {
2781
- await (this as unknown as { breakStaleRebuildLock: () => Promise<void> }).breakStaleRebuildLock();
2893
+ const seam = (this as unknown as { onBeforeBreakStaleUnlinkForTest?: () => Promise<void> })
2894
+ .onBeforeBreakStaleUnlinkForTest;
2895
+ // Match the catalog's lock config (stale/heartbeat/poll); only maxWaitMs
2896
+ // is shortened for test speed. The break-stale invariant does not depend
2897
+ // on maxWaitMs — the seam fires inside breakStaleLock regardless.
2898
+ await withHeldFileLock(
2899
+ (this as unknown as { rebuildLockPath: string }).rebuildLockPath,
2900
+ {
2901
+ staleMs: 30_000,
2902
+ maxWaitMs: 200,
2903
+ pollMs: 10,
2904
+ heartbeatMs: 10_000,
2905
+ onBeforeBreakStaleUnlinkForTest: seam,
2906
+ },
2907
+ async () => {
2908
+ // No-op: we only need the acquire loop to invoke breakStaleLock so the
2909
+ // seam fires and the replacement/stale-lock invariant is exercised.
2910
+ },
2911
+ );
2782
2912
  }
2783
2913
  }
2784
2914
 
@@ -3354,3 +3484,81 @@ test("listNamespaces prefers configured token owners over stale literal token al
3354
3484
  await rm(memoryDir, { recursive: true, force: true });
3355
3485
  }
3356
3486
  });
3487
+
3488
+ // ── Issue #1524 adoption prove-fail: catalog mutations route through the
3489
+ // shared MutationSerializer (instance-scoped `criticalSection`). The defect
3490
+ // class is a naive bare-.then(fn) chain that silently drops subsequent sections
3491
+ // after a rejection — exactly the poison-chain bug the shared util prevents.
3492
+ // We force the FIRST serialized section to reject, then assert the SECOND
3493
+ // section STILL runs (its record lands in the catalog). Pre-fix (a poison
3494
+ // chain) the second section would be skipped.
3495
+ test("catalog queueCritical recovers after a prior section rejects (issue #1524 poison-chain prove-fail)", async () => {
3496
+ const memoryDir = await mkMemoryDir();
3497
+ try {
3498
+ const catalog = new NamespaceCatalog(makeConfig(memoryDir));
3499
+ // Reach the private serializer to inject a failing section ahead of a real
3500
+ // one. Both target the SAME key ("catalog") so they share a chain.
3501
+ const serializer = (catalog as unknown as {
3502
+ criticalSection: {
3503
+ serialize<T>(key: string, task: () => Promise<T>): Promise<T>;
3504
+ };
3505
+ }).criticalSection;
3506
+
3507
+ let secondRan = false;
3508
+ const [, second] = await Promise.allSettled([
3509
+ serializer.serialize("catalog", async () => {
3510
+ throw new Error("intentional first-section failure");
3511
+ }),
3512
+ serializer.serialize("catalog", async () => {
3513
+ secondRan = true;
3514
+ }),
3515
+ ]);
3516
+ assert.equal(second.status, "fulfilled", "second section settled (ran or skipped?)");
3517
+ assert.equal(secondRan, true, "second section MUST run after the first rejected (chain recovered)");
3518
+ // And a real catalog op still works through the same chain after the failure.
3519
+ await catalog.markWrite("project-origin-poison-recovery", { discoveredBy: "write" });
3520
+ const record = await catalog.getNamespaceRecord("project-origin-poison-recovery");
3521
+ assert.ok(record, "the catalog is fully usable after a rejected section (chain not poisoned)");
3522
+ } finally {
3523
+ await rm(memoryDir, { recursive: true, force: true });
3524
+ }
3525
+ });
3526
+
3527
+ // ── Issue #1524 adoption prove-fail: NamespaceStorageRouter resolve-hooks
3528
+ // route through the shared MutationSerializer (`resolveSerializer`). Same
3529
+ // defect class — a poison chain would skip the second hook after the first
3530
+ // rejected. We fire two notifications for the SAME namespace; the first hook
3531
+ // rejects, the second MUST still run.
3532
+ test("router resolve-hook serializer recovers after a prior hook rejects (issue #1524 poison-chain prove-fail)", async () => {
3533
+ const memoryDir = await mkMemoryDir();
3534
+ try {
3535
+ let secondCalls = 0;
3536
+ let firstCalls = 0;
3537
+ let rejectNext = true;
3538
+ const router = new NamespaceStorageRouter(makeConfig(memoryDir), {
3539
+ onResolve: async () => {
3540
+ if (rejectNext) {
3541
+ firstCalls += 1;
3542
+ rejectNext = false;
3543
+ throw new Error("intentional first-hook failure");
3544
+ }
3545
+ secondCalls += 1;
3546
+ },
3547
+ });
3548
+ // Two storageFor calls for the same namespace. The first triggers the hook
3549
+ // (which rejects); the second queues behind it through the serializer.
3550
+ // Both calls themselves must resolve (the rejection is best-effort inside
3551
+ // the hook wrapper, never surfaced to the storage caller).
3552
+ await router.storageFor("project-origin-router-poison");
3553
+ await router.whenResolveHooksSettled();
3554
+ await router.storageFor("project-origin-router-poison");
3555
+ await router.whenResolveHooksSettled();
3556
+ assert.ok(firstCalls >= 1, "the first (rejecting) hook fired");
3557
+ assert.ok(
3558
+ secondCalls >= 1,
3559
+ "the second hook MUST fire after the first rejected (serializer recovered, not poisoned)",
3560
+ );
3561
+ } finally {
3562
+ await rm(memoryDir, { recursive: true, force: true });
3563
+ }
3564
+ });