@remnic/core 10.0.66 → 10.0.68

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 (240) hide show
  1. package/dist/access-admin-ops-surface.js +16 -16
  2. package/dist/access-authorization-probe.js +17 -17
  3. package/dist/access-boundary.js +16 -16
  4. package/dist/access-cli.js +60 -60
  5. package/dist/access-extraction-force-flush.js +16 -16
  6. package/dist/access-http-briefing.js +17 -17
  7. package/dist/access-http-query.js +18 -18
  8. package/dist/access-http.js +33 -33
  9. package/dist/access-identity-continuity-surface.js +16 -16
  10. package/dist/access-lcm-surface.js +16 -16
  11. package/dist/access-mcp.js +26 -26
  12. package/dist/access-namespace-preflight.js +16 -16
  13. package/dist/access-observe-write-surface.js +16 -16
  14. package/dist/access-operations-batch.js +17 -17
  15. package/dist/access-operations.js +24 -24
  16. package/dist/access-recall-concurrency.js +16 -16
  17. package/dist/access-recall-diagnostics.js +2 -2
  18. package/dist/access-recall-response.js +16 -16
  19. package/dist/access-recall-surface.js +16 -16
  20. package/dist/access-service.js +16 -16
  21. package/dist/access-surface-catalog.js +18 -18
  22. package/dist/active-recall.js +3 -3
  23. package/dist/briefing.js +3 -3
  24. package/dist/buffer.js +2 -2
  25. package/dist/causal-consolidation.js +4 -4
  26. package/dist/{chunk-KISOMGY2.js → chunk-3RHU5MD7.js} +2 -2
  27. package/dist/{chunk-3H25IRJJ.js → chunk-4MPAKYZU.js} +2 -2
  28. package/dist/{chunk-IPCSZLLF.js → chunk-4NV7O76Q.js} +2 -2
  29. package/dist/{chunk-BH7NVSEC.js → chunk-4SOWMA55.js} +3 -3
  30. package/dist/{chunk-GROHOWR5.js → chunk-5MGKGEL7.js} +2 -2
  31. package/dist/{chunk-ZPLQ27MY.js → chunk-5UM4H64E.js} +2 -2
  32. package/dist/{chunk-2DNFRLLL.js → chunk-5Y6VA6X2.js} +2 -2
  33. package/dist/{chunk-RAU25Q6J.js → chunk-6WKWL53V.js} +13 -13
  34. package/dist/{chunk-KFPJIKUW.js → chunk-75DCOS6I.js} +23 -23
  35. package/dist/{chunk-25QK3LR2.js → chunk-7O2MC42R.js} +2 -2
  36. package/dist/{chunk-7ETWIBTO.js → chunk-ADYP7HXE.js} +3 -3
  37. package/dist/{chunk-FUDKGJDN.js → chunk-AVGDYYMA.js} +2 -2
  38. package/dist/{chunk-ECF627KC.js → chunk-AYJQ644F.js} +2 -2
  39. package/dist/{chunk-T763ADEK.js → chunk-B3MWELZ3.js} +2 -2
  40. package/dist/{chunk-DRTSPRWL.js → chunk-B4DXREWC.js} +2 -2
  41. package/dist/{chunk-5UEGL5BF.js → chunk-CI3MAAIY.js} +2 -2
  42. package/dist/{chunk-JYXFIILI.js → chunk-CTH2CQCB.js} +2 -2
  43. package/dist/{chunk-UVHXLVFJ.js → chunk-EURPSF4Y.js} +2 -2
  44. package/dist/{chunk-VPEA63FC.js → chunk-EWJZZDRG.js} +2 -2
  45. package/dist/{chunk-BOZQAUAJ.js → chunk-EXQJMFRF.js} +2 -2
  46. package/dist/{chunk-XMBDI725.js → chunk-FAF4A76Q.js} +684 -529
  47. package/dist/chunk-FAF4A76Q.js.map +1 -0
  48. package/dist/{chunk-VC65GNUS.js → chunk-GGD2YDS6.js} +2 -2
  49. package/dist/{chunk-5JABRT5G.js → chunk-GHHQLSSY.js} +3 -3
  50. package/dist/{chunk-FMYAU5MP.js → chunk-GJ4HU6LK.js} +2 -2
  51. package/dist/{chunk-3NJJQAL3.js → chunk-H5DIRCZP.js} +12 -12
  52. package/dist/{chunk-434E4BTO.js → chunk-HH73M65H.js} +3 -3
  53. package/dist/{chunk-LIGZO7XN.js → chunk-HPB5TF55.js} +2 -2
  54. package/dist/{chunk-E4QIX2OB.js → chunk-IAGVM7EY.js} +2 -2
  55. package/dist/{chunk-VC7GKR2V.js → chunk-IG3EZ3TF.js} +2 -2
  56. package/dist/{chunk-QAN5ZDRS.js → chunk-IVUVQPTQ.js} +2 -2
  57. package/dist/{chunk-XOFIJGB2.js → chunk-IZBIDPMY.js} +2 -2
  58. package/dist/{chunk-HFEGRMFG.js → chunk-JLGBJ4AX.js} +7 -7
  59. package/dist/{chunk-BBHKN2CN.js → chunk-JLMOH5JA.js} +2 -2
  60. package/dist/{chunk-TPCRDPUY.js → chunk-KZSGBWOJ.js} +2 -2
  61. package/dist/{chunk-SHHZWSSE.js → chunk-KZZ5EZ7O.js} +2 -2
  62. package/dist/{chunk-A5FOXI3B.js → chunk-LN4X2NDA.js} +2 -2
  63. package/dist/{chunk-5ZHFPUBC.js → chunk-MOIJWWNA.js} +8 -8
  64. package/dist/{chunk-KO224R36.js → chunk-NCZGGGWI.js} +2 -2
  65. package/dist/{chunk-XVQVF2TK.js → chunk-NHWDTOBN.js} +269 -230
  66. package/dist/chunk-NHWDTOBN.js.map +1 -0
  67. package/dist/{chunk-7ZTGMEXF.js → chunk-O5ZINWW3.js} +2 -2
  68. package/dist/{chunk-WWH7GJ42.js → chunk-OCK4D5PC.js} +2 -2
  69. package/dist/{chunk-G6L3REL3.js → chunk-OPFVXM2D.js} +2 -2
  70. package/dist/{chunk-FR7PK7ED.js → chunk-P4TV3TQ2.js} +2 -2
  71. package/dist/{chunk-ZGHPELZ6.js → chunk-PMDOGXA2.js} +2 -2
  72. package/dist/{chunk-NM2II57L.js → chunk-PRKNAIGZ.js} +2 -1
  73. package/dist/chunk-PRKNAIGZ.js.map +1 -0
  74. package/dist/{chunk-F32JRMX2.js → chunk-QGFR73SN.js} +2 -2
  75. package/dist/{chunk-QNZPYH36.js → chunk-R2BIVU2Z.js} +352 -290
  76. package/dist/chunk-R2BIVU2Z.js.map +1 -0
  77. package/dist/{chunk-HG3CG557.js → chunk-RLVI2M5Q.js} +2 -2
  78. package/dist/{chunk-SH6ZYUTX.js → chunk-SXTVZ6JL.js} +2 -2
  79. package/dist/{chunk-CLSKGTAT.js → chunk-TECK5IVY.js} +2 -2
  80. package/dist/{chunk-SNS3DWG6.js → chunk-TVE7W67N.js} +2 -2
  81. package/dist/{chunk-KYSSUY66.js → chunk-U2CAGMUD.js} +2 -2
  82. package/dist/{chunk-XLCEFUAU.js → chunk-UVVFPCPX.js} +2 -2
  83. package/dist/{chunk-PL75KQGF.js → chunk-UW6LBFYN.js} +5 -5
  84. package/dist/{chunk-SEA56G6S.js → chunk-V4XE263I.js} +1 -1
  85. package/dist/chunk-V4XE263I.js.map +1 -0
  86. package/dist/{chunk-VSZNPAGL.js → chunk-V6SBA5O7.js} +3 -3
  87. package/dist/{chunk-B7VD36TI.js → chunk-WHROVQ5Q.js} +3 -3
  88. package/dist/{chunk-LDHR465K.js → chunk-Y3XPSGNF.js} +2 -2
  89. package/dist/{chunk-E6VQJ75T.js → chunk-ZORS5BG5.js} +4 -4
  90. package/dist/{chunk-KXY4HW2Y.js → chunk-ZXF3O7P4.js} +2 -2
  91. package/dist/cli.js +46 -46
  92. package/dist/coding/export-okf-codegraph.js +2 -2
  93. package/dist/compounding/engine.js +4 -4
  94. package/dist/config.d.ts.map +1 -1
  95. package/dist/config.js +3 -3
  96. package/dist/connectors/codex-materialize-runner.js +3 -3
  97. package/dist/connectors/index.js +3 -3
  98. package/dist/convergence-refresh.js +5 -5
  99. package/dist/corpus-watermark.js +5 -5
  100. package/dist/entity-retrieval.js +3 -3
  101. package/dist/external-wiki-access.js +17 -17
  102. package/dist/extraction.js +6 -6
  103. package/dist/harmonic-retrieval.js +3 -3
  104. package/dist/index.js +60 -60
  105. package/dist/location/index.js +5 -5
  106. package/dist/maintenance/memory-governance.js +3 -3
  107. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  108. package/dist/maintenance/rebuild-memory-projection-runner.js +5 -5
  109. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  110. package/dist/memory-browse-operations.js +17 -17
  111. package/dist/namespaces/migrate.js +4 -4
  112. package/dist/namespaces/storage.js +3 -3
  113. package/dist/operator-doctor-corpus.js +6 -6
  114. package/dist/operator-toolkit.js +12 -12
  115. package/dist/orchestration/buffer-drain-runner.js +60 -60
  116. package/dist/orchestration/consolidation-run.d.ts.map +1 -1
  117. package/dist/orchestration/extraction-drain.d.ts +4 -4
  118. package/dist/orchestration/extraction-drain.d.ts.map +1 -1
  119. package/dist/orchestration/extraction-persist.d.ts.map +1 -1
  120. package/dist/orchestration/extraction-queue-coordinator.d.ts +67 -33
  121. package/dist/orchestration/extraction-queue-coordinator.d.ts.map +1 -1
  122. package/dist/orchestration/extraction-run.d.ts +1 -8
  123. package/dist/orchestration/extraction-run.d.ts.map +1 -1
  124. package/dist/orchestration/fact-dedup-window.d.ts +49 -0
  125. package/dist/orchestration/fact-dedup-window.d.ts.map +1 -0
  126. package/dist/orchestration/maintenance.js +13 -13
  127. package/dist/orchestration/provider-breaker.d.ts +58 -0
  128. package/dist/orchestration/provider-breaker.d.ts.map +1 -0
  129. package/dist/orchestrator.js +60 -60
  130. package/dist/recall-explain-renderer.js +3 -3
  131. package/dist/recall-navigate-access.js +2 -2
  132. package/dist/recall-navigate-disclosure.js +2 -2
  133. package/dist/recall-navigate.js +2 -2
  134. package/dist/recall-navigation-operations.js +17 -17
  135. package/dist/recall-navigation.js +16 -16
  136. package/dist/recall-xray-cli.js +4 -4
  137. package/dist/recall-xray-renderer.js +3 -3
  138. package/dist/recall-xray.js +2 -2
  139. package/dist/resume-bundles.js +4 -4
  140. package/dist/semantic-consolidation.js +4 -4
  141. package/dist/semantic-rule-promotion.js +3 -3
  142. package/dist/semantic-rule-verifier.js +3 -3
  143. package/dist/source-agent-qualifier.js +5 -5
  144. package/dist/standup/build.js +3 -3
  145. package/dist/storage/profile-mutation.d.ts +25 -0
  146. package/dist/storage/profile-mutation.d.ts.map +1 -0
  147. package/dist/storage.d.ts +4 -6
  148. package/dist/storage.d.ts.map +1 -1
  149. package/dist/storage.js +2 -2
  150. package/dist/support-passport/index.js +19 -19
  151. package/dist/transfer/export-okf.js +3 -3
  152. package/dist/types.d.ts +2 -0
  153. package/dist/types.d.ts.map +1 -1
  154. package/dist/types.js +1 -1
  155. package/dist/verified-recall.js +3 -3
  156. package/dist/who-knows-cli.js +4 -4
  157. package/dist/who-knows.js +3 -3
  158. package/package.json +2 -2
  159. package/src/buffer-drain-handover.test.ts +3 -3
  160. package/src/buffer-drop-extraction-chunk.test.ts +0 -1
  161. package/src/config.test.ts +12 -0
  162. package/src/config.ts +2 -0
  163. package/src/extraction-parallel-dedup.test.ts +185 -0
  164. package/src/fact-dedup-window.test.ts +116 -0
  165. package/src/orchestration/consolidation-profile-race.test.ts +178 -0
  166. package/src/orchestration/consolidation-run.ts +19 -7
  167. package/src/orchestration/extraction-drain.ts +10 -3
  168. package/src/orchestration/extraction-persist.ts +156 -161
  169. package/src/orchestration/extraction-queue-coordinator.ts +167 -56
  170. package/src/orchestration/extraction-run.ts +50 -93
  171. package/src/orchestration/fact-dedup-window.ts +122 -0
  172. package/src/orchestration/provider-breaker.test.ts +111 -0
  173. package/src/orchestration/provider-breaker.ts +158 -0
  174. package/src/orchestration/turn-ingestion.ts +5 -5
  175. package/src/orchestrator-extraction-queue.test.ts +179 -8
  176. package/src/orchestrator.ts +1 -1
  177. package/src/storage/profile-mutation.ts +119 -0
  178. package/src/storage-append-profile.test.ts +114 -0
  179. package/src/storage.ts +17 -48
  180. package/src/types.ts +2 -0
  181. package/dist/chunk-NM2II57L.js.map +0 -1
  182. package/dist/chunk-QNZPYH36.js.map +0 -1
  183. package/dist/chunk-SEA56G6S.js.map +0 -1
  184. package/dist/chunk-XMBDI725.js.map +0 -1
  185. package/dist/chunk-XVQVF2TK.js.map +0 -1
  186. /package/dist/{chunk-KISOMGY2.js.map → chunk-3RHU5MD7.js.map} +0 -0
  187. /package/dist/{chunk-3H25IRJJ.js.map → chunk-4MPAKYZU.js.map} +0 -0
  188. /package/dist/{chunk-IPCSZLLF.js.map → chunk-4NV7O76Q.js.map} +0 -0
  189. /package/dist/{chunk-BH7NVSEC.js.map → chunk-4SOWMA55.js.map} +0 -0
  190. /package/dist/{chunk-GROHOWR5.js.map → chunk-5MGKGEL7.js.map} +0 -0
  191. /package/dist/{chunk-ZPLQ27MY.js.map → chunk-5UM4H64E.js.map} +0 -0
  192. /package/dist/{chunk-2DNFRLLL.js.map → chunk-5Y6VA6X2.js.map} +0 -0
  193. /package/dist/{chunk-RAU25Q6J.js.map → chunk-6WKWL53V.js.map} +0 -0
  194. /package/dist/{chunk-KFPJIKUW.js.map → chunk-75DCOS6I.js.map} +0 -0
  195. /package/dist/{chunk-25QK3LR2.js.map → chunk-7O2MC42R.js.map} +0 -0
  196. /package/dist/{chunk-7ETWIBTO.js.map → chunk-ADYP7HXE.js.map} +0 -0
  197. /package/dist/{chunk-FUDKGJDN.js.map → chunk-AVGDYYMA.js.map} +0 -0
  198. /package/dist/{chunk-ECF627KC.js.map → chunk-AYJQ644F.js.map} +0 -0
  199. /package/dist/{chunk-T763ADEK.js.map → chunk-B3MWELZ3.js.map} +0 -0
  200. /package/dist/{chunk-DRTSPRWL.js.map → chunk-B4DXREWC.js.map} +0 -0
  201. /package/dist/{chunk-5UEGL5BF.js.map → chunk-CI3MAAIY.js.map} +0 -0
  202. /package/dist/{chunk-JYXFIILI.js.map → chunk-CTH2CQCB.js.map} +0 -0
  203. /package/dist/{chunk-UVHXLVFJ.js.map → chunk-EURPSF4Y.js.map} +0 -0
  204. /package/dist/{chunk-VPEA63FC.js.map → chunk-EWJZZDRG.js.map} +0 -0
  205. /package/dist/{chunk-BOZQAUAJ.js.map → chunk-EXQJMFRF.js.map} +0 -0
  206. /package/dist/{chunk-VC65GNUS.js.map → chunk-GGD2YDS6.js.map} +0 -0
  207. /package/dist/{chunk-5JABRT5G.js.map → chunk-GHHQLSSY.js.map} +0 -0
  208. /package/dist/{chunk-FMYAU5MP.js.map → chunk-GJ4HU6LK.js.map} +0 -0
  209. /package/dist/{chunk-3NJJQAL3.js.map → chunk-H5DIRCZP.js.map} +0 -0
  210. /package/dist/{chunk-434E4BTO.js.map → chunk-HH73M65H.js.map} +0 -0
  211. /package/dist/{chunk-LIGZO7XN.js.map → chunk-HPB5TF55.js.map} +0 -0
  212. /package/dist/{chunk-E4QIX2OB.js.map → chunk-IAGVM7EY.js.map} +0 -0
  213. /package/dist/{chunk-VC7GKR2V.js.map → chunk-IG3EZ3TF.js.map} +0 -0
  214. /package/dist/{chunk-QAN5ZDRS.js.map → chunk-IVUVQPTQ.js.map} +0 -0
  215. /package/dist/{chunk-XOFIJGB2.js.map → chunk-IZBIDPMY.js.map} +0 -0
  216. /package/dist/{chunk-HFEGRMFG.js.map → chunk-JLGBJ4AX.js.map} +0 -0
  217. /package/dist/{chunk-BBHKN2CN.js.map → chunk-JLMOH5JA.js.map} +0 -0
  218. /package/dist/{chunk-TPCRDPUY.js.map → chunk-KZSGBWOJ.js.map} +0 -0
  219. /package/dist/{chunk-SHHZWSSE.js.map → chunk-KZZ5EZ7O.js.map} +0 -0
  220. /package/dist/{chunk-A5FOXI3B.js.map → chunk-LN4X2NDA.js.map} +0 -0
  221. /package/dist/{chunk-5ZHFPUBC.js.map → chunk-MOIJWWNA.js.map} +0 -0
  222. /package/dist/{chunk-KO224R36.js.map → chunk-NCZGGGWI.js.map} +0 -0
  223. /package/dist/{chunk-7ZTGMEXF.js.map → chunk-O5ZINWW3.js.map} +0 -0
  224. /package/dist/{chunk-WWH7GJ42.js.map → chunk-OCK4D5PC.js.map} +0 -0
  225. /package/dist/{chunk-G6L3REL3.js.map → chunk-OPFVXM2D.js.map} +0 -0
  226. /package/dist/{chunk-FR7PK7ED.js.map → chunk-P4TV3TQ2.js.map} +0 -0
  227. /package/dist/{chunk-ZGHPELZ6.js.map → chunk-PMDOGXA2.js.map} +0 -0
  228. /package/dist/{chunk-F32JRMX2.js.map → chunk-QGFR73SN.js.map} +0 -0
  229. /package/dist/{chunk-HG3CG557.js.map → chunk-RLVI2M5Q.js.map} +0 -0
  230. /package/dist/{chunk-SH6ZYUTX.js.map → chunk-SXTVZ6JL.js.map} +0 -0
  231. /package/dist/{chunk-CLSKGTAT.js.map → chunk-TECK5IVY.js.map} +0 -0
  232. /package/dist/{chunk-SNS3DWG6.js.map → chunk-TVE7W67N.js.map} +0 -0
  233. /package/dist/{chunk-KYSSUY66.js.map → chunk-U2CAGMUD.js.map} +0 -0
  234. /package/dist/{chunk-XLCEFUAU.js.map → chunk-UVVFPCPX.js.map} +0 -0
  235. /package/dist/{chunk-PL75KQGF.js.map → chunk-UW6LBFYN.js.map} +0 -0
  236. /package/dist/{chunk-VSZNPAGL.js.map → chunk-V6SBA5O7.js.map} +0 -0
  237. /package/dist/{chunk-B7VD36TI.js.map → chunk-WHROVQ5Q.js.map} +0 -0
  238. /package/dist/{chunk-LDHR465K.js.map → chunk-Y3XPSGNF.js.map} +0 -0
  239. /package/dist/{chunk-E6VQJ75T.js.map → chunk-ZORS5BG5.js.map} +0 -0
  240. /package/dist/{chunk-KXY4HW2Y.js.map → chunk-ZXF3O7P4.js.map} +0 -0
