@remnic/core 9.7.15 → 9.8.0
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.
- package/dist/access-admin-ops-surface.d.ts +13 -6
- package/dist/access-admin-ops-surface.js +85 -71
- package/dist/access-audit.js +2 -2
- package/dist/access-boundary.d.ts +35 -17
- package/dist/access-boundary.js +86 -74
- package/dist/access-citation.d.ts +28 -0
- package/dist/access-citation.js +9 -0
- package/dist/access-cli.js +194 -178
- package/dist/access-cli.js.map +1 -1
- package/dist/access-errors.d.ts +16 -1
- package/dist/access-errors.js +7 -3
- package/dist/access-http.d.ts +25 -19
- package/dist/access-http.js +100 -87
- package/dist/access-identity-continuity-surface.d.ts +13 -8
- package/dist/access-identity-continuity-surface.js +85 -71
- package/dist/access-lcm-surface.d.ts +16 -9
- package/dist/access-lcm-surface.js +85 -71
- package/dist/access-mcp.d.ts +23 -16
- package/dist/access-mcp.js +96 -84
- package/dist/access-memory-search-fanout.d.ts +93 -0
- package/dist/access-memory-search-fanout.js +18 -0
- package/dist/access-observe-write-surface.d.ts +26 -8
- package/dist/access-observe-write-surface.js +85 -71
- package/dist/access-operations-batch.js +92 -80
- package/dist/access-operations.d.ts +29 -22
- package/dist/access-operations.js +94 -82
- package/dist/access-recall-concurrency.d.ts +250 -0
- package/dist/access-recall-concurrency.js +157 -0
- package/dist/access-recall-response.d.ts +130 -0
- package/dist/access-recall-response.js +147 -0
- package/dist/access-recall-surface.d.ts +15 -7
- package/dist/access-recall-surface.js +85 -71
- package/dist/access-schema.d.ts +98 -98
- package/dist/access-schema.js +8 -6
- package/dist/{access-service-DpcYZ9Nl.d.ts → access-service-DEL4qCrc.d.ts} +25 -32
- package/dist/access-service.d.ts +13 -6
- package/dist/access-service.js +89 -72
- package/dist/access-surface-catalog.d.ts +23 -16
- package/dist/access-surface-catalog.js +1 -0
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/access-token-capabilities.d.ts +2 -1
- package/dist/access-token-capabilities.js +4 -2
- package/dist/action-confidence.d.ts +3 -1
- package/dist/active-memory-bridge.d.ts +3 -1
- package/dist/active-memory-bridge.js +4 -4
- package/dist/active-recall.d.ts +4 -1
- package/dist/active-recall.js +23 -13
- package/dist/active-recall.js.map +1 -1
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/{auto-sync-AAMTVZMK.js → auto-sync-UZG3XQZ5.js} +14 -8
- package/dist/{auto-sync-AAMTVZMK.js.map → auto-sync-UZG3XQZ5.js.map} +1 -1
- package/dist/behavior-learner.d.ts +3 -1
- package/dist/behavior-signals.d.ts +3 -1
- package/dist/bootstrap.d.ts +12 -7
- package/dist/bootstrap.js +2 -2
- package/dist/bounded-jsonl-state.d.ts +54 -0
- package/dist/bounded-jsonl-state.js +10 -0
- package/dist/boxes.js +2 -2
- package/dist/briefing.d.ts +5 -2
- package/dist/briefing.js +34 -24
- package/dist/buffer-surprise-report.d.ts +3 -1
- package/dist/buffer.d.ts +60 -2
- package/dist/buffer.js +3 -3
- package/dist/bulk-import/index.d.ts +5 -3
- package/dist/calibration.d.ts +3 -1
- package/dist/calibration.js +6 -6
- package/dist/capabilities.d.ts +7 -7
- package/dist/capabilities.js +1 -1
- package/dist/{capsule-crypto-CZJSLEFG.js → capsule-crypto-YO5QJ6L3.js} +3 -3
- package/dist/capsule-crypto-YO5QJ6L3.js.map +1 -0
- package/dist/{catalog-C9444P10.d.ts → catalog-BWqHb5HT.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +3 -1
- package/dist/causal-behavior.js +6 -6
- package/dist/causal-chain.js +4 -4
- package/dist/causal-consolidation.d.ts +3 -1
- package/dist/causal-consolidation.js +43 -34
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/causal-retrieval.js +6 -6
- package/dist/causal-trajectory-graph.d.ts +3 -1
- package/dist/causal-trajectory-graph.js +1 -1
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-A3GOMRJG.js → chunk-23L3IXFD.js} +2 -2
- package/dist/chunk-2CGLBUW3.js +15 -0
- package/dist/chunk-2CGLBUW3.js.map +1 -0
- package/dist/{chunk-MRHFJV44.js → chunk-2DQ2XREU.js} +2 -2
- package/dist/{chunk-QKQYXZVS.js → chunk-2EJOEICD.js} +1819 -1481
- package/dist/chunk-2EJOEICD.js.map +1 -0
- package/dist/{chunk-L2B3EE7B.js → chunk-2QMKMVHH.js} +2 -2
- package/dist/{chunk-VJVNLNDQ.js → chunk-2UFO7X4Q.js} +7 -7
- package/dist/{chunk-2MR3MFQB.js → chunk-2VEOKUPP.js} +3 -3
- package/dist/chunk-2Z24E5W2.js +22 -0
- package/dist/chunk-2Z24E5W2.js.map +1 -0
- package/dist/{chunk-RHOZ5LLO.js → chunk-347WD2LE.js} +2 -2
- package/dist/{chunk-HDTQVNWD.js → chunk-3QAH2S33.js} +3 -3
- package/dist/{chunk-37CTGERM.js → chunk-3WCASNQZ.js} +12 -1
- package/dist/chunk-3WCASNQZ.js.map +1 -0
- package/dist/{chunk-LRNMRXJO.js → chunk-3Y224NYC.js} +3 -3
- package/dist/{chunk-3IY64MV6.js → chunk-44XZ75OQ.js} +44 -10
- package/dist/chunk-44XZ75OQ.js.map +1 -0
- package/dist/{chunk-VFSP73LP.js → chunk-45GF7Y3V.js} +2 -2
- package/dist/{chunk-K7LOWSLU.js → chunk-4BJQODZF.js} +2479 -1520
- package/dist/chunk-4BJQODZF.js.map +1 -0
- package/dist/{chunk-JREB6UFH.js → chunk-57OWKFUQ.js} +17 -6
- package/dist/chunk-57OWKFUQ.js.map +1 -0
- package/dist/{chunk-OEHCMALU.js → chunk-5I4M26Q7.js} +5 -5
- package/dist/{chunk-2AZXIZEN.js → chunk-5Q7EVV4X.js} +6 -6
- package/dist/{chunk-LC7OODWZ.js → chunk-5SB7SC2G.js} +80 -49
- package/dist/chunk-5SB7SC2G.js.map +1 -0
- package/dist/{chunk-PUBTHW5O.js → chunk-6DRPPQEJ.js} +2 -2
- package/dist/{chunk-NWTGD4AN.js → chunk-6KIQT7RG.js} +10 -9
- package/dist/chunk-6KIQT7RG.js.map +1 -0
- package/dist/{chunk-FDCTQWLW.js → chunk-6O22TZSB.js} +3 -3
- package/dist/{chunk-ZQ7UBUZ6.js → chunk-6QYST5XN.js} +6 -6
- package/dist/{chunk-W4PGTAS2.js → chunk-6WAFYNVT.js} +3 -3
- package/dist/chunk-6ZMAMSHS.js +20 -0
- package/dist/chunk-6ZMAMSHS.js.map +1 -0
- package/dist/{chunk-R5VAVVHP.js → chunk-72A4L5JJ.js} +2 -2
- package/dist/{chunk-X64YLC24.js → chunk-72VAXGEI.js} +5 -13
- package/dist/chunk-72VAXGEI.js.map +1 -0
- package/dist/{chunk-IKQTFQMI.js → chunk-755Q3DGF.js} +22 -3
- package/dist/chunk-755Q3DGF.js.map +1 -0
- package/dist/{chunk-OVJ2HIPE.js → chunk-76GLCM57.js} +158 -125
- package/dist/chunk-76GLCM57.js.map +1 -0
- package/dist/{chunk-D7FOKIY5.js → chunk-7AMFUATK.js} +12 -9
- package/dist/{chunk-D7FOKIY5.js.map → chunk-7AMFUATK.js.map} +1 -1
- package/dist/{chunk-BFVEEB7W.js → chunk-7ON2JW73.js} +57 -22
- package/dist/chunk-7ON2JW73.js.map +1 -0
- package/dist/chunk-7RNBVZZO.js +270 -0
- package/dist/chunk-7RNBVZZO.js.map +1 -0
- package/dist/{chunk-3FAMU5TX.js → chunk-A6USBZTO.js} +2 -2
- package/dist/{chunk-ZHIVCODT.js → chunk-A72SWTWW.js} +7 -7
- package/dist/{chunk-FC77VUX5.js → chunk-A7Q43EUZ.js} +2 -2
- package/dist/{chunk-UJBESW6X.js → chunk-AE7WMV4V.js} +7 -7
- package/dist/{chunk-J2HSAU72.js → chunk-AIK5W3IW.js} +29 -1
- package/dist/chunk-AIK5W3IW.js.map +1 -0
- package/dist/{chunk-A5UXZRRM.js → chunk-AJ7QCSIS.js} +2 -2
- package/dist/{chunk-FNMFJWBY.js → chunk-ANEUPF6O.js} +5 -5
- package/dist/chunk-AREGMX2J.js +50 -0
- package/dist/chunk-AREGMX2J.js.map +1 -0
- package/dist/chunk-AVPXXV6J.js +448 -0
- package/dist/chunk-AVPXXV6J.js.map +1 -0
- package/dist/{chunk-2A3OXAP2.js → chunk-B7RWBZXW.js} +2 -2
- package/dist/{chunk-GWTFLF2P.js → chunk-BJMBJZ2Y.js} +59 -59
- package/dist/chunk-BJMBJZ2Y.js.map +1 -0
- package/dist/{chunk-74F5STS4.js → chunk-BLAKS3CP.js} +4 -4
- package/dist/{chunk-M7VGG3ID.js → chunk-BV5WMSIJ.js} +9 -9
- package/dist/{chunk-2BFLDH5F.js → chunk-BXLOS5AJ.js} +2 -2
- package/dist/{chunk-UXKMMNJD.js → chunk-C23BYTJV.js} +6 -6
- package/dist/{chunk-JZHHREBL.js → chunk-C3AKM55W.js} +2 -2
- package/dist/{chunk-RP7YXA67.js → chunk-C5JF6EZ7.js} +2 -2
- package/dist/{chunk-T7Y5KPLN.js → chunk-C6XBRYVD.js} +16 -15
- package/dist/chunk-C6XBRYVD.js.map +1 -0
- package/dist/{chunk-FTYDWKO6.js → chunk-CEN6DW6X.js} +12 -2
- package/dist/chunk-CEN6DW6X.js.map +1 -0
- package/dist/chunk-CGASUJVP.js +421 -0
- package/dist/chunk-CGASUJVP.js.map +1 -0
- package/dist/{chunk-EHCONI6E.js → chunk-CQMEM5CC.js} +2 -2
- package/dist/{chunk-PBNZVPFS.js → chunk-CXZQOVIE.js} +12 -12
- package/dist/chunk-D3V3UMET.js +44 -0
- package/dist/chunk-D3V3UMET.js.map +1 -0
- package/dist/{chunk-7N2RUBAP.js → chunk-DDA47TES.js} +2 -2
- package/dist/{chunk-64SE4X5G.js → chunk-DEBHXHJR.js} +3 -3
- package/dist/{chunk-EHK6KZHK.js → chunk-DNHVPZOD.js} +2 -2
- package/dist/{chunk-437S3G37.js → chunk-DQEMWVMT.js} +2 -2
- package/dist/{chunk-LZZNTPLR.js → chunk-DX3XRGLH.js} +2 -2
- package/dist/chunk-EA2H5LML.js +72 -0
- package/dist/chunk-EA2H5LML.js.map +1 -0
- package/dist/{chunk-INELTPRG.js → chunk-EEC7UJPX.js} +103 -106
- package/dist/chunk-EEC7UJPX.js.map +1 -0
- package/dist/{chunk-KSNVDPDF.js → chunk-EKBNDKDY.js} +21 -20
- package/dist/chunk-EKBNDKDY.js.map +1 -0
- package/dist/{chunk-YF6FMLBT.js → chunk-EQWEIIKQ.js} +64 -34
- package/dist/chunk-EQWEIIKQ.js.map +1 -0
- package/dist/{chunk-DZKHSDOM.js → chunk-ES36HTSC.js} +4 -4
- package/dist/{chunk-TN7THW5U.js → chunk-ET356KSB.js} +2 -2
- package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
- package/dist/{chunk-DMSDUGDM.js → chunk-F7GAIWFA.js} +20 -20
- package/dist/{chunk-G6XXLKC5.js → chunk-FFPJINJE.js} +6 -6
- package/dist/chunk-FMLKOL2Z.js +636 -0
- package/dist/chunk-FMLKOL2Z.js.map +1 -0
- package/dist/chunk-FQC3DHH2.js +20 -0
- package/dist/chunk-FQC3DHH2.js.map +1 -0
- package/dist/{chunk-G4CN3ZXL.js → chunk-FUTF72MJ.js} +15 -15
- package/dist/chunk-GCASGYIO.js +71 -0
- package/dist/chunk-GCASGYIO.js.map +1 -0
- package/dist/{chunk-HF77M34J.js → chunk-GFPZS3F5.js} +8 -8
- package/dist/{chunk-BCDKBDSY.js → chunk-GNVZQ5JN.js} +99 -41
- package/dist/chunk-GNVZQ5JN.js.map +1 -0
- package/dist/{chunk-G6NKALRN.js → chunk-H4WG3UQZ.js} +2 -2
- package/dist/{chunk-UDQUSU27.js → chunk-HNZKVLAN.js} +2 -2
- package/dist/{chunk-5EZXHU36.js → chunk-HYQJQGD3.js} +2 -2
- package/dist/{chunk-OAZVR2BV.js → chunk-IDZFBSHN.js} +8 -6
- package/dist/{chunk-OAZVR2BV.js.map → chunk-IDZFBSHN.js.map} +1 -1
- package/dist/{chunk-PLQE6MJA.js → chunk-IFH6LWTO.js} +50 -8
- package/dist/chunk-IFH6LWTO.js.map +1 -0
- package/dist/{chunk-RULE4VG5.js → chunk-ILGC6JH6.js} +21 -6
- package/dist/chunk-ILGC6JH6.js.map +1 -0
- package/dist/chunk-IUM3VSHI.js +113 -0
- package/dist/chunk-IUM3VSHI.js.map +1 -0
- package/dist/{chunk-DCFV7A7H.js → chunk-IYNGIMVG.js} +101 -91
- package/dist/chunk-IYNGIMVG.js.map +1 -0
- package/dist/{chunk-TRWPCR6P.js → chunk-JECBQBEX.js} +3 -3
- package/dist/{chunk-E6CBU7XP.js → chunk-JYIFGGEM.js} +2 -2
- package/dist/{chunk-VA4WSW54.js → chunk-JYIJNNN3.js} +3 -3
- package/dist/{chunk-L5HJQC36.js → chunk-K2TDK7GG.js} +3 -3
- package/dist/{chunk-KDXVOFYY.js → chunk-KAVBJSBQ.js} +4 -4
- package/dist/{chunk-BS4SGLWV.js → chunk-KF5EUVKQ.js} +16 -6
- package/dist/chunk-KF5EUVKQ.js.map +1 -0
- package/dist/chunk-KFZY2OFH.js +42 -0
- package/dist/chunk-KFZY2OFH.js.map +1 -0
- package/dist/{chunk-4OKLXWDZ.js → chunk-KNB7IBBU.js} +2 -2
- package/dist/{chunk-QLULYKWZ.js → chunk-KSXJOC4F.js} +2 -2
- package/dist/{chunk-2S6QHZGJ.js → chunk-L2DPF4DF.js} +1 -1
- package/dist/{chunk-JI5AVXYO.js → chunk-LEPOP46W.js} +6 -6
- package/dist/{chunk-O7BBFJLP.js → chunk-LTAVSOZW.js} +2 -2
- package/dist/{chunk-E3VODCC3.js → chunk-LUDXM7MD.js} +14 -2
- package/dist/{chunk-E3VODCC3.js.map → chunk-LUDXM7MD.js.map} +1 -1
- package/dist/{chunk-2IZAGAJR.js → chunk-LULR4IUW.js} +3 -3
- package/dist/{chunk-QQGPUKMT.js → chunk-MC5Q3J7S.js} +398 -7
- package/dist/chunk-MC5Q3J7S.js.map +1 -0
- package/dist/{chunk-WMWVO45V.js → chunk-MC6XO7T4.js} +2 -2
- package/dist/chunk-MKQBXQKU.js +107 -0
- package/dist/chunk-MKQBXQKU.js.map +1 -0
- package/dist/{chunk-7PND4PUC.js → chunk-MMGGVZ4Q.js} +29 -9
- package/dist/{chunk-7PND4PUC.js.map → chunk-MMGGVZ4Q.js.map} +1 -1
- package/dist/{chunk-D6RIFG4O.js → chunk-MNB5RMAY.js} +2 -2
- package/dist/{chunk-Q5WG43OP.js → chunk-MYFQ3ED4.js} +7 -7
- package/dist/{chunk-V3UE5I5F.js → chunk-MYVPLJOO.js} +2 -2
- package/dist/chunk-NINSR6LX.js +83 -0
- package/dist/chunk-NINSR6LX.js.map +1 -0
- package/dist/{chunk-MTWKDOVO.js → chunk-NLE6TSLG.js} +2 -2
- package/dist/{chunk-DBRYEIGY.js → chunk-OJP3N7UK.js} +2 -2
- package/dist/{chunk-VDW4VU7B.js → chunk-OQJIB32H.js} +28 -23
- package/dist/chunk-OQJIB32H.js.map +1 -0
- package/dist/{chunk-SMX5267P.js → chunk-OZKOPQCM.js} +2 -2
- package/dist/{chunk-JCK7V6SN.js → chunk-P65YJEEL.js} +2 -2
- package/dist/{chunk-MTR7BZQN.js → chunk-PBGWYM3H.js} +3 -3
- package/dist/chunk-PIG4WJFQ.js +87 -0
- package/dist/chunk-PIG4WJFQ.js.map +1 -0
- package/dist/chunk-PQVCH2AE.js +76 -0
- package/dist/chunk-PQVCH2AE.js.map +1 -0
- package/dist/{chunk-AXTXN4T6.js → chunk-PWFCHOB3.js} +2 -2
- package/dist/{chunk-FDPZVY6D.js → chunk-QFN4U7AG.js} +2 -2
- package/dist/{chunk-K2ZLHESQ.js → chunk-QIEUU4UK.js} +2 -2
- package/dist/{chunk-DYP3YYJJ.js → chunk-QJAQNXWI.js} +39 -5
- package/dist/chunk-QJAQNXWI.js.map +1 -0
- package/dist/{chunk-3D6AFGQR.js → chunk-QVT3E4CE.js} +7 -7
- package/dist/{chunk-FIJUY7GG.js → chunk-R26ZMXR2.js} +23 -23
- package/dist/{chunk-FIJUY7GG.js.map → chunk-R26ZMXR2.js.map} +1 -1
- package/dist/chunk-R3DJVOSM.js +34 -0
- package/dist/chunk-R3DJVOSM.js.map +1 -0
- package/dist/{chunk-SFQ6QNL7.js → chunk-R6YJI3HE.js} +2 -1
- package/dist/{chunk-5CJVLG3H.js → chunk-RFDD3B7W.js} +5 -5
- package/dist/{chunk-JBPKEARU.js → chunk-RKNJBZ55.js} +4 -4
- package/dist/chunk-ROI7BK7H.js +88 -0
- package/dist/chunk-ROI7BK7H.js.map +1 -0
- package/dist/{chunk-FNZQGN76.js → chunk-S2BBISHU.js} +4 -4
- package/dist/{chunk-IALLX7EE.js → chunk-S2FSBU4H.js} +3 -3
- package/dist/{chunk-BFPETA3S.js → chunk-SCCO46KZ.js} +3 -2
- package/dist/chunk-SCCO46KZ.js.map +1 -0
- package/dist/chunk-SICXL4MK.js +165 -0
- package/dist/chunk-SICXL4MK.js.map +1 -0
- package/dist/chunk-SK7AMF2J.js +168 -0
- package/dist/chunk-SK7AMF2J.js.map +1 -0
- package/dist/{chunk-LZOV7MX7.js → chunk-TDTXKOAJ.js} +6474 -5747
- package/dist/chunk-TDTXKOAJ.js.map +1 -0
- package/dist/{chunk-KSBEEMQF.js → chunk-TG6IHVE2.js} +151 -6
- package/dist/chunk-TG6IHVE2.js.map +1 -0
- package/dist/{chunk-7K5Q6COX.js → chunk-TVVEYCNW.js} +4 -4
- package/dist/{chunk-DPAENM7O.js → chunk-U3ZAVRP3.js} +2 -2
- package/dist/{chunk-ODPLEWB6.js → chunk-UFGFMWAD.js} +47 -17
- package/dist/chunk-UFGFMWAD.js.map +1 -0
- package/dist/{chunk-4KJLYTLS.js → chunk-V57NLO5P.js} +2 -2
- package/dist/{chunk-FLVL4L3L.js → chunk-VDX2J7OX.js} +2 -2
- package/dist/{chunk-AJLLIIML.js → chunk-W4FBGYGK.js} +1 -1
- package/dist/{chunk-TXLGHDHS.js → chunk-W4XJFLAL.js} +53 -32
- package/dist/chunk-W4XJFLAL.js.map +1 -0
- package/dist/{chunk-6SXVCD7W.js → chunk-W7IGLX4G.js} +2 -2
- package/dist/{chunk-KQDNAXOM.js → chunk-WG5HSQ4P.js} +19 -6
- package/dist/chunk-WG5HSQ4P.js.map +1 -0
- package/dist/{chunk-LTJAMRGI.js → chunk-WL5EBRDH.js} +17 -3
- package/dist/chunk-WL5EBRDH.js.map +1 -0
- package/dist/{chunk-KAWTOYAW.js → chunk-WLZYGLJ4.js} +2 -2
- package/dist/{chunk-RO4LQGT3.js → chunk-WON4APQY.js} +4 -4
- package/dist/{chunk-PQVK2S3J.js → chunk-WZFAPHIK.js} +6 -6
- package/dist/{chunk-KQPJLK3X.js → chunk-XL24QQ63.js} +2 -2
- package/dist/{chunk-RVZRYUQH.js → chunk-YROFNEG5.js} +7 -7
- package/dist/{chunk-RAR2BSEM.js → chunk-YV32N67O.js} +1 -1
- package/dist/chunk-YV32N67O.js.map +1 -0
- package/dist/{chunk-PBSAPHHA.js → chunk-YVKF3HEW.js} +1 -1
- package/dist/chunk-YVKF3HEW.js.map +1 -0
- package/dist/{chunk-VRAD2FGO.js → chunk-Z54EM5YZ.js} +12 -12
- package/dist/{chunk-2VN4JAX7.js → chunk-ZOKZIR5K.js} +95 -33
- package/dist/chunk-ZOKZIR5K.js.map +1 -0
- package/dist/{cli-Cd5fMmhq.d.ts → cli-C_pqqTCw.d.ts} +28 -12
- package/dist/cli.d.ts +16 -9
- package/dist/cli.js +142 -127
- package/dist/compounding/engine.d.ts +5 -2
- package/dist/compounding/engine.js +36 -26
- package/dist/compounding/preference-consolidator.d.ts +3 -1
- package/dist/compounding/preference-consolidator.js +1 -1
- package/dist/compression-optimizer.d.ts +3 -1
- package/dist/config-qmd.d.ts +8 -0
- package/dist/config-qmd.js +20 -0
- package/dist/config-qmd.js.map +1 -0
- package/dist/config.d.ts +3 -1
- package/dist/config.js +15 -10
- package/dist/connectors/codex-materialize-runner.d.ts +3 -1
- package/dist/connectors/codex-materialize-runner.js +36 -26
- package/dist/connectors/codex-materialize.d.ts +3 -1
- package/dist/connectors/codex-materialize.js +2 -2
- package/dist/connectors/index.d.ts +3 -1
- package/dist/connectors/index.js +41 -32
- package/dist/consolidation-provenance-check.d.ts +5 -2
- package/dist/consolidation-undo.d.ts +5 -2
- package/dist/contradiction/index.d.ts +5 -2
- package/dist/contradiction/index.js +10 -4
- package/dist/{contradiction-scan-J57T5QV7.js → contradiction-scan-RJBABNRH.js} +4 -4
- package/dist/contradiction-scan-RJBABNRH.js.map +1 -0
- package/dist/conversation-index/backend.d.ts +3 -1
- package/dist/conversation-index/backend.js +5 -5
- package/dist/conversation-index/chunker.d.ts +3 -1
- package/dist/conversation-index/cleanup.js +2 -2
- package/dist/conversation-index/faiss-adapter.d.ts +3 -1
- package/dist/conversation-index/faiss-adapter.js +2 -2
- package/dist/conversation-index/indexer.d.ts +3 -1
- package/dist/conversation-index/indexer.js +2 -2
- package/dist/conversation-index/search.d.ts +3 -1
- package/dist/conversation-index/search.js +2 -2
- package/dist/cross-namespace-budget.d.ts +45 -1
- package/dist/cross-namespace-budget.js +5 -3
- package/dist/dashboard-runtime.js +2 -2
- package/dist/day-summary.d.ts +3 -1
- package/dist/day-summary.js +3 -3
- package/dist/delinearize.d.ts +3 -1
- package/dist/direct-answer-wiring.d.ts +3 -1
- package/dist/direct-answer.d.ts +3 -1
- package/dist/{dreams-ledger-6UODQOX6.js → dreams-ledger-567VKVOU.js} +4 -2
- package/dist/dreams-ledger-567VKVOU.js.map +1 -0
- package/dist/embedding-fallback.d.ts +3 -1
- package/dist/embedding-fallback.js +3 -3
- package/dist/enrichment/index.d.ts +3 -1
- package/dist/entity-retrieval.d.ts +5 -2
- package/dist/entity-retrieval.js +35 -26
- package/dist/entity-schema.d.ts +3 -1
- package/dist/explicit-capture.d.ts +13 -8
- package/dist/explicit-capture.js +7 -3
- package/dist/extraction-faithfulness.d.ts +3 -1
- package/dist/extraction-faithfulness.js +5 -5
- package/dist/extraction-judge-telemetry.d.ts +3 -1
- package/dist/extraction-judge-telemetry.js +2 -2
- package/dist/extraction-judge-training.d.ts +3 -1
- package/dist/extraction-judge-training.js +2 -2
- package/dist/extraction-judge.d.ts +3 -1
- package/dist/extraction-judge.js +5 -5
- package/dist/extraction.d.ts +3 -1
- package/dist/extraction.js +13 -12
- package/dist/faithfulness-config.js +3 -2
- package/dist/fallback-llm.d.ts +3 -1
- package/dist/fallback-llm.js +4 -4
- package/dist/{first-start-migration-TI67YMLG.js → first-start-migration-CSRHCJIQ.js} +2 -2
- package/dist/{forget-CQCLB5LB.js → forget-HRPLVB3W.js} +5 -5
- package/dist/graph-dashboard-diff.d.ts +3 -1
- package/dist/graph-dashboard-key.d.ts +3 -1
- package/dist/graph-dashboard-parser.d.ts +3 -1
- package/dist/{graph-edge-decay-IOGBBAGC.js → graph-edge-decay-Z6ENALU2.js} +3 -3
- package/dist/graph-edge-decay-Z6ENALU2.js.map +1 -0
- package/dist/graph-edge-reinforcement.d.ts +3 -1
- package/dist/graph-snapshot.d.ts +3 -1
- package/dist/graph-snapshot.js +2 -2
- package/dist/graph.d.ts +3 -1
- package/dist/graph.js +1 -1
- package/dist/identity-continuity.d.ts +1 -2
- package/dist/importance.d.ts +3 -1
- package/dist/importers/index.d.ts +3 -1
- package/dist/in-flight-reads.d.ts +3 -1
- package/dist/index.d.ts +473 -464
- package/dist/index.js +287 -239
- package/dist/intent.d.ts +3 -1
- package/dist/lcm/archive.js +2 -2
- package/dist/lcm/engine.d.ts +3 -1
- package/dist/lcm/engine.js +6 -6
- package/dist/lcm/index.d.ts +3 -1
- package/dist/lcm/index.js +13 -13
- package/dist/lcm/schema.js +2 -2
- package/dist/lcm/summarizer.js +3 -3
- package/dist/lcm/tools.d.ts +3 -1
- package/dist/lifecycle.d.ts +3 -1
- package/dist/live-connectors-runner.d.ts +3 -1
- package/dist/local-llm.d.ts +5 -2
- package/dist/local-llm.js +3 -3
- package/dist/local-model-endpoint.d.ts +3 -1
- package/dist/logger.d.ts +3 -1
- package/dist/logger.js +7 -3
- package/dist/maintenance/archive-observations.js +2 -2
- package/dist/maintenance/memory-governance.d.ts +3 -1
- package/dist/maintenance/memory-governance.js +35 -25
- package/dist/maintenance/migrate-observations.js +3 -3
- package/dist/maintenance/observation-ledger-utils.js +2 -2
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +102 -1
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +37 -25
- package/dist/maintenance/rebuild-memory-projection.js +38 -28
- package/dist/maintenance/rebuild-observations.js +3 -3
- package/dist/mcp-memory-inspector-app.d.ts +22 -15
- package/dist/memory-action-policy.d.ts +3 -1
- package/dist/memory-cache.d.ts +3 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +25 -2
- package/dist/memory-lifecycle-ledger-utils.js +11 -1
- package/dist/memory-projection-store.d.ts +3 -1
- package/dist/memory-projection-store.js +2 -2
- package/dist/memory-provenance.d.ts +3 -1
- package/dist/memory-worth-outcomes.d.ts +5 -2
- package/dist/model-registry.js +2 -2
- package/dist/models-json.d.ts +3 -1
- package/dist/models-json.js +2 -2
- package/dist/namespaces/migrate.d.ts +6 -3
- package/dist/namespaces/migrate.js +51 -42
- package/dist/namespaces/principal.d.ts +27 -2
- package/dist/namespaces/principal.js +6 -2
- package/dist/namespaces/search.d.ts +16 -2
- package/dist/namespaces/search.js +24 -20
- package/dist/namespaces/storage.d.ts +24 -3
- package/dist/namespaces/storage.js +35 -27
- package/dist/native-knowledge.d.ts +3 -1
- package/dist/native-knowledge.js +2 -2
- package/dist/negative.js +2 -2
- package/dist/offline-sync-exclude-globs.d.ts +13 -0
- package/dist/offline-sync-exclude-globs.js +10 -0
- package/dist/offline-sync-exclude-globs.js.map +1 -0
- package/dist/offline-sync-impression-drain.d.ts +78 -0
- package/dist/offline-sync-impression-drain.js +37 -0
- package/dist/offline-sync-impression-drain.js.map +1 -0
- package/dist/offline-sync.js +4 -2
- package/dist/operator-toolkit.d.ts +5 -2
- package/dist/operator-toolkit.js +69 -54
- package/dist/orchestration/compression-guideline-coordinator.d.ts +5 -2
- package/dist/orchestration/compression-guideline-coordinator.js +3 -3
- package/dist/orchestration/maintenance.d.ts +121 -2
- package/dist/orchestration/maintenance.js +44 -31
- package/dist/{orchestrator-Bp_jeBHK.d.ts → orchestrator-CQJ8U7En.d.ts} +85 -79
- package/dist/orchestrator.d.ts +13 -8
- package/dist/orchestrator.js +194 -177
- package/dist/patterns-cli.d.ts +3 -1
- package/dist/{pipeline-BAj5bx7g.d.ts → pipeline-Cn-kvYeV.d.ts} +1 -1
- package/dist/policy-runtime.d.ts +3 -1
- package/dist/prioritized-embed.d.ts +19 -0
- package/dist/prioritized-embed.js +8 -0
- package/dist/prioritized-embed.js.map +1 -0
- package/dist/proactive-contention.d.ts +28 -0
- package/dist/proactive-contention.js +8 -0
- package/dist/proactive-contention.js.map +1 -0
- package/dist/profiling.js +2 -2
- package/dist/provenance.d.ts +3 -1
- package/dist/provenance.js +3 -2
- package/dist/{purge-NDWV6WYD.js → purge-IRF47ZTX.js} +3 -3
- package/dist/{qmd-_MDSlj1m.d.ts → qmd-oh9VXz1d.d.ts} +11 -1
- package/dist/qmd-preflight.d.ts +5 -2
- package/dist/qmd-recall-cache.d.ts +3 -1
- package/dist/qmd-status.d.ts +57 -0
- package/dist/qmd-status.js +18 -0
- package/dist/qmd-status.js.map +1 -0
- package/dist/qmd.d.ts +5 -2
- package/dist/qmd.js +7 -3
- package/dist/recall-concurrency-config.d.ts +16 -0
- package/dist/recall-concurrency-config.js +10 -0
- package/dist/recall-concurrency-config.js.map +1 -0
- package/dist/recall-disclosure-escalation.d.ts +3 -1
- package/dist/recall-explain-renderer.d.ts +4 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-memory-map.d.ts +26 -0
- package/dist/recall-memory-map.js +18 -0
- package/dist/recall-memory-map.js.map +1 -0
- package/dist/recall-planner-llm.d.ts +3 -1
- package/dist/recall-planner-llm.js +4 -4
- package/dist/recall-qos.js +2 -2
- package/dist/recall-state.d.ts +163 -2
- package/dist/recall-state.js +6 -3
- package/dist/recall-tag-filter.d.ts +3 -1
- package/dist/recall-timings.d.ts +11 -2
- package/dist/recall-timings.js +3 -1
- package/dist/recall-xray-cli.d.ts +3 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +3 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +3 -1
- package/dist/recall-xray.js +2 -2
- package/dist/relevance.js +3 -3
- package/dist/replay/normalizers/chatgpt.d.ts +3 -1
- package/dist/replay/normalizers/claude.d.ts +3 -1
- package/dist/replay/normalizers/openclaw.d.ts +3 -1
- package/dist/replay/normalizers/shared.d.ts +3 -1
- package/dist/replay/runner.d.ts +3 -1
- package/dist/replay/types.d.ts +3 -1
- package/dist/rerank.d.ts +8 -1
- package/dist/rerank.js +3 -1
- package/dist/resolution-3K27T4VO.js +18 -0
- package/dist/resolution-3K27T4VO.js.map +1 -0
- package/dist/resolve-auth-token.d.ts +3 -1
- package/dist/resolve-provider-secret.js +2 -2
- package/dist/resume-bundles.js +19 -14
- package/dist/retrieval-agents.d.ts +19 -16
- package/dist/retrieval-agents.js +3 -3
- package/dist/retrieval-tiers.d.ts +3 -1
- package/dist/routing/engine.d.ts +3 -1
- package/dist/routing/store.d.ts +3 -1
- package/dist/routing/store.js +2 -2
- package/dist/salvage-envelope.d.ts +18 -0
- package/dist/salvage-envelope.js +15 -0
- package/dist/salvage-envelope.js.map +1 -0
- package/dist/schemas.d.ts +98 -98
- package/dist/scope-profiles-EpJG7yKP.d.ts +30 -0
- package/dist/search/document-scanner.js +3 -2
- package/dist/search/embed-helper.d.ts +3 -1
- package/dist/search/embed-helper.js +3 -3
- package/dist/search/factory.d.ts +3 -1
- package/dist/search/factory.js +18 -16
- package/dist/search/index.d.ts +3 -1
- package/dist/search/index.js +22 -20
- package/dist/search/lancedb-backend.d.ts +3 -1
- package/dist/search/lancedb-backend.js +6 -5
- package/dist/search/meilisearch-backend.d.ts +3 -1
- package/dist/search/meilisearch-backend.js +6 -5
- package/dist/search/noop-backend.d.ts +3 -1
- package/dist/search/orama-backend.d.ts +3 -1
- package/dist/search/orama-backend.js +6 -5
- package/dist/search/port.d.ts +16 -2
- package/dist/search/port.js +1 -1
- package/dist/search/remote-backend.d.ts +3 -1
- package/dist/search/remote-backend.js +3 -3
- package/dist/secure-store/index.d.ts +12 -1
- package/dist/secure-store/index.js +6 -3
- package/dist/{semantic-consolidation-C2m5eHHE.d.ts → semantic-consolidation-DILZEQq3.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +4 -2
- package/dist/semantic-consolidation.js +38 -29
- package/dist/semantic-rule-promotion.js +34 -24
- package/dist/semantic-rule-verifier.d.ts +3 -1
- package/dist/semantic-rule-verifier.js +35 -25
- package/dist/session-observer-bands.d.ts +3 -1
- package/dist/session-observer-state.d.ts +3 -1
- package/dist/session-observer-state.js +2 -2
- package/dist/session-transcript-migration.js +2 -2
- package/dist/shared-context/manager.d.ts +11 -9
- package/dist/shared-context/manager.js +3 -3
- package/dist/signal.d.ts +3 -1
- package/dist/{storage-hr_Y2z5R.d.ts → storage-NIyKYD8g.d.ts} +489 -110
- package/dist/storage-guards.js +2 -2
- package/dist/storage.d.ts +6 -3
- package/dist/storage.js +38 -26
- package/dist/structured-attributes.d.ts +23 -1
- package/dist/structured-attributes.js +7 -1
- package/dist/summarizer.d.ts +3 -1
- package/dist/summarizer.js +11 -11
- package/dist/summary-snapshot.d.ts +3 -1
- package/dist/summary-snapshot.js +2 -2
- package/dist/temporal-index-batch.d.ts +1 -0
- package/dist/temporal-index-batch.js +10 -0
- package/dist/temporal-index-batch.js.map +1 -0
- package/dist/temporal-index-compat.d.ts +279 -1
- package/dist/temporal-index-compat.js +1 -1
- package/dist/temporal-index.d.ts +1 -225
- package/dist/temporal-index.js +35 -3
- package/dist/temporal-supersession.d.ts +5 -2
- package/dist/temporal-supersession.js +2 -2
- package/dist/temporal-timeline-recall.d.ts +3 -1
- package/dist/temporal-validity.d.ts +3 -1
- package/dist/threading.d.ts +3 -1
- package/dist/threading.js +2 -2
- package/dist/tier-migration.d.ts +5 -2
- package/dist/tier-routing.d.ts +3 -1
- package/dist/{tier-stats-KV7IWCJR.js → tier-stats-QEJSPFV2.js} +2 -2
- package/dist/tokens.js +3 -3
- package/dist/topics.d.ts +3 -1
- package/dist/transcript.d.ts +3 -1
- package/dist/transcript.js +2 -2
- package/dist/transfer/backup.js +3 -3
- package/dist/transfer/capsule-export.js +3 -3
- package/dist/transfer/capsule-import.js +3 -3
- package/dist/transfer/types.d.ts +90 -90
- package/dist/trust-score-stage.d.ts +75 -38
- package/dist/trust-score-stage.js +11 -3
- package/dist/trust-score.d.ts +3 -1
- package/dist/{types-Cg8q7W7_.d.ts → types-D_fG2ix8.d.ts} +39 -63
- package/dist/types-continuity.d.ts +52 -0
- package/dist/types-continuity.js +1 -0
- package/dist/types-continuity.js.map +1 -0
- package/dist/types.d.ts +3 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +3 -1
- package/dist/utils/serialize-mutations.d.ts +31 -2
- package/dist/utils/serialize-mutations.js +1 -1
- package/dist/verified-recall.js +36 -26
- package/dist/write-envelope.d.ts +282 -0
- package/dist/write-envelope.js +35 -0
- package/dist/write-envelope.js.map +1 -0
- package/dist/write-quarantine.d.ts +79 -0
- package/dist/write-quarantine.js +11 -0
- package/dist/write-quarantine.js.map +1 -0
- package/dist/write-rate-limiter.d.ts +45 -0
- package/dist/write-rate-limiter.js +9 -0
- package/dist/write-rate-limiter.js.map +1 -0
- package/package.json +37 -2
- package/src/access-admin-ops-surface.ts +17 -7
- package/src/access-boundary.test.ts +28 -0
- package/src/access-boundary.ts +31 -2
- package/src/access-citation.test.ts +72 -0
- package/src/access-citation.ts +139 -0
- package/src/access-errors.ts +21 -0
- package/src/access-fingerprint-parity.test.ts +211 -0
- package/src/access-http-cancellation.test.ts +59 -21
- package/src/access-http.test.ts +90 -0
- package/src/access-http.ts +79 -79
- package/src/access-mcp-cancellation.test.ts +16 -7
- package/src/access-memory-search-fanout.test.ts +220 -0
- package/src/access-memory-search-fanout.ts +168 -0
- package/src/access-observe-quarantine.test.ts +120 -0
- package/src/access-observe-write-surface.ts +130 -11
- package/src/access-operations-batch.ts +2 -0
- package/src/access-recall-concurrency.ts +773 -0
- package/src/access-recall-response.ts +307 -0
- package/src/access-recall-surface.ts +100 -252
- package/src/access-service-coding-write.test.ts +22 -4
- package/src/access-service-health.test.ts +49 -0
- package/src/access-service-namespace.test.ts +486 -8
- package/src/access-service-observe-scope.test.ts +4 -0
- package/src/access-service-recall-concurrency.test.ts +1990 -0
- package/src/access-service.ts +182 -175
- package/src/access-surface-catalog.ts +1 -0
- package/src/access-token-capabilities.ts +16 -9
- package/src/active-recall.test.ts +35 -0
- package/src/active-recall.ts +7 -0
- package/src/bounded-jsonl-state.ts +93 -0
- package/src/buffer-session.test.ts +422 -5
- package/src/buffer-surprise-trigger.test.ts +41 -0
- package/src/buffer.ts +218 -3
- package/src/capabilities-cluster-b.test.ts +1 -0
- package/src/capabilities.ts +5 -6
- package/src/cli.ts +21 -22
- package/src/coding/architecture-surfaces.test.ts +6 -3
- package/src/coding/architecture-surfaces.ts +36 -25
- package/src/coding/decision-surfaces.ts +42 -34
- package/src/compounding/engine.test.ts +5 -1
- package/src/compounding/engine.ts +7 -7
- package/src/config-qmd.ts +89 -0
- package/src/config.test.ts +136 -0
- package/src/config.ts +113 -118
- package/src/connector-provenance-lifecycle.test.ts +4 -4
- package/src/connectors/coerce.test.ts +131 -0
- package/src/connectors/coerce.ts +80 -7
- package/src/contradiction/contradiction.test.ts +10 -6
- package/src/contradiction/resolution.ts +17 -4
- package/src/correction/correction-access-wiring.ts +62 -18
- package/src/cross-namespace-budget.test.ts +35 -0
- package/src/cross-namespace-budget.ts +86 -19
- package/src/explicit-capture.ts +54 -17
- package/src/extraction.ts +2 -3
- package/src/faithfulness-graph-guard.test.ts +16 -69
- package/src/graph-recall.test.ts +31 -0
- package/src/index.ts +22 -2
- package/src/local-llm.ts +4 -6
- package/src/logger.ts +16 -0
- package/src/maintenance/dreams-ledger.ts +100 -49
- package/src/maintenance/rebuild-memory-lifecycle-ledger-cli.ts +77 -0
- package/src/maintenance/rebuild-memory-lifecycle-ledger.ts +373 -9
- package/src/memory-lifecycle-ledger-utils.ts +51 -5
- package/src/namespaces/principal.test.ts +37 -1
- package/src/namespaces/principal.ts +48 -0
- package/src/namespaces/search.test.ts +79 -5
- package/src/namespaces/search.ts +59 -6
- package/src/namespaces/storage.ts +43 -1
- package/src/offline-sync-exclude-globs.ts +58 -0
- package/src/offline-sync-impression-drain.test.ts +316 -0
- package/src/offline-sync-impression-drain.ts +260 -0
- package/src/offline-sync.test.ts +32 -5
- package/src/offline-sync.ts +1 -14
- package/src/operator-toolkit.ts +1 -1
- package/src/orchestration/batch-deindex-flush.test.ts +146 -0
- package/src/orchestration/consolidation-run.ts +96 -85
- package/src/orchestration/extraction-envelope.ts +94 -0
- package/src/orchestration/extraction-persist-promotion.ts +134 -0
- package/src/orchestration/extraction-persist.ts +325 -260
- package/src/orchestration/extraction-run.test.ts +1 -1
- package/src/orchestration/extraction-run.ts +2 -2
- package/src/orchestration/graph-recall-coordinator.ts +18 -7
- package/src/orchestration/lifecycle-policy-coordinator.ts +63 -48
- package/src/orchestration/maintenance.test.ts +2103 -3
- package/src/orchestration/maintenance.ts +574 -2
- package/src/orchestration/namespace-read-fanout.ts +14 -8
- package/src/orchestration/orchestrator-helpers.ts +8 -1
- package/src/orchestration/orchestrator-init.ts +22 -6
- package/src/orchestration/orchestrator-namespace-scope.ts +71 -0
- package/src/orchestration/persistence-index.ts +64 -9
- package/src/orchestration/qmd-result-resolver.ts +27 -0
- package/src/orchestration/recall-entry.ts +34 -19
- package/src/orchestration/recall-internal.ts +207 -79
- package/src/orchestration/recall-rerank-coordinator.ts +229 -82
- package/src/orchestration/recall-result-formatter.ts +76 -20
- package/src/orchestration/recall-search-pipeline.test.ts +70 -0
- package/src/orchestration/recall-search-pipeline.ts +102 -129
- package/src/orchestration/recall-section-coordinator.ts +315 -85
- package/src/orchestration/semantic-consolidation-coordinator.ts +69 -37
- package/src/orchestration/session-context.ts +21 -0
- package/src/orchestration/workspace-ops.test.ts +348 -0
- package/src/orchestration/workspace-ops.ts +168 -34
- package/src/orchestrator-chunked-hash-durability.test.ts +126 -0
- package/src/orchestrator-connector-dedup.test.ts +1061 -40
- package/src/orchestrator-flush.test.ts +155 -39
- package/src/orchestrator-nonchunked-hash-durability.test.ts +121 -0
- package/src/orchestrator-source-attribution.test.ts +9 -9
- package/src/orchestrator.ts +160 -149
- package/src/prioritized-embed.ts +123 -0
- package/src/proactive-contention.test.ts +80 -0
- package/src/proactive-contention.ts +39 -0
- package/src/procedural/procedure-miner.test.ts +19 -14
- package/src/procedural/procedure-miner.ts +44 -13
- package/src/qmd-client.test.ts +111 -1
- package/src/qmd-status.ts +186 -0
- package/src/qmd.ts +47 -47
- package/src/recall-budget-config.test.ts +30 -0
- package/src/recall-cold-deadline.test.ts +149 -0
- package/src/recall-concurrency-config.ts +28 -0
- package/src/recall-memory-map.ts +74 -0
- package/src/recall-rerank-coordinator.test.ts +508 -0
- package/src/recall-state.test.ts +825 -1
- package/src/recall-state.ts +406 -7
- package/src/recall-timings-access.test.ts +41 -0
- package/src/recall-timings.ts +16 -0
- package/src/relay/mission-access.test.ts +5 -5
- package/src/rerank.ts +22 -0
- package/src/retrieval-agents.test.ts +79 -0
- package/src/retrieval-agents.ts +71 -19
- package/src/salvage-envelope.ts +28 -0
- package/src/scopes/scope-plan.test.ts +42 -0
- package/src/scopes/scope-plan.ts +45 -0
- package/src/search/port.ts +13 -0
- package/src/secure-store/index.ts +1 -0
- package/src/secure-store/secure-fs.ts +48 -0
- package/src/secure-store/secure-store.test.ts +40 -0
- package/src/semantic-rule-promotion.ts +13 -4
- package/src/storage/content-hash-index.ts +655 -0
- package/src/storage/entity-timeline.ts +473 -0
- package/src/storage/memory-lifecycle-ledger-access.ts +603 -0
- package/src/storage/secure-line-reader.test.ts +197 -0
- package/src/storage/secure-line-reader.ts +181 -3
- package/src/storage-append-header-sniff.test.ts +195 -0
- package/src/storage-behavior-signals-cache.test.ts +237 -0
- package/src/storage-contract/cache-coherence.test.ts +96 -1
- package/src/storage-contract/content-hash.test.ts +47 -2
- package/src/storage-fact-hash-index-defer.test.ts +1200 -0
- package/src/storage-memory-lifecycle-ledger.test.ts +1146 -3
- package/src/storage.ts +882 -890
- package/src/structured-attributes.ts +52 -0
- package/src/temporal-index-batch.ts +112 -0
- package/src/temporal-index.test.ts +193 -0
- package/src/temporal-index.ts +49 -53
- package/src/testing/lifecycle-matrix.test.ts +212 -0
- package/src/testing/lifecycle-matrix.ts +271 -0
- package/src/testing/orchestrator-lite.ts +236 -0
- package/src/testing/subjects/dreams-ledger.test.ts +67 -0
- package/src/testing/subjects/extraction-lifecycle.test.ts +442 -0
- package/src/testing/subjects/lifecycle-ledger.test.ts +93 -0
- package/src/testing/subjects/recall-budget.test.ts +461 -0
- package/src/testing/subjects/serialized-write-chain.test.ts +224 -0
- package/src/trust-score-stage.test.ts +154 -0
- package/src/trust-score-stage.ts +216 -53
- package/src/types-continuity.ts +57 -0
- package/src/types.ts +51 -77
- package/src/utils/path-containment.test.ts +75 -0
- package/src/utils/path-containment.ts +76 -0
- package/src/utils/serialize-mutations.test.ts +122 -0
- package/src/utils/serialize-mutations.ts +68 -3
- package/src/wearables/memory-gen.test.ts +31 -9
- package/src/wearables/memory-gen.ts +95 -55
- package/src/wearables/pipeline.test.ts +10 -7
- package/src/wearables/service.test.ts +6 -8
- package/src/wearables/service.ts +2 -2
- package/src/write-envelope.extension.test.ts +111 -0
- package/src/write-envelope.test.ts +683 -0
- package/src/write-envelope.ts +902 -0
- package/src/write-quarantine.test.ts +193 -0
- package/src/write-quarantine.ts +241 -0
- package/src/write-rate-limiter.test.ts +105 -0
- package/src/write-rate-limiter.ts +135 -0
- package/src/write-sealed-parity.test.ts +204 -0
- package/dist/chunk-2VN4JAX7.js.map +0 -1
- package/dist/chunk-37CTGERM.js.map +0 -1
- package/dist/chunk-3IY64MV6.js.map +0 -1
- package/dist/chunk-BCDKBDSY.js.map +0 -1
- package/dist/chunk-BFPETA3S.js.map +0 -1
- package/dist/chunk-BFVEEB7W.js.map +0 -1
- package/dist/chunk-BS4SGLWV.js.map +0 -1
- package/dist/chunk-DCFV7A7H.js.map +0 -1
- package/dist/chunk-DYP3YYJJ.js.map +0 -1
- package/dist/chunk-E5BK6CGV.js +0 -335
- package/dist/chunk-E5BK6CGV.js.map +0 -1
- package/dist/chunk-FTYDWKO6.js.map +0 -1
- package/dist/chunk-GWTFLF2P.js.map +0 -1
- package/dist/chunk-IKQTFQMI.js.map +0 -1
- package/dist/chunk-INELTPRG.js.map +0 -1
- package/dist/chunk-J2HSAU72.js.map +0 -1
- package/dist/chunk-JREB6UFH.js.map +0 -1
- package/dist/chunk-K7LOWSLU.js.map +0 -1
- package/dist/chunk-KQDNAXOM.js.map +0 -1
- package/dist/chunk-KSBEEMQF.js.map +0 -1
- package/dist/chunk-KSNVDPDF.js.map +0 -1
- package/dist/chunk-LC7OODWZ.js.map +0 -1
- package/dist/chunk-LTJAMRGI.js.map +0 -1
- package/dist/chunk-LZOV7MX7.js.map +0 -1
- package/dist/chunk-NWTGD4AN.js.map +0 -1
- package/dist/chunk-ODPLEWB6.js.map +0 -1
- package/dist/chunk-OVJ2HIPE.js.map +0 -1
- package/dist/chunk-PBSAPHHA.js.map +0 -1
- package/dist/chunk-PHK3HARR.js +0 -32
- package/dist/chunk-PHK3HARR.js.map +0 -1
- package/dist/chunk-PLQE6MJA.js.map +0 -1
- package/dist/chunk-QKQYXZVS.js.map +0 -1
- package/dist/chunk-QQGPUKMT.js.map +0 -1
- package/dist/chunk-RANHFI3P.js +0 -8
- package/dist/chunk-RANHFI3P.js.map +0 -1
- package/dist/chunk-RAR2BSEM.js.map +0 -1
- package/dist/chunk-RULE4VG5.js.map +0 -1
- package/dist/chunk-SVOZFLIQ.js +0 -16
- package/dist/chunk-SVOZFLIQ.js.map +0 -1
- package/dist/chunk-T7Y5KPLN.js.map +0 -1
- package/dist/chunk-TXLGHDHS.js.map +0 -1
- package/dist/chunk-VDW4VU7B.js.map +0 -1
- package/dist/chunk-VSMRDBFT.js +0 -17
- package/dist/chunk-VSMRDBFT.js.map +0 -1
- package/dist/chunk-X64YLC24.js.map +0 -1
- package/dist/chunk-XDSXEKYK.js +0 -118
- package/dist/chunk-XDSXEKYK.js.map +0 -1
- package/dist/chunk-YF6FMLBT.js.map +0 -1
- package/dist/resolution-VH4NGENZ.js +0 -12
- /package/dist/{capsule-crypto-CZJSLEFG.js.map → access-citation.js.map} +0 -0
- /package/dist/{contradiction-scan-J57T5QV7.js.map → access-memory-search-fanout.js.map} +0 -0
- /package/dist/{dreams-ledger-6UODQOX6.js.map → access-recall-concurrency.js.map} +0 -0
- /package/dist/{graph-edge-decay-IOGBBAGC.js.map → access-recall-response.js.map} +0 -0
- /package/dist/{resolution-VH4NGENZ.js.map → bounded-jsonl-state.js.map} +0 -0
- /package/dist/{chunk-A3GOMRJG.js.map → chunk-23L3IXFD.js.map} +0 -0
- /package/dist/{chunk-MRHFJV44.js.map → chunk-2DQ2XREU.js.map} +0 -0
- /package/dist/{chunk-L2B3EE7B.js.map → chunk-2QMKMVHH.js.map} +0 -0
- /package/dist/{chunk-VJVNLNDQ.js.map → chunk-2UFO7X4Q.js.map} +0 -0
- /package/dist/{chunk-2MR3MFQB.js.map → chunk-2VEOKUPP.js.map} +0 -0
- /package/dist/{chunk-RHOZ5LLO.js.map → chunk-347WD2LE.js.map} +0 -0
- /package/dist/{chunk-HDTQVNWD.js.map → chunk-3QAH2S33.js.map} +0 -0
- /package/dist/{chunk-LRNMRXJO.js.map → chunk-3Y224NYC.js.map} +0 -0
- /package/dist/{chunk-VFSP73LP.js.map → chunk-45GF7Y3V.js.map} +0 -0
- /package/dist/{chunk-OEHCMALU.js.map → chunk-5I4M26Q7.js.map} +0 -0
- /package/dist/{chunk-2AZXIZEN.js.map → chunk-5Q7EVV4X.js.map} +0 -0
- /package/dist/{chunk-PUBTHW5O.js.map → chunk-6DRPPQEJ.js.map} +0 -0
- /package/dist/{chunk-FDCTQWLW.js.map → chunk-6O22TZSB.js.map} +0 -0
- /package/dist/{chunk-ZQ7UBUZ6.js.map → chunk-6QYST5XN.js.map} +0 -0
- /package/dist/{chunk-W4PGTAS2.js.map → chunk-6WAFYNVT.js.map} +0 -0
- /package/dist/{chunk-R5VAVVHP.js.map → chunk-72A4L5JJ.js.map} +0 -0
- /package/dist/{chunk-3FAMU5TX.js.map → chunk-A6USBZTO.js.map} +0 -0
- /package/dist/{chunk-ZHIVCODT.js.map → chunk-A72SWTWW.js.map} +0 -0
- /package/dist/{chunk-FC77VUX5.js.map → chunk-A7Q43EUZ.js.map} +0 -0
- /package/dist/{chunk-UJBESW6X.js.map → chunk-AE7WMV4V.js.map} +0 -0
- /package/dist/{chunk-A5UXZRRM.js.map → chunk-AJ7QCSIS.js.map} +0 -0
- /package/dist/{chunk-FNMFJWBY.js.map → chunk-ANEUPF6O.js.map} +0 -0
- /package/dist/{chunk-2A3OXAP2.js.map → chunk-B7RWBZXW.js.map} +0 -0
- /package/dist/{chunk-74F5STS4.js.map → chunk-BLAKS3CP.js.map} +0 -0
- /package/dist/{chunk-M7VGG3ID.js.map → chunk-BV5WMSIJ.js.map} +0 -0
- /package/dist/{chunk-2BFLDH5F.js.map → chunk-BXLOS5AJ.js.map} +0 -0
- /package/dist/{chunk-UXKMMNJD.js.map → chunk-C23BYTJV.js.map} +0 -0
- /package/dist/{chunk-JZHHREBL.js.map → chunk-C3AKM55W.js.map} +0 -0
- /package/dist/{chunk-RP7YXA67.js.map → chunk-C5JF6EZ7.js.map} +0 -0
- /package/dist/{chunk-EHCONI6E.js.map → chunk-CQMEM5CC.js.map} +0 -0
- /package/dist/{chunk-PBNZVPFS.js.map → chunk-CXZQOVIE.js.map} +0 -0
- /package/dist/{chunk-7N2RUBAP.js.map → chunk-DDA47TES.js.map} +0 -0
- /package/dist/{chunk-64SE4X5G.js.map → chunk-DEBHXHJR.js.map} +0 -0
- /package/dist/{chunk-EHK6KZHK.js.map → chunk-DNHVPZOD.js.map} +0 -0
- /package/dist/{chunk-437S3G37.js.map → chunk-DQEMWVMT.js.map} +0 -0
- /package/dist/{chunk-LZZNTPLR.js.map → chunk-DX3XRGLH.js.map} +0 -0
- /package/dist/{chunk-DZKHSDOM.js.map → chunk-ES36HTSC.js.map} +0 -0
- /package/dist/{chunk-TN7THW5U.js.map → chunk-ET356KSB.js.map} +0 -0
- /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
- /package/dist/{chunk-DMSDUGDM.js.map → chunk-F7GAIWFA.js.map} +0 -0
- /package/dist/{chunk-G6XXLKC5.js.map → chunk-FFPJINJE.js.map} +0 -0
- /package/dist/{chunk-G4CN3ZXL.js.map → chunk-FUTF72MJ.js.map} +0 -0
- /package/dist/{chunk-HF77M34J.js.map → chunk-GFPZS3F5.js.map} +0 -0
- /package/dist/{chunk-G6NKALRN.js.map → chunk-H4WG3UQZ.js.map} +0 -0
- /package/dist/{chunk-UDQUSU27.js.map → chunk-HNZKVLAN.js.map} +0 -0
- /package/dist/{chunk-5EZXHU36.js.map → chunk-HYQJQGD3.js.map} +0 -0
- /package/dist/{chunk-TRWPCR6P.js.map → chunk-JECBQBEX.js.map} +0 -0
- /package/dist/{chunk-E6CBU7XP.js.map → chunk-JYIFGGEM.js.map} +0 -0
- /package/dist/{chunk-VA4WSW54.js.map → chunk-JYIJNNN3.js.map} +0 -0
- /package/dist/{chunk-L5HJQC36.js.map → chunk-K2TDK7GG.js.map} +0 -0
- /package/dist/{chunk-KDXVOFYY.js.map → chunk-KAVBJSBQ.js.map} +0 -0
- /package/dist/{chunk-4OKLXWDZ.js.map → chunk-KNB7IBBU.js.map} +0 -0
- /package/dist/{chunk-QLULYKWZ.js.map → chunk-KSXJOC4F.js.map} +0 -0
- /package/dist/{chunk-2S6QHZGJ.js.map → chunk-L2DPF4DF.js.map} +0 -0
- /package/dist/{chunk-JI5AVXYO.js.map → chunk-LEPOP46W.js.map} +0 -0
- /package/dist/{chunk-O7BBFJLP.js.map → chunk-LTAVSOZW.js.map} +0 -0
- /package/dist/{chunk-2IZAGAJR.js.map → chunk-LULR4IUW.js.map} +0 -0
- /package/dist/{chunk-WMWVO45V.js.map → chunk-MC6XO7T4.js.map} +0 -0
- /package/dist/{chunk-D6RIFG4O.js.map → chunk-MNB5RMAY.js.map} +0 -0
- /package/dist/{chunk-Q5WG43OP.js.map → chunk-MYFQ3ED4.js.map} +0 -0
- /package/dist/{chunk-V3UE5I5F.js.map → chunk-MYVPLJOO.js.map} +0 -0
- /package/dist/{chunk-MTWKDOVO.js.map → chunk-NLE6TSLG.js.map} +0 -0
- /package/dist/{chunk-DBRYEIGY.js.map → chunk-OJP3N7UK.js.map} +0 -0
- /package/dist/{chunk-SMX5267P.js.map → chunk-OZKOPQCM.js.map} +0 -0
- /package/dist/{chunk-JCK7V6SN.js.map → chunk-P65YJEEL.js.map} +0 -0
- /package/dist/{chunk-MTR7BZQN.js.map → chunk-PBGWYM3H.js.map} +0 -0
- /package/dist/{chunk-AXTXN4T6.js.map → chunk-PWFCHOB3.js.map} +0 -0
- /package/dist/{chunk-FDPZVY6D.js.map → chunk-QFN4U7AG.js.map} +0 -0
- /package/dist/{chunk-K2ZLHESQ.js.map → chunk-QIEUU4UK.js.map} +0 -0
- /package/dist/{chunk-3D6AFGQR.js.map → chunk-QVT3E4CE.js.map} +0 -0
- /package/dist/{chunk-SFQ6QNL7.js.map → chunk-R6YJI3HE.js.map} +0 -0
- /package/dist/{chunk-5CJVLG3H.js.map → chunk-RFDD3B7W.js.map} +0 -0
- /package/dist/{chunk-JBPKEARU.js.map → chunk-RKNJBZ55.js.map} +0 -0
- /package/dist/{chunk-FNZQGN76.js.map → chunk-S2BBISHU.js.map} +0 -0
- /package/dist/{chunk-IALLX7EE.js.map → chunk-S2FSBU4H.js.map} +0 -0
- /package/dist/{chunk-7K5Q6COX.js.map → chunk-TVVEYCNW.js.map} +0 -0
- /package/dist/{chunk-DPAENM7O.js.map → chunk-U3ZAVRP3.js.map} +0 -0
- /package/dist/{chunk-4KJLYTLS.js.map → chunk-V57NLO5P.js.map} +0 -0
- /package/dist/{chunk-FLVL4L3L.js.map → chunk-VDX2J7OX.js.map} +0 -0
- /package/dist/{chunk-AJLLIIML.js.map → chunk-W4FBGYGK.js.map} +0 -0
- /package/dist/{chunk-6SXVCD7W.js.map → chunk-W7IGLX4G.js.map} +0 -0
- /package/dist/{chunk-KAWTOYAW.js.map → chunk-WLZYGLJ4.js.map} +0 -0
- /package/dist/{chunk-RO4LQGT3.js.map → chunk-WON4APQY.js.map} +0 -0
- /package/dist/{chunk-PQVK2S3J.js.map → chunk-WZFAPHIK.js.map} +0 -0
- /package/dist/{chunk-KQPJLK3X.js.map → chunk-XL24QQ63.js.map} +0 -0
- /package/dist/{chunk-RVZRYUQH.js.map → chunk-YROFNEG5.js.map} +0 -0
- /package/dist/{chunk-VRAD2FGO.js.map → chunk-Z54EM5YZ.js.map} +0 -0
- /package/dist/{first-start-migration-TI67YMLG.js.map → first-start-migration-CSRHCJIQ.js.map} +0 -0
- /package/dist/{forget-CQCLB5LB.js.map → forget-HRPLVB3W.js.map} +0 -0
- /package/dist/{purge-NDWV6WYD.js.map → purge-IRF47ZTX.js.map} +0 -0
- /package/dist/{tier-stats-KV7IWCJR.js.map → tier-stats-QEJSPFV2.js.map} +0 -0
|
@@ -0,0 +1,1200 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import test, { mock } from "node:test";
|
|
7
|
+
|
|
8
|
+
import { ContentHashIndex, FactHashIndexNotAuthoritativeError, StorageManager } from "./storage.js";
|
|
9
|
+
import type { MemoryFile } from "./types.js";
|
|
10
|
+
|
|
11
|
+
// Issue #1909 (Part B): writeMemory("fact") used to rewrite the whole
|
|
12
|
+
// fact-hash index (which grows with corpus size) on EVERY fact, even though
|
|
13
|
+
// the extraction persist path already batch-saves the authoritative superset
|
|
14
|
+
// once at the end. The
|
|
15
|
+
// `deferHashIndexSave` option marks the index dirty but skips the per-fact
|
|
16
|
+
// flush; single-write callers keep the immediate, crash-safe save.
|
|
17
|
+
|
|
18
|
+
async function withMemoryDir(run: (dir: string) => Promise<void>): Promise<void> {
|
|
19
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-hash-defer-"));
|
|
20
|
+
try {
|
|
21
|
+
await run(dir);
|
|
22
|
+
} finally {
|
|
23
|
+
await rm(dir, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test("deferred fact writes do NO per-fact index saves; a single flush persists all", async () => {
|
|
28
|
+
await withMemoryDir(async (dir) => {
|
|
29
|
+
const saveSpy = mock.method(ContentHashIndex.prototype, "save");
|
|
30
|
+
try {
|
|
31
|
+
const storage = new StorageManager(dir);
|
|
32
|
+
|
|
33
|
+
// Write 10 facts with the defer flag — the per-fact hot path in
|
|
34
|
+
// extraction persist. None of them should flush the index.
|
|
35
|
+
for (let i = 0; i < 10; i += 1) {
|
|
36
|
+
await storage.writeMemory("fact", `deferred fact number ${i}`, {
|
|
37
|
+
source: "extraction",
|
|
38
|
+
deferHashIndexSave: true,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
assert.equal(
|
|
42
|
+
saveSpy.mock.callCount(),
|
|
43
|
+
0,
|
|
44
|
+
"deferred fact writes must not flush the index per fact (was 10 flushes)",
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// All 10 hashes are live in the in-memory index (dirty), so same-session
|
|
48
|
+
// dedup still works despite no disk flush.
|
|
49
|
+
for (let i = 0; i < 10; i += 1) {
|
|
50
|
+
assert.equal(await storage.hasFactContentHash(`deferred fact number ${i}`), true);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// One authoritative batch save (mirrors saveContentHashIndexes) persists
|
|
54
|
+
// the whole superset in a single rewrite.
|
|
55
|
+
saveSpy.mock.resetCalls();
|
|
56
|
+
const index = await (storage as unknown as {
|
|
57
|
+
getFactHashIndex: () => Promise<ContentHashIndex>;
|
|
58
|
+
}).getFactHashIndex.call(storage);
|
|
59
|
+
await index.save();
|
|
60
|
+
assert.equal(saveSpy.mock.callCount(), 1, "exactly one batch save, down from 10+");
|
|
61
|
+
} finally {
|
|
62
|
+
saveSpy.mock.restore();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// A fresh session (new StorageManager) sees every hash on disk.
|
|
66
|
+
const reopened = new StorageManager(dir);
|
|
67
|
+
for (let i = 0; i < 10; i += 1) {
|
|
68
|
+
assert.equal(
|
|
69
|
+
await reopened.hasFactContentHash(`deferred fact number ${i}`),
|
|
70
|
+
true,
|
|
71
|
+
"batch-saved hashes survive into a new session",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("direct writeMemory('fact') without the flag persists the hash via the LOCKED reconcile", async () => {
|
|
78
|
+
// PR #2016 thread SDyCk: a direct (non-deferred) fact write used to publish
|
|
79
|
+
// fact-hashes.txt with the unlocked whole-file save(), which can clobber — or
|
|
80
|
+
// be clobbered by — a peer's concurrent locked rebuild/reconcile. It now flushes
|
|
81
|
+
// through the SAME cross-process locked reconcile the batch/append/rebuild paths
|
|
82
|
+
// use (saveMergingWithDisk), never the unlocked save().
|
|
83
|
+
await withMemoryDir(async (dir) => {
|
|
84
|
+
const saveSpy = mock.method(ContentHashIndex.prototype, "save");
|
|
85
|
+
const reconcileSpy = mock.method(ContentHashIndex.prototype, "saveMergingWithDisk");
|
|
86
|
+
try {
|
|
87
|
+
const storage = new StorageManager(dir);
|
|
88
|
+
await storage.writeMemory("fact", "single write fact", { source: "manual" });
|
|
89
|
+
assert.ok(
|
|
90
|
+
reconcileSpy.mock.callCount() >= 1,
|
|
91
|
+
"single-write callers flush via the locked reconcile (saveMergingWithDisk)",
|
|
92
|
+
);
|
|
93
|
+
assert.equal(
|
|
94
|
+
saveSpy.mock.callCount(),
|
|
95
|
+
0,
|
|
96
|
+
"the direct write must NOT use the unlocked whole-file save()",
|
|
97
|
+
);
|
|
98
|
+
assert.equal(await storage.hasFactContentHash("single write fact"), true);
|
|
99
|
+
} finally {
|
|
100
|
+
saveSpy.mock.restore();
|
|
101
|
+
reconcileSpy.mock.restore();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// No batch save was performed by the caller, yet a fresh session still finds
|
|
105
|
+
// the hash — it was flushed at write time (crash-safe).
|
|
106
|
+
const reopened = new StorageManager(dir);
|
|
107
|
+
assert.equal(await reopened.hasFactContentHash("single write fact"), true);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("#2016 thread SDzOT: a fact hash added during the authoritative rebuild's corpus scan is not lost", async () => {
|
|
112
|
+
// The rebuild used to build a fresh `factOnly` set and publish it with
|
|
113
|
+
// `this.factOnlyHashes = factOnly`, dropping any concurrent in-process
|
|
114
|
+
// writeMemory that added to the LIVE set during the readAllMemories /
|
|
115
|
+
// readAllColdMemories awaits. The rebuild now repopulates the live set in place
|
|
116
|
+
// (clear + add), so a concurrent add survives publication — exactly as the
|
|
117
|
+
// shared index preserves concurrent adds by mutating its `hashes` set in place.
|
|
118
|
+
await withMemoryDir(async (dir) => {
|
|
119
|
+
const storage = new StorageManager(dir);
|
|
120
|
+
// One durable fact so the rebuild has corpus content to scan.
|
|
121
|
+
await storage.writeMemory("fact", "alpha established fact", { source: "extraction" });
|
|
122
|
+
|
|
123
|
+
const s = storage as unknown as {
|
|
124
|
+
factOnlyHashes: Set<string>;
|
|
125
|
+
factHashIndexAuthoritative: boolean | null;
|
|
126
|
+
readAllColdMemories: () => Promise<unknown[]>;
|
|
127
|
+
ensureFactHashIndexAuthoritative: () => Promise<boolean>;
|
|
128
|
+
};
|
|
129
|
+
const sentinel = "sentinel-concurrent-fact-hash";
|
|
130
|
+
const realCold = s.readAllColdMemories.bind(storage);
|
|
131
|
+
let injected = false;
|
|
132
|
+
// Interpose on the SECOND corpus await inside the rebuild (after clear()):
|
|
133
|
+
// simulate a concurrent writeMemory adding a fact hash to the live set while
|
|
134
|
+
// the rebuild is mid-scan.
|
|
135
|
+
s.readAllColdMemories = async () => {
|
|
136
|
+
const res = await realCold();
|
|
137
|
+
if (!injected) {
|
|
138
|
+
injected = true;
|
|
139
|
+
s.factOnlyHashes.add(sentinel);
|
|
140
|
+
}
|
|
141
|
+
return res;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Force a fresh authoritative rebuild (clear -> scan -> publish).
|
|
145
|
+
s.factHashIndexAuthoritative = null;
|
|
146
|
+
assert.equal(await s.ensureFactHashIndexAuthoritative(), true, "rebuild published");
|
|
147
|
+
|
|
148
|
+
assert.equal(
|
|
149
|
+
s.factOnlyHashes.has(sentinel),
|
|
150
|
+
true,
|
|
151
|
+
"a fact hash added during the rebuild scan must survive publication (lost under reassign)",
|
|
152
|
+
);
|
|
153
|
+
// The corpus fact is present too — the in-place repopulate did not drop it.
|
|
154
|
+
assert.equal(await storage.hasFactContentHash("alpha established fact"), true);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("#2016 thread PRRT_kwDORJXyws6SEBri: a hash added during rebuildUnderLock's save() overwrite is not lost", async () => {
|
|
159
|
+
// The plain overwrite save() materializes its body, then awaits disk. A
|
|
160
|
+
// same-process add() that lands during that await used to be dropped: save()
|
|
161
|
+
// unconditionally cleared `added` and set `dirty = false` after the write, so
|
|
162
|
+
// the late hash lived only in memory (never in fact-hashes.txt) with no retry
|
|
163
|
+
// armed. The delta-preserving consume now keeps late deltas pending and arms a
|
|
164
|
+
// durable reconcile retry. We drive the exact interleave deterministically by
|
|
165
|
+
// injecting the add() from the write-key provider, which save() invokes while
|
|
166
|
+
// evaluating writeMaybeEncryptedFile's arguments — AFTER the overwrite body is
|
|
167
|
+
// materialized but BEFORE the disk write resolves.
|
|
168
|
+
await withMemoryDir(async (dir) => {
|
|
169
|
+
const stateDir = path.join(dir, "state");
|
|
170
|
+
const lateContent = "late-arriving-fact-body";
|
|
171
|
+
const lateHash = ContentHashIndex.computeHash(lateContent);
|
|
172
|
+
let idx!: ContentHashIndex;
|
|
173
|
+
let injected = false;
|
|
174
|
+
const injectingWriteKeyProvider = (): Buffer | null => {
|
|
175
|
+
if (!injected) {
|
|
176
|
+
injected = true;
|
|
177
|
+
idx.add(lateContent);
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
};
|
|
181
|
+
idx = new ContentHashIndex(stateDir, () => null, injectingWriteKeyProvider);
|
|
182
|
+
|
|
183
|
+
// Publish an authoritative rebuild under the lock; save() runs inside it.
|
|
184
|
+
const published = await idx.rebuildUnderLock(async () => {
|
|
185
|
+
idx.clear();
|
|
186
|
+
idx.addByHash(ContentHashIndex.computeHash("corpus fact one"));
|
|
187
|
+
idx.addByHash(ContentHashIndex.computeHash("corpus fact two"));
|
|
188
|
+
});
|
|
189
|
+
assert.equal(published, true, "rebuild published under the lock");
|
|
190
|
+
assert.equal(injected, true, "the concurrent add fired during save()'s write window");
|
|
191
|
+
|
|
192
|
+
// Drive the deferred reconcile retry inline. On the buggy source save()
|
|
193
|
+
// cleared dirty and armed nothing, so this is a no-op and the late hash is
|
|
194
|
+
// gone; on the fixed source it republishes the pending delta onto disk.
|
|
195
|
+
await idx.flushReconcileRetry();
|
|
196
|
+
|
|
197
|
+
const fresh = new ContentHashIndex(stateDir);
|
|
198
|
+
await fresh.load();
|
|
199
|
+
assert.ok(
|
|
200
|
+
fresh.has(lateContent),
|
|
201
|
+
"a hash added during the locked rebuild save must reach disk (lost under the unconditional clear)",
|
|
202
|
+
);
|
|
203
|
+
// The rebuilt corpus set survives too — the late-delta handling did not drop it.
|
|
204
|
+
assert.ok(fresh.has("corpus fact one"), "corpus hash one survives the rebuild+reconcile");
|
|
205
|
+
assert.ok(fresh.has("corpus fact two"), "corpus hash two survives the rebuild+reconcile");
|
|
206
|
+
assert.equal(ContentHashIndex.computeHash(lateContent), lateHash);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("promotion-style immediate write survives restart even with fact-hashes.ready present", async () => {
|
|
211
|
+
// Issue #1909 review finding 2: promotion writes (profile/shared) do NOT
|
|
212
|
+
// register with the orchestrator's batch save (no addContentHashDedup), so
|
|
213
|
+
// they must keep their immediate per-fact index save. If they deferred, the
|
|
214
|
+
// hash would be absent from fact-hashes.txt and — with fact-hashes.ready
|
|
215
|
+
// present — a restart would trust the on-disk index (no rebuild) and MISS the
|
|
216
|
+
// hash, re-creating the promoted fact on the next extraction.
|
|
217
|
+
await withMemoryDir(async (dir) => {
|
|
218
|
+
// Warm the index authoritative and create the .ready marker (empty backfill).
|
|
219
|
+
const warm = new StorageManager(dir);
|
|
220
|
+
assert.equal(await warm.hasFactContentHash("nothing yet"), false);
|
|
221
|
+
// Promotion-style write: no deferHashIndexSave → immediate index flush.
|
|
222
|
+
await warm.writeMemory("fact", "promoted profile fact", { source: "extraction" });
|
|
223
|
+
|
|
224
|
+
// A fresh session with .ready present trusts the on-disk index (no rebuild).
|
|
225
|
+
const restarted = new StorageManager(dir);
|
|
226
|
+
assert.equal(
|
|
227
|
+
await restarted.hasFactContentHash("promoted profile fact"),
|
|
228
|
+
true,
|
|
229
|
+
"the promoted hash is on disk and found after restart — no duplicate re-creation",
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("durability: deferred writes with NO batch save are rebuildable from on-disk facts (fail-open)", async () => {
|
|
235
|
+
await withMemoryDir(async (dir) => {
|
|
236
|
+
// Simulate a crash mid-extraction: facts written with deferHashIndexSave but
|
|
237
|
+
// the process dies before saveContentHashIndexes runs. The fact-hash file is
|
|
238
|
+
// never written, but the fact .md files ARE on disk.
|
|
239
|
+
{
|
|
240
|
+
const storage = new StorageManager(dir);
|
|
241
|
+
for (let i = 0; i < 5; i += 1) {
|
|
242
|
+
await storage.writeMemory("fact", `crash fact ${i}`, {
|
|
243
|
+
source: "extraction",
|
|
244
|
+
deferHashIndexSave: true,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
// Intentionally no batch save — the crash window.
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// A new session rebuilds the index authoritatively from the on-disk facts
|
|
251
|
+
// (ensureFactHashIndexAuthoritative), so dedup is correct and no data is lost.
|
|
252
|
+
const recovered = new StorageManager(dir);
|
|
253
|
+
for (let i = 0; i < 5; i += 1) {
|
|
254
|
+
assert.equal(
|
|
255
|
+
await recovered.hasFactContentHash(`crash fact ${i}`),
|
|
256
|
+
true,
|
|
257
|
+
"a lost hash is rebuilt from the durable fact corpus — never a data loss",
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("concurrent deferred fact writes both land in the index (no lost update)", async () => {
|
|
264
|
+
await withMemoryDir(async (dir) => {
|
|
265
|
+
const storage = new StorageManager(dir);
|
|
266
|
+
await Promise.all([
|
|
267
|
+
storage.writeMemory("fact", "concurrent alpha", {
|
|
268
|
+
source: "extraction",
|
|
269
|
+
deferHashIndexSave: true,
|
|
270
|
+
}),
|
|
271
|
+
storage.writeMemory("fact", "concurrent beta", {
|
|
272
|
+
source: "extraction",
|
|
273
|
+
deferHashIndexSave: true,
|
|
274
|
+
}),
|
|
275
|
+
]);
|
|
276
|
+
const index = await (storage as unknown as {
|
|
277
|
+
getFactHashIndex: () => Promise<ContentHashIndex>;
|
|
278
|
+
}).getFactHashIndex.call(storage);
|
|
279
|
+
await index.save();
|
|
280
|
+
|
|
281
|
+
const reopened = new StorageManager(dir);
|
|
282
|
+
assert.equal(await reopened.hasFactContentHash("concurrent alpha"), true);
|
|
283
|
+
assert.equal(await reopened.hasFactContentHash("concurrent beta"), true);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("#1909 round 11: a deferred write with no batch save is rebuilt from the corpus on restart", async () => {
|
|
288
|
+
// No fact-hashes.ready marker exists anymore — the fact-hash index is ALWAYS
|
|
289
|
+
// rebuilt from the durable fact corpus on first use per process. So a deferred
|
|
290
|
+
// write that never got flushed (crash before the batch save) is still deduped
|
|
291
|
+
// after restart because the fact .md is on disk and the rebuild includes it.
|
|
292
|
+
await withMemoryDir(async (dir) => {
|
|
293
|
+
const readyPath = path.join(dir, "state", "fact-hashes.ready");
|
|
294
|
+
const warm = new StorageManager(dir);
|
|
295
|
+
await warm.writeMemory("fact", "windowed fact", {
|
|
296
|
+
source: "extraction",
|
|
297
|
+
deferHashIndexSave: true,
|
|
298
|
+
});
|
|
299
|
+
// CRASH: no batch save. Drop the instance.
|
|
300
|
+
assert.equal(existsSync(readyPath), false, "no ready marker is ever written (round 11)");
|
|
301
|
+
|
|
302
|
+
const restarted = new StorageManager(dir);
|
|
303
|
+
assert.equal(
|
|
304
|
+
await restarted.hasFactContentHash("windowed fact"),
|
|
305
|
+
true,
|
|
306
|
+
"the fact is rebuilt from the corpus — never lost from dedup",
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("#1909 round 11: the fact-hash ready marker is never written (always rebuild on start)", async () => {
|
|
312
|
+
await withMemoryDir(async (dir) => {
|
|
313
|
+
const readyPath = path.join(dir, "state", "fact-hashes.ready");
|
|
314
|
+
const storage = new StorageManager(dir);
|
|
315
|
+
await storage.writeMemory("fact", "a durable fact", { source: "manual" });
|
|
316
|
+
assert.equal(await storage.hasFactContentHash("a durable fact"), true); // triggers rebuild
|
|
317
|
+
assert.equal(existsSync(readyPath), false, "no ready marker written after a rebuild");
|
|
318
|
+
|
|
319
|
+
// A fresh instance rebuilds authoritatively from the corpus and still dedups.
|
|
320
|
+
const restarted = new StorageManager(dir);
|
|
321
|
+
assert.equal(await restarted.hasFactContentHash("a durable fact"), true);
|
|
322
|
+
assert.equal(existsSync(readyPath), false, "still no marker after the restart rebuild");
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("#1909: saveMergingWithDisk unions with a concurrent writer instead of clobbering", async () => {
|
|
327
|
+
// Review round 6 finding 2: two independent index instances (two processes)
|
|
328
|
+
// that both snapshot an empty file must not clobber each other on save.
|
|
329
|
+
await withMemoryDir(async (dir) => {
|
|
330
|
+
const stateDir = path.join(dir, "state");
|
|
331
|
+
await mkdir(stateDir, { recursive: true });
|
|
332
|
+
const a = new ContentHashIndex(stateDir);
|
|
333
|
+
const b = new ContentHashIndex(stateDir);
|
|
334
|
+
await a.load();
|
|
335
|
+
await b.load();
|
|
336
|
+
a.add("interleaved fact A");
|
|
337
|
+
b.add("interleaved fact B");
|
|
338
|
+
|
|
339
|
+
// Interleave the saves: a blind whole-file overwrite by B would drop A's
|
|
340
|
+
// hash; the union-merge preserves both regardless of order.
|
|
341
|
+
await a.saveMergingWithDisk();
|
|
342
|
+
await b.saveMergingWithDisk();
|
|
343
|
+
|
|
344
|
+
const fresh = new ContentHashIndex(stateDir);
|
|
345
|
+
await fresh.load();
|
|
346
|
+
assert.ok(fresh.has("interleaved fact A"), "A's hash preserved after B's concurrent save");
|
|
347
|
+
assert.ok(fresh.has("interleaved fact B"), "B's hash preserved");
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("#1909: concurrent (parallel) merge-saves serialize via the file lock — both survive", async () => {
|
|
352
|
+
// Review round 7 finding 2: the per-file advisory lock serializes the
|
|
353
|
+
// read-union→write window across truly concurrent writers (here two instances
|
|
354
|
+
// saving in parallel), closing the TOCTOU where both read {prior} then both
|
|
355
|
+
// atomically replace and one drop wins.
|
|
356
|
+
await withMemoryDir(async (dir) => {
|
|
357
|
+
const stateDir = path.join(dir, "state");
|
|
358
|
+
await mkdir(stateDir, { recursive: true });
|
|
359
|
+
const a = new ContentHashIndex(stateDir);
|
|
360
|
+
const b = new ContentHashIndex(stateDir);
|
|
361
|
+
await a.load();
|
|
362
|
+
await b.load();
|
|
363
|
+
a.add("parallel fact A");
|
|
364
|
+
b.add("parallel fact B");
|
|
365
|
+
|
|
366
|
+
await Promise.all([a.saveMergingWithDisk(), b.saveMergingWithDisk()]);
|
|
367
|
+
|
|
368
|
+
const fresh = new ContentHashIndex(stateDir);
|
|
369
|
+
await fresh.load();
|
|
370
|
+
assert.ok(fresh.has("parallel fact A"), "A survived the concurrent publish");
|
|
371
|
+
assert.ok(fresh.has("parallel fact B"), "B survived the concurrent publish");
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("#1909: removing a hash and reconcile-saving drops it from disk (no resurrection)", async () => {
|
|
376
|
+
// Review round 7/8: a removal must never resurrect. The reconciling save is
|
|
377
|
+
// removal-aware — it subtracts removed hashes from the latest on-disk state
|
|
378
|
+
// rather than blindly unioning the pre-removal contents back in.
|
|
379
|
+
await withMemoryDir(async (dir) => {
|
|
380
|
+
const stateDir = path.join(dir, "state");
|
|
381
|
+
await mkdir(stateDir, { recursive: true });
|
|
382
|
+
const idx = new ContentHashIndex(stateDir);
|
|
383
|
+
await idx.load();
|
|
384
|
+
idx.add("archived fact body");
|
|
385
|
+
await idx.save();
|
|
386
|
+
const mid = new ContentHashIndex(stateDir);
|
|
387
|
+
await mid.load();
|
|
388
|
+
assert.ok(mid.has("archived fact body"), "hash is on disk before archival");
|
|
389
|
+
|
|
390
|
+
// Archival: remove + reconciling save (the production saveContentHashIndexes path).
|
|
391
|
+
idx.remove("archived fact body");
|
|
392
|
+
await idx.saveMergingWithDisk();
|
|
393
|
+
|
|
394
|
+
const fresh = new ContentHashIndex(stateDir);
|
|
395
|
+
await fresh.load();
|
|
396
|
+
assert.equal(
|
|
397
|
+
fresh.has("archived fact body"),
|
|
398
|
+
false,
|
|
399
|
+
"removed hash stays gone → re-extraction of that content is allowed",
|
|
400
|
+
);
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("#1909: reconciling removal drops the removed hash AND preserves a concurrent append", async () => {
|
|
405
|
+
// Review round 8 thread 3: removal and append batches serialize under the same
|
|
406
|
+
// per-file lock and both reconcile with the latest on-disk state — so a removal
|
|
407
|
+
// neither resurrects its own hash nor clobbers a concurrent extraction's append.
|
|
408
|
+
await withMemoryDir(async (dir) => {
|
|
409
|
+
const stateDir = path.join(dir, "state");
|
|
410
|
+
await mkdir(stateDir, { recursive: true });
|
|
411
|
+
const seed = new ContentHashIndex(stateDir);
|
|
412
|
+
await seed.load();
|
|
413
|
+
seed.add("to be archived");
|
|
414
|
+
await seed.save();
|
|
415
|
+
|
|
416
|
+
// Remover loads {archived} and marks it removed (not yet published).
|
|
417
|
+
const remover = new ContentHashIndex(stateDir);
|
|
418
|
+
await remover.load();
|
|
419
|
+
remover.remove("to be archived");
|
|
420
|
+
|
|
421
|
+
// Concurrent appender (another instance) adds a new hash and publishes first.
|
|
422
|
+
const appender = new ContentHashIndex(stateDir);
|
|
423
|
+
await appender.load();
|
|
424
|
+
appender.add("freshly appended");
|
|
425
|
+
await appender.saveMergingWithDisk();
|
|
426
|
+
|
|
427
|
+
// Remover reconciles against the now-updated disk: drops its removal, keeps
|
|
428
|
+
// the concurrent append.
|
|
429
|
+
await remover.saveMergingWithDisk();
|
|
430
|
+
|
|
431
|
+
const fresh = new ContentHashIndex(stateDir);
|
|
432
|
+
await fresh.load();
|
|
433
|
+
assert.equal(fresh.has("to be archived"), false, "removed hash dropped — no resurrection");
|
|
434
|
+
assert.ok(fresh.has("freshly appended"), "concurrent append preserved — no clobber");
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test("#1909: saveMergingWithDisk is a no-op when the index is not dirty (dirty short-circuit)", async () => {
|
|
439
|
+
// Review round 7 finding 3: a no-fact/deduped run must not re-read+rewrite the
|
|
440
|
+
// whole fact-hashes.txt. An unmutated index skips the O(file) work entirely.
|
|
441
|
+
await withMemoryDir(async (dir) => {
|
|
442
|
+
const stateDir = path.join(dir, "state");
|
|
443
|
+
await mkdir(stateDir, { recursive: true });
|
|
444
|
+
const seed = new ContentHashIndex(stateDir);
|
|
445
|
+
await seed.load();
|
|
446
|
+
seed.add("seed fact");
|
|
447
|
+
await seed.save();
|
|
448
|
+
|
|
449
|
+
// A fresh instance that only LOADS (adds nothing) is not dirty → merge-save
|
|
450
|
+
// writes nothing. Prove it by making a foreign writer append, then a clean
|
|
451
|
+
// merge-save must NOT clobber that foreign row.
|
|
452
|
+
const clean = new ContentHashIndex(stateDir);
|
|
453
|
+
await clean.load();
|
|
454
|
+
const foreign = new ContentHashIndex(stateDir);
|
|
455
|
+
await foreign.load();
|
|
456
|
+
foreign.add("foreign fact");
|
|
457
|
+
await foreign.save();
|
|
458
|
+
|
|
459
|
+
await clean.saveMergingWithDisk(); // not dirty → must be a no-op
|
|
460
|
+
|
|
461
|
+
const fresh = new ContentHashIndex(stateDir);
|
|
462
|
+
await fresh.load();
|
|
463
|
+
assert.ok(fresh.has("seed fact"));
|
|
464
|
+
assert.ok(fresh.has("foreign fact"), "a non-dirty merge-save did not rewrite/clobber the file");
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Issue #1909 review round 14: the markerless authoritative rebuild
|
|
469
|
+
// (ensureFactHashIndexAuthoritative) used to read ONLY the hot tier
|
|
470
|
+
// (readAllMemories). A fact or procedure demoted to the cold tier is still
|
|
471
|
+
// active, so a restart dropped its content-hash from the dedup index and the
|
|
472
|
+
// next extraction re-created the demoted memory. The rebuild now unions the hot
|
|
473
|
+
// AND cold tiers (readAllColdMemories), so hashes survive a hot/cold restart.
|
|
474
|
+
|
|
475
|
+
test("#1909: rebuild indexes ACTIVE cold-tier facts; a demoted fact's hash survives restart", async () => {
|
|
476
|
+
await withMemoryDir(async (dir) => {
|
|
477
|
+
StorageManager.clearAllStaticCaches();
|
|
478
|
+
const storage = new StorageManager(dir);
|
|
479
|
+
|
|
480
|
+
// Two facts: one stays hot, one is demoted to cold.
|
|
481
|
+
await storage.writeMemory("fact", "hot tier fact stays put", { source: "manual" });
|
|
482
|
+
await storage.writeMemory("fact", "cold tier fact was demoted", { source: "manual" });
|
|
483
|
+
|
|
484
|
+
storage.invalidateAllMemoriesCacheForDir();
|
|
485
|
+
const demoted = (await storage.readAllMemories()).find(
|
|
486
|
+
(m) => m.content.includes("cold tier fact was demoted"),
|
|
487
|
+
);
|
|
488
|
+
assert.ok(demoted, "the fact to demote must be readable in hot before migration");
|
|
489
|
+
await storage.migrateMemoryToTier(demoted!, "cold");
|
|
490
|
+
|
|
491
|
+
// Simulate a process restart: drop every static/in-memory cache and open a
|
|
492
|
+
// fresh StorageManager over the same baseDir. Its first dedup lookup forces
|
|
493
|
+
// a full corpus rebuild with no marker to trust.
|
|
494
|
+
StorageManager.clearAllStaticCaches();
|
|
495
|
+
const reopened = new StorageManager(dir);
|
|
496
|
+
|
|
497
|
+
// The cold-demoted fact must be confirmed present in the rebuilt index (this
|
|
498
|
+
// FAILS on a hot-only rebuild), and the hot fact must still be present too.
|
|
499
|
+
assert.equal(
|
|
500
|
+
await reopened.hasFactContentHash("cold tier fact was demoted"),
|
|
501
|
+
true,
|
|
502
|
+
"a fact demoted to cold must survive the corpus rebuild (hot+cold union)",
|
|
503
|
+
);
|
|
504
|
+
assert.equal(
|
|
505
|
+
await reopened.hasFactContentHash("hot tier fact stays put"),
|
|
506
|
+
true,
|
|
507
|
+
"the hot-tier fact must also survive the rebuild",
|
|
508
|
+
);
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// PR #2016: when saveMergingWithDisk cannot acquire the cross-process lock in
|
|
513
|
+
// time, it used to resolve "successfully" while retaining the added hash ONLY
|
|
514
|
+
// in this process's memory — a silent gap. A long-lived peer that already built
|
|
515
|
+
// its authoritative in-memory index would never see the write, and no later
|
|
516
|
+
// save was guaranteed. The fix schedules a bounded, durable background retry
|
|
517
|
+
// that keeps re-attempting the locked publish until the addition lands.
|
|
518
|
+
//
|
|
519
|
+
// These tests hold the advisory lock deterministically (write the <index>.lock
|
|
520
|
+
// file with a fresh mtime so it is neither acquirable nor stale-breakable within
|
|
521
|
+
// the short test window) and use small injected lock/retry timings.
|
|
522
|
+
//
|
|
523
|
+
// ts-no-test-timers exception (same rationale as serialize-mutations.test.ts):
|
|
524
|
+
// the retry is driven by a REAL setTimeout coupled to withHeldFileLock's REAL
|
|
525
|
+
// filesystem mtime-staleness + poll loop — fake timers cannot advance the fs
|
|
526
|
+
// lock's platform clock, so time control here must be real. We never guess a
|
|
527
|
+
// duration: each test awaits the code's own `whenReconcileRetrySettled()`
|
|
528
|
+
// signal. The retry timer is `unref`'d in production so a pending retry never
|
|
529
|
+
// keeps a daemon alive, which means an awaiting test needs a ref'd keep-alive
|
|
530
|
+
// interval to hold the event loop open until that signal fires; it is cleared
|
|
531
|
+
// the instant the retry chain settles.
|
|
532
|
+
|
|
533
|
+
async function withRetryLoopAlive(run: () => Promise<void>): Promise<void> {
|
|
534
|
+
const keepAlive = setInterval(() => {}, 1000);
|
|
535
|
+
try {
|
|
536
|
+
await run();
|
|
537
|
+
} finally {
|
|
538
|
+
clearInterval(keepAlive);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
test("#2016: a lock-timed-out append is NOT silently dropped and eventually persists via durable retry", async () => {
|
|
543
|
+
await withMemoryDir(async (dir) => {
|
|
544
|
+
const stateDir = path.join(dir, "state");
|
|
545
|
+
await mkdir(stateDir, { recursive: true });
|
|
546
|
+
const lockPath = path.join(stateDir, "fact-hashes.txt.lock");
|
|
547
|
+
|
|
548
|
+
// A peer holds the lock: a fresh, non-stale lock file the acquirer cannot take.
|
|
549
|
+
await writeFile(lockPath, `99999 peer-owner ${new Date().toISOString()}\n`);
|
|
550
|
+
|
|
551
|
+
const idx = new ContentHashIndex(stateDir, undefined, undefined, undefined, {
|
|
552
|
+
maxWaitMs: 50,
|
|
553
|
+
pollMs: 10,
|
|
554
|
+
retryBaseMs: 15,
|
|
555
|
+
retryMaxAttempts: 5,
|
|
556
|
+
});
|
|
557
|
+
await idx.load();
|
|
558
|
+
idx.add("deferred-under-contended-lock");
|
|
559
|
+
|
|
560
|
+
// The batch save times out on the lock. It must NOT publish (peer's lock is
|
|
561
|
+
// held) but MUST arm a durable retry instead of resolving as "complete".
|
|
562
|
+
await idx.saveMergingWithDisk();
|
|
563
|
+
assert.equal(idx.hasPendingReconcileRetry, true, "a durable retry must be armed after the lock timeout");
|
|
564
|
+
|
|
565
|
+
// Disk is untouched while the lock is held — proves it did not write unlocked.
|
|
566
|
+
const early = new ContentHashIndex(stateDir);
|
|
567
|
+
await early.load();
|
|
568
|
+
assert.equal(early.has("deferred-under-contended-lock"), false, "no unlocked publish while the peer holds the lock");
|
|
569
|
+
// In-memory dedup still works this session (dirty addition retained).
|
|
570
|
+
assert.equal(idx.has("deferred-under-contended-lock"), true, "addition stays live in-memory");
|
|
571
|
+
|
|
572
|
+
// The peer releases the lock; the background retry must then land the write.
|
|
573
|
+
await rm(lockPath, { force: true });
|
|
574
|
+
await withRetryLoopAlive(() => idx.whenReconcileRetrySettled());
|
|
575
|
+
|
|
576
|
+
assert.equal(idx.hasPendingReconcileRetry, false, "retry chain settled after publishing");
|
|
577
|
+
const fresh = new ContentHashIndex(stateDir);
|
|
578
|
+
await fresh.load();
|
|
579
|
+
assert.ok(
|
|
580
|
+
fresh.has("deferred-under-contended-lock"),
|
|
581
|
+
"the deferred addition eventually reached disk via the durable retry",
|
|
582
|
+
);
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
test("#2016: exhausting retries under a permanently held lock is best-effort, never a fatal append failure", async () => {
|
|
587
|
+
await withMemoryDir(async (dir) => {
|
|
588
|
+
const stateDir = path.join(dir, "state");
|
|
589
|
+
await mkdir(stateDir, { recursive: true });
|
|
590
|
+
const lockPath = path.join(stateDir, "fact-hashes.txt.lock");
|
|
591
|
+
await writeFile(lockPath, `99999 peer-owner ${new Date().toISOString()}\n`);
|
|
592
|
+
|
|
593
|
+
const idx = new ContentHashIndex(stateDir, undefined, undefined, undefined, {
|
|
594
|
+
maxWaitMs: 25,
|
|
595
|
+
pollMs: 10,
|
|
596
|
+
retryBaseMs: 10,
|
|
597
|
+
retryMaxAttempts: 2,
|
|
598
|
+
});
|
|
599
|
+
await idx.load();
|
|
600
|
+
idx.add("never-persistable-while-locked");
|
|
601
|
+
|
|
602
|
+
// The save resolves (best-effort) — a busy lock must never throw and turn a
|
|
603
|
+
// durable-.md append into a fatal failure.
|
|
604
|
+
await assert.doesNotReject(idx.saveMergingWithDisk());
|
|
605
|
+
|
|
606
|
+
// Let the bounded retry chain run to exhaustion (lock is never released).
|
|
607
|
+
await withRetryLoopAlive(() => idx.whenReconcileRetrySettled());
|
|
608
|
+
|
|
609
|
+
assert.equal(idx.hasPendingReconcileRetry, false, "retries are bounded — the chain gives up, it does not spin forever");
|
|
610
|
+
// The addition is still held in-memory (dedup stays correct this session);
|
|
611
|
+
// disk stays empty, so the corpus-rebuild-on-restart safety net covers it.
|
|
612
|
+
assert.equal(idx.has("never-persistable-while-locked"), true, "addition retained in-memory after giving up");
|
|
613
|
+
const fresh = new ContentHashIndex(stateDir);
|
|
614
|
+
await fresh.load();
|
|
615
|
+
assert.equal(fresh.has("never-persistable-while-locked"), false, "nothing written unlocked while the lock stayed held");
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
test("#2016: successive lock-timed-out saves do not stack duplicate retries (reentrancy guard)", async () => {
|
|
620
|
+
await withMemoryDir(async (dir) => {
|
|
621
|
+
const stateDir = path.join(dir, "state");
|
|
622
|
+
await mkdir(stateDir, { recursive: true });
|
|
623
|
+
const lockPath = path.join(stateDir, "fact-hashes.txt.lock");
|
|
624
|
+
await writeFile(lockPath, `99999 peer-owner ${new Date().toISOString()}\n`);
|
|
625
|
+
|
|
626
|
+
const idx = new ContentHashIndex(stateDir, undefined, undefined, undefined, {
|
|
627
|
+
maxWaitMs: 40,
|
|
628
|
+
pollMs: 10,
|
|
629
|
+
retryBaseMs: 500, // long enough that neither timed-out save fires the retry mid-test
|
|
630
|
+
retryMaxAttempts: 5,
|
|
631
|
+
});
|
|
632
|
+
await idx.load();
|
|
633
|
+
idx.add("first-deferred");
|
|
634
|
+
await idx.saveMergingWithDisk();
|
|
635
|
+
assert.equal(idx.hasPendingReconcileRetry, true, "first timeout arms exactly one retry");
|
|
636
|
+
|
|
637
|
+
// A second timed-out save while a retry is already armed must be a no-op for
|
|
638
|
+
// scheduling — the single armed timer is the reentrancy guard.
|
|
639
|
+
idx.add("second-deferred");
|
|
640
|
+
await idx.saveMergingWithDisk();
|
|
641
|
+
assert.equal(idx.hasPendingReconcileRetry, true, "still exactly one retry armed (no duplicate/parallel retries)");
|
|
642
|
+
|
|
643
|
+
// Release the lock and let the single retry drain BOTH deferred additions in
|
|
644
|
+
// one locked publish.
|
|
645
|
+
await rm(lockPath, { force: true });
|
|
646
|
+
await withRetryLoopAlive(() => idx.whenReconcileRetrySettled());
|
|
647
|
+
|
|
648
|
+
const fresh = new ContentHashIndex(stateDir);
|
|
649
|
+
await fresh.load();
|
|
650
|
+
assert.ok(fresh.has("first-deferred"), "first deferred addition persisted");
|
|
651
|
+
assert.ok(fresh.has("second-deferred"), "second deferred addition persisted by the same single retry");
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// PR #2016 (startup hash-index rebuild race): ensureFactHashIndexAuthoritative
|
|
656
|
+
// used to run its corpus scan and publish with an UNLOCKED clear→scan→save().
|
|
657
|
+
// A peer process that committed a newer hash to fact-hashes.txt under the
|
|
658
|
+
// per-file lock — or landed a deferred reconcile-retry — in the window between
|
|
659
|
+
// the rebuild's corpus scan and its overwrite was silently clobbered, and the
|
|
660
|
+
// in-process authoritative flag then suppressed any further rebuild that
|
|
661
|
+
// session. The rebuild now runs its scan + publish under the SAME per-file lock
|
|
662
|
+
// the reconciling saves use (ContentHashIndex.rebuildUnderLock): a concurrent
|
|
663
|
+
// locked writer serializes against it and its addition is preserved (reconciled
|
|
664
|
+
// on top), and a contended lock defers the rebuild rather than publishing an
|
|
665
|
+
// unlocked overwrite. Same real-timer rationale as the retry tests above.
|
|
666
|
+
|
|
667
|
+
test("#2016: rebuildUnderLock publishes the rebuilt set under an uncontended lock", async () => {
|
|
668
|
+
await withMemoryDir(async (dir) => {
|
|
669
|
+
const stateDir = path.join(dir, "state");
|
|
670
|
+
await mkdir(stateDir, { recursive: true });
|
|
671
|
+
const idx = new ContentHashIndex(stateDir);
|
|
672
|
+
await idx.load();
|
|
673
|
+
|
|
674
|
+
const published = await idx.rebuildUnderLock(async () => {
|
|
675
|
+
idx.clear();
|
|
676
|
+
idx.addByHash(ContentHashIndex.computeHash("rebuilt-fact"));
|
|
677
|
+
});
|
|
678
|
+
assert.equal(published, true, "rebuild published under an uncontended lock");
|
|
679
|
+
|
|
680
|
+
const fresh = new ContentHashIndex(stateDir);
|
|
681
|
+
await fresh.load();
|
|
682
|
+
assert.ok(fresh.has("rebuilt-fact"), "the rebuilt hash reached disk under the lock");
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
test("#2016: rebuildUnderLock refuses to publish while a peer holds the lock (no unlocked overwrite)", async () => {
|
|
687
|
+
await withMemoryDir(async (dir) => {
|
|
688
|
+
const stateDir = path.join(dir, "state");
|
|
689
|
+
await mkdir(stateDir, { recursive: true });
|
|
690
|
+
// A peer already committed a hash to disk.
|
|
691
|
+
const peerHash = ContentHashIndex.computeHash("peer-committed-fact");
|
|
692
|
+
await writeFile(path.join(stateDir, "fact-hashes.txt"), `${peerHash}\n`);
|
|
693
|
+
// The peer holds the advisory lock (fresh, non-stale, not stale-breakable).
|
|
694
|
+
const lockPath = path.join(stateDir, "fact-hashes.txt.lock");
|
|
695
|
+
await writeFile(lockPath, `99999 peer-owner ${new Date().toISOString()}\n`);
|
|
696
|
+
|
|
697
|
+
const idx = new ContentHashIndex(stateDir, undefined, undefined, undefined, {
|
|
698
|
+
maxWaitMs: 50,
|
|
699
|
+
pollMs: 10,
|
|
700
|
+
});
|
|
701
|
+
await idx.load();
|
|
702
|
+
let populateRan = false;
|
|
703
|
+
const published = await idx.rebuildUnderLock(async () => {
|
|
704
|
+
populateRan = true;
|
|
705
|
+
idx.clear();
|
|
706
|
+
idx.addByHash(ContentHashIndex.computeHash("rebuild-would-clobber"));
|
|
707
|
+
});
|
|
708
|
+
assert.equal(published, false, "rebuild does not publish when the lock is contended");
|
|
709
|
+
assert.equal(populateRan, false, "populate never runs without the lock — no half-cleared state");
|
|
710
|
+
|
|
711
|
+
// Disk is untouched: the peer's committed hash is preserved and the
|
|
712
|
+
// rebuild's set was NOT written unlocked.
|
|
713
|
+
const disk = new ContentHashIndex(stateDir);
|
|
714
|
+
await disk.load();
|
|
715
|
+
assert.ok(disk.has("peer-committed-fact"), "peer's committed hash preserved (not clobbered)");
|
|
716
|
+
assert.equal(disk.has("rebuild-would-clobber"), false, "no unlocked overwrite while the lock is held");
|
|
717
|
+
// The rebuilder's in-memory index is non-destructive (still the loaded set).
|
|
718
|
+
assert.ok(idx.has("peer-committed-fact"), "loaded set retained in-memory after a deferred rebuild");
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
test("#2016: a rebuild serializes with a concurrent locked writer — the peer's addition is preserved, not clobbered", async () => {
|
|
723
|
+
await withMemoryDir(async (dir) => {
|
|
724
|
+
const stateDir = path.join(dir, "state");
|
|
725
|
+
await mkdir(stateDir, { recursive: true });
|
|
726
|
+
|
|
727
|
+
const rebuilder = new ContentHashIndex(stateDir);
|
|
728
|
+
await rebuilder.load();
|
|
729
|
+
const peer = new ContentHashIndex(stateDir, undefined, undefined, undefined, {
|
|
730
|
+
maxWaitMs: 50,
|
|
731
|
+
pollMs: 10,
|
|
732
|
+
retryBaseMs: 15,
|
|
733
|
+
retryMaxAttempts: 5,
|
|
734
|
+
});
|
|
735
|
+
await peer.load();
|
|
736
|
+
peer.add("peer-append-during-rebuild");
|
|
737
|
+
|
|
738
|
+
let peerBlockedDuringRebuild = false;
|
|
739
|
+
const published = await rebuilder.rebuildUnderLock(async () => {
|
|
740
|
+
rebuilder.clear();
|
|
741
|
+
rebuilder.addByHash(ContentHashIndex.computeHash("rebuilt-corpus-fact"));
|
|
742
|
+
// While the rebuild holds the lock the peer's locked publish must NOT be
|
|
743
|
+
// able to interleave: it times out and arms a durable retry instead of
|
|
744
|
+
// writing. This is also the deadlock-freedom proof — the non-reentrant
|
|
745
|
+
// file lock yields acquired=false rather than blocking forever.
|
|
746
|
+
await peer.saveMergingWithDisk();
|
|
747
|
+
peerBlockedDuringRebuild = peer.hasPendingReconcileRetry;
|
|
748
|
+
});
|
|
749
|
+
assert.equal(published, true, "the rebuild published under the lock");
|
|
750
|
+
assert.equal(
|
|
751
|
+
peerBlockedDuringRebuild,
|
|
752
|
+
true,
|
|
753
|
+
"the peer could not acquire the lock while the rebuild held it (serialized)",
|
|
754
|
+
);
|
|
755
|
+
|
|
756
|
+
// The peer's deferred retry lands after the rebuild releases the lock and
|
|
757
|
+
// reconciles ON TOP of the freshly-published set — losing neither hash.
|
|
758
|
+
await withRetryLoopAlive(() => peer.whenReconcileRetrySettled());
|
|
759
|
+
|
|
760
|
+
const fresh = new ContentHashIndex(stateDir);
|
|
761
|
+
await fresh.load();
|
|
762
|
+
assert.ok(fresh.has("rebuilt-corpus-fact"), "the rebuild's set was published");
|
|
763
|
+
assert.ok(
|
|
764
|
+
fresh.has("peer-append-during-rebuild"),
|
|
765
|
+
"the peer's concurrent addition survived — reconciled on top of the rebuild, never clobbered",
|
|
766
|
+
);
|
|
767
|
+
});
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
test("#2016: a peer's committed fact survives a fresh-session authoritative rebuild (multi-process)", async () => {
|
|
771
|
+
await withMemoryDir(async (dir) => {
|
|
772
|
+
// Process A: writes a fact (real .md + committed hash).
|
|
773
|
+
const peer = new StorageManager(dir);
|
|
774
|
+
await peer.writeMemory("fact", "durable multiprocess fact", { source: "extraction" });
|
|
775
|
+
|
|
776
|
+
// Process B: a fresh session rebuilds authoritatively under the lock and
|
|
777
|
+
// still dedups the peer's fact (corpus scan + locked publish through
|
|
778
|
+
// ensureFactHashIndexAuthoritative → rebuildUnderLock).
|
|
779
|
+
const rebuilder = new StorageManager(dir);
|
|
780
|
+
assert.equal(
|
|
781
|
+
await rebuilder.hasFactContentHash("durable multiprocess fact"),
|
|
782
|
+
true,
|
|
783
|
+
"the peer's fact is deduped after a fresh-session locked rebuild",
|
|
784
|
+
);
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
// PR #2016 findings 1-2 + 3: when the authoritative rebuild lock cannot be
|
|
789
|
+
// acquired, hasFactContentHash()/getAuthoritativeFactHashIndex() must NOT answer
|
|
790
|
+
// from the stale loaded snapshot as if it were authoritative; and a deferred
|
|
791
|
+
// lock-timeout append must be drainable inline at a short-lived writer's
|
|
792
|
+
// shutdown boundary rather than relying on the unref'd background timer.
|
|
793
|
+
//
|
|
794
|
+
// These tests hold the advisory lock deterministically (a fresh, non-stale
|
|
795
|
+
// <index>.lock the acquirer cannot take within the short window) and inject
|
|
796
|
+
// tight lock/retry budgets so the miss path is exercised without real
|
|
797
|
+
// multi-second waits. Same real-timer rationale as the retry tests above.
|
|
798
|
+
|
|
799
|
+
test("#2016 finding 1: a lock-contended miss is verified against the corpus, never a stale-snapshot false miss", async () => {
|
|
800
|
+
await withMemoryDir(async (dir) => {
|
|
801
|
+
// Process A writes a durable fact (.md corpus + fact-hashes.txt).
|
|
802
|
+
const writer = new StorageManager(dir);
|
|
803
|
+
await writer.writeMemory("fact", "durable fact under contention", { source: "extraction" });
|
|
804
|
+
|
|
805
|
+
// Drop the on-disk hash index so a fresh session's LOADED snapshot is EMPTY,
|
|
806
|
+
// but keep the durable .md — the corpus is now the only source of truth.
|
|
807
|
+
await rm(path.join(dir, "state", "fact-hashes.txt"), { force: true });
|
|
808
|
+
|
|
809
|
+
// A peer holds the rebuild lock so process B cannot become authoritative.
|
|
810
|
+
const lockPath = path.join(dir, "state", "fact-hashes.txt.lock");
|
|
811
|
+
await mkdir(path.dirname(lockPath), { recursive: true });
|
|
812
|
+
await writeFile(lockPath, `99999 peer-owner ${new Date().toISOString()}\n`);
|
|
813
|
+
|
|
814
|
+
try {
|
|
815
|
+
const reader = new StorageManager(dir);
|
|
816
|
+
reader.factHashIndexLockOptions = { maxWaitMs: 40, pollMs: 10, retryMaxAttempts: 2, retryBaseMs: 10 };
|
|
817
|
+
|
|
818
|
+
assert.equal(
|
|
819
|
+
await reader.isFactContentHashAuthoritative(),
|
|
820
|
+
false,
|
|
821
|
+
"index cannot be authoritative while the peer holds the rebuild lock",
|
|
822
|
+
);
|
|
823
|
+
assert.equal(
|
|
824
|
+
await reader.hasFactContentHash("durable fact under contention"),
|
|
825
|
+
true,
|
|
826
|
+
"a miss on the empty loaded snapshot is verified against the durable corpus — no false dedup miss",
|
|
827
|
+
);
|
|
828
|
+
assert.equal(
|
|
829
|
+
await reader.hasFactContentHash("was never written anywhere"),
|
|
830
|
+
false,
|
|
831
|
+
"a genuine miss is still a miss under contention (corpus confirms absence)",
|
|
832
|
+
);
|
|
833
|
+
} finally {
|
|
834
|
+
await rm(lockPath, { force: true });
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
test("#2016 finding 2: getAuthoritativeFactHashIndex fails explicitly instead of returning a non-authoritative index", async () => {
|
|
840
|
+
await withMemoryDir(async (dir) => {
|
|
841
|
+
const storage = new StorageManager(dir);
|
|
842
|
+
storage.factHashIndexLockOptions = { maxWaitMs: 40, pollMs: 10, retryMaxAttempts: 2, retryBaseMs: 10 };
|
|
843
|
+
const lockPath = path.join(dir, "state", "fact-hashes.txt.lock");
|
|
844
|
+
await mkdir(path.dirname(lockPath), { recursive: true });
|
|
845
|
+
await writeFile(lockPath, `99999 peer-owner ${new Date().toISOString()}\n`);
|
|
846
|
+
|
|
847
|
+
try {
|
|
848
|
+
await assert.rejects(
|
|
849
|
+
() => storage.getAuthoritativeFactHashIndex(),
|
|
850
|
+
(err: unknown) => err instanceof FactHashIndexNotAuthoritativeError,
|
|
851
|
+
"must throw rather than return a stale/non-authoritative index",
|
|
852
|
+
);
|
|
853
|
+
assert.equal(
|
|
854
|
+
await storage.isFactContentHashAuthoritative(),
|
|
855
|
+
false,
|
|
856
|
+
"the non-authoritative state is propagated, not masked as authoritative",
|
|
857
|
+
);
|
|
858
|
+
} finally {
|
|
859
|
+
await rm(lockPath, { force: true });
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Once the lock is free the rebuild publishes and the accessor returns.
|
|
863
|
+
const idx = await storage.getAuthoritativeFactHashIndex();
|
|
864
|
+
assert.ok(idx, "returns the authoritative index after the lock clears");
|
|
865
|
+
assert.equal(await storage.isFactContentHashAuthoritative(), true);
|
|
866
|
+
});
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
test("#2016 finding 3: flushReconcileRetry drains a deferred lock-timeout append to disk at shutdown", async () => {
|
|
870
|
+
await withMemoryDir(async (dir) => {
|
|
871
|
+
const stateDir = path.join(dir, "state");
|
|
872
|
+
await mkdir(stateDir, { recursive: true });
|
|
873
|
+
const lockPath = path.join(stateDir, "fact-hashes.txt.lock");
|
|
874
|
+
await writeFile(lockPath, `99999 peer-owner ${new Date().toISOString()}\n`);
|
|
875
|
+
|
|
876
|
+
const idx = new ContentHashIndex(stateDir, undefined, undefined, undefined, {
|
|
877
|
+
maxWaitMs: 30,
|
|
878
|
+
pollMs: 10,
|
|
879
|
+
retryBaseMs: 10,
|
|
880
|
+
retryMaxAttempts: 5,
|
|
881
|
+
});
|
|
882
|
+
await idx.load();
|
|
883
|
+
idx.add("deferred-drained-on-shutdown");
|
|
884
|
+
await idx.saveMergingWithDisk();
|
|
885
|
+
assert.equal(idx.hasPendingReconcileRetry, true, "the lock timeout armed a background retry");
|
|
886
|
+
|
|
887
|
+
// The peer releases the lock; a short-lived writer drains INLINE at its
|
|
888
|
+
// shutdown boundary instead of relying on the unref'd background timer.
|
|
889
|
+
await rm(lockPath, { force: true });
|
|
890
|
+
await idx.flushReconcileRetry();
|
|
891
|
+
|
|
892
|
+
assert.equal(idx.hasPendingReconcileRetry, false, "no lingering retry after the inline drain");
|
|
893
|
+
const fresh = new ContentHashIndex(stateDir);
|
|
894
|
+
await fresh.load();
|
|
895
|
+
assert.ok(
|
|
896
|
+
fresh.has("deferred-drained-on-shutdown"),
|
|
897
|
+
"the deferred hash reached disk via the inline shutdown drain",
|
|
898
|
+
);
|
|
899
|
+
});
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
// PR #2016 thread SDzOP: the storage-owned removal
|
|
903
|
+
// (removeFactContentHashesForMemories) and the approval re-add
|
|
904
|
+
// (addActiveFactContentHash) used to publish fact-hashes.txt with the unlocked
|
|
905
|
+
// whole-file save(). That republishes THIS instance's cached in-memory set, so a
|
|
906
|
+
// hash a peer appended to disk after this instance last synced — the exact
|
|
907
|
+
// concurrent-index window the reconcile path exists to close — is silently
|
|
908
|
+
// clobbered. Both paths now flush through the SAME cross-process locked,
|
|
909
|
+
// removal-aware reconcile (saveMergingWithDisk) the write/batch/rebuild paths
|
|
910
|
+
// use. This test reproduces the concurrent-append race deterministically: it
|
|
911
|
+
// re-baselines the cached index freshness fingerprint to the peer-advanced file
|
|
912
|
+
// so the removal keeps the authoritative fast path (no corpus rebuild) and its
|
|
913
|
+
// SAVE step alone is exercised — an unlocked save() drops the peer append, the
|
|
914
|
+
// locked reconcile keeps it.
|
|
915
|
+
test("#2016 thread SDzOP: a storage fact-hash removal reconciles under the lock, preserving a concurrent peer append", async () => {
|
|
916
|
+
await withMemoryDir(async (dir) => {
|
|
917
|
+
const stateDir = path.join(dir, "state");
|
|
918
|
+
const storage = new StorageManager(dir);
|
|
919
|
+
|
|
920
|
+
await storage.writeMemory("fact", "alpha stays active", { source: "manual" });
|
|
921
|
+
await storage.writeMemory("fact", "beta gets archived", { source: "manual" });
|
|
922
|
+
|
|
923
|
+
// Make the shared index authoritative and cached (the production hot path).
|
|
924
|
+
assert.equal(await storage.hasFactContentHash("alpha stays active"), true);
|
|
925
|
+
// Test-only access to storage/index internals: the shape is structurally
|
|
926
|
+
// known and a runtime check would be meaningless here (private members).
|
|
927
|
+
const storageInternals = storage as unknown as {
|
|
928
|
+
getFactHashIndex: () => Promise<ContentHashIndex>;
|
|
929
|
+
readAllMemories: () => Promise<MemoryFile[]>;
|
|
930
|
+
};
|
|
931
|
+
const idx = await storageInternals.getFactHashIndex.call(storage);
|
|
932
|
+
|
|
933
|
+
// Archive beta off disk so the removal actually drops its hash (it is no
|
|
934
|
+
// longer owned by an active corpus fact).
|
|
935
|
+
const all = await storageInternals.readAllMemories.call(storage);
|
|
936
|
+
const beta = all.find((m) => (m.content ?? "").includes("beta gets archived"));
|
|
937
|
+
assert.ok(beta, "beta fact must exist in the corpus");
|
|
938
|
+
await rm(beta!.path, { force: true });
|
|
939
|
+
storage.invalidateAllMemoriesCacheForDir();
|
|
940
|
+
|
|
941
|
+
// Concurrent index activity: a peer instance appends a NEW hash under the
|
|
942
|
+
// cross-process lock. The storage's cached index does not know about it.
|
|
943
|
+
const peer = new ContentHashIndex(stateDir);
|
|
944
|
+
await peer.load();
|
|
945
|
+
peer.add("peer concurrent append");
|
|
946
|
+
await peer.saveMergingWithDisk();
|
|
947
|
+
|
|
948
|
+
// Re-baseline the cached index freshness fingerprint to the peer-advanced
|
|
949
|
+
// file so ensureFactHashIndexAuthoritative keeps the fast path (no rebuild),
|
|
950
|
+
// isolating the removal's SAVE step — the exact code the reviewer flagged.
|
|
951
|
+
const idxInternals = idx as unknown as {
|
|
952
|
+
captureSyncedFingerprint: () => Promise<void>;
|
|
953
|
+
};
|
|
954
|
+
await idxInternals.captureSyncedFingerprint.call(idx);
|
|
955
|
+
|
|
956
|
+
const saveSpy = mock.method(ContentHashIndex.prototype, "save");
|
|
957
|
+
const reconcileSpy = mock.method(ContentHashIndex.prototype, "saveMergingWithDisk");
|
|
958
|
+
try {
|
|
959
|
+
await storage.removeFactContentHashesForMemories([beta!]);
|
|
960
|
+
assert.ok(
|
|
961
|
+
reconcileSpy.mock.callCount() >= 1,
|
|
962
|
+
"the removal flushes via the locked reconcile (saveMergingWithDisk)",
|
|
963
|
+
);
|
|
964
|
+
assert.equal(
|
|
965
|
+
saveSpy.mock.callCount(),
|
|
966
|
+
0,
|
|
967
|
+
"the removal must NOT use the unlocked whole-file save()",
|
|
968
|
+
);
|
|
969
|
+
} finally {
|
|
970
|
+
saveSpy.mock.restore();
|
|
971
|
+
reconcileSpy.mock.restore();
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const fresh = new ContentHashIndex(stateDir);
|
|
975
|
+
await fresh.load();
|
|
976
|
+
assert.ok(fresh.has("alpha stays active"), "the surviving fact's hash is intact");
|
|
977
|
+
assert.equal(
|
|
978
|
+
fresh.has("beta gets archived"),
|
|
979
|
+
false,
|
|
980
|
+
"the archived fact's hash is dropped — no resurrection",
|
|
981
|
+
);
|
|
982
|
+
assert.ok(
|
|
983
|
+
fresh.has("peer concurrent append"),
|
|
984
|
+
"the concurrent peer append survives the removal (locked reconcile, not unlocked overwrite)",
|
|
985
|
+
);
|
|
986
|
+
});
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
// PR #2016 thread SD7Tj: the reconcile publish() snapshots this.added at its
|
|
990
|
+
// start, then read+rewrites fact-hashes.txt under the lock. A concurrent
|
|
991
|
+
// add()/remove() that lands during those disk awaits was silently dropped: the
|
|
992
|
+
// old publish cleared this.added/this.removed WHOLESALE and set dirty=false, so
|
|
993
|
+
// the mid-flight delta vanished from memory AND was never persisted. publish now
|
|
994
|
+
// consumes ONLY the deltas it published; late arrivals stay pending (dirty
|
|
995
|
+
// retained) and a bounded durable retry is re-armed to land them.
|
|
996
|
+
test("#2016 thread SD7Tj: a hash added while a reconcile save awaits disk is not lost", async () => {
|
|
997
|
+
await withMemoryDir(async (dir) => {
|
|
998
|
+
const stateDir = path.join(dir, "state");
|
|
999
|
+
await mkdir(stateDir, { recursive: true });
|
|
1000
|
+
|
|
1001
|
+
// The write-key provider is invoked synchronously at publish's write step —
|
|
1002
|
+
// AFTER publish has snapshotted this.added and BEFORE it consumes/clears it.
|
|
1003
|
+
// Injecting an add() there deterministically reproduces a concurrent add
|
|
1004
|
+
// landing in publish's mid-flight window, with no module mocking.
|
|
1005
|
+
let idx!: ContentHashIndex;
|
|
1006
|
+
let injected = false;
|
|
1007
|
+
const writeKeyProvider = (): Buffer | null => {
|
|
1008
|
+
if (!injected) {
|
|
1009
|
+
injected = true;
|
|
1010
|
+
idx.add("added-during-save");
|
|
1011
|
+
}
|
|
1012
|
+
return null;
|
|
1013
|
+
};
|
|
1014
|
+
idx = new ContentHashIndex(stateDir, () => null, writeKeyProvider, undefined, {
|
|
1015
|
+
maxWaitMs: 50,
|
|
1016
|
+
pollMs: 10,
|
|
1017
|
+
retryBaseMs: 10,
|
|
1018
|
+
retryMaxAttempts: 5,
|
|
1019
|
+
});
|
|
1020
|
+
await idx.load();
|
|
1021
|
+
idx.add("added-before-save");
|
|
1022
|
+
|
|
1023
|
+
await idx.saveMergingWithDisk();
|
|
1024
|
+
|
|
1025
|
+
// The pre-save addition published as expected.
|
|
1026
|
+
const afterFirst = new ContentHashIndex(stateDir);
|
|
1027
|
+
await afterFirst.load();
|
|
1028
|
+
assert.equal(afterFirst.has("added-before-save"), true, "the pre-save addition is on disk");
|
|
1029
|
+
|
|
1030
|
+
// The mid-save addition must survive in-memory (old code wiped it via
|
|
1031
|
+
// `this.hashes = merged` + `this.added.clear()`).
|
|
1032
|
+
assert.equal(idx.has("added-during-save"), true, "mid-save addition retained in-memory");
|
|
1033
|
+
|
|
1034
|
+
// ...and it must still be durably persistable — old code set dirty=false so
|
|
1035
|
+
// the drain below would be a no-op and the delta would be lost forever.
|
|
1036
|
+
await withRetryLoopAlive(() => idx.flushReconcileRetry());
|
|
1037
|
+
const fresh = new ContentHashIndex(stateDir);
|
|
1038
|
+
await fresh.load();
|
|
1039
|
+
assert.ok(fresh.has("added-before-save"), "pre-save addition still durable after drain");
|
|
1040
|
+
assert.ok(
|
|
1041
|
+
fresh.has("added-during-save"),
|
|
1042
|
+
"the mid-save addition reached disk — the mid-flight delta was preserved, not dropped",
|
|
1043
|
+
);
|
|
1044
|
+
});
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
// PR #2016 thread SD7Tk: a direct (non-deferred) writeMemory("fact") awaited
|
|
1048
|
+
// saveMergingWithDisk, which on a lock timeout defers to an UNREF'd background
|
|
1049
|
+
// retry and returns without publishing. writeMemory then returned as if the
|
|
1050
|
+
// index were durable while the addition lived only in memory + a timer that a
|
|
1051
|
+
// short-lived process can exit before it fires. writeMemory now drains that
|
|
1052
|
+
// deferred retry inline (flushReconcileRetry) before returning, so a caller can
|
|
1053
|
+
// never observe a lingering, exit-losable retry.
|
|
1054
|
+
test("#2016 thread SD7Tk: a direct fact write drains its deferred hash retry before returning", async () => {
|
|
1055
|
+
await withMemoryDir(async (dir) => {
|
|
1056
|
+
const flushSpy = mock.method(ContentHashIndex.prototype, "flushReconcileRetry");
|
|
1057
|
+
const lockPath = path.join(dir, "state", "fact-hashes.txt.lock");
|
|
1058
|
+
await mkdir(path.dirname(lockPath), { recursive: true });
|
|
1059
|
+
// A peer holds the lock so the direct write's reconcile save times out and
|
|
1060
|
+
// defers to a background retry instead of publishing.
|
|
1061
|
+
await writeFile(lockPath, `99999 peer-owner ${new Date().toISOString()}\n`);
|
|
1062
|
+
try {
|
|
1063
|
+
const storage = new StorageManager(dir);
|
|
1064
|
+
storage.factHashIndexLockOptions = { maxWaitMs: 30, pollMs: 10, retryMaxAttempts: 2, retryBaseMs: 10 };
|
|
1065
|
+
|
|
1066
|
+
await withRetryLoopAlive(async () => {
|
|
1067
|
+
await storage.writeMemory("fact", "direct-write-under-contended-lock", { source: "manual" });
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
// The direct write must have drained the deferred retry inline before
|
|
1071
|
+
// returning. Under the old code writeMemory awaited only
|
|
1072
|
+
// saveMergingWithDisk (which returned with the retry still armed) and never
|
|
1073
|
+
// called flushReconcileRetry — so this count was 0.
|
|
1074
|
+
assert.ok(
|
|
1075
|
+
flushSpy.mock.callCount() >= 1,
|
|
1076
|
+
"direct writeMemory drains the deferred hash retry (flushReconcileRetry) before returning",
|
|
1077
|
+
);
|
|
1078
|
+
} finally {
|
|
1079
|
+
flushSpy.mock.restore();
|
|
1080
|
+
await rm(lockPath, { force: true });
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
});
|
|
1084
|
+
test("#2016 thread SD-nG: a direct re-add of a hash the local snapshot still holds but a peer removed republishes it durably", async () => {
|
|
1085
|
+
await withMemoryDir(async (dir) => {
|
|
1086
|
+
const stateDir = path.join(dir, "state");
|
|
1087
|
+
await mkdir(stateDir, { recursive: true });
|
|
1088
|
+
const CONTENT = "the reintroduced fact content";
|
|
1089
|
+
|
|
1090
|
+
// Peer A registers the hash and publishes it to the shared on-disk index.
|
|
1091
|
+
const peerA = new ContentHashIndex(stateDir);
|
|
1092
|
+
await peerA.load();
|
|
1093
|
+
peerA.add(CONTENT);
|
|
1094
|
+
await peerA.saveMergingWithDisk();
|
|
1095
|
+
|
|
1096
|
+
// Peer B loads the same on-disk index — its in-memory snapshot HOLDS the hash.
|
|
1097
|
+
const peerB = new ContentHashIndex(stateDir);
|
|
1098
|
+
await peerB.load();
|
|
1099
|
+
assert.equal(peerB.has(CONTENT), true, "B loaded the hash from disk");
|
|
1100
|
+
|
|
1101
|
+
// Peer A removes the hash and publishes the removal — disk no longer holds
|
|
1102
|
+
// it, but B's snapshot is now STALE (still holds the hash).
|
|
1103
|
+
peerA.remove(CONTENT);
|
|
1104
|
+
await peerA.saveMergingWithDisk();
|
|
1105
|
+
|
|
1106
|
+
// B directly re-adds the same content. Pre-fix this was a no-op: the local
|
|
1107
|
+
// snapshot already held the hash, so add() recorded no delta and left the
|
|
1108
|
+
// instance not dirty, so B's reconcile short-circuited and the reintroduced
|
|
1109
|
+
// hash never reached disk — a peer view permanently lost it.
|
|
1110
|
+
peerB.add(CONTENT);
|
|
1111
|
+
await peerB.saveMergingWithDisk();
|
|
1112
|
+
|
|
1113
|
+
// A fresh reader (another peer) must now see the reintroduced hash.
|
|
1114
|
+
const reader = new ContentHashIndex(stateDir);
|
|
1115
|
+
await reader.load();
|
|
1116
|
+
assert.equal(
|
|
1117
|
+
reader.has(CONTENT),
|
|
1118
|
+
true,
|
|
1119
|
+
"the reintroduced hash is durably republished for peers (stale re-add recorded as a delta)",
|
|
1120
|
+
);
|
|
1121
|
+
});
|
|
1122
|
+
});
|
|
1123
|
+
test("#2016 thread PRRT_kwDORJXyws6SEHve: addByHash re-add of a hash the local snapshot still holds but a peer removed republishes it durably", async () => {
|
|
1124
|
+
// Parallel to thread SD-nG for add(): the reactivation path
|
|
1125
|
+
// (StorageManager.addActiveFactContentHash) re-registers a hash via addByHash
|
|
1126
|
+
// OUTSIDE a rebuild. Pre-fix, when this instance's in-memory set still held
|
|
1127
|
+
// the hash but a peer had REMOVED it on disk, addByHash no-op'd (hash present
|
|
1128
|
+
// in `hashes`), recording no delta and leaving the instance not dirty. The
|
|
1129
|
+
// subsequent reconcile then short-circuited (not dirty), so the reintroduced
|
|
1130
|
+
// hash never reached disk and the peer view lost it permanently.
|
|
1131
|
+
await withMemoryDir(async (dir) => {
|
|
1132
|
+
const stateDir = path.join(dir, "state");
|
|
1133
|
+
await mkdir(stateDir, { recursive: true });
|
|
1134
|
+
const hash = ContentHashIndex.computeHash("reactivated-fact-body");
|
|
1135
|
+
|
|
1136
|
+
// Peer A publishes the hash to the shared on-disk index.
|
|
1137
|
+
const peerA = new ContentHashIndex(stateDir);
|
|
1138
|
+
await peerA.load();
|
|
1139
|
+
peerA.addByHash(hash);
|
|
1140
|
+
await peerA.saveMergingWithDisk();
|
|
1141
|
+
|
|
1142
|
+
// Peer B loads the same index — its in-memory snapshot HOLDS the hash.
|
|
1143
|
+
const peerB = new ContentHashIndex(stateDir);
|
|
1144
|
+
await peerB.load();
|
|
1145
|
+
assert.equal(peerB.has("reactivated-fact-body"), true, "B loaded the hash from disk");
|
|
1146
|
+
|
|
1147
|
+
// Peer A removes the hash and publishes the removal — disk no longer holds
|
|
1148
|
+
// it, but B's snapshot is now STALE (still holds the hash).
|
|
1149
|
+
peerA.removeByHash(hash);
|
|
1150
|
+
await peerA.saveMergingWithDisk();
|
|
1151
|
+
|
|
1152
|
+
// B re-registers the same hash via addByHash (the reactivation path). Pre-fix
|
|
1153
|
+
// a no-op; fixed, it records the durable delta.
|
|
1154
|
+
peerB.addByHash(hash);
|
|
1155
|
+
await peerB.saveMergingWithDisk();
|
|
1156
|
+
|
|
1157
|
+
const reader = new ContentHashIndex(stateDir);
|
|
1158
|
+
await reader.load();
|
|
1159
|
+
assert.equal(
|
|
1160
|
+
reader.has("reactivated-fact-body"),
|
|
1161
|
+
true,
|
|
1162
|
+
"addByHash on a stale local hit must republish the hash durably for peers",
|
|
1163
|
+
);
|
|
1164
|
+
});
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
test("#2016 thread PRRT_kwDORJXyws6SEHvh: reactivation drains the deferred reconcile retry inline like writeMemory", async () => {
|
|
1168
|
+
// restoreFactHashAfterApproval -> addActiveFactContentHash used to publish the
|
|
1169
|
+
// reintroduced hash with a bare saveMergingWithDisk() and return. On a lock
|
|
1170
|
+
// timeout that call DEFERS to an unref'd background retry WITHOUT publishing,
|
|
1171
|
+
// so a short-lived caller could exit before the hash reached disk. writeMemory
|
|
1172
|
+
// guards this by draining the deferred retry inline (flushReconcileRetry); the
|
|
1173
|
+
// reactivation path is the same lifecycle boundary and must do the same. This
|
|
1174
|
+
// asserts the wiring — the drain's durability semantics are covered by the
|
|
1175
|
+
// ContentHashIndex lock-timeout tests above.
|
|
1176
|
+
await withMemoryDir(async (dir) => {
|
|
1177
|
+
const storage = new StorageManager(dir);
|
|
1178
|
+
const { id } = await storage.writeMemory("fact", "reactivated fact body", { source: "manual" });
|
|
1179
|
+
assert.ok(id, "wrote the fact to reactivate");
|
|
1180
|
+
|
|
1181
|
+
// Reset spies AFTER the write so we only measure the reactivation path
|
|
1182
|
+
// (writeMemory itself also flushes).
|
|
1183
|
+
const reconcileSpy = mock.method(ContentHashIndex.prototype, "saveMergingWithDisk");
|
|
1184
|
+
const flushSpy = mock.method(ContentHashIndex.prototype, "flushReconcileRetry");
|
|
1185
|
+
try {
|
|
1186
|
+
await storage.restoreFactHashAfterApproval(id);
|
|
1187
|
+
assert.ok(
|
|
1188
|
+
reconcileSpy.mock.callCount() >= 1,
|
|
1189
|
+
"reactivation publishes via the locked reconcile (saveMergingWithDisk)",
|
|
1190
|
+
);
|
|
1191
|
+
assert.ok(
|
|
1192
|
+
flushSpy.mock.callCount() >= 1,
|
|
1193
|
+
"reactivation drains the deferred lock-timeout retry inline (flushReconcileRetry) — same durability guarantee as writeMemory",
|
|
1194
|
+
);
|
|
1195
|
+
} finally {
|
|
1196
|
+
reconcileSpy.mock.restore();
|
|
1197
|
+
flushSpy.mock.restore();
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
});
|