@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
package/src/storage.ts
CHANGED
|
@@ -20,13 +20,28 @@ import { log } from "./logger.js";
|
|
|
20
20
|
import { assertMemoryFrontmatterId, warnProjectionFallback } from "./storage-guards.js";
|
|
21
21
|
import { MemoryReadStore } from "./storage/memory-read-store.js";
|
|
22
22
|
import { readMaybeEncryptedLines, readMemoryActionEventRowsFromLines } from "./storage/secure-line-reader.js";
|
|
23
|
+
import {
|
|
24
|
+
appendLifecycleEventsSerialized,
|
|
25
|
+
type DrainPendingLifecycleForSyncResult,
|
|
26
|
+
drainPendingLifecycleLedgerForSync,
|
|
27
|
+
drainPendingLifecycleLedgerIfAny,
|
|
28
|
+
readAllLifecycleEventsFromLedger,
|
|
29
|
+
readAllLifecycleEventsFromLedgerBuffer,
|
|
30
|
+
readBoundedLifecycleEventsFromLedger,
|
|
31
|
+
serializeLifecycleAppendPayload,
|
|
32
|
+
} from "./storage/memory-lifecycle-ledger-access.js";
|
|
23
33
|
import { selfDeps } from "./orchestration/self-deps.js";
|
|
24
34
|
import { EntityStore } from "./storage/entity-store.js";
|
|
25
35
|
import { IdentityContinuityStore } from "./storage/identity-continuity-store.js";
|
|
26
36
|
import { isErrnoCode } from "./utils/errno.js";
|
|
37
|
+
import { withHeldFileLock } from "./utils/serialize-mutations.js";
|
|
27
38
|
import { RECALL_FALLBACK_DIRS, getCategoryDir, categoryDirName } from "./utils/category-dir.js";
|
|
28
39
|
import { assertPathInsideRoot } from "./utils/path-containment.js";
|
|
29
40
|
import { decodeYamlScalar } from "./utils/yaml-scalar.js";
|
|
41
|
+
import {
|
|
42
|
+
qmdCollectionPathParts,
|
|
43
|
+
qmdResultPathCandidates,
|
|
44
|
+
} from "./orchestration/qmd-result-resolver.js";
|
|
30
45
|
import {
|
|
31
46
|
clearMemoryCache,
|
|
32
47
|
getCachedEntities,
|
|
@@ -63,6 +78,7 @@ import {
|
|
|
63
78
|
SecureStoreLockedError,
|
|
64
79
|
MAGIC_HEADER_SIZE,
|
|
65
80
|
isEncryptedFile,
|
|
81
|
+
probeEncryptedRegularFileHeader,
|
|
66
82
|
readMaybeEncryptedFileBuffer,
|
|
67
83
|
readMaybeEncryptedFile,
|
|
68
84
|
writeMaybeEncryptedFile,
|
|
@@ -138,6 +154,35 @@ import type {
|
|
|
138
154
|
ProvenanceSource,
|
|
139
155
|
} from "./types.js";
|
|
140
156
|
import { confidenceTier, SPECULATIVE_TTL_DAYS } from "./types.js";
|
|
157
|
+
import {
|
|
158
|
+
collectStructuredSectionFacts,
|
|
159
|
+
compareEntityTimestamps,
|
|
160
|
+
compileEntityFacts,
|
|
161
|
+
isEntitySynthesisStale,
|
|
162
|
+
isEntitySynthesisTimelinePromotionBullet,
|
|
163
|
+
latestEntityTimelineTimestamp,
|
|
164
|
+
normalizeEntitySectionFact,
|
|
165
|
+
normalizeStructuredSectionFacts,
|
|
166
|
+
parseEntityTimelineBullet,
|
|
167
|
+
partitionEntityStructuredSections,
|
|
168
|
+
serializeEntityTimelineEntry,
|
|
169
|
+
} from "./storage/entity-timeline.js";
|
|
170
|
+
import {
|
|
171
|
+
ContentHashIndex,
|
|
172
|
+
FactHashIndexNotAuthoritativeError,
|
|
173
|
+
CONTENT_HASH_INDEX_RETRY_MAX_DELAY_MS,
|
|
174
|
+
type ContentHashIndexLockOptions,
|
|
175
|
+
} from "./storage/content-hash-index.js";
|
|
176
|
+
export { ContentHashIndex, FactHashIndexNotAuthoritativeError };
|
|
177
|
+
export type { ContentHashIndexLockOptions };
|
|
178
|
+
export {
|
|
179
|
+
compareEntityTimestamps,
|
|
180
|
+
compileEntityFacts,
|
|
181
|
+
countEntityStructuredFacts,
|
|
182
|
+
fingerprintEntityStructuredFacts,
|
|
183
|
+
isEntitySynthesisStale,
|
|
184
|
+
normalizeStructuredSectionFacts,
|
|
185
|
+
} from "./storage/entity-timeline.js";
|
|
141
186
|
import {
|
|
142
187
|
type ProjectedMemoryBrowseOptions,
|
|
143
188
|
type ProjectedMemoryBrowsePage,
|
|
@@ -151,11 +196,16 @@ import {
|
|
|
151
196
|
import {
|
|
152
197
|
inferMemoryStatus,
|
|
153
198
|
isArchivedMemoryPath,
|
|
154
|
-
sortMemoryLifecycleEvents,
|
|
155
199
|
toMemoryPathRel,
|
|
156
200
|
} from "./memory-lifecycle-ledger-utils.js";
|
|
157
201
|
import { normalizeProjectionPreview, normalizeProjectionTags } from "./memory-projection-format.js";
|
|
158
202
|
import { parseFlexibleIsoTimestamp } from "./utils/iso-timestamp.js";
|
|
203
|
+
import {
|
|
204
|
+
composeMemoryEnvelope,
|
|
205
|
+
isSealedMemoryEnvelope,
|
|
206
|
+
sealedWriteToLegacyArgs,
|
|
207
|
+
type SealedMemoryEnvelope,
|
|
208
|
+
} from "./write-envelope.js";
|
|
159
209
|
// stripCitation import removed: legacy rebuild fallback was replaced by a
|
|
160
210
|
// skip-with-warning strategy (Finding 1 — Uhol). See ensureFactHashIndexAuthoritative.
|
|
161
211
|
|
|
@@ -1115,138 +1165,10 @@ function dehyphenate(s: string): string {
|
|
|
1115
1165
|
return s.replace(/-/g, "");
|
|
1116
1166
|
}
|
|
1117
1167
|
|
|
1118
|
-
/**
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
* Prevents writing semantically identical facts.
|
|
1123
|
-
*/
|
|
1124
|
-
export class ContentHashIndex {
|
|
1125
|
-
private hashes: Set<string> = new Set();
|
|
1126
|
-
private dirty = false;
|
|
1127
|
-
private readonly filePath: string;
|
|
1128
|
-
private readonly secureStoreKeyProvider: () => Buffer | null;
|
|
1129
|
-
private readonly secureStoreWriteKeyProvider: () => Buffer | null;
|
|
1130
|
-
private readonly memoryDir: string;
|
|
1131
|
-
|
|
1132
|
-
constructor(
|
|
1133
|
-
stateDir: string,
|
|
1134
|
-
secureStoreKeyProvider: () => Buffer | null = () => null,
|
|
1135
|
-
secureStoreWriteKeyProvider: () => Buffer | null = secureStoreKeyProvider,
|
|
1136
|
-
memoryDir: string = path.dirname(stateDir)
|
|
1137
|
-
) {
|
|
1138
|
-
this.filePath = path.join(stateDir, "fact-hashes.txt");
|
|
1139
|
-
this.secureStoreKeyProvider = secureStoreKeyProvider;
|
|
1140
|
-
this.secureStoreWriteKeyProvider = secureStoreWriteKeyProvider;
|
|
1141
|
-
this.memoryDir = memoryDir;
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
/** Load existing hashes from disk. Safe to call multiple times. */
|
|
1145
|
-
async load(): Promise<void> {
|
|
1146
|
-
try {
|
|
1147
|
-
const raw = await readMaybeEncryptedFile(this.filePath, this.secureStoreKeyProvider(), this.memoryDir);
|
|
1148
|
-
for (const line of raw.split("\n")) {
|
|
1149
|
-
const trimmed = line.trim();
|
|
1150
|
-
if (trimmed.length > 0) {
|
|
1151
|
-
this.hashes.add(trimmed);
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
log.debug(`content-hash index: loaded ${this.hashes.size} hashes`);
|
|
1155
|
-
} catch (err) {
|
|
1156
|
-
if (err instanceof SecureStoreLockedError) throw err;
|
|
1157
|
-
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
1158
|
-
log.debug("content-hash index: no existing index — starting fresh");
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
/** Check if content already exists in the index. */
|
|
1163
|
-
has(content: string): boolean {
|
|
1164
|
-
return this.hashes.has(ContentHashIndex.computeHash(content));
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
/** Add content hash to the index. */
|
|
1168
|
-
add(content: string): void {
|
|
1169
|
-
const hash = ContentHashIndex.computeHash(content);
|
|
1170
|
-
if (!this.hashes.has(hash)) {
|
|
1171
|
-
this.hashes.add(hash);
|
|
1172
|
-
this.dirty = true;
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
get size(): number {
|
|
1177
|
-
return this.hashes.size;
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
/** Clear all loaded hashes so the next save rewrites the index from scratch. */
|
|
1181
|
-
clear(): void {
|
|
1182
|
-
if (this.hashes.size > 0) {
|
|
1183
|
-
this.hashes.clear();
|
|
1184
|
-
}
|
|
1185
|
-
this.dirty = true;
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
/** Persist index to disk if changed. */
|
|
1189
|
-
async save(): Promise<void> {
|
|
1190
|
-
if (!this.dirty) return;
|
|
1191
|
-
await mkdir(path.dirname(this.filePath), { recursive: true });
|
|
1192
|
-
await writeMaybeEncryptedFile(
|
|
1193
|
-
this.filePath,
|
|
1194
|
-
[...this.hashes].join("\n") + "\n",
|
|
1195
|
-
this.secureStoreWriteKeyProvider(),
|
|
1196
|
-
{},
|
|
1197
|
-
this.memoryDir
|
|
1198
|
-
);
|
|
1199
|
-
this.dirty = false;
|
|
1200
|
-
log.debug(`content-hash index: saved ${this.hashes.size} hashes`);
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
/** Remove a hash from the index (used when archiving/deleting). */
|
|
1204
|
-
remove(content: string): void {
|
|
1205
|
-
const hash = ContentHashIndex.computeHash(content);
|
|
1206
|
-
if (this.hashes.delete(hash)) {
|
|
1207
|
-
this.dirty = true;
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
/**
|
|
1212
|
-
* Remove a pre-computed SHA-256 hash directly from the index without
|
|
1213
|
-
* re-hashing. Use this when the caller already holds the stored hash
|
|
1214
|
-
* (e.g. `memory.frontmatter.contentHash`) to avoid the double-hash bug
|
|
1215
|
-
* where `remove(hash)` would compute `hash(hash)` and never match the
|
|
1216
|
-
* entry.
|
|
1217
|
-
*/
|
|
1218
|
-
removeByHash(hash: string): void {
|
|
1219
|
-
if (this.hashes.delete(hash)) {
|
|
1220
|
-
this.dirty = true;
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
/**
|
|
1225
|
-
* Add a pre-computed SHA-256 hash directly to the index without re-hashing.
|
|
1226
|
-
* Use this when the caller already holds the stored hash
|
|
1227
|
-
* (e.g. `memory.frontmatter.contentHash`) so that the index records the raw
|
|
1228
|
-
* content hash rather than re-hashing the citation-annotated body.
|
|
1229
|
-
*
|
|
1230
|
-
* @internal Only called from `StorageManager.ensureFactHashIndexAuthoritative`.
|
|
1231
|
-
* Not part of the public API — prefer `add(content)` for external callers.
|
|
1232
|
-
*/
|
|
1233
|
-
addByHash(hash: string): void {
|
|
1234
|
-
if (!this.hashes.has(hash)) {
|
|
1235
|
-
this.hashes.add(hash);
|
|
1236
|
-
this.dirty = true;
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
/** Normalize content (delegates to content-hash.ts for a single source of truth). */
|
|
1241
|
-
static normalizeContent(content: string): string {
|
|
1242
|
-
return normalizeContent(content);
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
/** Compute SHA-256 hash (delegates to content-hash.ts for a single source of truth). */
|
|
1246
|
-
static computeHash(content: string): string {
|
|
1247
|
-
return computeContentHash(content);
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1168
|
+
/** Bounded attempts to acquire the rebuild lock before ensureFactHashIndexAuthoritative surrenders authority (PR #2016). */
|
|
1169
|
+
const FACT_HASH_INDEX_REBUILD_MAX_ATTEMPTS = 3;
|
|
1170
|
+
/** Base backoff (ms) between rebuild-lock attempts; doubles each attempt, capped at CONTENT_HASH_INDEX_RETRY_MAX_DELAY_MS. */
|
|
1171
|
+
const FACT_HASH_INDEX_REBUILD_RETRY_BASE_MS = 50;
|
|
1250
1172
|
|
|
1251
1173
|
// ---------------------------------------------------------------------------
|
|
1252
1174
|
// Attribute normalization helper
|
|
@@ -1273,16 +1195,14 @@ export class ContentHashIndex {
|
|
|
1273
1195
|
// the coding surfaces + wearable service). Imported here so internal callers
|
|
1274
1196
|
// (snapshotBeforeWrite, snapshotForProvenance) resolve, and re-exported to
|
|
1275
1197
|
// keep the public storage API stable for existing callers (wearables, dist).
|
|
1276
|
-
import { stripAttributesSuffix } from "./structured-attributes.js";
|
|
1198
|
+
import { assemblePersistedBody, stripAttributesSuffix } from "./structured-attributes.js";
|
|
1277
1199
|
export { stripAttributesSuffix };
|
|
1278
1200
|
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
.join("; ");
|
|
1285
|
-
}
|
|
1201
|
+
// `normalizeAttributePairs` moved to ./structured-attributes.ts (issue #1989
|
|
1202
|
+
// PR2) beside `assemblePersistedBody` so the write path and the sealed
|
|
1203
|
+
// envelope composer share ONE assembly definition. Re-exported to keep the
|
|
1204
|
+
// public storage API stable for existing callers.
|
|
1205
|
+
export { normalizeAttributePairs } from "./structured-attributes.js";
|
|
1286
1206
|
|
|
1287
1207
|
// ---------------------------------------------------------------------------
|
|
1288
1208
|
// Entity file parsing / serialization (Knowledge Graph v7.0)
|
|
@@ -1429,471 +1349,6 @@ function readEntitySectionText(
|
|
|
1429
1349
|
return sectionLines.join(options.preserveBullets === true ? "\n" : " ");
|
|
1430
1350
|
}
|
|
1431
1351
|
|
|
1432
|
-
function parseEntityTimelineBullet(bullet: string, fallbackTimestamp: string): EntityTimelineEntry | null {
|
|
1433
|
-
const trimmed = bullet.trim();
|
|
1434
|
-
if (!trimmed) return null;
|
|
1435
|
-
|
|
1436
|
-
let rest = trimmed;
|
|
1437
|
-
const entry: EntityTimelineEntry = {
|
|
1438
|
-
timestamp: trimmed.startsWith("[") ? "" : fallbackTimestamp,
|
|
1439
|
-
text: "",
|
|
1440
|
-
};
|
|
1441
|
-
const consumedMetadataSegments: string[] = [];
|
|
1442
|
-
let literalSingleSourceSegment: string | undefined;
|
|
1443
|
-
|
|
1444
|
-
if (!trimmed.startsWith("[")) {
|
|
1445
|
-
entry.text = trimmed;
|
|
1446
|
-
return entry.text ? entry : null;
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
const firstEnd = trimmed.indexOf("]");
|
|
1450
|
-
if (firstEnd === -1) {
|
|
1451
|
-
entry.text = trimmed;
|
|
1452
|
-
return entry.text ? entry : null;
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
const firstToken = trimmed.slice(1, firstEnd).trim();
|
|
1456
|
-
const parsedTimestamp = Date.parse(firstToken);
|
|
1457
|
-
if (Number.isFinite(parsedTimestamp)) {
|
|
1458
|
-
entry.timestamp = firstToken || fallbackTimestamp;
|
|
1459
|
-
rest = trimmed.slice(firstEnd + 1).trimStart();
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
while (rest.startsWith("[")) {
|
|
1463
|
-
const end = findEntityTimelineTokenEnd(rest);
|
|
1464
|
-
if (end === -1) break;
|
|
1465
|
-
const rawSegment = rest.slice(0, end + 1);
|
|
1466
|
-
const token = rest.slice(1, end).trim();
|
|
1467
|
-
const equalsIdx = token.indexOf("=");
|
|
1468
|
-
if (equalsIdx === -1) {
|
|
1469
|
-
if (rest === trimmed) {
|
|
1470
|
-
entry.text = trimmed;
|
|
1471
|
-
return entry.text ? entry : null;
|
|
1472
|
-
}
|
|
1473
|
-
break;
|
|
1474
|
-
}
|
|
1475
|
-
const key = token.slice(0, equalsIdx).trim().toLowerCase();
|
|
1476
|
-
const value = unescapeEntityTimelineMetadataValue(token.slice(equalsIdx + 1).trim());
|
|
1477
|
-
if (!value) break;
|
|
1478
|
-
const nextRest = rest.slice(end + 1).trimStart();
|
|
1479
|
-
switch (key) {
|
|
1480
|
-
case "source_meta":
|
|
1481
|
-
entry.source = value;
|
|
1482
|
-
break;
|
|
1483
|
-
case "source":
|
|
1484
|
-
if (
|
|
1485
|
-
consumedMetadataSegments.length === 0 &&
|
|
1486
|
-
!nextRest.startsWith("[") &&
|
|
1487
|
-
nextRest.length > 0 &&
|
|
1488
|
-
!isManagedEntityTimelineSource(value)
|
|
1489
|
-
) {
|
|
1490
|
-
literalSingleSourceSegment = rawSegment;
|
|
1491
|
-
rest = nextRest;
|
|
1492
|
-
break;
|
|
1493
|
-
}
|
|
1494
|
-
entry.source = value;
|
|
1495
|
-
break;
|
|
1496
|
-
case "session":
|
|
1497
|
-
case "sessionkey":
|
|
1498
|
-
entry.sessionKey = value;
|
|
1499
|
-
break;
|
|
1500
|
-
case "principal":
|
|
1501
|
-
entry.principal = value;
|
|
1502
|
-
break;
|
|
1503
|
-
default:
|
|
1504
|
-
entry.text = rest.trim();
|
|
1505
|
-
return entry.text ? entry : null;
|
|
1506
|
-
}
|
|
1507
|
-
if (literalSingleSourceSegment) break;
|
|
1508
|
-
consumedMetadataSegments.push(rawSegment);
|
|
1509
|
-
rest = nextRest;
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
if (literalSingleSourceSegment) {
|
|
1513
|
-
return {
|
|
1514
|
-
timestamp: entry.timestamp,
|
|
1515
|
-
text: `${literalSingleSourceSegment} ${rest}`.trim(),
|
|
1516
|
-
};
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
entry.text = rest.trim();
|
|
1520
|
-
if (!entry.text) return null;
|
|
1521
|
-
return entry;
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
function isEntitySynthesisTimelinePromotionBullet(bullet: string): boolean {
|
|
1525
|
-
const trimmed = bullet.trim();
|
|
1526
|
-
if (!trimmed.startsWith("[")) return false;
|
|
1527
|
-
|
|
1528
|
-
const firstEnd = findEntityTimelineTokenEnd(trimmed);
|
|
1529
|
-
if (firstEnd === -1) return false;
|
|
1530
|
-
|
|
1531
|
-
const firstToken = trimmed.slice(1, firstEnd).trim();
|
|
1532
|
-
return looksLikeEntityTimelineTimestamp(firstToken);
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
function looksLikeEntityTimelineTimestamp(token: string): boolean {
|
|
1536
|
-
if (!/^\d{4}-\d{2}-\d{2}(?:[T\s].*)?$/.test(token)) return false;
|
|
1537
|
-
return Number.isFinite(Date.parse(token));
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
function isManagedEntityTimelineSource(source: string): boolean {
|
|
1541
|
-
switch (source.trim().toLowerCase()) {
|
|
1542
|
-
case "artifact":
|
|
1543
|
-
case "chunking":
|
|
1544
|
-
case "cli-migrate":
|
|
1545
|
-
case "compounding-promotion":
|
|
1546
|
-
case "consolidation":
|
|
1547
|
-
case "contradiction-detection":
|
|
1548
|
-
case "entity_extraction":
|
|
1549
|
-
case "explicit":
|
|
1550
|
-
case "explicit-inline":
|
|
1551
|
-
case "explicit-inline-review":
|
|
1552
|
-
case "explicit-review":
|
|
1553
|
-
case "extraction":
|
|
1554
|
-
case "extraction-shared-promotion":
|
|
1555
|
-
case "manual":
|
|
1556
|
-
case "migration":
|
|
1557
|
-
case "migration-rechunk":
|
|
1558
|
-
case "proactive":
|
|
1559
|
-
case "replay":
|
|
1560
|
-
case "semantic-consolidation":
|
|
1561
|
-
case "unknown":
|
|
1562
|
-
return true;
|
|
1563
|
-
default:
|
|
1564
|
-
return false;
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
function findEntityTimelineTokenEnd(input: string): number {
|
|
1569
|
-
let escaped = false;
|
|
1570
|
-
for (let index = 0; index < input.length; index += 1) {
|
|
1571
|
-
const char = input[index];
|
|
1572
|
-
if (escaped) {
|
|
1573
|
-
escaped = false;
|
|
1574
|
-
continue;
|
|
1575
|
-
}
|
|
1576
|
-
if (char === "\\") {
|
|
1577
|
-
escaped = true;
|
|
1578
|
-
continue;
|
|
1579
|
-
}
|
|
1580
|
-
if (char === "]") return index;
|
|
1581
|
-
}
|
|
1582
|
-
return -1;
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
function escapeEntityTimelineMetadataValue(value: string): string {
|
|
1586
|
-
let escaped = "";
|
|
1587
|
-
for (const char of value) {
|
|
1588
|
-
switch (char) {
|
|
1589
|
-
case "\\":
|
|
1590
|
-
escaped += "\\\\";
|
|
1591
|
-
break;
|
|
1592
|
-
case "]":
|
|
1593
|
-
escaped += "\\]";
|
|
1594
|
-
break;
|
|
1595
|
-
case "\n":
|
|
1596
|
-
escaped += "\\n";
|
|
1597
|
-
break;
|
|
1598
|
-
case "\r":
|
|
1599
|
-
escaped += "\\r";
|
|
1600
|
-
break;
|
|
1601
|
-
case "\t":
|
|
1602
|
-
escaped += "\\t";
|
|
1603
|
-
break;
|
|
1604
|
-
default: {
|
|
1605
|
-
const codePoint = char.codePointAt(0) ?? 0;
|
|
1606
|
-
if (codePoint < 0x20) {
|
|
1607
|
-
escaped += `\\u${codePoint.toString(16).padStart(4, "0")}`;
|
|
1608
|
-
} else {
|
|
1609
|
-
escaped += char;
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
return escaped;
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
function unescapeEntityTimelineMetadataValue(value: string): string {
|
|
1618
|
-
if (!value.includes("\\")) return value;
|
|
1619
|
-
|
|
1620
|
-
let result = "";
|
|
1621
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
1622
|
-
const char = value[index];
|
|
1623
|
-
if (char !== "\\") {
|
|
1624
|
-
result += char;
|
|
1625
|
-
continue;
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
const next = value[index + 1];
|
|
1629
|
-
if (!next) {
|
|
1630
|
-
result += "\\";
|
|
1631
|
-
break;
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
switch (next) {
|
|
1635
|
-
case "n":
|
|
1636
|
-
result += "\n";
|
|
1637
|
-
index += 1;
|
|
1638
|
-
break;
|
|
1639
|
-
case "r":
|
|
1640
|
-
result += "\r";
|
|
1641
|
-
index += 1;
|
|
1642
|
-
break;
|
|
1643
|
-
case "t":
|
|
1644
|
-
result += "\t";
|
|
1645
|
-
index += 1;
|
|
1646
|
-
break;
|
|
1647
|
-
case "u": {
|
|
1648
|
-
const hex = value.slice(index + 2, index + 6);
|
|
1649
|
-
if (/^[0-9a-fA-F]{4}$/.test(hex)) {
|
|
1650
|
-
result += String.fromCharCode(parseInt(hex, 16));
|
|
1651
|
-
index += 5;
|
|
1652
|
-
break;
|
|
1653
|
-
}
|
|
1654
|
-
result += "u";
|
|
1655
|
-
index += 1;
|
|
1656
|
-
break;
|
|
1657
|
-
}
|
|
1658
|
-
default:
|
|
1659
|
-
result += next;
|
|
1660
|
-
index += 1;
|
|
1661
|
-
break;
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
return result;
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
function serializeEntityTimelineEntry(entry: EntityTimelineEntry): string {
|
|
1668
|
-
const tokens: string[] = [];
|
|
1669
|
-
if (entry.timestamp.trim().length > 0) {
|
|
1670
|
-
tokens.push(`[${entry.timestamp}]`);
|
|
1671
|
-
}
|
|
1672
|
-
if (entry.source) {
|
|
1673
|
-
const sourceKey = isManagedEntityTimelineSource(entry.source) ? "source" : "source_meta";
|
|
1674
|
-
tokens.push(`[${sourceKey}=${escapeEntityTimelineMetadataValue(entry.source)}]`);
|
|
1675
|
-
}
|
|
1676
|
-
if (entry.sessionKey) {
|
|
1677
|
-
tokens.push(`[session=${escapeEntityTimelineMetadataValue(entry.sessionKey)}]`);
|
|
1678
|
-
}
|
|
1679
|
-
if (entry.principal) {
|
|
1680
|
-
tokens.push(`[principal=${escapeEntityTimelineMetadataValue(entry.principal)}]`);
|
|
1681
|
-
}
|
|
1682
|
-
const serializedMetadata = tokens.length > 0 ? `${tokens.join(" ")} ` : "";
|
|
1683
|
-
return `- ${serializedMetadata}${entry.text}`.trimEnd();
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
function dedupeEntityTimelineFacts(timeline: EntityTimelineEntry[]): string[] {
|
|
1687
|
-
return [...new Set(timeline.map((entry) => entry.text.trim()).filter((entry) => entry.length > 0))];
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
function normalizeEntitySectionFact(value: string): string {
|
|
1691
|
-
return value.replace(/\s+/g, " ").trim();
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
export function normalizeStructuredSectionFacts(facts: string[]): string[] {
|
|
1695
|
-
return [...new Set(facts.map((fact) => normalizeEntitySectionFact(fact)).filter((fact) => fact.length > 0))];
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
function collectStructuredSectionFacts(structuredSections: EntityStructuredSection[]): string[] {
|
|
1699
|
-
const facts: string[] = [];
|
|
1700
|
-
for (const section of structuredSections) {
|
|
1701
|
-
for (const fact of section.facts) {
|
|
1702
|
-
const normalized = normalizeEntitySectionFact(fact);
|
|
1703
|
-
if (!normalized) continue;
|
|
1704
|
-
facts.push(normalized);
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
return [...new Set(facts)];
|
|
1708
|
-
}
|
|
1709
|
-
|
|
1710
|
-
export function compileEntityFacts(
|
|
1711
|
-
timeline: EntityTimelineEntry[],
|
|
1712
|
-
structuredSections: EntityStructuredSection[]
|
|
1713
|
-
): string[] {
|
|
1714
|
-
const facts: string[] = [];
|
|
1715
|
-
const seen = new Set<string>();
|
|
1716
|
-
for (const fact of dedupeEntityTimelineFacts(timeline)) {
|
|
1717
|
-
if (seen.has(fact)) continue;
|
|
1718
|
-
seen.add(fact);
|
|
1719
|
-
facts.push(fact);
|
|
1720
|
-
}
|
|
1721
|
-
for (const fact of collectStructuredSectionFacts(structuredSections)) {
|
|
1722
|
-
if (seen.has(fact)) continue;
|
|
1723
|
-
seen.add(fact);
|
|
1724
|
-
facts.push(fact);
|
|
1725
|
-
}
|
|
1726
|
-
return facts;
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1729
|
-
function parseEntityStructuredSectionFacts(lines: string[]): string[] {
|
|
1730
|
-
const facts: string[] = [];
|
|
1731
|
-
let currentBlock: string[] = [];
|
|
1732
|
-
|
|
1733
|
-
const flushCurrentBlock = (): void => {
|
|
1734
|
-
const normalized = normalizeEntitySectionFact(currentBlock.join(" "));
|
|
1735
|
-
if (normalized.length > 0) facts.push(normalized);
|
|
1736
|
-
currentBlock = [];
|
|
1737
|
-
};
|
|
1738
|
-
|
|
1739
|
-
for (const rawLine of lines) {
|
|
1740
|
-
const line = rawLine.trim();
|
|
1741
|
-
if (!line) {
|
|
1742
|
-
flushCurrentBlock();
|
|
1743
|
-
continue;
|
|
1744
|
-
}
|
|
1745
|
-
if (line.startsWith("- ")) {
|
|
1746
|
-
flushCurrentBlock();
|
|
1747
|
-
currentBlock = [line.slice(2).trim()];
|
|
1748
|
-
continue;
|
|
1749
|
-
}
|
|
1750
|
-
currentBlock.push(line);
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
|
-
flushCurrentBlock();
|
|
1754
|
-
return [...new Set(facts)];
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
function looksLikeStructuredSectionFactList(lines: string[]): boolean {
|
|
1758
|
-
const firstNonBlank = lines.find((line) => line.trim().length > 0)?.trim() ?? "";
|
|
1759
|
-
return firstNonBlank.startsWith("- ");
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
function partitionEntityStructuredSections(
|
|
1763
|
-
entityType: string,
|
|
1764
|
-
extraSections: Array<{ title: string; lines: string[] }>,
|
|
1765
|
-
entitySchemas?: PluginConfig["entitySchemas"]
|
|
1766
|
-
): {
|
|
1767
|
-
structuredSections: EntityStructuredSection[];
|
|
1768
|
-
remainingExtraSections: Array<{ title: string; lines: string[] }>;
|
|
1769
|
-
} {
|
|
1770
|
-
const structuredSections: EntityStructuredSection[] = [];
|
|
1771
|
-
const remainingExtraSections: Array<{ title: string; lines: string[] }> = [];
|
|
1772
|
-
const structuredSectionIndex = new Map<string, EntityStructuredSection>();
|
|
1773
|
-
|
|
1774
|
-
for (const section of extraSections) {
|
|
1775
|
-
const matchedSection = matchEntitySchemaSection(entityType, section.title, entitySchemas);
|
|
1776
|
-
if (!matchedSection && !looksLikeStructuredSectionFactList(section.lines)) {
|
|
1777
|
-
remainingExtraSections.push(section);
|
|
1778
|
-
continue;
|
|
1779
|
-
}
|
|
1780
|
-
const facts = parseEntityStructuredSectionFacts(section.lines);
|
|
1781
|
-
if (!matchedSection && facts.length === 0) {
|
|
1782
|
-
remainingExtraSections.push(section);
|
|
1783
|
-
continue;
|
|
1784
|
-
}
|
|
1785
|
-
const normalizedSection = matchedSection
|
|
1786
|
-
? { key: matchedSection.key, title: matchedSection.title }
|
|
1787
|
-
: normalizeEntityStructuredSection(entityType, { key: section.title, title: section.title }, entitySchemas);
|
|
1788
|
-
if (facts.length === 0) {
|
|
1789
|
-
remainingExtraSections.push(section);
|
|
1790
|
-
continue;
|
|
1791
|
-
}
|
|
1792
|
-
const existing = structuredSectionIndex.get(normalizedSection.key);
|
|
1793
|
-
if (existing) {
|
|
1794
|
-
existing.facts = normalizeStructuredSectionFacts([...existing.facts, ...facts]);
|
|
1795
|
-
continue;
|
|
1796
|
-
}
|
|
1797
|
-
const structuredSection: EntityStructuredSection = {
|
|
1798
|
-
key: normalizedSection.key,
|
|
1799
|
-
title: normalizedSection.title,
|
|
1800
|
-
facts: normalizeStructuredSectionFacts(facts),
|
|
1801
|
-
};
|
|
1802
|
-
structuredSections.push(structuredSection);
|
|
1803
|
-
structuredSectionIndex.set(normalizedSection.key, structuredSection);
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
|
-
return {
|
|
1807
|
-
structuredSections,
|
|
1808
|
-
remainingExtraSections,
|
|
1809
|
-
};
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
function latestEntityTimelineTimestamp(entity: EntityFile): string | undefined {
|
|
1813
|
-
let latestRaw: string | undefined;
|
|
1814
|
-
for (const entry of entity.timeline) {
|
|
1815
|
-
const timestamp = entry.timestamp.trim();
|
|
1816
|
-
if (!timestamp) continue;
|
|
1817
|
-
if (!latestRaw || compareEntityTimestamps(timestamp, latestRaw) > 0) {
|
|
1818
|
-
latestRaw = timestamp;
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
return latestRaw;
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
export function compareEntityTimestamps(left?: string, right?: string): number {
|
|
1825
|
-
const leftValue = left?.trim() ?? "";
|
|
1826
|
-
const rightValue = right?.trim() ?? "";
|
|
1827
|
-
|
|
1828
|
-
if (!leftValue && !rightValue) return 0;
|
|
1829
|
-
if (!leftValue) return -1;
|
|
1830
|
-
if (!rightValue) return 1;
|
|
1831
|
-
|
|
1832
|
-
const leftMs = Date.parse(leftValue);
|
|
1833
|
-
const rightMs = Date.parse(rightValue);
|
|
1834
|
-
const leftParsed = Number.isFinite(leftMs);
|
|
1835
|
-
const rightParsed = Number.isFinite(rightMs);
|
|
1836
|
-
|
|
1837
|
-
if (leftParsed && rightParsed) {
|
|
1838
|
-
if (leftMs === rightMs) return 0;
|
|
1839
|
-
return leftMs > rightMs ? 1 : -1;
|
|
1840
|
-
}
|
|
1841
|
-
if (leftParsed) return 1;
|
|
1842
|
-
if (rightParsed) return -1;
|
|
1843
|
-
return leftValue.localeCompare(rightValue);
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
export function countEntityStructuredFacts(entity: EntityFile): number {
|
|
1847
|
-
return (entity.structuredSections ?? []).reduce((count, section) => count + section.facts.length, 0);
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
|
-
export function fingerprintEntityStructuredFacts(entity: Pick<EntityFile, "structuredSections">): string | undefined {
|
|
1851
|
-
const normalizedSections = (entity.structuredSections ?? [])
|
|
1852
|
-
.map((section) => ({
|
|
1853
|
-
key: section.key.trim().toLowerCase(),
|
|
1854
|
-
title: section.title.replace(/\s+/g, " ").trim(),
|
|
1855
|
-
facts: normalizeStructuredSectionFacts(section.facts)
|
|
1856
|
-
.slice()
|
|
1857
|
-
.sort((left, right) => left.localeCompare(right)),
|
|
1858
|
-
}))
|
|
1859
|
-
.filter((section) => section.facts.length > 0)
|
|
1860
|
-
.sort(
|
|
1861
|
-
(left, right) =>
|
|
1862
|
-
left.key.localeCompare(right.key) ||
|
|
1863
|
-
left.title.localeCompare(right.title) ||
|
|
1864
|
-
left.facts.join("\n").localeCompare(right.facts.join("\n"))
|
|
1865
|
-
);
|
|
1866
|
-
if (normalizedSections.length === 0) return undefined;
|
|
1867
|
-
return createHash("sha256").update(JSON.stringify(normalizedSections)).digest("hex");
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
export function isEntitySynthesisStale(entity: EntityFile): boolean {
|
|
1871
|
-
const structuredFactCount = countEntityStructuredFacts(entity);
|
|
1872
|
-
const structuredFactDigest = fingerprintEntityStructuredFacts(entity);
|
|
1873
|
-
const storedStructuredFactDigest = entity.synthesisStructuredFactDigest?.trim() || undefined;
|
|
1874
|
-
if (entity.timeline.length === 0 && structuredFactCount === 0) return false;
|
|
1875
|
-
if (!entity.synthesis?.trim()) return true;
|
|
1876
|
-
if (entity.synthesisTimelineCount === undefined) return true;
|
|
1877
|
-
if (structuredFactCount > 0 && entity.synthesisStructuredFactCount === undefined) return true;
|
|
1878
|
-
if (structuredFactCount > 0 && !storedStructuredFactDigest) return true;
|
|
1879
|
-
const latestTimelineTimestamp = latestEntityTimelineTimestamp(entity);
|
|
1880
|
-
if (!latestTimelineTimestamp) {
|
|
1881
|
-
return (
|
|
1882
|
-
entity.timeline.length > entity.synthesisTimelineCount ||
|
|
1883
|
-
structuredFactCount > (entity.synthesisStructuredFactCount ?? 0) ||
|
|
1884
|
-
structuredFactDigest !== storedStructuredFactDigest
|
|
1885
|
-
);
|
|
1886
|
-
}
|
|
1887
|
-
if (!entity.synthesisUpdatedAt?.trim()) return true;
|
|
1888
|
-
const timelineFreshness = compareEntityTimestamps(latestTimelineTimestamp, entity.synthesisUpdatedAt);
|
|
1889
|
-
if (timelineFreshness > 0) return true;
|
|
1890
|
-
return (
|
|
1891
|
-
entity.timeline.length > entity.synthesisTimelineCount ||
|
|
1892
|
-
structuredFactCount > (entity.synthesisStructuredFactCount ?? 0) ||
|
|
1893
|
-
structuredFactDigest !== storedStructuredFactDigest
|
|
1894
|
-
);
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
1352
|
/**
|
|
1898
1353
|
* Parse an entity markdown file into a structured EntityFile.
|
|
1899
1354
|
* Backward compatible: old files without new sections get empty arrays.
|
|
@@ -2315,6 +1770,105 @@ function parseExtractionRetryStateEntries(
|
|
|
2315
1770
|
return valid;
|
|
2316
1771
|
}
|
|
2317
1772
|
|
|
1773
|
+
/**
|
|
1774
|
+
* Options accepted by `StorageManager.writeMemory` (extracted verbatim in
|
|
1775
|
+
* issue #1989 PR2 so `SealedWriteExtras` can be derived from it — the
|
|
1776
|
+
* sealed-envelope path takes envelope-owned fields from the envelope and
|
|
1777
|
+
* everything else from these extras).
|
|
1778
|
+
*/
|
|
1779
|
+
export interface WriteMemoryOptions {
|
|
1780
|
+
actor?: string;
|
|
1781
|
+
confidence?: number;
|
|
1782
|
+
tags?: string[];
|
|
1783
|
+
entityRef?: string;
|
|
1784
|
+
source?: string;
|
|
1785
|
+
supersedes?: string;
|
|
1786
|
+
lineage?: string[];
|
|
1787
|
+
importance?: ImportanceScore;
|
|
1788
|
+
links?: MemoryLink[];
|
|
1789
|
+
intentGoal?: string;
|
|
1790
|
+
intentActionType?: string;
|
|
1791
|
+
intentEntityTypes?: string[];
|
|
1792
|
+
artifactType?: MemoryFrontmatter["artifactType"];
|
|
1793
|
+
sourceMemoryId?: string;
|
|
1794
|
+
sourceTurnId?: string;
|
|
1795
|
+
memoryKind?: MemoryFrontmatter["memoryKind"];
|
|
1796
|
+
expiresAt?: string;
|
|
1797
|
+
validAt?: string;
|
|
1798
|
+
// Issue #1578 — bi-temporal ingestion provenance. `observedAt` is the
|
|
1799
|
+
// ingestion time (when Remnic learned the fact); `eventTimeSource`
|
|
1800
|
+
// records whether `validAt` was resolved from an extracted expression
|
|
1801
|
+
// or assumed from the ingestion anchor. Both validate on serialize.
|
|
1802
|
+
observedAt?: string;
|
|
1803
|
+
eventTimeSource?: "extracted" | "assumed";
|
|
1804
|
+
invalidAt?: string;
|
|
1805
|
+
structuredAttributes?: Record<string, string>;
|
|
1806
|
+
/**
|
|
1807
|
+
* When provided, this string is used as the source for the fact-content
|
|
1808
|
+
* dedup hash index instead of the persisted body (`content`).
|
|
1809
|
+
*
|
|
1810
|
+
* Use this when the persisted body differs from the canonical fact text
|
|
1811
|
+
* — for example when `content` is a citation-annotated variant of a raw
|
|
1812
|
+
* fact. Passing the raw fact as `contentHashSource` ensures that
|
|
1813
|
+
* `hasFactContentHash(rawFact)` returns `true` after the write, so
|
|
1814
|
+
* subsequent extractions of the same logical fact are correctly deduped
|
|
1815
|
+
* even when their citation timestamp differs.
|
|
1816
|
+
*/
|
|
1817
|
+
contentHashSource?: string;
|
|
1818
|
+
/**
|
|
1819
|
+
* When true, writeMemory marks the fact-hash index dirty via `.add(...)`
|
|
1820
|
+
* but does NOT flush it to disk — the caller is responsible for a
|
|
1821
|
+
* subsequent batch save (issue #1909). The extraction persist path sets
|
|
1822
|
+
* this and relies on the orchestrator's authoritative
|
|
1823
|
+
* `saveContentHashIndexes()` batch save plus a per-storage union flush,
|
|
1824
|
+
* avoiding a whole-index (which grows with corpus size) rewrite per fact.
|
|
1825
|
+
* Default false: every
|
|
1826
|
+
* other (single-write) caller keeps the immediate, crash-safe save.
|
|
1827
|
+
*/
|
|
1828
|
+
deferHashIndexSave?: boolean;
|
|
1829
|
+
status?: MemoryStatus;
|
|
1830
|
+
/**
|
|
1831
|
+
* Consolidation provenance (issue #561 PR 2). When the caller is a
|
|
1832
|
+
* consolidation / supersession / dedup-merge path, these fields wire
|
|
1833
|
+
* the page-version snapshots the new memory was derived from and the
|
|
1834
|
+
* operator that produced it. Persisted onto frontmatter as
|
|
1835
|
+
* `derived_from` + `derived_via`; validated at serialize time.
|
|
1836
|
+
*/
|
|
1837
|
+
derivedFrom?: string[];
|
|
1838
|
+
derivedVia?: ConsolidationOperator;
|
|
1839
|
+
/**
|
|
1840
|
+
* Faithfulness gate verdict (issue #1576). When provided, persisted to
|
|
1841
|
+
* frontmatter so downstream readers (TrustScore #1577, review queue)
|
|
1842
|
+
* can consume it. Absent = gate was off or fact predates #1576.
|
|
1843
|
+
*/
|
|
1844
|
+
faithfulness?: import("./types.js").FaithfulnessFrontmatter;
|
|
1845
|
+
/**
|
|
1846
|
+
* Claim-level provenance spans (issue #1575 PR 2). When provided,
|
|
1847
|
+
* persisted to frontmatter so downstream readers (memory_get, x-ray,
|
|
1848
|
+
* faithfulness gate #1576) can consume them. Absent = fact predates
|
|
1849
|
+
* #1575 or provenance was disabled.
|
|
1850
|
+
*/
|
|
1851
|
+
sources?: ProvenanceSource[];
|
|
1852
|
+
provenance?: "verified" | "unverified" | "none";
|
|
1853
|
+
sourceConnector?: string;
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
/**
|
|
1857
|
+
* `WriteMemoryOptions` minus the fields a `SealedMemoryEnvelope` owns.
|
|
1858
|
+
* Passing an envelope-owned field twice is a compile error by construction.
|
|
1859
|
+
*/
|
|
1860
|
+
export type SealedWriteExtras = Omit<
|
|
1861
|
+
WriteMemoryOptions,
|
|
1862
|
+
| "confidence"
|
|
1863
|
+
| "tags"
|
|
1864
|
+
| "entityRef"
|
|
1865
|
+
| "source"
|
|
1866
|
+
| "expiresAt"
|
|
1867
|
+
| "validAt"
|
|
1868
|
+
| "structuredAttributes"
|
|
1869
|
+
| "sourceConnector"
|
|
1870
|
+
>;
|
|
1871
|
+
|
|
2318
1872
|
export class StorageManager {
|
|
2319
1873
|
private knowledgeIndexCache: { result: string; builtAt: number } | null = null;
|
|
2320
1874
|
private artifactIndexCache: { memories: MemoryFile[]; loadedAtMs: number; writeVersion: number } | null = null;
|
|
@@ -2442,8 +1996,39 @@ export class StorageManager {
|
|
|
2442
1996
|
private factHashIndex: ContentHashIndex | null = null;
|
|
2443
1997
|
private factHashIndexLoadPromise: Promise<ContentHashIndex> | null = null;
|
|
2444
1998
|
private factHashIndexAuthoritative: boolean | null = null;
|
|
2445
|
-
private factHashIndexAuthoritativePromise: Promise<
|
|
1999
|
+
private factHashIndexAuthoritativePromise: Promise<boolean> | null = null;
|
|
2000
|
+
/**
|
|
2001
|
+
* Fact-ONLY hash membership (PR #2016). The shared `factHashIndex` above is
|
|
2002
|
+
* category-agnostic (the round-15 corpus rebuild + addContentHashDedup index
|
|
2003
|
+
* every category), so it cannot answer a fact-only question. This set carries
|
|
2004
|
+
* only `category === "fact"` hashes and is the sole source for
|
|
2005
|
+
* `hasFactContentHash`, so an over-included non-fact body can never suppress a
|
|
2006
|
+
* real fact candidate. Kept in lockstep with the shared index on EVERY path:
|
|
2007
|
+
* the authoritative corpus rebuild (repopulated in place), the write path
|
|
2008
|
+
* (`writeMemory` / `addActiveFactContentHash`), the storage-owned removal
|
|
2009
|
+
* (`removeFactContentHashesForMemories`), AND the orchestrator's archival /
|
|
2010
|
+
* consolidation removal (`removeContentHashForMemory` ->
|
|
2011
|
+
* `removeFactOnlyHashForMemory`). In-memory only; never persisted.
|
|
2012
|
+
*/
|
|
2013
|
+
private factOnlyHashes: Set<string> = new Set();
|
|
2014
|
+
/** Optional lock/retry tuning for the fact-hash index cross-process lock (PR #2016; tests inject tight budgets). */
|
|
2015
|
+
factHashIndexLockOptions: ContentHashIndexLockOptions = {};
|
|
2446
2016
|
private readonly secureAppendChains = new Map<string, Promise<void>>();
|
|
2017
|
+
/**
|
|
2018
|
+
* Cache of "is this file encrypted?" keyed by absolute path, VALIDATED by file
|
|
2019
|
+
* identity (size + mtimeMs) — issue #1909 review round 10 finding 1. A prior
|
|
2020
|
+
* design cached only a boolean and relied on this instance's own writes to
|
|
2021
|
+
* invalidate it; a PEER process encrypting the file would not invalidate,
|
|
2022
|
+
* leaving a stale `false` that made the append path write RAW bytes into an
|
|
2023
|
+
* encrypted file (corruption). Now each entry records the (size, mtime) the
|
|
2024
|
+
* classification was read at; `isEncryptedFileHeader` re-sniffs (header-only)
|
|
2025
|
+
* whenever the on-disk identity no longer matches, so a foreign rewrite is
|
|
2026
|
+
* detected. The whole-file read is still never performed.
|
|
2027
|
+
*/
|
|
2028
|
+
private readonly secureFileEncryptionSniffCache = new Map<
|
|
2029
|
+
string,
|
|
2030
|
+
{ identity: { size: number; mtimeMs: number }; encrypted: boolean }
|
|
2031
|
+
>();
|
|
2447
2032
|
private offlineSyncDigestCache: Map<string, OfflineSyncDigestCacheEntry> | null = null;
|
|
2448
2033
|
private offlineSyncDigestCacheLoadPromise: Promise<Map<string, OfflineSyncDigestCacheEntry>> | null = null;
|
|
2449
2034
|
private offlineSyncDigestCacheWriteChain: Promise<void> = Promise.resolve();
|
|
@@ -2452,6 +2037,15 @@ export class StorageManager {
|
|
|
2452
2037
|
citationTemplate: string = DEFAULT_CITATION_FORMAT;
|
|
2453
2038
|
/** Post-write catalog hook (#1522). Installed by the namespace router; fire-and-forget. */
|
|
2454
2039
|
onCatalogWrite?: () => void;
|
|
2040
|
+
/** Post-write embedding hook (#2019). Installed by the orchestrator; fire-and-forget. */
|
|
2041
|
+
onMemoryWrite?: (filePath: string) => void;
|
|
2042
|
+
private notifyMemoryWrite(filePath: string): void {
|
|
2043
|
+
try {
|
|
2044
|
+
this.onMemoryWrite?.(filePath);
|
|
2045
|
+
} catch {
|
|
2046
|
+
/* fire-and-forget — embedding failures must not block writes */
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2455
2049
|
private notifyCatalogWrite(): void {
|
|
2456
2050
|
try {
|
|
2457
2051
|
this.onCatalogWrite?.();
|
|
@@ -2550,6 +2144,12 @@ export class StorageManager {
|
|
|
2550
2144
|
// decrypted under the previous key, not just the entity layer.
|
|
2551
2145
|
invalidateAllForDir(this.baseDir);
|
|
2552
2146
|
this.invalidateKnowledgeIndexCache();
|
|
2147
|
+
// A key change flips resolveWriteKey(), so a file's plain/encrypted class on
|
|
2148
|
+
// the next full rewrite may differ from what was cached (issue #1909).
|
|
2149
|
+
this.secureFileEncryptionSniffCache.clear();
|
|
2150
|
+
// Same rationale for the behavior-signals dedup key cache (issue #1909):
|
|
2151
|
+
// drop it on a key change so a re-encrypted/re-decrypted ledger is reloaded.
|
|
2152
|
+
this.behaviorSignalsKeyCache = null;
|
|
2553
2153
|
}
|
|
2554
2154
|
|
|
2555
2155
|
private getEntityCacheSecureStoreKey(): string {
|
|
@@ -2624,20 +2224,28 @@ export class StorageManager {
|
|
|
2624
2224
|
return this._secureStoreKey !== null;
|
|
2625
2225
|
}
|
|
2626
2226
|
|
|
2227
|
+
/** Whether a state-file write encrypts at rest (encrypt-on-write on AND key
|
|
2228
|
+
* set) — the write mode lifecycle compaction reserves the envelope for (#2033). */
|
|
2229
|
+
willEncryptStateWrites(): boolean {
|
|
2230
|
+
return this._secureStoreEncryptOnWrite && this._secureStoreKey !== null;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2627
2233
|
/**
|
|
2628
|
-
* Resolve the effective write key
|
|
2234
|
+
* Resolve the effective write key: null when encrypt-on-write is off or the
|
|
2235
|
+
* store is unlocked-optional; the key when set; else throws
|
|
2236
|
+
* SecureStoreLockedError under a required-but-locked store (PR #767) instead
|
|
2237
|
+
* of silently writing plaintext.
|
|
2629
2238
|
*
|
|
2630
|
-
*
|
|
2631
|
-
*
|
|
2632
|
-
*
|
|
2633
|
-
*
|
|
2634
|
-
*
|
|
2635
|
-
*
|
|
2636
|
-
*
|
|
2637
|
-
* `_secureStoreRequired` is false: returns null (unencrypted store).
|
|
2239
|
+
* `forceEncrypt` overrides the encrypt-on-write policy flag ONLY (issue
|
|
2240
|
+
* #2033): a lifecycle-ledger compaction of an already-encrypted ledger/backup
|
|
2241
|
+
* must preserve encryption at rest even when `secureStoreEncryptOnWrite` is
|
|
2242
|
+
* paused, or it would silently downgrade encrypted state to plaintext. It
|
|
2243
|
+
* never bypasses the LOCK: when the store has no key it still returns null
|
|
2244
|
+
* (optional) or throws (required), so a keyless caller can never fabricate an
|
|
2245
|
+
* encrypted write.
|
|
2638
2246
|
*/
|
|
2639
|
-
private resolveWriteKey(): Buffer | null {
|
|
2640
|
-
if (!this._secureStoreEncryptOnWrite) return null;
|
|
2247
|
+
private resolveWriteKey(forceEncrypt = false): Buffer | null {
|
|
2248
|
+
if (!forceEncrypt && !this._secureStoreEncryptOnWrite) return null;
|
|
2641
2249
|
if (this._secureStoreKey !== null) return this._secureStoreKey;
|
|
2642
2250
|
if (this._secureStoreRequired) {
|
|
2643
2251
|
throw new SecureStoreLockedError(
|
|
@@ -3129,15 +2737,21 @@ export class StorageManager {
|
|
|
3129
2737
|
private readStorageSecureFile(filePath: string): Promise<string> {
|
|
3130
2738
|
return readMaybeEncryptedFile(filePath, this._secureStoreKey, this.baseDir);
|
|
3131
2739
|
}
|
|
3132
|
-
private writeStorageSecureFile(filePath: string, content: string | Buffer): Promise<void> {
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
2740
|
+
private writeStorageSecureFile(filePath: string, content: string | Buffer, forceEncrypt = false): Promise<void> {
|
|
2741
|
+
const writeKey = this.resolveWriteKey(forceEncrypt);
|
|
2742
|
+
return writeMaybeEncryptedFile(filePath, content, writeKey, {}, this.baseDir).then(() => {
|
|
2743
|
+
// No manual sniff-cache update needed (issue #1909 round 10): this rewrite
|
|
2744
|
+
// changes the file's (size, mtime), so isEncryptedFileHeader re-sniffs on
|
|
2745
|
+
// the next append when its identity check misses. Identity validation also
|
|
2746
|
+
// covers PEER rewrites that this hook never sees.
|
|
2747
|
+
this.notifyCatalogWriteForPath(filePath);
|
|
2748
|
+
});
|
|
3136
2749
|
}
|
|
3137
2750
|
private writeStorageSecureFileChunks(filePath: string, chunks: AsyncIterable<Buffer>): Promise<void> {
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
2751
|
+
const writeKey = this.resolveWriteKey();
|
|
2752
|
+
return writeMaybeEncryptedFileFromChunks(filePath, chunks, writeKey, {}, this.baseDir).then(() => {
|
|
2753
|
+
this.notifyCatalogWriteForPath(filePath);
|
|
2754
|
+
});
|
|
3141
2755
|
}
|
|
3142
2756
|
|
|
3143
2757
|
private assertManagedStoragePath(filePath: string, method: string): string {
|
|
@@ -3343,11 +2957,9 @@ export class StorageManager {
|
|
|
3343
2957
|
// chokepoint, which also covers the entity cache (issue #1535).
|
|
3344
2958
|
this.invalidateAllMemoriesCache();
|
|
3345
2959
|
this.invalidateKnowledgeIndexCache();
|
|
2960
|
+
// Force a corpus rebuild of the fact-hash index on next use (round 11: no
|
|
2961
|
+
// on-disk ready marker — the in-memory authoritative flag is the only gate).
|
|
3346
2962
|
this.factHashIndexAuthoritative = false;
|
|
3347
|
-
await unlink(this.factHashIndexReadyPath).catch((error: unknown) => {
|
|
3348
|
-
if (isErrnoCode(error, "ENOENT")) return;
|
|
3349
|
-
throw error;
|
|
3350
|
-
});
|
|
3351
2963
|
if (filePath.includes(`${path.sep}cold${path.sep}`)) {
|
|
3352
2964
|
this.invalidateColdMemoriesCache();
|
|
3353
2965
|
}
|
|
@@ -3362,7 +2974,8 @@ export class StorageManager {
|
|
|
3362
2974
|
this.stateDir,
|
|
3363
2975
|
() => this._secureStoreKey,
|
|
3364
2976
|
() => this.resolveWriteKey(),
|
|
3365
|
-
this.baseDir
|
|
2977
|
+
this.baseDir,
|
|
2978
|
+
this.factHashIndexLockOptions,
|
|
3366
2979
|
);
|
|
3367
2980
|
}
|
|
3368
2981
|
|
|
@@ -3380,12 +2993,60 @@ export class StorageManager {
|
|
|
3380
2993
|
}
|
|
3381
2994
|
}
|
|
3382
2995
|
|
|
2996
|
+
/**
|
|
2997
|
+
* Classify a file as encrypted by reading ONLY the fixed-size magic header
|
|
2998
|
+
* (issue #1909), never the whole file. The result is cached per-path but
|
|
2999
|
+
* VALIDATED against the file's (size, mtime) identity (review round 10 finding
|
|
3000
|
+
* 1): a `stat` on each call is O(1), and the 12-byte header read happens only
|
|
3001
|
+
* on a cache miss or when the on-disk identity changed — so a PEER process
|
|
3002
|
+
* rewriting/encrypting the file is detected and we never append raw bytes into
|
|
3003
|
+
* an encrypted body. ENOENT and short/empty files classify as not-encrypted
|
|
3004
|
+
* (fall through to a plain `appendFile`).
|
|
3005
|
+
*/
|
|
3006
|
+
private async isEncryptedFileHeader(filePath: string): Promise<boolean> {
|
|
3007
|
+
let st;
|
|
3008
|
+
try {
|
|
3009
|
+
st = await stat(filePath);
|
|
3010
|
+
} catch (err) {
|
|
3011
|
+
if (isErrnoCode(err, "ENOENT")) return false;
|
|
3012
|
+
throw err;
|
|
3013
|
+
}
|
|
3014
|
+
const cached = this.secureFileEncryptionSniffCache.get(filePath);
|
|
3015
|
+
if (cached && cached.identity.size === st.size && cached.identity.mtimeMs === st.mtimeMs) {
|
|
3016
|
+
return cached.encrypted;
|
|
3017
|
+
}
|
|
3018
|
+
let handle;
|
|
3019
|
+
try {
|
|
3020
|
+
handle = await open(filePath, "r");
|
|
3021
|
+
} catch (err) {
|
|
3022
|
+
if (isErrnoCode(err, "ENOENT")) return false;
|
|
3023
|
+
throw err;
|
|
3024
|
+
}
|
|
3025
|
+
try {
|
|
3026
|
+
const header = Buffer.alloc(MAGIC_HEADER_SIZE);
|
|
3027
|
+
const { bytesRead } = await handle.read(header, 0, header.length, 0);
|
|
3028
|
+
const encrypted = isEncryptedFile(header.subarray(0, bytesRead));
|
|
3029
|
+
this.secureFileEncryptionSniffCache.set(filePath, {
|
|
3030
|
+
identity: { size: st.size, mtimeMs: st.mtimeMs },
|
|
3031
|
+
encrypted,
|
|
3032
|
+
});
|
|
3033
|
+
return encrypted;
|
|
3034
|
+
} finally {
|
|
3035
|
+
await handle.close().catch(() => undefined);
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3383
3039
|
private async appendStorageSecureFileUnlocked(filePath: string, content: string): Promise<void> {
|
|
3384
3040
|
const writeKey = this.resolveWriteKey();
|
|
3385
3041
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
3386
3042
|
if (writeKey === null) {
|
|
3387
3043
|
try {
|
|
3388
|
-
|
|
3044
|
+
// Header-only sniff (issue #1909): reads at most MAGIC_HEADER_SIZE bytes
|
|
3045
|
+
// instead of the whole target file (a lifecycle ledger can grow to
|
|
3046
|
+
// hundreds of MB on a large corpus) to decide "is this encrypted?". A
|
|
3047
|
+
// plaintext append below cannot flip the classification, so a cached
|
|
3048
|
+
// `false` stays valid across appends.
|
|
3049
|
+
if (await this.isEncryptedFileHeader(filePath)) {
|
|
3389
3050
|
const existing = await this.readStorageSecureFile(filePath);
|
|
3390
3051
|
await writeMaybeEncryptedFile(filePath, `${existing}${content}`, null, {}, this.baseDir);
|
|
3391
3052
|
this.notifyCatalogWriteForPath(filePath);
|
|
@@ -3394,11 +3055,21 @@ export class StorageManager {
|
|
|
3394
3055
|
} catch (err) {
|
|
3395
3056
|
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
3396
3057
|
}
|
|
3058
|
+
// Plaintext append does not touch the header. isEncryptedFileHeader is now
|
|
3059
|
+
// (size, mtime)-identity-validated (issue #1909 round 10), so a PEER
|
|
3060
|
+
// process that encrypted this file since our last sniff is detected on the
|
|
3061
|
+
// stat above and re-sniffed — we will NOT append raw bytes into an
|
|
3062
|
+
// encrypted body. A residual sub-call TOCTOU (a peer encrypts between this
|
|
3063
|
+
// stat and the appendFile below) is inherent to any check-then-act and is
|
|
3064
|
+
// out of scope; the identity check closes the long-lived stale-cache hole.
|
|
3397
3065
|
await appendFile(filePath, content, "utf-8");
|
|
3398
3066
|
this.notifyCatalogWriteForPath(filePath);
|
|
3399
3067
|
return;
|
|
3400
3068
|
}
|
|
3401
3069
|
|
|
3070
|
+
// Encrypted-store branch (issue #1909, known limitation): the sealed format
|
|
3071
|
+
// has no append primitive, so this inherently decrypts + concatenates +
|
|
3072
|
+
// rewrites the whole file. Only the plaintext-path sniff read was wasteful.
|
|
3402
3073
|
let existing = "";
|
|
3403
3074
|
try {
|
|
3404
3075
|
existing = await this.readStorageSecureFile(filePath);
|
|
@@ -3417,10 +3088,6 @@ export class StorageManager {
|
|
|
3417
3088
|
private get entitySynthesisQueuePath(): string {
|
|
3418
3089
|
return path.join(this.stateDir, "entity-synthesis-queue.json");
|
|
3419
3090
|
}
|
|
3420
|
-
private get factHashIndexReadyPath(): string {
|
|
3421
|
-
return path.join(this.stateDir, "fact-hashes.ready");
|
|
3422
|
-
}
|
|
3423
|
-
|
|
3424
3091
|
// ── Tombstone store access (issue #1579) ─────────────────────────────────
|
|
3425
3092
|
/**
|
|
3426
3093
|
* The on-disk tombstone log path. Lives under `<stateDir>/tombstones.jsonl`
|
|
@@ -3618,101 +3285,200 @@ export class StorageManager {
|
|
|
3618
3285
|
return this.factHashIndexLoadPromise;
|
|
3619
3286
|
}
|
|
3620
3287
|
|
|
3621
|
-
|
|
3288
|
+
/**
|
|
3289
|
+
* Return the fact-hash index after ensuring it is authoritative — i.e. rebuilt
|
|
3290
|
+
* from the durable fact corpus (issue #1909 review round 12). This is the ONE
|
|
3291
|
+
* coherent dedup source: the orchestrator's dedup layer
|
|
3292
|
+
* (contentHashIndexForStorage) shares THIS instance instead of loading a raw,
|
|
3293
|
+
* possibly-stale fact-hashes.txt, so after a crash+restart the orchestrator's
|
|
3294
|
+
* hasContentHashDedup sees the same corpus-rebuilt hashes StorageManager does
|
|
3295
|
+
* and never re-creates a fact whose per-write flush was deferred and lost.
|
|
3296
|
+
*/
|
|
3297
|
+
async getAuthoritativeFactHashIndex(): Promise<ContentHashIndex> {
|
|
3298
|
+
if (!(await this.ensureFactHashIndexAuthoritative())) {
|
|
3299
|
+
throw new FactHashIndexNotAuthoritativeError(this.stateDir);
|
|
3300
|
+
}
|
|
3301
|
+
return this.getFactHashIndex();
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3304
|
+
/**
|
|
3305
|
+
* Return the shared fact-hash index instance, corpus-rebuilt under the lock
|
|
3306
|
+
* when the lock is free. Unlike {@link getAuthoritativeFactHashIndex} this
|
|
3307
|
+
* NEVER throws on lock contention — it returns the shared instance (the loaded
|
|
3308
|
+
* snapshot when a locked rebuild could not run) so registration writes still
|
|
3309
|
+
* land in the one shared index and reconcile to disk. Callers that trust a
|
|
3310
|
+
* dedup MISS MUST also consult {@link isFactContentHashAuthoritative} and
|
|
3311
|
+
* confirm against the corpus when it is false (PR #2016).
|
|
3312
|
+
*/
|
|
3313
|
+
async getSharedFactHashIndex(): Promise<ContentHashIndex> {
|
|
3314
|
+
await this.ensureFactHashIndexAuthoritative();
|
|
3315
|
+
return this.getFactHashIndex();
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
private async ensureFactHashIndexAuthoritative(): Promise<boolean> {
|
|
3622
3319
|
if (this.factHashIndexAuthoritative === true) {
|
|
3623
|
-
|
|
3320
|
+
// PR #2016 review: authority is NOT permanent. A peer process can advance
|
|
3321
|
+
// the durable fact-hash index after our rebuild, leaving our in-memory
|
|
3322
|
+
// snapshot stale but still flagged authoritative — so a dedup MISS is
|
|
3323
|
+
// wrongly trusted and a duplicate active memory is written. Gate the fast
|
|
3324
|
+
// path on a cheap one-stat freshness check: when the durable index file is
|
|
3325
|
+
// unchanged since our last sync we stay authoritative (hot path preserved);
|
|
3326
|
+
// when a peer advanced it (or freshness cannot be established) drop
|
|
3327
|
+
// authority and rebuild from the corpus below so the miss confirms against
|
|
3328
|
+
// ground truth.
|
|
3329
|
+
if (this.factHashIndex && (await this.factHashIndex.isDiskFingerprintCurrent())) {
|
|
3330
|
+
return true;
|
|
3331
|
+
}
|
|
3332
|
+
this.factHashIndexAuthoritative = null;
|
|
3624
3333
|
}
|
|
3625
3334
|
if (this.factHashIndexAuthoritativePromise) {
|
|
3626
|
-
|
|
3627
|
-
return;
|
|
3335
|
+
return this.factHashIndexAuthoritativePromise;
|
|
3628
3336
|
}
|
|
3629
3337
|
|
|
3338
|
+
this.factHashIndexAuthoritative = false;
|
|
3630
3339
|
this.factHashIndexAuthoritativePromise = (async () => {
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3340
|
+
// Round 11: ALWAYS rebuild the fact-hash index from the durable corpus on
|
|
3341
|
+
// first use per process — no on-disk "ready" marker is written or trusted,
|
|
3342
|
+
// so a deferred write, crash, or multi-process interleave can never leave a
|
|
3343
|
+
// stale index trusted. The scan AND publish run under the SAME per-index
|
|
3344
|
+
// cross-process lock the reconciling saves use (rebuildUnderLock), so the
|
|
3345
|
+
// rebuild can never overwrite a peer's newer lock-merged or deferred
|
|
3346
|
+
// additions with an unlocked overwrite.
|
|
3347
|
+
//
|
|
3348
|
+
// PR #2016 (findings 1-2): bounded-retry the LOCKED rebuild so transient
|
|
3349
|
+
// contention (a peer mid reconcile-save) clears within a short budget
|
|
3350
|
+
// instead of surrendering authority on the first miss. Each attempt uses
|
|
3351
|
+
// the same non-reentrant, bounded-wait file lock, so it can never deadlock
|
|
3352
|
+
// with a deferred reconcile-retry. On exhaustion the index is left
|
|
3353
|
+
// non-authoritative: getAuthoritativeFactHashIndex() fails explicitly and
|
|
3354
|
+
// hasFactContentHash()/isFactContentHashAuthoritative() fall back to the
|
|
3355
|
+
// durable corpus so a stale loaded snapshot never answers as current.
|
|
3639
3356
|
const factHashIndex = await this.getFactHashIndex();
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
continue;
|
|
3653
|
-
}
|
|
3654
|
-
// Legacy fact written before contentHash was introduced (Finding 1 —
|
|
3655
|
-
// Uhol). Apply nuanced handling based on whether the citation can be
|
|
3656
|
-
// reliably stripped:
|
|
3657
|
-
//
|
|
3658
|
-
// 1. Default citation present → strip it and index the raw body.
|
|
3659
|
-
// 2. No citation at all → index the raw body as-is.
|
|
3660
|
-
// 3. Unknown/custom citation template → skip with a warning.
|
|
3661
|
-
//
|
|
3662
|
-
// Rationale for (3): for facts annotated with a custom citation
|
|
3663
|
-
// template, stripCitationForTemplate cannot reliably detect the inline
|
|
3664
|
-
// marker and would hash the cited body — producing a hash that never
|
|
3665
|
-
// matches what hasFactContentHash(rawContent) computes. A
|
|
3666
|
-
// false-negative miss (the fact is not in the index) is preferable to
|
|
3667
|
-
// a wrong index entry that permanently suppresses legitimate duplicate
|
|
3668
|
-
// writes.
|
|
3669
|
-
//
|
|
3670
|
-
// Limitation (Thread 2 — stale hash): even when contentHash IS present
|
|
3671
|
-
// it may be stale if updateMemory() rewrote the body without updating
|
|
3672
|
-
// the frontmatter hash. The hash is trusted as-is here; a future
|
|
3673
|
-
// migration pass can recompute it from the current content.
|
|
3674
|
-
const content = memory.content;
|
|
3675
|
-
// Use the configured template (Thread 1 fix): citationTemplate is set
|
|
3676
|
-
// by the orchestrator to the active inlineSourceAttributionFormat so
|
|
3677
|
-
// the rebuild can strip both the default and any custom template.
|
|
3678
|
-
// Falls back to DEFAULT_CITATION_FORMAT when the orchestrator has not
|
|
3679
|
-
// configured a custom template (e.g. direct StorageManager construction
|
|
3680
|
-
// in tests).
|
|
3681
|
-
const stripped = stripCitationForTemplate(content, this.citationTemplate);
|
|
3682
|
-
if (stripped !== content) {
|
|
3683
|
-
// Citation was stripped — index the bare body.
|
|
3684
|
-
factHashIndex.addByHash(ContentHashIndex.computeHash(sanitizeMemoryContent(stripped).text));
|
|
3685
|
-
continue;
|
|
3686
|
-
}
|
|
3687
|
-
// No citation was removed. Decide whether to index or skip.
|
|
3688
|
-
// Thread 4 fix: use hasCitation() rather than the too-broad endsWith("]")
|
|
3689
|
-
// heuristic. Facts that legitimately end with "]" (e.g. "User prefers
|
|
3690
|
-
// [dark mode]") have no citation marker and should be indexed as-is.
|
|
3691
|
-
// Only skip when hasCitation() confirms a citation is present — that
|
|
3692
|
-
// means the citation is from an unknown/custom template we cannot strip.
|
|
3693
|
-
if (!hasCitation(content)) {
|
|
3694
|
-
// Content has no recognisable citation marker — index raw body.
|
|
3695
|
-
factHashIndex.addByHash(ContentHashIndex.computeHash(sanitizeMemoryContent(content).text));
|
|
3696
|
-
continue;
|
|
3697
|
-
}
|
|
3698
|
-
// Content carries a citation from an unknown/custom template
|
|
3699
|
-
// that we cannot safely strip. Skip rather than index a wrong hash.
|
|
3700
|
-
legacyRecovered++;
|
|
3701
|
-
continue;
|
|
3357
|
+
const maxAttempts =
|
|
3358
|
+
this.factHashIndexLockOptions.retryMaxAttempts ?? FACT_HASH_INDEX_REBUILD_MAX_ATTEMPTS;
|
|
3359
|
+
const baseMs =
|
|
3360
|
+
this.factHashIndexLockOptions.retryBaseMs ?? FACT_HASH_INDEX_REBUILD_RETRY_BASE_MS;
|
|
3361
|
+
let published = false;
|
|
3362
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
3363
|
+
published = await factHashIndex.rebuildUnderLock(() =>
|
|
3364
|
+
this.rebuildFactHashIndexFromCorpus(factHashIndex),
|
|
3365
|
+
);
|
|
3366
|
+
if (published || attempt === maxAttempts - 1) break;
|
|
3367
|
+
const wait = Math.min(baseMs * 2 ** attempt, CONTENT_HASH_INDEX_RETRY_MAX_DELAY_MS);
|
|
3368
|
+
await new Promise<void>((resolve) => setTimeout(resolve, wait));
|
|
3702
3369
|
}
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3370
|
+
this.factHashIndexAuthoritative = published;
|
|
3371
|
+
if (!published) {
|
|
3372
|
+
log.warn(
|
|
3373
|
+
`ensureFactHashIndexAuthoritative: fact-hash index lock unavailable after ${maxAttempts} attempt(s); ` +
|
|
3374
|
+
`index left non-authoritative (reads verify against the durable corpus; next use retries the locked rebuild)`,
|
|
3706
3375
|
);
|
|
3707
3376
|
}
|
|
3708
|
-
|
|
3709
|
-
await mkdir(path.dirname(this.factHashIndexReadyPath), { recursive: true });
|
|
3710
|
-
await writeFile(this.factHashIndexReadyPath, "v1\n", "utf-8");
|
|
3711
|
-
this.factHashIndexAuthoritative = true;
|
|
3377
|
+
return published;
|
|
3712
3378
|
})().finally(() => {
|
|
3713
3379
|
this.factHashIndexAuthoritativePromise = null;
|
|
3714
3380
|
});
|
|
3715
|
-
|
|
3381
|
+
return this.factHashIndexAuthoritativePromise;
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
/**
|
|
3385
|
+
* Repopulate `factHashIndex` in memory from the durable HOT+COLD corpus. Runs
|
|
3386
|
+
* WHILE the per-index cross-process lock is held (see
|
|
3387
|
+
* ContentHashIndex.rebuildUnderLock, which publishes the rebuilt set); this
|
|
3388
|
+
* method itself never writes to disk.
|
|
3389
|
+
*/
|
|
3390
|
+
private async rebuildFactHashIndexFromCorpus(factHashIndex: ContentHashIndex): Promise<void> {
|
|
3391
|
+
factHashIndex.clear();
|
|
3392
|
+
// Fact-ONLY membership rebuilt in lockstep (PR #2016): hasFactContentHash
|
|
3393
|
+
// reads THIS set, never the category-agnostic shared index below, so an
|
|
3394
|
+
// over-included non-fact body can never satisfy a fact-hash check. Repopulate
|
|
3395
|
+
// the LIVE set in place (clear + add) rather than building a fresh set and
|
|
3396
|
+
// reassigning: a concurrent in-process writeMemory that adds a fact hash
|
|
3397
|
+
// during the corpus-read awaits below would be lost by a publish-time
|
|
3398
|
+
// reassignment (PR #2016 thread SDzOT), exactly as the shared index avoids
|
|
3399
|
+
// by mutating its `hashes` set in place.
|
|
3400
|
+
this.factOnlyHashes.clear();
|
|
3401
|
+
// #1909 review round 14: index the HOT and COLD tiers together. A fact or
|
|
3402
|
+
// procedure demoted to cold/ is still active and its content-hash must
|
|
3403
|
+
// survive the corpus rebuild, or a restart would drop the hash and let the
|
|
3404
|
+
// next extraction re-create the demoted memory. Matches the hot+cold union
|
|
3405
|
+
// that removeFactContentHashesForMemories already reconciles against.
|
|
3406
|
+
const existing = [
|
|
3407
|
+
...(await this.readAllMemories()),
|
|
3408
|
+
...(await this.readAllColdMemories()),
|
|
3409
|
+
];
|
|
3410
|
+
let legacyRecovered = 0;
|
|
3411
|
+
for (const memory of existing) {
|
|
3412
|
+
// #1909 review round 15 (PR #2016): the content-hash dedup index is
|
|
3413
|
+
// SHARED across EVERY registered write category. persistExtraction calls
|
|
3414
|
+
// addContentHashDedup for every writeCategory it persists — fact,
|
|
3415
|
+
// procedure, preference, decision, commitment, correction, and any other
|
|
3416
|
+
// extracted category — into THIS one index. A rebuild restricted to
|
|
3417
|
+
// fact+procedure dropped every other category's hash on restart, so the
|
|
3418
|
+
// next extraction re-created identical active preference/decision/
|
|
3419
|
+
// commitment memories (the retired fact-hashes.txt load used to preserve
|
|
3420
|
+
// them). Index every active memory regardless of category so the corpus
|
|
3421
|
+
// rebuild covers the full registration surface. Over-inclusion is safe:
|
|
3422
|
+
// the dedup consumers (hasContentHashDedup in persistExtraction, the
|
|
3423
|
+
// explicit-capture negative pre-filter) confirm a hash hit with a
|
|
3424
|
+
// same-category corpus scan before dropping anything, so a surplus hash
|
|
3425
|
+
// only costs a scan — it can never wrongly suppress a write. Procedures
|
|
3426
|
+
// keep their round-13 behaviour: they carry no frontmatter contentHash
|
|
3427
|
+
// (writeMemory sets it only for facts), so they fall through to the
|
|
3428
|
+
// citation-strip reconstruction below, hashing the stored persist body
|
|
3429
|
+
// (title + steps) exactly as buildProcedurePersistBody registered it.
|
|
3430
|
+
if (inferMemoryStatus(memory.frontmatter, memory.path) !== "active") continue;
|
|
3431
|
+
const hash = this.corpusRegisteredHash(memory);
|
|
3432
|
+
if (hash === null) {
|
|
3433
|
+
// Body carries a citation from an unknown/custom template we cannot
|
|
3434
|
+
// safely strip — skip rather than register a wrong hash. A
|
|
3435
|
+
// false-negative miss beats a wrong entry that would permanently
|
|
3436
|
+
// suppress legitimate duplicate writes (see corpusRegisteredHash).
|
|
3437
|
+
legacyRecovered++;
|
|
3438
|
+
continue;
|
|
3439
|
+
}
|
|
3440
|
+
factHashIndex.addByHash(hash);
|
|
3441
|
+
if (memory.frontmatter.category === "fact") this.factOnlyHashes.add(hash);
|
|
3442
|
+
}
|
|
3443
|
+
if (legacyRecovered > 0) {
|
|
3444
|
+
log.info(
|
|
3445
|
+
`ensureFactHashIndexAuthoritative: skipped ${legacyRecovered} legacy memory(ies) with no contentHash in frontmatter`
|
|
3446
|
+
);
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
|
|
3450
|
+
/**
|
|
3451
|
+
* The content-hash the corpus rebuild registers for `memory`, or null when the
|
|
3452
|
+
* body carries a citation from an unknown/custom template that cannot be
|
|
3453
|
+
* safely stripped. Shared by the authoritative rebuild and the fact-only
|
|
3454
|
+
* corpus confirmation (`hasFactContentHash`) so both derive the identical
|
|
3455
|
+
* hash for a given stored body.
|
|
3456
|
+
*
|
|
3457
|
+
* Preference order:
|
|
3458
|
+
* 1. frontmatter.contentHash — the raw pre-citation hash writeMemory records
|
|
3459
|
+
* for facts (issue #369 round 8); matches hasFactContentHash(rawFact).
|
|
3460
|
+
* 2. Reconstruct from the stored body: strip the "[Attributes: …]" suffix
|
|
3461
|
+
* writeMemory appends for structuredAttributes (registration hashed the
|
|
3462
|
+
* raw canonical content WITHOUT it — a no-op when absent), then strip a
|
|
3463
|
+
* recognised citation and hash the bare body, or hash a citation-free
|
|
3464
|
+
* body as-is.
|
|
3465
|
+
* 3. A body with a citation from an unknown/custom template → null: a
|
|
3466
|
+
* false-negative miss beats a wrong hash that would permanently suppress
|
|
3467
|
+
* legitimate duplicate writes.
|
|
3468
|
+
*/
|
|
3469
|
+
private corpusRegisteredHash(memory: MemoryFile): string | null {
|
|
3470
|
+
if (memory.frontmatter.contentHash) {
|
|
3471
|
+
return memory.frontmatter.contentHash;
|
|
3472
|
+
}
|
|
3473
|
+
const content = stripAttributesSuffix(memory.content);
|
|
3474
|
+
const stripped = stripCitationForTemplate(content, this.citationTemplate);
|
|
3475
|
+
if (stripped !== content) {
|
|
3476
|
+
return ContentHashIndex.computeHash(sanitizeMemoryContent(stripped).text);
|
|
3477
|
+
}
|
|
3478
|
+
if (!hasCitation(content)) {
|
|
3479
|
+
return ContentHashIndex.computeHash(sanitizeMemoryContent(content).text);
|
|
3480
|
+
}
|
|
3481
|
+
return null;
|
|
3716
3482
|
}
|
|
3717
3483
|
private get questionsDir(): string {
|
|
3718
3484
|
return path.join(this.baseDir, "questions");
|
|
@@ -3765,6 +3531,27 @@ export class StorageManager {
|
|
|
3765
3531
|
private get behaviorSignalsPath(): string {
|
|
3766
3532
|
return path.join(this.stateDir, "behavior-signals.jsonl");
|
|
3767
3533
|
}
|
|
3534
|
+
/**
|
|
3535
|
+
* In-memory dedup key set for behavior-signals appends (issue #1909),
|
|
3536
|
+
* validated by (size, mtime) file identity like the catalog `compactedCache`.
|
|
3537
|
+
* Avoids re-reading + JSON.parsing the whole `behavior-signals.jsonl` on
|
|
3538
|
+
* every append. A foreign write changes size/mtime, forcing a reload; this
|
|
3539
|
+
* process's own appends refresh the identity in place. null => reload.
|
|
3540
|
+
*/
|
|
3541
|
+
private behaviorSignalsKeyCache:
|
|
3542
|
+
| { identity: { size: number; mtimeMs: number }; keys: Set<string> }
|
|
3543
|
+
| null = null;
|
|
3544
|
+
/**
|
|
3545
|
+
* Per-instance serializer for `appendBehaviorSignals` (issue #1909 review
|
|
3546
|
+
* round 3). The read→dedup→append→cache-commit transaction must run to
|
|
3547
|
+
* completion before the next append starts; otherwise two concurrent callers
|
|
3548
|
+
* each snapshot their own `existingKeys`, and the later one commits an
|
|
3549
|
+
* INCOMPLETE set under the final file identity, so a subsequent call
|
|
3550
|
+
* cache-hits a set missing the earlier batch and writes duplicate signals.
|
|
3551
|
+
* The `.catch` links keep the chain alive after a rejected append
|
|
3552
|
+
* (AGENTS.md #28).
|
|
3553
|
+
*/
|
|
3554
|
+
private behaviorSignalsAppendChain: Promise<unknown> = Promise.resolve();
|
|
3768
3555
|
/**
|
|
3769
3556
|
* Buffer surprise telemetry ledger (issue #563 PR 3).
|
|
3770
3557
|
*
|
|
@@ -3892,71 +3679,7 @@ export class StorageManager {
|
|
|
3892
3679
|
async writeMemory(
|
|
3893
3680
|
category: MemoryCategory,
|
|
3894
3681
|
content: string,
|
|
3895
|
-
options: {
|
|
3896
|
-
actor?: string;
|
|
3897
|
-
confidence?: number;
|
|
3898
|
-
tags?: string[];
|
|
3899
|
-
entityRef?: string;
|
|
3900
|
-
source?: string;
|
|
3901
|
-
supersedes?: string;
|
|
3902
|
-
lineage?: string[];
|
|
3903
|
-
importance?: ImportanceScore;
|
|
3904
|
-
links?: MemoryLink[];
|
|
3905
|
-
intentGoal?: string;
|
|
3906
|
-
intentActionType?: string;
|
|
3907
|
-
intentEntityTypes?: string[];
|
|
3908
|
-
artifactType?: MemoryFrontmatter["artifactType"];
|
|
3909
|
-
sourceMemoryId?: string;
|
|
3910
|
-
sourceTurnId?: string;
|
|
3911
|
-
memoryKind?: MemoryFrontmatter["memoryKind"];
|
|
3912
|
-
expiresAt?: string;
|
|
3913
|
-
validAt?: string;
|
|
3914
|
-
// Issue #1578 — bi-temporal ingestion provenance. `observedAt` is the
|
|
3915
|
-
// ingestion time (when Remnic learned the fact); `eventTimeSource`
|
|
3916
|
-
// records whether `validAt` was resolved from an extracted expression
|
|
3917
|
-
// or assumed from the ingestion anchor. Both validate on serialize.
|
|
3918
|
-
observedAt?: string;
|
|
3919
|
-
eventTimeSource?: "extracted" | "assumed";
|
|
3920
|
-
invalidAt?: string;
|
|
3921
|
-
structuredAttributes?: Record<string, string>;
|
|
3922
|
-
/**
|
|
3923
|
-
* When provided, this string is used as the source for the fact-content
|
|
3924
|
-
* dedup hash index instead of the persisted body (`content`).
|
|
3925
|
-
*
|
|
3926
|
-
* Use this when the persisted body differs from the canonical fact text
|
|
3927
|
-
* — for example when `content` is a citation-annotated variant of a raw
|
|
3928
|
-
* fact. Passing the raw fact as `contentHashSource` ensures that
|
|
3929
|
-
* `hasFactContentHash(rawFact)` returns `true` after the write, so
|
|
3930
|
-
* subsequent extractions of the same logical fact are correctly deduped
|
|
3931
|
-
* even when their citation timestamp differs.
|
|
3932
|
-
*/
|
|
3933
|
-
contentHashSource?: string;
|
|
3934
|
-
status?: MemoryStatus;
|
|
3935
|
-
/**
|
|
3936
|
-
* Consolidation provenance (issue #561 PR 2). When the caller is a
|
|
3937
|
-
* consolidation / supersession / dedup-merge path, these fields wire
|
|
3938
|
-
* the page-version snapshots the new memory was derived from and the
|
|
3939
|
-
* operator that produced it. Persisted onto frontmatter as
|
|
3940
|
-
* `derived_from` + `derived_via`; validated at serialize time.
|
|
3941
|
-
*/
|
|
3942
|
-
derivedFrom?: string[];
|
|
3943
|
-
derivedVia?: ConsolidationOperator;
|
|
3944
|
-
/**
|
|
3945
|
-
* Faithfulness gate verdict (issue #1576). When provided, persisted to
|
|
3946
|
-
* frontmatter so downstream readers (TrustScore #1577, review queue)
|
|
3947
|
-
* can consume it. Absent = gate was off or fact predates #1576.
|
|
3948
|
-
*/
|
|
3949
|
-
faithfulness?: import("./types.js").FaithfulnessFrontmatter;
|
|
3950
|
-
/**
|
|
3951
|
-
* Claim-level provenance spans (issue #1575 PR 2). When provided,
|
|
3952
|
-
* persisted to frontmatter so downstream readers (memory_get, x-ray,
|
|
3953
|
-
* faithfulness gate #1576) can consume them. Absent = fact predates
|
|
3954
|
-
* #1575 or provenance was disabled.
|
|
3955
|
-
*/
|
|
3956
|
-
sources?: ProvenanceSource[];
|
|
3957
|
-
provenance?: "verified" | "unverified" | "none";
|
|
3958
|
-
sourceConnector?: string;
|
|
3959
|
-
} = {}
|
|
3682
|
+
options: WriteMemoryOptions = {}
|
|
3960
3683
|
): Promise<MemoryWriteResult> {
|
|
3961
3684
|
await this.ensureDirectories();
|
|
3962
3685
|
const now = new Date();
|
|
@@ -4039,17 +3762,10 @@ export class StorageManager {
|
|
|
4039
3762
|
fm.sourceConnector = options.sourceConnector;
|
|
4040
3763
|
}
|
|
4041
3764
|
|
|
4042
|
-
//
|
|
4043
|
-
//
|
|
4044
|
-
//
|
|
4045
|
-
|
|
4046
|
-
// orchestrator's hash-dedup path.
|
|
4047
|
-
let enrichedContent = content;
|
|
4048
|
-
if (options.structuredAttributes && Object.keys(options.structuredAttributes).length > 0) {
|
|
4049
|
-
enrichedContent = `${content}\n[Attributes: ${normalizeAttributePairs(options.structuredAttributes)}]`;
|
|
4050
|
-
}
|
|
4051
|
-
|
|
4052
|
-
const sanitized = sanitizeMemoryContent(enrichedContent);
|
|
3765
|
+
// Assemble the persisted body (attribute-suffix enrichment + combined
|
|
3766
|
+
// sanitize) via the SHARED helper — the sealed-envelope composer uses the
|
|
3767
|
+
// same one, so the two write paths cannot diverge (issue #1989 PR2).
|
|
3768
|
+
const sanitized = assemblePersistedBody(content, options.structuredAttributes);
|
|
4053
3769
|
if (!sanitized.clean) {
|
|
4054
3770
|
log.warn(`memory content sanitized for ${id}; violations=${sanitized.violations.join(", ")}`);
|
|
4055
3771
|
}
|
|
@@ -4133,6 +3849,7 @@ export class StorageManager {
|
|
|
4133
3849
|
await this.snapshotBeforeWrite(filePath, "write");
|
|
4134
3850
|
await this.writeStorageSecureFile(filePath, fileContent);
|
|
4135
3851
|
await this.patchHotMemoriesCache({ addedPath: filePath }, "memory-create");
|
|
3852
|
+
this.notifyMemoryWrite(filePath);
|
|
4136
3853
|
await this.appendGeneratedMemoryLifecycleEventFailOpen("storage.writeMemory", {
|
|
4137
3854
|
memoryId: id,
|
|
4138
3855
|
eventType: "created",
|
|
@@ -4155,13 +3872,31 @@ export class StorageManager {
|
|
|
4155
3872
|
// fact text before citation annotation), index THAT string so that
|
|
4156
3873
|
// hasFactContentHash(rawFact) returns true on subsequent extractions.
|
|
4157
3874
|
// Otherwise fall back to the sanitized persisted body as before.
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
3875
|
+
const hashText =
|
|
3876
|
+
options.contentHashSource !== undefined && options.contentHashSource.length > 0
|
|
3877
|
+
? sanitizeMemoryContent(options.contentHashSource).text
|
|
3878
|
+
: sanitized.text;
|
|
3879
|
+
factHashIndex.add(hashText);
|
|
3880
|
+
this.factOnlyHashes.add(ContentHashIndex.computeHash(hashText));
|
|
3881
|
+
// Gate only the flush (issue #1909): the `.add(...)` above already set
|
|
3882
|
+
// dirty=true. When the caller defers, it owns the batch save
|
|
3883
|
+
// (extraction persist -> saveContentHashIndexes()). Single-write callers
|
|
3884
|
+
// (explicit capture, import, wearable, native writes) flush immediately —
|
|
3885
|
+
// via the SAME cross-process locked reconcile the batch/append and
|
|
3886
|
+
// rebuild paths use (PR #2016 thread SDyCk), never the unlocked whole-file
|
|
3887
|
+
// save() that could clobber, or be clobbered by, a peer's concurrent
|
|
3888
|
+
// locked rebuild/reconcile and drop this durable fact from the index.
|
|
3889
|
+
if (!options.deferHashIndexSave) {
|
|
3890
|
+
await factHashIndex.saveMergingWithDisk();
|
|
3891
|
+
// A locked reconcile that times out defers to an unref'd background
|
|
3892
|
+
// retry and returns WITHOUT publishing (dirty retained). A single-write
|
|
3893
|
+
// caller must not observe that as durable (PR #2016 thread SD7Tk):
|
|
3894
|
+
// drain the deferred retry inline so the addition lands on disk (or
|
|
3895
|
+
// exhausts its bounded attempts, falling back to the corpus-rebuild
|
|
3896
|
+
// safety net) before writeMemory returns. No-op when the save already
|
|
3897
|
+
// published (not dirty).
|
|
3898
|
+
await factHashIndex.flushReconcileRetry();
|
|
4163
3899
|
}
|
|
4164
|
-
await factHashIndex.save();
|
|
4165
3900
|
} catch (err) {
|
|
4166
3901
|
log.warn(`storage.writeMemory completed but failed to update fact hash index: ${err}`);
|
|
4167
3902
|
}
|
|
@@ -4170,11 +3905,73 @@ export class StorageManager {
|
|
|
4170
3905
|
return { id, tombstoneBlocked, ...(fm.blockedBy ? { blockedBy: fm.blockedBy } : {}) };
|
|
4171
3906
|
}
|
|
4172
3907
|
|
|
3908
|
+
/**
|
|
3909
|
+
* Sealed-envelope write entry point (issue #1989 PR2).
|
|
3910
|
+
*
|
|
3911
|
+
* Byte-identity with `writeMemory` is BY DELEGATION: the envelope-owned
|
|
3912
|
+
* fields are unpacked into the exact `writeMemory` arguments a legacy
|
|
3913
|
+
* caller would have passed, so the persisted output is produced by the
|
|
3914
|
+
* same code path. The composer's `persistedBody` was assembled with the
|
|
3915
|
+
* same `assemblePersistedBody` helper `writeMemory` uses, so fingerprints
|
|
3916
|
+
* derived from the envelope match the stored body (§13).
|
|
3917
|
+
*
|
|
3918
|
+
* Differences from a raw `writeMemory` call are REJECTIONS, not silent
|
|
3919
|
+
* changes: the composer enforces tag/attribute caps, strict validAt, and
|
|
3920
|
+
* plain-object attributes that the legacy path never validated.
|
|
3921
|
+
*
|
|
3922
|
+
* `envelope.ttl` maps to `expiresAt` verbatim; converting duration
|
|
3923
|
+
* expressions (`"90d"`) to instants remains the access layer's job (PR3).
|
|
3924
|
+
* `envelope.sourceReason` is access-layer metadata with no frontmatter
|
|
3925
|
+
* field and is deliberately not persisted here.
|
|
3926
|
+
*/
|
|
3927
|
+
async writeSealedMemory(
|
|
3928
|
+
envelope: SealedMemoryEnvelope,
|
|
3929
|
+
extras: SealedWriteExtras = {},
|
|
3930
|
+
): Promise<MemoryWriteResult> {
|
|
3931
|
+
if (!isSealedMemoryEnvelope(envelope)) {
|
|
3932
|
+
throw new Error(
|
|
3933
|
+
"writeSealedMemory: value is not a valid sealed memory envelope (fails the composeMemoryEnvelope contract)",
|
|
3934
|
+
);
|
|
3935
|
+
}
|
|
3936
|
+
const { category, content, options } = sealedWriteToLegacyArgs(envelope, extras);
|
|
3937
|
+
return this.writeMemory(category, content, options as WriteMemoryOptions);
|
|
3938
|
+
}
|
|
3939
|
+
|
|
4173
3940
|
async hasFactContentHash(content: string): Promise<boolean> {
|
|
4174
|
-
await this.ensureFactHashIndexAuthoritative();
|
|
4175
|
-
const factHashIndex = await this.getFactHashIndex();
|
|
3941
|
+
const authoritative = await this.ensureFactHashIndexAuthoritative();
|
|
4176
3942
|
const sanitized = sanitizeMemoryContent(content);
|
|
4177
|
-
|
|
3943
|
+
const hash = ContentHashIndex.computeHash(sanitized.text);
|
|
3944
|
+
// Fact-ONLY answer (PR #2016). The shared content-hash index is
|
|
3945
|
+
// category-agnostic — the round-15 authoritative rebuild indexes EVERY
|
|
3946
|
+
// active category and addContentHashDedup registers all of them — so it
|
|
3947
|
+
// cannot tell a FACT from an unrelated preference/decision/note/moment with
|
|
3948
|
+
// the same normalized body. Direct consumers
|
|
3949
|
+
// (createWearableMemoryWriter.hasFactContentHash, the explicit-capture
|
|
3950
|
+
// negative pre-filter) treat a hit as terminal BEFORE their own source/
|
|
3951
|
+
// category confirmation, so answering from the shared index would let a
|
|
3952
|
+
// non-fact suppress a real fact candidate. Only category === "fact" registers
|
|
3953
|
+
// a fact-content hash at write time (writeMemory), so answer from the
|
|
3954
|
+
// fact-only membership rebuilt in lockstep with the shared index. When the
|
|
3955
|
+
// index is authoritative that set is current (rebuild + write/removal
|
|
3956
|
+
// upkeep); otherwise the snapshot may be stale, so verify against the durable
|
|
3957
|
+
// fact corpus (ground truth) so a lock-contended read never suppresses a fact.
|
|
3958
|
+
if (authoritative) {
|
|
3959
|
+
return this.factOnlyHashes.has(hash);
|
|
3960
|
+
}
|
|
3961
|
+
return await this.factContentHashPresentInCorpus(hash);
|
|
3962
|
+
}
|
|
3963
|
+
|
|
3964
|
+
private async factContentHashPresentInCorpus(targetHash: string): Promise<boolean> {
|
|
3965
|
+
const existing = [
|
|
3966
|
+
...(await this.readAllMemories()),
|
|
3967
|
+
...(await this.readAllColdMemories()),
|
|
3968
|
+
];
|
|
3969
|
+
for (const memory of existing) {
|
|
3970
|
+
if (memory.frontmatter.category !== "fact") continue;
|
|
3971
|
+
if (inferMemoryStatus(memory.frontmatter, memory.path) !== "active") continue;
|
|
3972
|
+
if (this.corpusRegisteredHash(memory) === targetHash) return true;
|
|
3973
|
+
}
|
|
3974
|
+
return false;
|
|
4178
3975
|
}
|
|
4179
3976
|
|
|
4180
3977
|
private factContentHashForRemoval(memory: MemoryFile): string | null {
|
|
@@ -4199,7 +3996,22 @@ export class StorageManager {
|
|
|
4199
3996
|
await this.ensureFactHashIndexAuthoritative();
|
|
4200
3997
|
const factHashIndex = await this.getFactHashIndex();
|
|
4201
3998
|
factHashIndex.addByHash(hash);
|
|
4202
|
-
|
|
3999
|
+
this.factOnlyHashes.add(hash);
|
|
4000
|
+
// PR #2016 thread SDzOP: flush through the SAME cross-process locked
|
|
4001
|
+
// reconcile the write/batch/rebuild paths use, never the unlocked whole-file
|
|
4002
|
+
// save() — an unlocked overwrite drops a concurrent extraction's appended
|
|
4003
|
+
// hash and can be clobbered by a peer's locked publish. saveMergingWithDisk
|
|
4004
|
+
// republishes only OUR delta ((on-disk \ removed) ∪ added) under the lock.
|
|
4005
|
+
await factHashIndex.saveMergingWithDisk();
|
|
4006
|
+
// A locked reconcile that times out defers to an unref'd background retry
|
|
4007
|
+
// and returns WITHOUT publishing (dirty retained). The reactivation path is
|
|
4008
|
+
// a lifecycle boundary just like writeMemory (PR #2016 thread
|
|
4009
|
+
// PRRT_kwDORJXyws6SEHvh): a short-lived caller must not observe the deferral
|
|
4010
|
+
// as durable. Drain the deferred retry inline so the reintroduced hash lands
|
|
4011
|
+
// on disk (or exhausts its bounded attempts, falling back to the
|
|
4012
|
+
// corpus-rebuild safety net) before returning. No-op — no duplicated retry
|
|
4013
|
+
// work — when the save already published (not dirty).
|
|
4014
|
+
await factHashIndex.flushReconcileRetry();
|
|
4203
4015
|
}
|
|
4204
4016
|
|
|
4205
4017
|
/**
|
|
@@ -4268,14 +4080,38 @@ export class StorageManager {
|
|
|
4268
4080
|
for (const hash of removedHashes.values()) {
|
|
4269
4081
|
if (!remainingActiveHashes.has(hash)) {
|
|
4270
4082
|
factHashIndex.removeByHash(hash);
|
|
4083
|
+
this.factOnlyHashes.delete(hash);
|
|
4271
4084
|
}
|
|
4272
4085
|
}
|
|
4273
|
-
|
|
4086
|
+
// PR #2016 thread SDzOP: serialize the removal with the per-index lock via
|
|
4087
|
+
// the removal-aware reconcile, never the unlocked whole-file save(). The
|
|
4088
|
+
// unlocked overwrite republished this instance's stale in-memory set,
|
|
4089
|
+
// silently clobbering a concurrent extraction's appended hash; the reconcile
|
|
4090
|
+
// reads the latest on-disk state, drops only OUR removed hashes, and keeps a
|
|
4091
|
+
// peer's concurrent append.
|
|
4092
|
+
await factHashIndex.saveMergingWithDisk();
|
|
4093
|
+
}
|
|
4094
|
+
|
|
4095
|
+
/**
|
|
4096
|
+
* Remove a memory's fact-ONLY hash membership in lockstep with the shared,
|
|
4097
|
+
* category-agnostic index removal the orchestrator's
|
|
4098
|
+
* `removeContentHashForMemory` performs on archival / semantic consolidation
|
|
4099
|
+
* (PR #2016 threads SDzOP / SDzOR). Without this, `factOnlyHashes` kept a
|
|
4100
|
+
* removed/superseded fact's hash and `hasFactContentHash` returned a stale
|
|
4101
|
+
* `true` until the next corpus rebuild, so wearable / explicit-capture /
|
|
4102
|
+
* promotion callers skipped a valid write. In-memory only — `factOnlyHashes`
|
|
4103
|
+
* is never persisted; it is rebuilt from the corpus. No-op for non-facts.
|
|
4104
|
+
* Matches the shared index's unconditional per-hash removal so the two stay
|
|
4105
|
+
* coherent.
|
|
4106
|
+
*/
|
|
4107
|
+
removeFactOnlyHashForMemory(memory: MemoryFile): void {
|
|
4108
|
+
if (memory.frontmatter.category !== "fact") return;
|
|
4109
|
+
const hash = this.factContentHashForRemoval(memory);
|
|
4110
|
+
if (hash) this.factOnlyHashes.delete(hash);
|
|
4274
4111
|
}
|
|
4275
4112
|
|
|
4276
4113
|
async isFactContentHashAuthoritative(): Promise<boolean> {
|
|
4277
|
-
await this.ensureFactHashIndexAuthoritative();
|
|
4278
|
-
return true;
|
|
4114
|
+
return await this.ensureFactHashIndexAuthoritative();
|
|
4279
4115
|
}
|
|
4280
4116
|
|
|
4281
4117
|
async writeArtifact(
|
|
@@ -5468,20 +5304,77 @@ export class StorageManager {
|
|
|
5468
5304
|
async appendMemoryLifecycleEvents(events: MemoryLifecycleEvent[]): Promise<number> {
|
|
5469
5305
|
if (events.length === 0) return 0;
|
|
5470
5306
|
await this.ensureDirectories();
|
|
5307
|
+
// Lock-serialized against compaction; on lock-budget exhaustion the event
|
|
5308
|
+
// spills to a durable encrypted pending queue, not dropped (#1910, #2033).
|
|
5309
|
+
await appendLifecycleEventsSerialized(
|
|
5310
|
+
this.memoryLifecycleLedgerPath,
|
|
5311
|
+
(p) => this.appendStorageSecureFile(this.memoryLifecycleLedgerPath, p),
|
|
5312
|
+
serializeLifecycleAppendPayload(events),
|
|
5313
|
+
{ writeSecure: (p, c) => this.writeStorageSecureFile(p, c), readSecure: (p) => this.readStorageSecureFile(p) },
|
|
5314
|
+
);
|
|
5315
|
+
return events.length;
|
|
5316
|
+
}
|
|
5471
5317
|
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5318
|
+
/** Drain the durable pending lifecycle-append spill into the ledger (#2033);
|
|
5319
|
+
* fast no-op when nothing is pending. Returns true when rows were drained. */
|
|
5320
|
+
async drainPendingMemoryLifecycleEvents(): Promise<boolean> {
|
|
5321
|
+
return drainPendingLifecycleLedgerIfAny(
|
|
5322
|
+
this.memoryLifecycleLedgerPath,
|
|
5323
|
+
{ writeSecure: (p, c) => this.writeStorageSecureFile(p, c), readSecure: (p) => this.readStorageSecureFile(p) },
|
|
5324
|
+
(p) => this.appendStorageSecureFile(this.memoryLifecycleLedgerPath, p),
|
|
5325
|
+
() => this.ensureDirectories(),
|
|
5326
|
+
);
|
|
5327
|
+
}
|
|
5482
5328
|
|
|
5483
|
-
|
|
5484
|
-
|
|
5329
|
+
/** Offline-sync pre-snapshot drain (#2033): fold pending lifecycle spills into
|
|
5330
|
+
* the active ledger and report whether durable rows STILL remain in the
|
|
5331
|
+
* offline-sync-EXCLUDED pending queue. The caller aborts the snapshot when
|
|
5332
|
+
* `pendingDeferred` is true so append-only rows are never silently omitted. */
|
|
5333
|
+
async drainPendingMemoryLifecycleEventsForSync(): Promise<DrainPendingLifecycleForSyncResult> {
|
|
5334
|
+
return drainPendingLifecycleLedgerForSync(
|
|
5335
|
+
this.memoryLifecycleLedgerPath,
|
|
5336
|
+
{ writeSecure: (p, c) => this.writeStorageSecureFile(p, c), readSecure: (p) => this.readStorageSecureFile(p) },
|
|
5337
|
+
(p) => this.appendStorageSecureFile(this.memoryLifecycleLedgerPath, p),
|
|
5338
|
+
() => this.ensureDirectories(),
|
|
5339
|
+
);
|
|
5340
|
+
}
|
|
5341
|
+
|
|
5342
|
+
/** Drain pending lifecycle spills for any ledger inside this storage root.
|
|
5343
|
+
* The offline CLI uses this path-aware variant for per-namespace ledgers so
|
|
5344
|
+
* secure-store encryption and path-bound authentication remain consistent. */
|
|
5345
|
+
async drainPendingMemoryLifecycleEventsForSyncAt(
|
|
5346
|
+
ledgerPath: string,
|
|
5347
|
+
): Promise<DrainPendingLifecycleForSyncResult> {
|
|
5348
|
+
const target = this.assertManagedStoragePath(
|
|
5349
|
+
ledgerPath,
|
|
5350
|
+
"storage.drainPendingMemoryLifecycleEventsForSyncAt",
|
|
5351
|
+
);
|
|
5352
|
+
return drainPendingLifecycleLedgerForSync(
|
|
5353
|
+
target,
|
|
5354
|
+
{
|
|
5355
|
+
writeSecure: (p, c) => this.writeStorageSecureFile(p, c),
|
|
5356
|
+
readSecure: (p) => this.readStorageSecureFile(p),
|
|
5357
|
+
},
|
|
5358
|
+
(payload) => this.appendStorageSecureFile(target, payload),
|
|
5359
|
+
async () => {
|
|
5360
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
5361
|
+
},
|
|
5362
|
+
);
|
|
5363
|
+
}
|
|
5364
|
+
|
|
5365
|
+
/** Rewrite the ledger through the secure writer (#1910); re-encrypts when unlocked, throws when locked.
|
|
5366
|
+
* `targetPath` re-encrypts under a decryptable backup path's own AAD;
|
|
5367
|
+
* Buffer content passes through verbatim (#2033). `forceEncrypt` preserves
|
|
5368
|
+
* encryption at rest for an already-encrypted ledger/backup even when the
|
|
5369
|
+
* `secureStoreEncryptOnWrite` policy is paused, so a compaction never
|
|
5370
|
+
* downgrades encrypted state to plaintext (#2033); it never bypasses the lock. */
|
|
5371
|
+
async writeMemoryLifecycleLedgerContent(
|
|
5372
|
+
content: string | Buffer,
|
|
5373
|
+
targetPath: string = this.memoryLifecycleLedgerPath,
|
|
5374
|
+
forceEncrypt = false,
|
|
5375
|
+
): Promise<void> {
|
|
5376
|
+
await this.ensureDirectories();
|
|
5377
|
+
await this.writeStorageSecureFile(targetPath, content, forceEncrypt);
|
|
5485
5378
|
}
|
|
5486
5379
|
|
|
5487
5380
|
async appendBufferSurpriseEvents(events: BufferSurpriseEvent[]): Promise<number> {
|
|
@@ -5493,46 +5386,113 @@ export class StorageManager {
|
|
|
5493
5386
|
}
|
|
5494
5387
|
|
|
5495
5388
|
async appendBehaviorSignals(events: BehaviorSignalEvent[]): Promise<number> {
|
|
5389
|
+
if (events.length === 0) return 0;
|
|
5390
|
+
// Serialize the whole read→dedup→append→cache-commit transaction per
|
|
5391
|
+
// instance (issue #1909 review round 3) so concurrent callers cannot each
|
|
5392
|
+
// commit an incomplete dedup set. The chain recovers after a rejection.
|
|
5393
|
+
const run = this.behaviorSignalsAppendChain
|
|
5394
|
+
.catch(() => undefined)
|
|
5395
|
+
.then(() => this.appendBehaviorSignalsUnlocked(events));
|
|
5396
|
+
this.behaviorSignalsAppendChain = run.catch(() => undefined);
|
|
5397
|
+
return run;
|
|
5398
|
+
}
|
|
5399
|
+
|
|
5400
|
+
private async appendBehaviorSignalsUnlocked(events: BehaviorSignalEvent[]): Promise<number> {
|
|
5496
5401
|
if (events.length === 0) return 0;
|
|
5497
5402
|
await this.ensureDirectories();
|
|
5498
5403
|
|
|
5499
|
-
let existingKeys
|
|
5404
|
+
let existingKeys: Set<string>;
|
|
5405
|
+
let identity: { size: number; mtimeMs: number } | null = null;
|
|
5500
5406
|
try {
|
|
5501
|
-
const
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5407
|
+
const st = await stat(this.behaviorSignalsPath);
|
|
5408
|
+
identity = { size: st.size, mtimeMs: st.mtimeMs };
|
|
5409
|
+
if (
|
|
5410
|
+
this.behaviorSignalsKeyCache &&
|
|
5411
|
+
this.behaviorSignalsKeyCache.identity.size === identity.size &&
|
|
5412
|
+
this.behaviorSignalsKeyCache.identity.mtimeMs === identity.mtimeMs
|
|
5413
|
+
) {
|
|
5414
|
+
// Cache hit (issue #1909): reuse the dedup set — no read, no parse.
|
|
5415
|
+
existingKeys = this.behaviorSignalsKeyCache.keys;
|
|
5416
|
+
} else {
|
|
5417
|
+
// Foreign change (or first load): rebuild by streaming the file so we
|
|
5418
|
+
// never materialize the whole (unbounded) ledger as one string.
|
|
5419
|
+
existingKeys = new Set<string>();
|
|
5420
|
+
for await (const line of readMaybeEncryptedLines(this.behaviorSignalsPath, () =>
|
|
5421
|
+
this.readStorageSecureFile(this.behaviorSignalsPath)
|
|
5422
|
+
)) {
|
|
5423
|
+
const row = line.trim();
|
|
5424
|
+
if (!row) continue;
|
|
5425
|
+
try {
|
|
5426
|
+
const parsed = JSON.parse(row) as Partial<BehaviorSignalEvent>;
|
|
5427
|
+
if (typeof parsed.memoryId === "string" && typeof parsed.signalHash === "string") {
|
|
5428
|
+
existingKeys.add(`${parsed.memoryId}:${parsed.signalHash}`);
|
|
5429
|
+
}
|
|
5430
|
+
} catch {
|
|
5431
|
+
// Ignore malformed rows (fail-open).
|
|
5510
5432
|
}
|
|
5511
|
-
} catch {
|
|
5512
|
-
// Ignore malformed rows (fail-open).
|
|
5513
5433
|
}
|
|
5514
5434
|
}
|
|
5515
5435
|
} catch (err) {
|
|
5516
5436
|
if (err instanceof SecureStoreLockedError) throw err;
|
|
5517
5437
|
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
5518
5438
|
existingKeys = new Set<string>();
|
|
5439
|
+
identity = null;
|
|
5519
5440
|
}
|
|
5520
5441
|
|
|
5521
5442
|
const nowIso = new Date().toISOString();
|
|
5443
|
+
// Collect newly-seen keys in a SEPARATE set — never mutate `existingKeys`
|
|
5444
|
+
// (which may alias the cached set) before the append is durable. A failed
|
|
5445
|
+
// append must leave the cached dedup set matching what is actually on disk,
|
|
5446
|
+
// otherwise a retry would cache-hit a poisoned set and silently drop the
|
|
5447
|
+
// events forever (issue #1909 review: pre-durability aliasing).
|
|
5448
|
+
const pending = new Set<string>();
|
|
5522
5449
|
const deduped: BehaviorSignalEvent[] = [];
|
|
5523
5450
|
for (const event of events) {
|
|
5524
5451
|
const key = `${event.memoryId}:${event.signalHash}`;
|
|
5525
|
-
if (existingKeys.has(key)) continue;
|
|
5526
|
-
|
|
5452
|
+
if (existingKeys.has(key) || pending.has(key)) continue;
|
|
5453
|
+
pending.add(key);
|
|
5527
5454
|
deduped.push({
|
|
5528
5455
|
...event,
|
|
5529
5456
|
timestamp: event.timestamp && event.timestamp.length > 0 ? event.timestamp : nowIso,
|
|
5530
5457
|
});
|
|
5531
5458
|
}
|
|
5532
5459
|
|
|
5533
|
-
if (deduped.length === 0)
|
|
5460
|
+
if (deduped.length === 0) {
|
|
5461
|
+
// Nothing appended — the file identity is unchanged and `existingKeys` was
|
|
5462
|
+
// not mutated, so caching it lets the next append hit (issue #1909).
|
|
5463
|
+
if (identity) this.behaviorSignalsKeyCache = { identity, keys: existingKeys };
|
|
5464
|
+
return 0;
|
|
5465
|
+
}
|
|
5534
5466
|
const payload = deduped.map((event) => `${JSON.stringify(event)}\n`).join("");
|
|
5467
|
+
// May throw (I/O error, SecureStoreLockedError). If it does, we fall through
|
|
5468
|
+
// to the caller WITHOUT having touched `existingKeys` or the cache, so the
|
|
5469
|
+
// dropped events can be retried.
|
|
5535
5470
|
await this.appendStorageSecureFile(this.behaviorSignalsPath, payload);
|
|
5471
|
+
// Durable now: fold the new keys into the set (in place, O(new keys) — keeps
|
|
5472
|
+
// the per-append win). Only cache the refreshed identity if the file grew by
|
|
5473
|
+
// EXACTLY our payload — otherwise a foreign writer (another instance/process)
|
|
5474
|
+
// interleaved and the file now holds rows whose keys are NOT in existingKeys;
|
|
5475
|
+
// caching that identity would let a later same-instance append cache-hit an
|
|
5476
|
+
// incomplete set and write duplicates (review round 8 thread 5). On any
|
|
5477
|
+
// mismatch, invalidate so the next append reloads from disk.
|
|
5478
|
+
for (const key of pending) existingKeys.add(key);
|
|
5479
|
+
try {
|
|
5480
|
+
const st = await stat(this.behaviorSignalsPath);
|
|
5481
|
+
const expectedSize = (identity?.size ?? 0) + Buffer.byteLength(payload, "utf-8");
|
|
5482
|
+
if (st.size === expectedSize) {
|
|
5483
|
+
this.behaviorSignalsKeyCache = {
|
|
5484
|
+
identity: { size: st.size, mtimeMs: st.mtimeMs },
|
|
5485
|
+
keys: existingKeys,
|
|
5486
|
+
};
|
|
5487
|
+
} else {
|
|
5488
|
+
// Foreign interleave (or encrypted whole-file rewrite): our key set may
|
|
5489
|
+
// be missing peer rows — force a reload on the next append.
|
|
5490
|
+
this.behaviorSignalsKeyCache = null;
|
|
5491
|
+
}
|
|
5492
|
+
} catch {
|
|
5493
|
+
// Fail-open: force a reload on the next append.
|
|
5494
|
+
this.behaviorSignalsKeyCache = null;
|
|
5495
|
+
}
|
|
5536
5496
|
return deduped.length;
|
|
5537
5497
|
}
|
|
5538
5498
|
|
|
@@ -5649,42 +5609,30 @@ export class StorageManager {
|
|
|
5649
5609
|
}
|
|
5650
5610
|
|
|
5651
5611
|
async readAllMemoryLifecycleEvents(): Promise<MemoryLifecycleEvent[]> {
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
typeof parsed.actor === "string" &&
|
|
5667
|
-
typeof parsed.ruleVersion === "string"
|
|
5668
|
-
) {
|
|
5669
|
-
out.push(parsed as MemoryLifecycleEvent);
|
|
5670
|
-
}
|
|
5671
|
-
} catch {
|
|
5672
|
-
// Ignore malformed rows (fail-open).
|
|
5673
|
-
}
|
|
5674
|
-
}
|
|
5675
|
-
return sortMemoryLifecycleEvents(out);
|
|
5676
|
-
} catch (err) {
|
|
5677
|
-
if (err instanceof SecureStoreLockedError) throw err;
|
|
5678
|
-
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
5679
|
-
return [];
|
|
5612
|
+
return readAllLifecycleEventsFromLedger(this.memoryLifecycleLedgerPath, (p) => this.readStorageSecureFile(p));
|
|
5613
|
+
}
|
|
5614
|
+
|
|
5615
|
+
/** Raw decrypted ledger bytes as a Buffer, never a string, so an oversized
|
|
5616
|
+
* ledger cannot throw on decode before recovery (#2033). */
|
|
5617
|
+
async readMemoryLifecycleLedgerRawBufferForCompaction(): Promise<Buffer> {
|
|
5618
|
+
return readMaybeEncryptedFileBuffer(this.memoryLifecycleLedgerPath, this._secureStoreKey, this.baseDir);
|
|
5619
|
+
}
|
|
5620
|
+
async readAllMemoryLifecycleEventsForCompaction(): Promise<MemoryLifecycleEvent[]> {
|
|
5621
|
+
if (!(await probeEncryptedRegularFileHeader(this.memoryLifecycleLedgerPath))) {
|
|
5622
|
+
return readAllLifecycleEventsFromLedger(
|
|
5623
|
+
this.memoryLifecycleLedgerPath,
|
|
5624
|
+
(p) => this.readStorageSecureFile(p),
|
|
5625
|
+
);
|
|
5680
5626
|
}
|
|
5627
|
+
return readAllLifecycleEventsFromLedgerBuffer(
|
|
5628
|
+
this.memoryLifecycleLedgerPath,
|
|
5629
|
+
(p) => readMaybeEncryptedFileBuffer(p, this._secureStoreKey, this.baseDir),
|
|
5630
|
+
);
|
|
5681
5631
|
}
|
|
5682
5632
|
|
|
5683
5633
|
async readMemoryLifecycleEvents(limit: number = 200): Promise<MemoryLifecycleEvent[]> {
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
const events = await this.readAllMemoryLifecycleEvents();
|
|
5687
|
-
return events.slice(-cappedLimit);
|
|
5634
|
+
return readBoundedLifecycleEventsFromLedger(
|
|
5635
|
+
this.memoryLifecycleLedgerPath, (p) => this.readStorageSecureFile(p), limit);
|
|
5688
5636
|
}
|
|
5689
5637
|
|
|
5690
5638
|
async writeCompressionGuidelines(content: string): Promise<void> {
|
|
@@ -6412,6 +6360,7 @@ export class StorageManager {
|
|
|
6412
6360
|
|
|
6413
6361
|
const memories = await this.readAllMemories();
|
|
6414
6362
|
const memoryMap = new Map(memories.map((m) => [m.frontmatter.id, m]));
|
|
6363
|
+
const memoryPathMap = new Map(memories.map((m) => [path.resolve(m.path), m]));
|
|
6415
6364
|
let updated = 0;
|
|
6416
6365
|
// Capture the corpus version + warmth BEFORE writing so we can patch the hot
|
|
6417
6366
|
// entries in place and then re-key them to the version this flush produces
|
|
@@ -6431,7 +6380,9 @@ export class StorageManager {
|
|
|
6431
6380
|
const appliedPatches = new Map<string, { accessCount: number; lastAccessed: string }>();
|
|
6432
6381
|
|
|
6433
6382
|
for (const entry of entries) {
|
|
6434
|
-
const memory =
|
|
6383
|
+
const memory = entry.memoryPath
|
|
6384
|
+
? memoryPathMap.get(path.resolve(entry.memoryPath))
|
|
6385
|
+
: memoryMap.get(entry.memoryId);
|
|
6435
6386
|
if (!memory) continue;
|
|
6436
6387
|
|
|
6437
6388
|
const newFm: MemoryFrontmatter = {
|
|
@@ -6450,7 +6401,10 @@ export class StorageManager {
|
|
|
6450
6401
|
if (warm) {
|
|
6451
6402
|
updateCacheOnWrite(this.baseDir, { ...memory, frontmatter: newFm }, keyId);
|
|
6452
6403
|
}
|
|
6453
|
-
appliedPatches.set(
|
|
6404
|
+
appliedPatches.set(path.resolve(memory.path), {
|
|
6405
|
+
accessCount: entry.newCount,
|
|
6406
|
+
lastAccessed: entry.lastAccessed,
|
|
6407
|
+
});
|
|
6454
6408
|
updated++;
|
|
6455
6409
|
} catch (err) {
|
|
6456
6410
|
log.debug(`failed to update access tracking for ${entry.memoryId}: ${err}`);
|
|
@@ -6483,7 +6437,7 @@ export class StorageManager {
|
|
|
6483
6437
|
// re-key robust to that race; the publish guard blocks any scan
|
|
6484
6438
|
// finishing AFTER our bump, so this is the only window.
|
|
6485
6439
|
const reapplied = cur.map((m) => {
|
|
6486
|
-
const patch = appliedPatches.get(m.
|
|
6440
|
+
const patch = appliedPatches.get(path.resolve(m.path));
|
|
6487
6441
|
return patch
|
|
6488
6442
|
? { ...m, frontmatter: { ...m.frontmatter, accessCount: patch.accessCount, lastAccessed: patch.lastAccessed } }
|
|
6489
6443
|
: m;
|
|
@@ -6511,28 +6465,68 @@ export class StorageManager {
|
|
|
6511
6465
|
}
|
|
6512
6466
|
|
|
6513
6467
|
/**
|
|
6514
|
-
*
|
|
6468
|
+
* Resolve existing active memory IDs to their on-disk paths.
|
|
6515
6469
|
*
|
|
6516
6470
|
* Uses a lightweight directory scan (collectActiveMemoryPaths) that reads
|
|
6517
6471
|
* file names without parsing frontmatter — much cheaper than readAllMemories()
|
|
6518
|
-
* for
|
|
6519
|
-
*
|
|
6520
|
-
* Returns the subset of `ids` that correspond to real memory files.
|
|
6472
|
+
* for citation usage tracking and other existence checks.
|
|
6521
6473
|
*/
|
|
6522
|
-
async
|
|
6523
|
-
|
|
6474
|
+
async findExistingMemoryPaths(
|
|
6475
|
+
ids: string[],
|
|
6476
|
+
preferredPaths: Map<string, string[]> = new Map(),
|
|
6477
|
+
): Promise<Map<string, string[]>> {
|
|
6478
|
+
if (ids.length === 0) return new Map();
|
|
6524
6479
|
const wantedIds = new Set(ids);
|
|
6525
6480
|
const filePaths = await this.collectActiveMemoryPaths();
|
|
6526
|
-
const
|
|
6481
|
+
const pathsById = new Map<string, string[]>();
|
|
6482
|
+
const filePathsById = new Map<string, string[]>();
|
|
6527
6483
|
for (const filePath of filePaths) {
|
|
6528
|
-
const
|
|
6529
|
-
if (wantedIds.has(
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6484
|
+
const memoryId = path.basename(filePath, ".md");
|
|
6485
|
+
if (!wantedIds.has(memoryId)) continue;
|
|
6486
|
+
const paths = filePathsById.get(memoryId) ?? [];
|
|
6487
|
+
paths.push(filePath);
|
|
6488
|
+
filePathsById.set(memoryId, paths);
|
|
6489
|
+
}
|
|
6490
|
+
for (const id of wantedIds) {
|
|
6491
|
+
const existingPaths = filePathsById.get(id) ?? [];
|
|
6492
|
+
const preferred = preferredPaths.get(id) ?? [];
|
|
6493
|
+
const preferredMatches: string[] = [];
|
|
6494
|
+
for (const preferredPath of preferred) {
|
|
6495
|
+
const directCandidates = new Set(
|
|
6496
|
+
qmdResultPathCandidates(this.baseDir, preferredPath),
|
|
6497
|
+
);
|
|
6498
|
+
const directMatch = existingPaths.find((filePath) =>
|
|
6499
|
+
directCandidates.has(path.resolve(filePath)),
|
|
6500
|
+
);
|
|
6501
|
+
if (directMatch) {
|
|
6502
|
+
preferredMatches.push(directMatch);
|
|
6503
|
+
continue;
|
|
6504
|
+
}
|
|
6505
|
+
|
|
6506
|
+
const candidates = new Set<string>();
|
|
6507
|
+
const parts = qmdCollectionPathParts(preferredPath);
|
|
6508
|
+
if (parts) {
|
|
6509
|
+
for (const candidate of qmdResultPathCandidates(this.baseDir, parts.relativePath)) {
|
|
6510
|
+
candidates.add(candidate);
|
|
6511
|
+
}
|
|
6512
|
+
}
|
|
6513
|
+
const match = existingPaths.find((filePath) => candidates.has(path.resolve(filePath)));
|
|
6514
|
+
if (match) preferredMatches.push(match);
|
|
6515
|
+
}
|
|
6516
|
+
if (preferredMatches.length > 0) {
|
|
6517
|
+
pathsById.set(id, preferredMatches);
|
|
6518
|
+
} else if (existingPaths.length > 0) {
|
|
6519
|
+
pathsById.set(id, existingPaths);
|
|
6533
6520
|
}
|
|
6534
6521
|
}
|
|
6535
|
-
return
|
|
6522
|
+
return pathsById;
|
|
6523
|
+
}
|
|
6524
|
+
|
|
6525
|
+
/**
|
|
6526
|
+
* Check which of the given memory IDs actually exist on disk.
|
|
6527
|
+
*/
|
|
6528
|
+
async filterExistingMemoryIds(ids: string[]): Promise<Set<string>> {
|
|
6529
|
+
return new Set((await this.findExistingMemoryPaths(ids)).keys());
|
|
6536
6530
|
}
|
|
6537
6531
|
|
|
6538
6532
|
async getProjectedMemoryState(id: string): Promise<MemoryProjectionCurrentState | null> {
|
|
@@ -6591,8 +6585,8 @@ export class StorageManager {
|
|
|
6591
6585
|
const projected = readProjectedMemoryTimeline(this.baseDir, memoryId, cappedLimit);
|
|
6592
6586
|
if (projected && projected.length > 0) return projected;
|
|
6593
6587
|
warnProjectionFallback(this.baseDir, "getMemoryTimeline");
|
|
6594
|
-
|
|
6595
|
-
|
|
6588
|
+
return readBoundedLifecycleEventsFromLedger(
|
|
6589
|
+
this.memoryLifecycleLedgerPath, (p) => this.readStorageSecureFile(p), cappedLimit, memoryId);
|
|
6596
6590
|
}
|
|
6597
6591
|
|
|
6598
6592
|
// ---------------------------------------------------------------------------
|
|
@@ -6783,13 +6777,11 @@ export class StorageManager {
|
|
|
6783
6777
|
}
|
|
6784
6778
|
}
|
|
6785
6779
|
|
|
6786
|
-
//
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
lineage: [oldMemoryId, newMemoryId],
|
|
6792
|
-
});
|
|
6780
|
+
// Audit-trail correction — sealed even INSIDE the engine (#2022 review).
|
|
6781
|
+
const auditBody = `Superseded: ${oldMemory.content}\n\nReason: ${reason}`;
|
|
6782
|
+
const auditInput = { content: auditBody, category: "correction" as const, confidence: 1.0, tags: ["supersession", "auto-resolved"] };
|
|
6783
|
+
const auditEnvelope = composeMemoryEnvelope(auditInput, { source: "contradiction-detection" });
|
|
6784
|
+
await this.writeSealedMemory(auditEnvelope, { lineage: [oldMemoryId, newMemoryId] });
|
|
6793
6785
|
|
|
6794
6786
|
return true;
|
|
6795
6787
|
} catch (err) {
|