@@ -1,4 +1,5 @@
1
1
  import { classifyExtractionOrigin, type ExtractionSourceContext } from "./extraction-origin-context.js";
2
+ import { confirmActiveFactDuplicate, persistUnderFactDedupWindow } from "./fact-dedup-window.js";
2
3
  import { evaluateInjectionScreen, screenEntityForIndex, screenPersistStrings } from "./extraction-injection-gate.js";
3
4
  /**
4
5
  * Extraction persistence coordinator (issue #1526, seam 16).
@@ -1663,148 +1664,123 @@ export class ExtractionPersistCoordinator {
1663
1664
  fact.tags,
1664
1665
  );
1665
1666
  let exactDuplicate = false;
1666
- let needsCorpusConfirm = false;
1667
1667
  try {
1668
1668
  exactDuplicate = await this.deps.hasContentHashDedup(targetStorage, contentHashDedupKey);
1669
- if (factDedupEnabled && !exactDuplicate) {
1670
- // Fact dedup disabled → skip the authority check AND (via the guarded
1671
- // corpus block below) corpus confirmation, so a disabled deployment
1672
- // never suppresses a write via dedup (PR #2016 thread SD-nH). Enabled:
1673
- // a MISS is trustworthy only against an AUTHORITATIVE index — re-check.
1674
- if (await targetStorage.isFactContentHashAuthoritative()) {
1675
- exactDuplicate = await this.deps.hasContentHashDedup(targetStorage, contentHashDedupKey);
1676
- } else {
1677
- needsCorpusConfirm = true;
1678
- }
1679
- }
1680
1669
  } catch (err) {
1681
- // Dedup lookup failed (e.g. the authoritative rebuild could not run). Do
1682
- // NOT fail-open into a possible duplicate — confirm against the corpus.
1683
- needsCorpusConfirm = true;
1670
+ // Dedup lookup failed (e.g. the authoritative rebuild could not run).
1671
+ // When dedup is enabled the confirm below still scans the corpus; when
1672
+ // it is disabled, never suppress a write via dedup (PR #2016 SD-nH).
1673
+ exactDuplicate = false;
1684
1674
  log.warn(
1685
1675
  `content-hash dedup lookup unavailable for storage ${targetStorage.dir}; confirming against corpus: ${err}`,
1686
1676
  );
1687
1677
  }
1688
- // Connector-aware dedup (QOjlB): if the hash says duplicate — or the index
1689
- // could not authoritatively confirm a miss — verify a same-content,
1690
- // same-connector active fact exists across the hot+cold tiers. Different
1691
- // connectors with the same content are NOT duplicates. Fail open (write) on
1692
- // scan failure so an unverifiable state cannot silently drop content.
1693
- if (factDedupEnabled && (exactDuplicate || needsCorpusConfirm)) {
1694
- try {
1695
- // #2016 cold-tier finding: the authoritative content-hash rebuild
1696
- // unions the HOT and COLD tiers, so a hash hit can name an active copy
1697
- // that was demoted to cold/. A hot-only confirmation scan misses it,
1698
- // flips exactDuplicate back to false, and writes a second active hot
1699
- // copy of the same content. Scan both tiers so a cold-only active copy
1700
- // still confirms the duplicate and suppresses the redundant hot write.
1701
- const [hotMems, coldMems] = await Promise.all([
1702
- targetStorage.readAllMemories(),
1703
- targetStorage.readAllColdMemories(),
1704
- ]);
1705
- const nc = sourceContext?.sourceConnector?.trim() || undefined;
1706
- const matchesActiveDuplicate = (m: MemoryFile): boolean => {
1707
- if (m.frontmatter.category !== writeCategory) return false;
1708
- if ((m.frontmatter.status ?? "active") !== "active") return false;
1709
- // Thread 5 (QPDE5): for procedures the hash is keyed on the full
1710
- // body (title + steps via buildProcedurePersistBody), so compare
1711
- // against contentHashDedupKey — not canonicalContentForHash which
1712
- // is the title only. Thread 3 (QO42V): facts with
1713
- // structuredAttributes get an appended [Attributes: ...] suffix in
1714
- // the stored body that the hash key lacks; strip it before
1715
- // comparing so same-connector enriched facts dedup correctly.
1716
- if (normalizeStoredHashSource(stripAttributesSuffix(m.content ?? "")) !==
1717
- ContentHashIndex.normalizeContent(contentHashDedupKey)) return false;
1718
- return (m.frontmatter.sourceConnector?.trim() || undefined) === nc;
1719
- };
1720
- exactDuplicate =
1721
- hotMems.some(matchesActiveDuplicate) || coldMems.some(matchesActiveDuplicate);
1722
- } catch (err) {
1723
- log.warn(
1724
- `connector-aware dedup scan failed for storage ${targetStorage.dir}; writing fail-open: ${err instanceof Error ? err.message : String(err)}`,
1725
- );
1726
- exactDuplicate = false;
1727
- }
1678
+ // Shared confirm options for the persist-time check and the write-window
1679
+ // race recheck below.
1680
+ const raceDedupOpts = (key: string) => ({
1681
+ category: writeCategory,
1682
+ contentHashDedupKey: key,
1683
+ connector: sourceContext?.sourceConnector?.trim() || undefined,
1684
+ citationEnabled,
1685
+ citationTemplate,
1686
+ });
1687
+ // Fact dedup disabled → skip the authority check AND the corpus
1688
+ // confirmation, so a disabled deployment never suppresses a write via
1689
+ // dedup (PR #2016 thread SD-nH).
1690
+ if (factDedupEnabled) {
1691
+ exactDuplicate = await confirmActiveFactDuplicate(
1692
+ { hasContentHashDedup: (storage, key) => this.deps.hasContentHashDedup(storage, key) },
1693
+ targetStorage,
1694
+ raceDedupOpts(contentHashDedupKey),
1695
+ );
1728
1696
  }
1729
- if (exactDuplicate && injectionScreenStatus !== "pending_review") {
1730
- // #1671 — before short-circuiting, backfill bi-temporal bounds
1731
- // onto the existing source-namespace copy if it lacks bounds the
1732
- // incoming fact now carries (re-extraction with a resolved invalidAt).
1733
- // Skip when the fact would be rejected/deferred/pending by downstream
1734
- // gates — a non-durable candidate must not expire an active fact
1735
- // (chatgpt-codex P1: faithfulness, requireSpans, extraction judge).
1736
- // #1671 + #1707: backfill bi-temporal bounds onto the existing
1737
- // source-namespace copy if it lacks bounds the incoming fact now
1738
- // carries (re-extraction with a resolved bound). #1707 thread 3:
1739
- // gate on writeCategory === "fact" — the helper only matches facts,
1740
- // so a non-fact duplicate must not reach the fact-only scan.
1741
- // #1707 thread 2: also fire when only a corrected start bound
1742
- // (validFrom) is present, not just an end bound (validUntil). The
1743
- // downstream-gate skip still applies — a non-durable candidate must
1744
- // not expire an active fact (chatgpt-codex P1).
1697
+ // PR #2016/#3231: finalize committed writes before a fail-closed
1698
+ // window throw propagates (PRRT_kwDORJXyws6zxG6H).
1699
+ const finalizeOnWindowFailure = async (err: Error): Promise<never> => {
1700
+ await finalizeCommittedWrites(true).catch((finalizeErr) =>
1701
+ log.warn(`committed-write finalization after fact-dedup window failure failed: ${finalizeErr}`),
1702
+ );
1703
+ throw err;
1704
+ };
1705
+ const raceRecheck = (storage: StorageManager, key: string) => () => {
1706
+ // Gates mirror the persist-time suppression gates (PR #2016 SD-nH; #1645 quarantine).
1707
+ if (!factDedupEnabled || injectionScreenStatus === "pending_review") return Promise.resolve(false);
1708
+ return confirmActiveFactDuplicate(
1709
+ { hasContentHashDedup: (s, k) => this.deps.hasContentHashDedup(s, k) },
1710
+ storage,
1711
+ raceDedupOpts(key),
1712
+ );
1713
+ };
1714
+ // #1671/#1707 — backfill bi-temporal bounds onto the surviving copy
1715
+ // when a duplicate short-circuits: persist-time hit OR raced duplicate
1716
+ // suppressed in the window (codex P2, PRRT_kwDORJXyws6zxlor). Facts
1717
+ // only (#1707 t3); a corrected start bound counts (t2). Skipped when a
1718
+ // downstream gate would reject the candidate — a non-durable candidate
1719
+ // must not expire an active fact (faithfulness, requireSpans, judge,
1720
+ // #372 importance).
1721
+ const backfillDuplicateTemporalBounds = async () => {
1722
+ if (!(biTemporal && writeCategory === "fact" && (biTemporal.validUntil || biTemporal.validFrom))) return;
1723
+ const fr = faithfulnessResultsByFactIndex?.get(factLoopIndex);
1724
+ const faithfulnessWouldPending =
1725
+ faithfulnessMode === "enforce" &&
1726
+ fr?.ok === true &&
1727
+ (fr.verdict === "unsupported" || fr.verdict === "contradicted");
1728
+ const requireSpansWouldPending =
1729
+ this.deps.config.provenance?.requireSpans === true &&
1730
+ fact.requireSpansPending === true;
1731
+ const judgeVerdict = judgeVerdictsByFactIndex?.get(factLoopIndex);
1732
+ const judgeWouldGate =
1733
+ !this.deps.config.extractionJudgeShadow &&
1734
+ judgeVerdict !== undefined &&
1735
+ !judgeVerdict.durable;
1736
+ // Importance gate (cursor PRRT_OvKnS): below-threshold duplicates
1737
+ // would never persist (#372) and carry no judge verdict (the
1738
+ // pre-filter skips them), so they must not expire an active fact.
1745
1739
  if (
1746
- biTemporal &&
1747
- writeCategory === "fact" &&
1748
- (biTemporal.validUntil || biTemporal.validFrom)
1740
+ !isAboveImportanceThreshold(importance.level, this.deps.config.extractionMinImportanceLevel) ||
1741
+ faithfulnessWouldPending ||
1742
+ requireSpansWouldPending ||
1743
+ judgeWouldGate
1749
1744
  ) {
1750
- const fr = faithfulnessResultsByFactIndex?.get(factLoopIndex);
1751
- const faithfulnessWouldPending =
1752
- faithfulnessMode === "enforce" &&
1753
- fr?.ok === true &&
1754
- (fr.verdict === "unsupported" || fr.verdict === "contradicted");
1755
- const requireSpansWouldPending =
1756
- this.deps.config.provenance?.requireSpans === true &&
1757
- fact.requireSpansPending === true;
1758
- const judgeVerdict = judgeVerdictsByFactIndex?.get(factLoopIndex);
1759
- const judgeWouldGate =
1760
- !this.deps.config.extractionJudgeShadow &&
1761
- judgeVerdict !== undefined &&
1762
- !judgeVerdict.durable;
1763
- // Importance gate (cursor PRRT_OvKnS): below-threshold duplicates
1764
- // would never persist (#372) and carry no judge verdict (the
1765
- // pre-filter skips them), so they must not expire an active fact.
1766
- if (
1767
- isAboveImportanceThreshold(importance.level, this.deps.config.extractionMinImportanceLevel) &&
1768
- !faithfulnessWouldPending &&
1769
- !requireSpansWouldPending &&
1770
- !judgeWouldGate
1771
- ) {
1772
- await this.deps.backfillTemporalBoundsOnDedupHit(
1773
- targetStorage,
1774
- contentHashDedupKey,
1775
- {
1776
- invalidAt: biTemporal.validUntil,
1777
- // #1707 thread 2 — carry the corrected start bound too.
1778
- validFrom: biTemporal.validFrom,
1779
- observedAt: biTemporal.observedAt,
1780
- eventTimeSource: biTemporal.eventTimeSource,
1781
- },
1782
- fact.entityRef,
1783
- sourceContext?.sourceConnector,
1784
- );
1785
- // #1707 thread 1 — the source branch short-circuits (`continue`
1786
- // below) before the promotion dedup paths run, so promoted
1787
- // shared/profile copies written before the source fact carried
1788
- // resolved bounds stay stale and cross-namespace recall surfaces
1789
- // an expired fact. Backfill the promotion targets too (fail-open,
1790
- // backfill-only — never writes a new promoted copy).
1791
- await backfillTemporalBoundsOnPromotionCopies({
1792
- sourceStorage: targetStorage,
1793
- content: fact.content,
1794
- category: writeCategory,
1795
- confidence: fact.confidence,
1796
- entityRef: fact.entityRef,
1797
- structuredAttributes: fact.structuredAttributes,
1798
- sourceConnector: sourceContext?.sourceConnector,
1799
- bounds: {
1800
- invalidAt: biTemporal.validUntil,
1801
- validFrom: biTemporal.validFrom,
1802
- observedAt: biTemporal.observedAt,
1803
- eventTimeSource: biTemporal.eventTimeSource,
1804
- },
1805
- });
1806
- }
1745
+ return;
1807
1746
  }
1747
+ await this.deps.backfillTemporalBoundsOnDedupHit(
1748
+ targetStorage,
1749
+ contentHashDedupKey,
1750
+ {
1751
+ invalidAt: biTemporal.validUntil,
1752
+ // #1707 thread 2 — carry the corrected start bound too.
1753
+ validFrom: biTemporal.validFrom,
1754
+ observedAt: biTemporal.observedAt,
1755
+ eventTimeSource: biTemporal.eventTimeSource,
1756
+ },
1757
+ fact.entityRef,
1758
+ sourceContext?.sourceConnector,
1759
+ );
1760
+ // #1707 thread 1 — a short-circuited source branch never reaches the
1761
+ // promotion dedup paths, so promoted shared/profile copies written
1762
+ // before the source fact carried resolved bounds stay stale and
1763
+ // cross-namespace recall surfaces an expired fact. Backfill the
1764
+ // promotion targets too (fail-open, backfill-only — never writes a
1765
+ // new promoted copy).
1766
+ await backfillTemporalBoundsOnPromotionCopies({
1767
+ sourceStorage: targetStorage,
1768
+ content: fact.content,
1769
+ category: writeCategory,
1770
+ confidence: fact.confidence,
1771
+ entityRef: fact.entityRef,
1772
+ structuredAttributes: fact.structuredAttributes,
1773
+ sourceConnector: sourceContext?.sourceConnector,
1774
+ bounds: {
1775
+ invalidAt: biTemporal.validUntil,
1776
+ validFrom: biTemporal.validFrom,
1777
+ observedAt: biTemporal.observedAt,
1778
+ eventTimeSource: biTemporal.eventTimeSource,
1779
+ },
1780
+ });
1781
+ };
1782
+ if (exactDuplicate && injectionScreenStatus !== "pending_review") {
1783
+ await backfillDuplicateTemporalBounds();
1808
1784
  log.debug(
1809
1785
  `dedup: skipping duplicate fact "${fact.content.slice(0, 60)}…" in storage ${targetStorage.dir}`,
1810
1786
  );
@@ -2198,7 +2174,11 @@ export class ExtractionPersistCoordinator {
2198
2174
  },
2199
2175
  { source: extractionWriteSource },
2200
2176
  );
2201
- const parentWrite = await targetStorage.writeSealedMemory(parentWriteEnvelope, {
2177
+ const parentWriteOutcome = await persistUnderFactDedupWindow(
2178
+ targetStorage.dir,
2179
+ raceRecheck(targetStorage, contentHashDedupKey),
2180
+ async () =>
2181
+ targetStorage.writeSealedMemory(parentWriteEnvelope, {
2202
2182
  importance,
2203
2183
  supersedes,
2204
2184
  links: links.length > 0 ? links : undefined,
@@ -2218,7 +2198,16 @@ export class ExtractionPersistCoordinator {
2218
2198
  // durable parent .md; flush the hash now so a throw can't strand it (dup).
2219
2199
  deferHashIndexSave: false,
2220
2200
  ...(factToolScoped ? { toolScoped: true as const } : {}),
2221
- });
2201
+ }),
2202
+ ).catch(finalizeOnWindowFailure);
2203
+ if (parentWriteOutcome.suppressed) {
2204
+ // Codex P2: backfill bounds onto the winning copy first.
2205
+ await backfillDuplicateTemporalBounds();
2206
+ log.info(`dedup: raced duplicate suppressed in fact-dedup window (chunked parent) for storage ${targetStorage.dir}`);
2207
+ dedupedCount++;
2208
+ continue;
2209
+ }
2210
+ const parentWrite = parentWriteOutcome.result;
2222
2211
  const parentId = parentWrite.id;
2223
2212
  // #1645: surface the tombstone block and gate active post-write paths
2224
2213
  const tombstoneBlocked = parentWrite.tombstoneBlocked;
@@ -2589,7 +2578,11 @@ export class ExtractionPersistCoordinator {
2589
2578
  },
2590
2579
  { source: extractionWriteSource },
2591
2580
  );
2592
- const factWrite = await targetStorage.writeSealedMemory(factWriteEnvelope, {
2581
+ const factWriteOutcome = await persistUnderFactDedupWindow(
2582
+ targetStorage.dir,
2583
+ raceRecheck(targetStorage, contentHashDedupKey),
2584
+ async () => {
2585
+ const factWrite = await targetStorage.writeSealedMemory(factWriteEnvelope, {
2593
2586
  importance,
2594
2587
  supersedes,
2595
2588
  links: links.length > 0 ? links : undefined,
@@ -2607,11 +2600,36 @@ export class ExtractionPersistCoordinator {
2607
2600
  // through to frontmatter so they survive end-to-end.
2608
2601
  ...(fact.sources && fact.sources.length > 0 ? { sources: fact.sources } : {}),
2609
2602
  ...(fact.provenance ? { provenance: fact.provenance } : {}),
2610
- // #1909: defer the per-fact index flush to the batch save only when
2611
- // fact dedup is on (the batch saver is a no-op otherwise).
2612
- deferHashIndexSave: factDedupEnabled,
2603
+ // PR #2016: never defer inside the commit window (chunked-path
2604
+ // rule): a peer loading an authoritative index after the release
2605
+ // must see this hash, or its recheck misses and duplicates it.
2606
+ deferHashIndexSave: false,
2613
2607
  ...(factToolScoped ? { toolScoped: true as const } : {}),
2614
- });
2608
+ });
2609
+ // Register the dedup hash inside the same commit window: writeMemory
2610
+ // indexes facts only (rule 44 #1579), so non-fact categories
2611
+ // previously registered after the window released, letting a
2612
+ // concurrent session's in-window recheck miss and both write the
2613
+ // same content (issue #3312). Fail-open, like the post-write
2614
+ // registration this replaces.
2615
+ if (!factWrite.tombstoneBlocked) {
2616
+ try {
2617
+ await this.deps.addContentHashDedup(targetStorage, contentHashDedupKey);
2618
+ } catch (err) {
2619
+ log.warn(`content-hash dedup registration failed for memory ${factWrite.id}: ${err}`);
2620
+ }
2621
+ }
2622
+ return factWrite;
2623
+ },
2624
+ ).catch(finalizeOnWindowFailure);
2625
+ if (factWriteOutcome.suppressed) {
2626
+ // Codex P2: backfill bounds onto the window's winning copy first.
2627
+ await backfillDuplicateTemporalBounds();
2628
+ log.info(`dedup: raced duplicate suppressed in fact-dedup window for storage ${targetStorage.dir}`);
2629
+ dedupedCount++;
2630
+ continue;
2631
+ }
2632
+ const factWrite = factWriteOutcome.result;
2615
2633
  const memoryId = factWrite.id;
2616
2634
  // #1645: surface the tombstone block; gate active post-write paths like #1576
2617
2635
  // so a blocked fact creates no active shared copy / supersession / graph entry.
@@ -2797,29 +2815,6 @@ export class ExtractionPersistCoordinator {
2797
2815
  ...(factToolScoped ? { toolScoped: true as const } : {}),
2798
2816
  });
2799
2817
  }
2800
- // Register in the target storage content-hash index after successful
2801
- // write. Thread 3 fix: canonicalize by stripping any pre-existing
2802
- // citation so the stored hash matches what the dedup check computes.
2803
- try {
2804
- const canonicalFactContent =
2805
- citationEnabled &&
2806
- hasCitationForTemplate(fact.content, citationTemplate)
2807
- ? stripCitationForTemplate(fact.content, citationTemplate)
2808
- : fact.content;
2809
- const hashRegisterKey =
2810
- writeCategory === "procedure"
2811
- ? buildProcedurePersistBody(fact.content, fact.procedureSteps)
2812
- : canonicalFactContent;
2813
- // #1645: do NOT register a tombstone-blocked fact's content in the dedup
2814
- // index (rule 44 defeat) — see chunked path comment.
2815
- if (!tombstoneBlocked) {
2816
- await this.deps.addContentHashDedup(targetStorage, hashRegisterKey);
2817
- }
2818
- } catch (err) {
2819
- log.warn(
2820
- `content-hash dedup registration failed for memory ${memoryId}: ${err}`,
2821
- );
2822
- }
2823
2818
  } catch (err) {
2824
2819
  // PR #2016 / #3231: finalize deferred side effects for facts already
2825
2820
  // durable before propagating so hash/behavior/harmonic/temporal/QMD