@remnic/core 9.7.14 → 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 +88 -74
- package/dist/access-audit.js +2 -2
- package/dist/access-boundary.d.ts +35 -17
- package/dist/access-boundary.js +89 -77
- package/dist/access-citation.d.ts +28 -0
- package/dist/access-citation.js +9 -0
- package/dist/access-cli.js +197 -181
- 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 +103 -90
- package/dist/access-identity-continuity-surface.d.ts +13 -8
- package/dist/access-identity-continuity-surface.js +88 -74
- package/dist/access-lcm-surface.d.ts +16 -9
- package/dist/access-lcm-surface.js +88 -74
- package/dist/access-mcp.d.ts +23 -16
- package/dist/access-mcp.js +99 -87
- 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 +88 -74
- package/dist/access-operations-batch.js +95 -83
- package/dist/access-operations.d.ts +26 -19
- package/dist/access-operations.js +97 -85
- 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 +88 -74
- package/dist/access-schema.d.ts +34 -34
- package/dist/access-schema.js +8 -6
- package/dist/{access-service-CPhSnLOH.d.ts → access-service-DEL4qCrc.d.ts} +25 -32
- package/dist/access-service.d.ts +13 -6
- package/dist/access-service.js +92 -75
- 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 +37 -27
- 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/{capsule-merge-Z2HUO6IK.js → capsule-merge-5KKJQBWQ.js} +4 -4
- package/dist/{catalog-uX9sT9ws.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 +46 -37
- 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-ASKEM4AR.js → chunk-2EJOEICD.js} +1823 -1483
- package/dist/chunk-2EJOEICD.js.map +1 -0
- package/dist/{chunk-L2B3EE7B.js → chunk-2QMKMVHH.js} +2 -2
- package/dist/{chunk-6CALL5VN.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-5S7Z5SHA.js → chunk-34RC5RT7.js} +2 -2
- package/dist/{chunk-KGWXHWAH.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-ZIDSD6VC.js → chunk-3Y224NYC.js} +3 -3
- package/dist/{chunk-RBQLJSJ7.js → chunk-44XZ75OQ.js} +46 -10
- package/dist/chunk-44XZ75OQ.js.map +1 -0
- package/dist/{chunk-VFSP73LP.js → chunk-45GF7Y3V.js} +2 -2
- package/dist/{chunk-7ICYQUOL.js → chunk-4BJQODZF.js} +2514 -1526
- package/dist/chunk-4BJQODZF.js.map +1 -0
- package/dist/{chunk-JMBVCAR4.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-2E6G7FCE.js → chunk-5Q7EVV4X.js} +7 -7
- package/dist/{chunk-LC7OODWZ.js → chunk-5SB7SC2G.js} +80 -49
- package/dist/chunk-5SB7SC2G.js.map +1 -0
- package/dist/{chunk-VZ5OJE4X.js → chunk-5VQJBH2G.js} +2 -2
- 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-4LEF6XWJ.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-F7AU34FC.js → chunk-76GLCM57.js} +160 -127
- 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-TXVD7PLT.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-OKUEZOGW.js → chunk-AITCS2RC.js} +2 -2
- package/dist/{chunk-OC35KQIN.js → chunk-AJ7QCSIS.js} +4 -4
- 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-N3JDIRBW.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-FBNWOMCE.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-DDGV7NAX.js → chunk-CQMEM5CC.js} +2 -2
- package/dist/{chunk-A7B4RMQG.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-IFJ23BHU.js → chunk-EEC7UJPX.js} +112 -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-7MW3CVLD.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-7LFXDGLH.js → chunk-F7GAIWFA.js} +20 -20
- package/dist/{chunk-IKSELOIV.js → chunk-FFPJINJE.js} +7 -7
- 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-V53GJUSL.js → chunk-FUTF72MJ.js} +15 -15
- package/dist/chunk-GCASGYIO.js +71 -0
- package/dist/chunk-GCASGYIO.js.map +1 -0
- package/dist/{chunk-5UNWK7KU.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-HLTAPHZZ.js → chunk-HNZKVLAN.js} +2 -2
- package/dist/{chunk-5EZXHU36.js → chunk-HYQJQGD3.js} +2 -2
- package/dist/{chunk-ANADNYWK.js → chunk-IDZFBSHN.js} +8 -6
- package/dist/{chunk-ANADNYWK.js.map → chunk-IDZFBSHN.js.map} +1 -1
- package/dist/{chunk-XVLEGMJI.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-ND5M64JJ.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-HG32URJJ.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-MKDRQAA3.js → chunk-KAVBJSBQ.js} +4 -4
- package/dist/{chunk-EEARRVZ2.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-OBMR4VIX.js → chunk-L2DPF4DF.js} +186 -15
- package/dist/chunk-L2DPF4DF.js.map +1 -0
- 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-NUWYOKS6.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-67ZCJLJY.js → chunk-MYFQ3ED4.js} +8 -8
- 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-AD6AMF2N.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-AHLPZ2SF.js → chunk-QJAQNXWI.js} +44 -6
- package/dist/chunk-QJAQNXWI.js.map +1 -0
- package/dist/{chunk-3D6AFGQR.js → chunk-QVT3E4CE.js} +7 -7
- package/dist/{chunk-VYOFVVNK.js → chunk-R26ZMXR2.js} +24 -24
- package/dist/{chunk-VYOFVVNK.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-UTRJFDP2.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-GMSDQUCR.js → chunk-TDTXKOAJ.js} +6475 -5748
- 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-PK5XBDA6.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-7XGSIUP5.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-W5NKDU24.js → chunk-WON4APQY.js} +4 -4
- package/dist/{chunk-NKCPUHCI.js → chunk-WZFAPHIK.js} +6 -6
- package/dist/{chunk-KQPJLK3X.js → chunk-XL24QQ63.js} +2 -2
- package/dist/{chunk-X47LWMKA.js → chunk-XMMESCPD.js} +26 -1
- package/dist/chunk-XMMESCPD.js.map +1 -0
- package/dist/{chunk-4B4MUS5D.js → chunk-YROFNEG5.js} +7 -7
- package/dist/{chunk-WU46YPY6.js → chunk-YV32N67O.js} +1 -1
- package/dist/chunk-YV32N67O.js.map +1 -0
- package/dist/{chunk-B3F4CWQR.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-_Y2Bmt6A.d.ts → cli-C_pqqTCw.d.ts} +28 -12
- package/dist/cli.d.ts +16 -9
- package/dist/cli.js +145 -130
- package/dist/compounding/engine.d.ts +5 -2
- package/dist/compounding/engine.js +39 -29
- 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 +39 -29
- 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 +44 -35
- package/dist/consolidation-provenance-check.d.ts +5 -2
- package/dist/consolidation-provenance-check.js +4 -4
- package/dist/consolidation-undo.d.ts +5 -2
- package/dist/consolidation-undo.js +3 -3
- 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 +38 -29
- 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-FO7AM4UR.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 +57 -10
- 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 +101 -92
- package/dist/index.js +290 -242
- 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 +38 -28
- 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 +40 -28
- package/dist/maintenance/rebuild-memory-projection.js +41 -31
- 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 +46 -2
- package/dist/memory-cache.js +7 -1
- package/dist/memory-corpus-version.js +2 -2
- 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 +54 -45
- 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 +25 -21
- package/dist/namespaces/storage.d.ts +24 -3
- package/dist/namespaces/storage.js +38 -30
- 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 +73 -58
- 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 +47 -34
- package/dist/{orchestrator-BbZ_dQSU.d.ts → orchestrator-CQJ8U7En.d.ts} +85 -79
- package/dist/orchestrator.d.ts +13 -8
- package/dist/orchestrator.js +197 -180
- package/dist/page-versioning.js +3 -3
- package/dist/patterns-cli.d.ts +3 -1
- package/dist/{pipeline-FRMDb1fn.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-xJRrHFbA.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 +8 -4
- 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 +24 -24
- 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 +19 -17
- package/dist/search/index.d.ts +3 -1
- package/dist/search/index.js +23 -21
- 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 +27 -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-fvdZRa5Z.d.ts → semantic-consolidation-DILZEQq3.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +4 -2
- package/dist/semantic-consolidation.js +41 -32
- package/dist/semantic-rule-promotion.js +37 -27
- package/dist/semantic-rule-verifier.d.ts +3 -1
- package/dist/semantic-rule-verifier.js +38 -28
- 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 +3 -1
- package/dist/shared-context/manager.js +3 -3
- package/dist/signal.d.ts +3 -1
- package/dist/{storage-CrvzaM9C.d.ts → storage-NIyKYD8g.d.ts} +505 -110
- package/dist/storage-guards.js +2 -2
- package/dist/storage.d.ts +6 -3
- package/dist/storage.js +41 -29
- 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 +6 -6
- package/dist/transfer/types.d.ts +24 -24
- 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-Bl4BWDM8.d.ts → types-D_fG2ix8.d.ts} +49 -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 +39 -29
- 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 +122 -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/graph.test.ts +137 -1
- package/src/graph.ts +274 -17
- 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-cache.ts +70 -0
- 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 +45 -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 +581 -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 +172 -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 +166 -150
- 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.test.ts +22 -0
- package/src/qmd.ts +60 -48
- 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 +24 -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 +942 -903
- 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 +66 -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-7ICYQUOL.js.map +0 -1
- package/dist/chunk-AD6AMF2N.js.map +0 -1
- package/dist/chunk-AHLPZ2SF.js.map +0 -1
- package/dist/chunk-ASKEM4AR.js.map +0 -1
- package/dist/chunk-B3F4CWQR.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-E5BK6CGV.js +0 -335
- package/dist/chunk-E5BK6CGV.js.map +0 -1
- package/dist/chunk-EEARRVZ2.js.map +0 -1
- package/dist/chunk-F7AU34FC.js.map +0 -1
- package/dist/chunk-FBNWOMCE.js.map +0 -1
- package/dist/chunk-FTYDWKO6.js.map +0 -1
- package/dist/chunk-GMSDQUCR.js.map +0 -1
- package/dist/chunk-GWTFLF2P.js.map +0 -1
- package/dist/chunk-IFJ23BHU.js.map +0 -1
- package/dist/chunk-IKQTFQMI.js.map +0 -1
- package/dist/chunk-J2HSAU72.js.map +0 -1
- package/dist/chunk-JMBVCAR4.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-ND5M64JJ.js.map +0 -1
- package/dist/chunk-NUWYOKS6.js.map +0 -1
- package/dist/chunk-NWTGD4AN.js.map +0 -1
- package/dist/chunk-OBMR4VIX.js.map +0 -1
- package/dist/chunk-ODPLEWB6.js.map +0 -1
- package/dist/chunk-PHK3HARR.js +0 -32
- package/dist/chunk-PHK3HARR.js.map +0 -1
- package/dist/chunk-RANHFI3P.js +0 -8
- package/dist/chunk-RANHFI3P.js.map +0 -1
- package/dist/chunk-RBQLJSJ7.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-TXLGHDHS.js.map +0 -1
- package/dist/chunk-UN3CH3CO.js +0 -118
- package/dist/chunk-UN3CH3CO.js.map +0 -1
- package/dist/chunk-VSMRDBFT.js +0 -17
- package/dist/chunk-VSMRDBFT.js.map +0 -1
- package/dist/chunk-WU46YPY6.js.map +0 -1
- package/dist/chunk-X47LWMKA.js.map +0 -1
- package/dist/chunk-X64YLC24.js.map +0 -1
- package/dist/chunk-XVLEGMJI.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-FO7AM4UR.js.map → access-recall-response.js.map} +0 -0
- /package/dist/{resolution-VH4NGENZ.js.map → bounded-jsonl-state.js.map} +0 -0
- /package/dist/{capsule-merge-Z2HUO6IK.js.map → capsule-merge-5KKJQBWQ.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-6CALL5VN.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-5S7Z5SHA.js.map → chunk-34RC5RT7.js.map} +0 -0
- /package/dist/{chunk-KGWXHWAH.js.map → chunk-3QAH2S33.js.map} +0 -0
- /package/dist/{chunk-ZIDSD6VC.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-2E6G7FCE.js.map → chunk-5Q7EVV4X.js.map} +0 -0
- /package/dist/{chunk-VZ5OJE4X.js.map → chunk-5VQJBH2G.js.map} +0 -0
- /package/dist/{chunk-PUBTHW5O.js.map → chunk-6DRPPQEJ.js.map} +0 -0
- /package/dist/{chunk-4LEF6XWJ.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-TXVD7PLT.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-OKUEZOGW.js.map → chunk-AITCS2RC.js.map} +0 -0
- /package/dist/{chunk-OC35KQIN.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-N3JDIRBW.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-DDGV7NAX.js.map → chunk-CQMEM5CC.js.map} +0 -0
- /package/dist/{chunk-A7B4RMQG.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-7MW3CVLD.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-7LFXDGLH.js.map → chunk-F7GAIWFA.js.map} +0 -0
- /package/dist/{chunk-IKSELOIV.js.map → chunk-FFPJINJE.js.map} +0 -0
- /package/dist/{chunk-V53GJUSL.js.map → chunk-FUTF72MJ.js.map} +0 -0
- /package/dist/{chunk-5UNWK7KU.js.map → chunk-GFPZS3F5.js.map} +0 -0
- /package/dist/{chunk-G6NKALRN.js.map → chunk-H4WG3UQZ.js.map} +0 -0
- /package/dist/{chunk-HLTAPHZZ.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-HG32URJJ.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-MKDRQAA3.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-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-67ZCJLJY.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-UTRJFDP2.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-PK5XBDA6.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-7XGSIUP5.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-W5NKDU24.js.map → chunk-WON4APQY.js.map} +0 -0
- /package/dist/{chunk-NKCPUHCI.js.map → chunk-WZFAPHIK.js.map} +0 -0
- /package/dist/{chunk-KQPJLK3X.js.map → chunk-XL24QQ63.js.map} +0 -0
- /package/dist/{chunk-4B4MUS5D.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,19 +20,36 @@ 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,
|
|
33
48
|
getCachedMemories,
|
|
34
49
|
invalidateAllForDir,
|
|
35
50
|
invalidateDerivedAndGlobalForDir,
|
|
51
|
+
invalidateForScope,
|
|
52
|
+
invalidateForScopeExceptHot,
|
|
36
53
|
setCachedEntities,
|
|
37
54
|
setCachedMemories,
|
|
38
55
|
updateCacheOnDelete,
|
|
@@ -61,6 +78,7 @@ import {
|
|
|
61
78
|
SecureStoreLockedError,
|
|
62
79
|
MAGIC_HEADER_SIZE,
|
|
63
80
|
isEncryptedFile,
|
|
81
|
+
probeEncryptedRegularFileHeader,
|
|
64
82
|
readMaybeEncryptedFileBuffer,
|
|
65
83
|
readMaybeEncryptedFile,
|
|
66
84
|
writeMaybeEncryptedFile,
|
|
@@ -136,6 +154,35 @@ import type {
|
|
|
136
154
|
ProvenanceSource,
|
|
137
155
|
} from "./types.js";
|
|
138
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";
|
|
139
186
|
import {
|
|
140
187
|
type ProjectedMemoryBrowseOptions,
|
|
141
188
|
type ProjectedMemoryBrowsePage,
|
|
@@ -149,11 +196,16 @@ import {
|
|
|
149
196
|
import {
|
|
150
197
|
inferMemoryStatus,
|
|
151
198
|
isArchivedMemoryPath,
|
|
152
|
-
sortMemoryLifecycleEvents,
|
|
153
199
|
toMemoryPathRel,
|
|
154
200
|
} from "./memory-lifecycle-ledger-utils.js";
|
|
155
201
|
import { normalizeProjectionPreview, normalizeProjectionTags } from "./memory-projection-format.js";
|
|
156
202
|
import { parseFlexibleIsoTimestamp } from "./utils/iso-timestamp.js";
|
|
203
|
+
import {
|
|
204
|
+
composeMemoryEnvelope,
|
|
205
|
+
isSealedMemoryEnvelope,
|
|
206
|
+
sealedWriteToLegacyArgs,
|
|
207
|
+
type SealedMemoryEnvelope,
|
|
208
|
+
} from "./write-envelope.js";
|
|
157
209
|
// stripCitation import removed: legacy rebuild fallback was replaced by a
|
|
158
210
|
// skip-with-warning strategy (Finding 1 — Uhol). See ensureFactHashIndexAuthoritative.
|
|
159
211
|
|
|
@@ -1113,138 +1165,10 @@ function dehyphenate(s: string): string {
|
|
|
1113
1165
|
return s.replace(/-/g, "");
|
|
1114
1166
|
}
|
|
1115
1167
|
|
|
1116
|
-
/**
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
* Prevents writing semantically identical facts.
|
|
1121
|
-
*/
|
|
1122
|
-
export class ContentHashIndex {
|
|
1123
|
-
private hashes: Set<string> = new Set();
|
|
1124
|
-
private dirty = false;
|
|
1125
|
-
private readonly filePath: string;
|
|
1126
|
-
private readonly secureStoreKeyProvider: () => Buffer | null;
|
|
1127
|
-
private readonly secureStoreWriteKeyProvider: () => Buffer | null;
|
|
1128
|
-
private readonly memoryDir: string;
|
|
1129
|
-
|
|
1130
|
-
constructor(
|
|
1131
|
-
stateDir: string,
|
|
1132
|
-
secureStoreKeyProvider: () => Buffer | null = () => null,
|
|
1133
|
-
secureStoreWriteKeyProvider: () => Buffer | null = secureStoreKeyProvider,
|
|
1134
|
-
memoryDir: string = path.dirname(stateDir)
|
|
1135
|
-
) {
|
|
1136
|
-
this.filePath = path.join(stateDir, "fact-hashes.txt");
|
|
1137
|
-
this.secureStoreKeyProvider = secureStoreKeyProvider;
|
|
1138
|
-
this.secureStoreWriteKeyProvider = secureStoreWriteKeyProvider;
|
|
1139
|
-
this.memoryDir = memoryDir;
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
/** Load existing hashes from disk. Safe to call multiple times. */
|
|
1143
|
-
async load(): Promise<void> {
|
|
1144
|
-
try {
|
|
1145
|
-
const raw = await readMaybeEncryptedFile(this.filePath, this.secureStoreKeyProvider(), this.memoryDir);
|
|
1146
|
-
for (const line of raw.split("\n")) {
|
|
1147
|
-
const trimmed = line.trim();
|
|
1148
|
-
if (trimmed.length > 0) {
|
|
1149
|
-
this.hashes.add(trimmed);
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
log.debug(`content-hash index: loaded ${this.hashes.size} hashes`);
|
|
1153
|
-
} catch (err) {
|
|
1154
|
-
if (err instanceof SecureStoreLockedError) throw err;
|
|
1155
|
-
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
1156
|
-
log.debug("content-hash index: no existing index — starting fresh");
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
/** Check if content already exists in the index. */
|
|
1161
|
-
has(content: string): boolean {
|
|
1162
|
-
return this.hashes.has(ContentHashIndex.computeHash(content));
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
/** Add content hash to the index. */
|
|
1166
|
-
add(content: string): void {
|
|
1167
|
-
const hash = ContentHashIndex.computeHash(content);
|
|
1168
|
-
if (!this.hashes.has(hash)) {
|
|
1169
|
-
this.hashes.add(hash);
|
|
1170
|
-
this.dirty = true;
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
get size(): number {
|
|
1175
|
-
return this.hashes.size;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
/** Clear all loaded hashes so the next save rewrites the index from scratch. */
|
|
1179
|
-
clear(): void {
|
|
1180
|
-
if (this.hashes.size > 0) {
|
|
1181
|
-
this.hashes.clear();
|
|
1182
|
-
}
|
|
1183
|
-
this.dirty = true;
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
/** Persist index to disk if changed. */
|
|
1187
|
-
async save(): Promise<void> {
|
|
1188
|
-
if (!this.dirty) return;
|
|
1189
|
-
await mkdir(path.dirname(this.filePath), { recursive: true });
|
|
1190
|
-
await writeMaybeEncryptedFile(
|
|
1191
|
-
this.filePath,
|
|
1192
|
-
[...this.hashes].join("\n") + "\n",
|
|
1193
|
-
this.secureStoreWriteKeyProvider(),
|
|
1194
|
-
{},
|
|
1195
|
-
this.memoryDir
|
|
1196
|
-
);
|
|
1197
|
-
this.dirty = false;
|
|
1198
|
-
log.debug(`content-hash index: saved ${this.hashes.size} hashes`);
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
/** Remove a hash from the index (used when archiving/deleting). */
|
|
1202
|
-
remove(content: string): void {
|
|
1203
|
-
const hash = ContentHashIndex.computeHash(content);
|
|
1204
|
-
if (this.hashes.delete(hash)) {
|
|
1205
|
-
this.dirty = true;
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
/**
|
|
1210
|
-
* Remove a pre-computed SHA-256 hash directly from the index without
|
|
1211
|
-
* re-hashing. Use this when the caller already holds the stored hash
|
|
1212
|
-
* (e.g. `memory.frontmatter.contentHash`) to avoid the double-hash bug
|
|
1213
|
-
* where `remove(hash)` would compute `hash(hash)` and never match the
|
|
1214
|
-
* entry.
|
|
1215
|
-
*/
|
|
1216
|
-
removeByHash(hash: string): void {
|
|
1217
|
-
if (this.hashes.delete(hash)) {
|
|
1218
|
-
this.dirty = true;
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
/**
|
|
1223
|
-
* Add a pre-computed SHA-256 hash directly to the index without re-hashing.
|
|
1224
|
-
* Use this when the caller already holds the stored hash
|
|
1225
|
-
* (e.g. `memory.frontmatter.contentHash`) so that the index records the raw
|
|
1226
|
-
* content hash rather than re-hashing the citation-annotated body.
|
|
1227
|
-
*
|
|
1228
|
-
* @internal Only called from `StorageManager.ensureFactHashIndexAuthoritative`.
|
|
1229
|
-
* Not part of the public API — prefer `add(content)` for external callers.
|
|
1230
|
-
*/
|
|
1231
|
-
addByHash(hash: string): void {
|
|
1232
|
-
if (!this.hashes.has(hash)) {
|
|
1233
|
-
this.hashes.add(hash);
|
|
1234
|
-
this.dirty = true;
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
/** Normalize content (delegates to content-hash.ts for a single source of truth). */
|
|
1239
|
-
static normalizeContent(content: string): string {
|
|
1240
|
-
return normalizeContent(content);
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
/** Compute SHA-256 hash (delegates to content-hash.ts for a single source of truth). */
|
|
1244
|
-
static computeHash(content: string): string {
|
|
1245
|
-
return computeContentHash(content);
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
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;
|
|
1248
1172
|
|
|
1249
1173
|
// ---------------------------------------------------------------------------
|
|
1250
1174
|
// Attribute normalization helper
|
|
@@ -1271,16 +1195,14 @@ export class ContentHashIndex {
|
|
|
1271
1195
|
// the coding surfaces + wearable service). Imported here so internal callers
|
|
1272
1196
|
// (snapshotBeforeWrite, snapshotForProvenance) resolve, and re-exported to
|
|
1273
1197
|
// keep the public storage API stable for existing callers (wearables, dist).
|
|
1274
|
-
import { stripAttributesSuffix } from "./structured-attributes.js";
|
|
1198
|
+
import { assemblePersistedBody, stripAttributesSuffix } from "./structured-attributes.js";
|
|
1275
1199
|
export { stripAttributesSuffix };
|
|
1276
1200
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
.join("; ");
|
|
1283
|
-
}
|
|
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";
|
|
1284
1206
|
|
|
1285
1207
|
// ---------------------------------------------------------------------------
|
|
1286
1208
|
// Entity file parsing / serialization (Knowledge Graph v7.0)
|
|
@@ -1427,471 +1349,6 @@ function readEntitySectionText(
|
|
|
1427
1349
|
return sectionLines.join(options.preserveBullets === true ? "\n" : " ");
|
|
1428
1350
|
}
|
|
1429
1351
|
|
|
1430
|
-
function parseEntityTimelineBullet(bullet: string, fallbackTimestamp: string): EntityTimelineEntry | null {
|
|
1431
|
-
const trimmed = bullet.trim();
|
|
1432
|
-
if (!trimmed) return null;
|
|
1433
|
-
|
|
1434
|
-
let rest = trimmed;
|
|
1435
|
-
const entry: EntityTimelineEntry = {
|
|
1436
|
-
timestamp: trimmed.startsWith("[") ? "" : fallbackTimestamp,
|
|
1437
|
-
text: "",
|
|
1438
|
-
};
|
|
1439
|
-
const consumedMetadataSegments: string[] = [];
|
|
1440
|
-
let literalSingleSourceSegment: string | undefined;
|
|
1441
|
-
|
|
1442
|
-
if (!trimmed.startsWith("[")) {
|
|
1443
|
-
entry.text = trimmed;
|
|
1444
|
-
return entry.text ? entry : null;
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
const firstEnd = trimmed.indexOf("]");
|
|
1448
|
-
if (firstEnd === -1) {
|
|
1449
|
-
entry.text = trimmed;
|
|
1450
|
-
return entry.text ? entry : null;
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
const firstToken = trimmed.slice(1, firstEnd).trim();
|
|
1454
|
-
const parsedTimestamp = Date.parse(firstToken);
|
|
1455
|
-
if (Number.isFinite(parsedTimestamp)) {
|
|
1456
|
-
entry.timestamp = firstToken || fallbackTimestamp;
|
|
1457
|
-
rest = trimmed.slice(firstEnd + 1).trimStart();
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
while (rest.startsWith("[")) {
|
|
1461
|
-
const end = findEntityTimelineTokenEnd(rest);
|
|
1462
|
-
if (end === -1) break;
|
|
1463
|
-
const rawSegment = rest.slice(0, end + 1);
|
|
1464
|
-
const token = rest.slice(1, end).trim();
|
|
1465
|
-
const equalsIdx = token.indexOf("=");
|
|
1466
|
-
if (equalsIdx === -1) {
|
|
1467
|
-
if (rest === trimmed) {
|
|
1468
|
-
entry.text = trimmed;
|
|
1469
|
-
return entry.text ? entry : null;
|
|
1470
|
-
}
|
|
1471
|
-
break;
|
|
1472
|
-
}
|
|
1473
|
-
const key = token.slice(0, equalsIdx).trim().toLowerCase();
|
|
1474
|
-
const value = unescapeEntityTimelineMetadataValue(token.slice(equalsIdx + 1).trim());
|
|
1475
|
-
if (!value) break;
|
|
1476
|
-
const nextRest = rest.slice(end + 1).trimStart();
|
|
1477
|
-
switch (key) {
|
|
1478
|
-
case "source_meta":
|
|
1479
|
-
entry.source = value;
|
|
1480
|
-
break;
|
|
1481
|
-
case "source":
|
|
1482
|
-
if (
|
|
1483
|
-
consumedMetadataSegments.length === 0 &&
|
|
1484
|
-
!nextRest.startsWith("[") &&
|
|
1485
|
-
nextRest.length > 0 &&
|
|
1486
|
-
!isManagedEntityTimelineSource(value)
|
|
1487
|
-
) {
|
|
1488
|
-
literalSingleSourceSegment = rawSegment;
|
|
1489
|
-
rest = nextRest;
|
|
1490
|
-
break;
|
|
1491
|
-
}
|
|
1492
|
-
entry.source = value;
|
|
1493
|
-
break;
|
|
1494
|
-
case "session":
|
|
1495
|
-
case "sessionkey":
|
|
1496
|
-
entry.sessionKey = value;
|
|
1497
|
-
break;
|
|
1498
|
-
case "principal":
|
|
1499
|
-
entry.principal = value;
|
|
1500
|
-
break;
|
|
1501
|
-
default:
|
|
1502
|
-
entry.text = rest.trim();
|
|
1503
|
-
return entry.text ? entry : null;
|
|
1504
|
-
}
|
|
1505
|
-
if (literalSingleSourceSegment) break;
|
|
1506
|
-
consumedMetadataSegments.push(rawSegment);
|
|
1507
|
-
rest = nextRest;
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
if (literalSingleSourceSegment) {
|
|
1511
|
-
return {
|
|
1512
|
-
timestamp: entry.timestamp,
|
|
1513
|
-
text: `${literalSingleSourceSegment} ${rest}`.trim(),
|
|
1514
|
-
};
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
entry.text = rest.trim();
|
|
1518
|
-
if (!entry.text) return null;
|
|
1519
|
-
return entry;
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
function isEntitySynthesisTimelinePromotionBullet(bullet: string): boolean {
|
|
1523
|
-
const trimmed = bullet.trim();
|
|
1524
|
-
if (!trimmed.startsWith("[")) return false;
|
|
1525
|
-
|
|
1526
|
-
const firstEnd = findEntityTimelineTokenEnd(trimmed);
|
|
1527
|
-
if (firstEnd === -1) return false;
|
|
1528
|
-
|
|
1529
|
-
const firstToken = trimmed.slice(1, firstEnd).trim();
|
|
1530
|
-
return looksLikeEntityTimelineTimestamp(firstToken);
|
|
1531
|
-
}
|
|
1532
|
-
|
|
1533
|
-
function looksLikeEntityTimelineTimestamp(token: string): boolean {
|
|
1534
|
-
if (!/^\d{4}-\d{2}-\d{2}(?:[T\s].*)?$/.test(token)) return false;
|
|
1535
|
-
return Number.isFinite(Date.parse(token));
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
function isManagedEntityTimelineSource(source: string): boolean {
|
|
1539
|
-
switch (source.trim().toLowerCase()) {
|
|
1540
|
-
case "artifact":
|
|
1541
|
-
case "chunking":
|
|
1542
|
-
case "cli-migrate":
|
|
1543
|
-
case "compounding-promotion":
|
|
1544
|
-
case "consolidation":
|
|
1545
|
-
case "contradiction-detection":
|
|
1546
|
-
case "entity_extraction":
|
|
1547
|
-
case "explicit":
|
|
1548
|
-
case "explicit-inline":
|
|
1549
|
-
case "explicit-inline-review":
|
|
1550
|
-
case "explicit-review":
|
|
1551
|
-
case "extraction":
|
|
1552
|
-
case "extraction-shared-promotion":
|
|
1553
|
-
case "manual":
|
|
1554
|
-
case "migration":
|
|
1555
|
-
case "migration-rechunk":
|
|
1556
|
-
case "proactive":
|
|
1557
|
-
case "replay":
|
|
1558
|
-
case "semantic-consolidation":
|
|
1559
|
-
case "unknown":
|
|
1560
|
-
return true;
|
|
1561
|
-
default:
|
|
1562
|
-
return false;
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
function findEntityTimelineTokenEnd(input: string): number {
|
|
1567
|
-
let escaped = false;
|
|
1568
|
-
for (let index = 0; index < input.length; index += 1) {
|
|
1569
|
-
const char = input[index];
|
|
1570
|
-
if (escaped) {
|
|
1571
|
-
escaped = false;
|
|
1572
|
-
continue;
|
|
1573
|
-
}
|
|
1574
|
-
if (char === "\\") {
|
|
1575
|
-
escaped = true;
|
|
1576
|
-
continue;
|
|
1577
|
-
}
|
|
1578
|
-
if (char === "]") return index;
|
|
1579
|
-
}
|
|
1580
|
-
return -1;
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
function escapeEntityTimelineMetadataValue(value: string): string {
|
|
1584
|
-
let escaped = "";
|
|
1585
|
-
for (const char of value) {
|
|
1586
|
-
switch (char) {
|
|
1587
|
-
case "\\":
|
|
1588
|
-
escaped += "\\\\";
|
|
1589
|
-
break;
|
|
1590
|
-
case "]":
|
|
1591
|
-
escaped += "\\]";
|
|
1592
|
-
break;
|
|
1593
|
-
case "\n":
|
|
1594
|
-
escaped += "\\n";
|
|
1595
|
-
break;
|
|
1596
|
-
case "\r":
|
|
1597
|
-
escaped += "\\r";
|
|
1598
|
-
break;
|
|
1599
|
-
case "\t":
|
|
1600
|
-
escaped += "\\t";
|
|
1601
|
-
break;
|
|
1602
|
-
default: {
|
|
1603
|
-
const codePoint = char.codePointAt(0) ?? 0;
|
|
1604
|
-
if (codePoint < 0x20) {
|
|
1605
|
-
escaped += `\\u${codePoint.toString(16).padStart(4, "0")}`;
|
|
1606
|
-
} else {
|
|
1607
|
-
escaped += char;
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
return escaped;
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
function unescapeEntityTimelineMetadataValue(value: string): string {
|
|
1616
|
-
if (!value.includes("\\")) return value;
|
|
1617
|
-
|
|
1618
|
-
let result = "";
|
|
1619
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
1620
|
-
const char = value[index];
|
|
1621
|
-
if (char !== "\\") {
|
|
1622
|
-
result += char;
|
|
1623
|
-
continue;
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
const next = value[index + 1];
|
|
1627
|
-
if (!next) {
|
|
1628
|
-
result += "\\";
|
|
1629
|
-
break;
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
switch (next) {
|
|
1633
|
-
case "n":
|
|
1634
|
-
result += "\n";
|
|
1635
|
-
index += 1;
|
|
1636
|
-
break;
|
|
1637
|
-
case "r":
|
|
1638
|
-
result += "\r";
|
|
1639
|
-
index += 1;
|
|
1640
|
-
break;
|
|
1641
|
-
case "t":
|
|
1642
|
-
result += "\t";
|
|
1643
|
-
index += 1;
|
|
1644
|
-
break;
|
|
1645
|
-
case "u": {
|
|
1646
|
-
const hex = value.slice(index + 2, index + 6);
|
|
1647
|
-
if (/^[0-9a-fA-F]{4}$/.test(hex)) {
|
|
1648
|
-
result += String.fromCharCode(parseInt(hex, 16));
|
|
1649
|
-
index += 5;
|
|
1650
|
-
break;
|
|
1651
|
-
}
|
|
1652
|
-
result += "u";
|
|
1653
|
-
index += 1;
|
|
1654
|
-
break;
|
|
1655
|
-
}
|
|
1656
|
-
default:
|
|
1657
|
-
result += next;
|
|
1658
|
-
index += 1;
|
|
1659
|
-
break;
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
return result;
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
function serializeEntityTimelineEntry(entry: EntityTimelineEntry): string {
|
|
1666
|
-
const tokens: string[] = [];
|
|
1667
|
-
if (entry.timestamp.trim().length > 0) {
|
|
1668
|
-
tokens.push(`[${entry.timestamp}]`);
|
|
1669
|
-
}
|
|
1670
|
-
if (entry.source) {
|
|
1671
|
-
const sourceKey = isManagedEntityTimelineSource(entry.source) ? "source" : "source_meta";
|
|
1672
|
-
tokens.push(`[${sourceKey}=${escapeEntityTimelineMetadataValue(entry.source)}]`);
|
|
1673
|
-
}
|
|
1674
|
-
if (entry.sessionKey) {
|
|
1675
|
-
tokens.push(`[session=${escapeEntityTimelineMetadataValue(entry.sessionKey)}]`);
|
|
1676
|
-
}
|
|
1677
|
-
if (entry.principal) {
|
|
1678
|
-
tokens.push(`[principal=${escapeEntityTimelineMetadataValue(entry.principal)}]`);
|
|
1679
|
-
}
|
|
1680
|
-
const serializedMetadata = tokens.length > 0 ? `${tokens.join(" ")} ` : "";
|
|
1681
|
-
return `- ${serializedMetadata}${entry.text}`.trimEnd();
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
function dedupeEntityTimelineFacts(timeline: EntityTimelineEntry[]): string[] {
|
|
1685
|
-
return [...new Set(timeline.map((entry) => entry.text.trim()).filter((entry) => entry.length > 0))];
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
function normalizeEntitySectionFact(value: string): string {
|
|
1689
|
-
return value.replace(/\s+/g, " ").trim();
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
export function normalizeStructuredSectionFacts(facts: string[]): string[] {
|
|
1693
|
-
return [...new Set(facts.map((fact) => normalizeEntitySectionFact(fact)).filter((fact) => fact.length > 0))];
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
function collectStructuredSectionFacts(structuredSections: EntityStructuredSection[]): string[] {
|
|
1697
|
-
const facts: string[] = [];
|
|
1698
|
-
for (const section of structuredSections) {
|
|
1699
|
-
for (const fact of section.facts) {
|
|
1700
|
-
const normalized = normalizeEntitySectionFact(fact);
|
|
1701
|
-
if (!normalized) continue;
|
|
1702
|
-
facts.push(normalized);
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
return [...new Set(facts)];
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
export function compileEntityFacts(
|
|
1709
|
-
timeline: EntityTimelineEntry[],
|
|
1710
|
-
structuredSections: EntityStructuredSection[]
|
|
1711
|
-
): string[] {
|
|
1712
|
-
const facts: string[] = [];
|
|
1713
|
-
const seen = new Set<string>();
|
|
1714
|
-
for (const fact of dedupeEntityTimelineFacts(timeline)) {
|
|
1715
|
-
if (seen.has(fact)) continue;
|
|
1716
|
-
seen.add(fact);
|
|
1717
|
-
facts.push(fact);
|
|
1718
|
-
}
|
|
1719
|
-
for (const fact of collectStructuredSectionFacts(structuredSections)) {
|
|
1720
|
-
if (seen.has(fact)) continue;
|
|
1721
|
-
seen.add(fact);
|
|
1722
|
-
facts.push(fact);
|
|
1723
|
-
}
|
|
1724
|
-
return facts;
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
function parseEntityStructuredSectionFacts(lines: string[]): string[] {
|
|
1728
|
-
const facts: string[] = [];
|
|
1729
|
-
let currentBlock: string[] = [];
|
|
1730
|
-
|
|
1731
|
-
const flushCurrentBlock = (): void => {
|
|
1732
|
-
const normalized = normalizeEntitySectionFact(currentBlock.join(" "));
|
|
1733
|
-
if (normalized.length > 0) facts.push(normalized);
|
|
1734
|
-
currentBlock = [];
|
|
1735
|
-
};
|
|
1736
|
-
|
|
1737
|
-
for (const rawLine of lines) {
|
|
1738
|
-
const line = rawLine.trim();
|
|
1739
|
-
if (!line) {
|
|
1740
|
-
flushCurrentBlock();
|
|
1741
|
-
continue;
|
|
1742
|
-
}
|
|
1743
|
-
if (line.startsWith("- ")) {
|
|
1744
|
-
flushCurrentBlock();
|
|
1745
|
-
currentBlock = [line.slice(2).trim()];
|
|
1746
|
-
continue;
|
|
1747
|
-
}
|
|
1748
|
-
currentBlock.push(line);
|
|
1749
|
-
}
|
|
1750
|
-
|
|
1751
|
-
flushCurrentBlock();
|
|
1752
|
-
return [...new Set(facts)];
|
|
1753
|
-
}
|
|
1754
|
-
|
|
1755
|
-
function looksLikeStructuredSectionFactList(lines: string[]): boolean {
|
|
1756
|
-
const firstNonBlank = lines.find((line) => line.trim().length > 0)?.trim() ?? "";
|
|
1757
|
-
return firstNonBlank.startsWith("- ");
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
function partitionEntityStructuredSections(
|
|
1761
|
-
entityType: string,
|
|
1762
|
-
extraSections: Array<{ title: string; lines: string[] }>,
|
|
1763
|
-
entitySchemas?: PluginConfig["entitySchemas"]
|
|
1764
|
-
): {
|
|
1765
|
-
structuredSections: EntityStructuredSection[];
|
|
1766
|
-
remainingExtraSections: Array<{ title: string; lines: string[] }>;
|
|
1767
|
-
} {
|
|
1768
|
-
const structuredSections: EntityStructuredSection[] = [];
|
|
1769
|
-
const remainingExtraSections: Array<{ title: string; lines: string[] }> = [];
|
|
1770
|
-
const structuredSectionIndex = new Map<string, EntityStructuredSection>();
|
|
1771
|
-
|
|
1772
|
-
for (const section of extraSections) {
|
|
1773
|
-
const matchedSection = matchEntitySchemaSection(entityType, section.title, entitySchemas);
|
|
1774
|
-
if (!matchedSection && !looksLikeStructuredSectionFactList(section.lines)) {
|
|
1775
|
-
remainingExtraSections.push(section);
|
|
1776
|
-
continue;
|
|
1777
|
-
}
|
|
1778
|
-
const facts = parseEntityStructuredSectionFacts(section.lines);
|
|
1779
|
-
if (!matchedSection && facts.length === 0) {
|
|
1780
|
-
remainingExtraSections.push(section);
|
|
1781
|
-
continue;
|
|
1782
|
-
}
|
|
1783
|
-
const normalizedSection = matchedSection
|
|
1784
|
-
? { key: matchedSection.key, title: matchedSection.title }
|
|
1785
|
-
: normalizeEntityStructuredSection(entityType, { key: section.title, title: section.title }, entitySchemas);
|
|
1786
|
-
if (facts.length === 0) {
|
|
1787
|
-
remainingExtraSections.push(section);
|
|
1788
|
-
continue;
|
|
1789
|
-
}
|
|
1790
|
-
const existing = structuredSectionIndex.get(normalizedSection.key);
|
|
1791
|
-
if (existing) {
|
|
1792
|
-
existing.facts = normalizeStructuredSectionFacts([...existing.facts, ...facts]);
|
|
1793
|
-
continue;
|
|
1794
|
-
}
|
|
1795
|
-
const structuredSection: EntityStructuredSection = {
|
|
1796
|
-
key: normalizedSection.key,
|
|
1797
|
-
title: normalizedSection.title,
|
|
1798
|
-
facts: normalizeStructuredSectionFacts(facts),
|
|
1799
|
-
};
|
|
1800
|
-
structuredSections.push(structuredSection);
|
|
1801
|
-
structuredSectionIndex.set(normalizedSection.key, structuredSection);
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
return {
|
|
1805
|
-
structuredSections,
|
|
1806
|
-
remainingExtraSections,
|
|
1807
|
-
};
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
function latestEntityTimelineTimestamp(entity: EntityFile): string | undefined {
|
|
1811
|
-
let latestRaw: string | undefined;
|
|
1812
|
-
for (const entry of entity.timeline) {
|
|
1813
|
-
const timestamp = entry.timestamp.trim();
|
|
1814
|
-
if (!timestamp) continue;
|
|
1815
|
-
if (!latestRaw || compareEntityTimestamps(timestamp, latestRaw) > 0) {
|
|
1816
|
-
latestRaw = timestamp;
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
return latestRaw;
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
export function compareEntityTimestamps(left?: string, right?: string): number {
|
|
1823
|
-
const leftValue = left?.trim() ?? "";
|
|
1824
|
-
const rightValue = right?.trim() ?? "";
|
|
1825
|
-
|
|
1826
|
-
if (!leftValue && !rightValue) return 0;
|
|
1827
|
-
if (!leftValue) return -1;
|
|
1828
|
-
if (!rightValue) return 1;
|
|
1829
|
-
|
|
1830
|
-
const leftMs = Date.parse(leftValue);
|
|
1831
|
-
const rightMs = Date.parse(rightValue);
|
|
1832
|
-
const leftParsed = Number.isFinite(leftMs);
|
|
1833
|
-
const rightParsed = Number.isFinite(rightMs);
|
|
1834
|
-
|
|
1835
|
-
if (leftParsed && rightParsed) {
|
|
1836
|
-
if (leftMs === rightMs) return 0;
|
|
1837
|
-
return leftMs > rightMs ? 1 : -1;
|
|
1838
|
-
}
|
|
1839
|
-
if (leftParsed) return 1;
|
|
1840
|
-
if (rightParsed) return -1;
|
|
1841
|
-
return leftValue.localeCompare(rightValue);
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
export function countEntityStructuredFacts(entity: EntityFile): number {
|
|
1845
|
-
return (entity.structuredSections ?? []).reduce((count, section) => count + section.facts.length, 0);
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
|
-
export function fingerprintEntityStructuredFacts(entity: Pick<EntityFile, "structuredSections">): string | undefined {
|
|
1849
|
-
const normalizedSections = (entity.structuredSections ?? [])
|
|
1850
|
-
.map((section) => ({
|
|
1851
|
-
key: section.key.trim().toLowerCase(),
|
|
1852
|
-
title: section.title.replace(/\s+/g, " ").trim(),
|
|
1853
|
-
facts: normalizeStructuredSectionFacts(section.facts)
|
|
1854
|
-
.slice()
|
|
1855
|
-
.sort((left, right) => left.localeCompare(right)),
|
|
1856
|
-
}))
|
|
1857
|
-
.filter((section) => section.facts.length > 0)
|
|
1858
|
-
.sort(
|
|
1859
|
-
(left, right) =>
|
|
1860
|
-
left.key.localeCompare(right.key) ||
|
|
1861
|
-
left.title.localeCompare(right.title) ||
|
|
1862
|
-
left.facts.join("\n").localeCompare(right.facts.join("\n"))
|
|
1863
|
-
);
|
|
1864
|
-
if (normalizedSections.length === 0) return undefined;
|
|
1865
|
-
return createHash("sha256").update(JSON.stringify(normalizedSections)).digest("hex");
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
|
-
export function isEntitySynthesisStale(entity: EntityFile): boolean {
|
|
1869
|
-
const structuredFactCount = countEntityStructuredFacts(entity);
|
|
1870
|
-
const structuredFactDigest = fingerprintEntityStructuredFacts(entity);
|
|
1871
|
-
const storedStructuredFactDigest = entity.synthesisStructuredFactDigest?.trim() || undefined;
|
|
1872
|
-
if (entity.timeline.length === 0 && structuredFactCount === 0) return false;
|
|
1873
|
-
if (!entity.synthesis?.trim()) return true;
|
|
1874
|
-
if (entity.synthesisTimelineCount === undefined) return true;
|
|
1875
|
-
if (structuredFactCount > 0 && entity.synthesisStructuredFactCount === undefined) return true;
|
|
1876
|
-
if (structuredFactCount > 0 && !storedStructuredFactDigest) return true;
|
|
1877
|
-
const latestTimelineTimestamp = latestEntityTimelineTimestamp(entity);
|
|
1878
|
-
if (!latestTimelineTimestamp) {
|
|
1879
|
-
return (
|
|
1880
|
-
entity.timeline.length > entity.synthesisTimelineCount ||
|
|
1881
|
-
structuredFactCount > (entity.synthesisStructuredFactCount ?? 0) ||
|
|
1882
|
-
structuredFactDigest !== storedStructuredFactDigest
|
|
1883
|
-
);
|
|
1884
|
-
}
|
|
1885
|
-
if (!entity.synthesisUpdatedAt?.trim()) return true;
|
|
1886
|
-
const timelineFreshness = compareEntityTimestamps(latestTimelineTimestamp, entity.synthesisUpdatedAt);
|
|
1887
|
-
if (timelineFreshness > 0) return true;
|
|
1888
|
-
return (
|
|
1889
|
-
entity.timeline.length > entity.synthesisTimelineCount ||
|
|
1890
|
-
structuredFactCount > (entity.synthesisStructuredFactCount ?? 0) ||
|
|
1891
|
-
structuredFactDigest !== storedStructuredFactDigest
|
|
1892
|
-
);
|
|
1893
|
-
}
|
|
1894
|
-
|
|
1895
1352
|
/**
|
|
1896
1353
|
* Parse an entity markdown file into a structured EntityFile.
|
|
1897
1354
|
* Backward compatible: old files without new sections get empty arrays.
|
|
@@ -2313,6 +1770,105 @@ function parseExtractionRetryStateEntries(
|
|
|
2313
1770
|
return valid;
|
|
2314
1771
|
}
|
|
2315
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
|
+
|
|
2316
1872
|
export class StorageManager {
|
|
2317
1873
|
private knowledgeIndexCache: { result: string; builtAt: number } | null = null;
|
|
2318
1874
|
private artifactIndexCache: { memories: MemoryFile[]; loadedAtMs: number; writeVersion: number } | null = null;
|
|
@@ -2385,6 +1941,17 @@ export class StorageManager {
|
|
|
2385
1941
|
*/
|
|
2386
1942
|
private static hotMemoriesCacheProcessDefaultSeeded = false;
|
|
2387
1943
|
|
|
1944
|
+
/**
|
|
1945
|
+
* Per-memory-directory gate for scope-aware cache invalidation (issue #1904).
|
|
1946
|
+
* Registered from config by the orchestrator via
|
|
1947
|
+
* setScopedCacheInvalidationDefault() so every StorageManager built over a
|
|
1948
|
+
* memory dir — including ephemeral recall sub-stage instances — honors the
|
|
1949
|
+
* operator's setting. Unregistered dirs default to `true` (scoped on). Set
|
|
1950
|
+
* `false` to restore the pre-#1904 full-clear-per-write behavior (rollback
|
|
1951
|
+
* lever). Read once per write via isScopedCacheInvalidationEnabled().
|
|
1952
|
+
*/
|
|
1953
|
+
private static readonly scopedCacheInvalidationByDir = new Map<string, boolean>();
|
|
1954
|
+
|
|
2388
1955
|
// Module-level maps for readAllMemories() keyed by base directory, shared
|
|
2389
1956
|
// across all StorageManager instances (static) so concurrent callers over
|
|
2390
1957
|
// the same dir cooperate.
|
|
@@ -2429,8 +1996,39 @@ export class StorageManager {
|
|
|
2429
1996
|
private factHashIndex: ContentHashIndex | null = null;
|
|
2430
1997
|
private factHashIndexLoadPromise: Promise<ContentHashIndex> | null = null;
|
|
2431
1998
|
private factHashIndexAuthoritative: boolean | null = null;
|
|
2432
|
-
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 = {};
|
|
2433
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
|
+
>();
|
|
2434
2032
|
private offlineSyncDigestCache: Map<string, OfflineSyncDigestCacheEntry> | null = null;
|
|
2435
2033
|
private offlineSyncDigestCacheLoadPromise: Promise<Map<string, OfflineSyncDigestCacheEntry>> | null = null;
|
|
2436
2034
|
private offlineSyncDigestCacheWriteChain: Promise<void> = Promise.resolve();
|
|
@@ -2439,6 +2037,15 @@ export class StorageManager {
|
|
|
2439
2037
|
citationTemplate: string = DEFAULT_CITATION_FORMAT;
|
|
2440
2038
|
/** Post-write catalog hook (#1522). Installed by the namespace router; fire-and-forget. */
|
|
2441
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
|
+
}
|
|
2442
2049
|
private notifyCatalogWrite(): void {
|
|
2443
2050
|
try {
|
|
2444
2051
|
this.onCatalogWrite?.();
|
|
@@ -2537,6 +2144,12 @@ export class StorageManager {
|
|
|
2537
2144
|
// decrypted under the previous key, not just the entity layer.
|
|
2538
2145
|
invalidateAllForDir(this.baseDir);
|
|
2539
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;
|
|
2540
2153
|
}
|
|
2541
2154
|
|
|
2542
2155
|
private getEntityCacheSecureStoreKey(): string {
|
|
@@ -2611,20 +2224,28 @@ export class StorageManager {
|
|
|
2611
2224
|
return this._secureStoreKey !== null;
|
|
2612
2225
|
}
|
|
2613
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
|
+
|
|
2614
2233
|
/**
|
|
2615
|
-
* 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.
|
|
2616
2238
|
*
|
|
2617
|
-
*
|
|
2618
|
-
*
|
|
2619
|
-
*
|
|
2620
|
-
*
|
|
2621
|
-
*
|
|
2622
|
-
*
|
|
2623
|
-
*
|
|
2624
|
-
* `_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.
|
|
2625
2246
|
*/
|
|
2626
|
-
private resolveWriteKey(): Buffer | null {
|
|
2627
|
-
if (!this._secureStoreEncryptOnWrite) return null;
|
|
2247
|
+
private resolveWriteKey(forceEncrypt = false): Buffer | null {
|
|
2248
|
+
if (!forceEncrypt && !this._secureStoreEncryptOnWrite) return null;
|
|
2628
2249
|
if (this._secureStoreKey !== null) return this._secureStoreKey;
|
|
2629
2250
|
if (this._secureStoreRequired) {
|
|
2630
2251
|
throw new SecureStoreLockedError(
|
|
@@ -2752,6 +2373,15 @@ export class StorageManager {
|
|
|
2752
2373
|
}
|
|
2753
2374
|
}
|
|
2754
2375
|
|
|
2376
|
+
/** Register the operator's scope-aware cache invalidation gate for a memory
|
|
2377
|
+
* dir (issue #1904). The orchestrator/access layer calls this with
|
|
2378
|
+
* the operator's scopedCacheInvalidationEnabled flag so the setting reaches every
|
|
2379
|
+
* StorageManager built over the dir, including ephemeral recall-sub-stage
|
|
2380
|
+
* instances. Only an explicit `false` disables; `undefined` leaves it on. */
|
|
2381
|
+
static setScopedCacheInvalidationDefault(memoryDir: string, enabled: boolean | undefined): void {
|
|
2382
|
+
StorageManager.scopedCacheInvalidationByDir.set(memoryDir, enabled !== false);
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2755
2385
|
/**
|
|
2756
2386
|
* The LIVE class object of this instance (issue #1809 review of the
|
|
2757
2387
|
* storage decomposition): extracted store modules must read/write the
|
|
@@ -3107,15 +2737,21 @@ export class StorageManager {
|
|
|
3107
2737
|
private readStorageSecureFile(filePath: string): Promise<string> {
|
|
3108
2738
|
return readMaybeEncryptedFile(filePath, this._secureStoreKey, this.baseDir);
|
|
3109
2739
|
}
|
|
3110
|
-
private writeStorageSecureFile(filePath: string, content: string | Buffer): Promise<void> {
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
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
|
+
});
|
|
3114
2749
|
}
|
|
3115
2750
|
private writeStorageSecureFileChunks(filePath: string, chunks: AsyncIterable<Buffer>): Promise<void> {
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
2751
|
+
const writeKey = this.resolveWriteKey();
|
|
2752
|
+
return writeMaybeEncryptedFileFromChunks(filePath, chunks, writeKey, {}, this.baseDir).then(() => {
|
|
2753
|
+
this.notifyCatalogWriteForPath(filePath);
|
|
2754
|
+
});
|
|
3119
2755
|
}
|
|
3120
2756
|
|
|
3121
2757
|
private assertManagedStoragePath(filePath: string, method: string): string {
|
|
@@ -3321,11 +2957,9 @@ export class StorageManager {
|
|
|
3321
2957
|
// chokepoint, which also covers the entity cache (issue #1535).
|
|
3322
2958
|
this.invalidateAllMemoriesCache();
|
|
3323
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).
|
|
3324
2962
|
this.factHashIndexAuthoritative = false;
|
|
3325
|
-
await unlink(this.factHashIndexReadyPath).catch((error: unknown) => {
|
|
3326
|
-
if (isErrnoCode(error, "ENOENT")) return;
|
|
3327
|
-
throw error;
|
|
3328
|
-
});
|
|
3329
2963
|
if (filePath.includes(`${path.sep}cold${path.sep}`)) {
|
|
3330
2964
|
this.invalidateColdMemoriesCache();
|
|
3331
2965
|
}
|
|
@@ -3340,7 +2974,8 @@ export class StorageManager {
|
|
|
3340
2974
|
this.stateDir,
|
|
3341
2975
|
() => this._secureStoreKey,
|
|
3342
2976
|
() => this.resolveWriteKey(),
|
|
3343
|
-
this.baseDir
|
|
2977
|
+
this.baseDir,
|
|
2978
|
+
this.factHashIndexLockOptions,
|
|
3344
2979
|
);
|
|
3345
2980
|
}
|
|
3346
2981
|
|
|
@@ -3358,12 +2993,60 @@ export class StorageManager {
|
|
|
3358
2993
|
}
|
|
3359
2994
|
}
|
|
3360
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
|
+
|
|
3361
3039
|
private async appendStorageSecureFileUnlocked(filePath: string, content: string): Promise<void> {
|
|
3362
3040
|
const writeKey = this.resolveWriteKey();
|
|
3363
3041
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
3364
3042
|
if (writeKey === null) {
|
|
3365
3043
|
try {
|
|
3366
|
-
|
|
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)) {
|
|
3367
3050
|
const existing = await this.readStorageSecureFile(filePath);
|
|
3368
3051
|
await writeMaybeEncryptedFile(filePath, `${existing}${content}`, null, {}, this.baseDir);
|
|
3369
3052
|
this.notifyCatalogWriteForPath(filePath);
|
|
@@ -3372,11 +3055,21 @@ export class StorageManager {
|
|
|
3372
3055
|
} catch (err) {
|
|
3373
3056
|
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
3374
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.
|
|
3375
3065
|
await appendFile(filePath, content, "utf-8");
|
|
3376
3066
|
this.notifyCatalogWriteForPath(filePath);
|
|
3377
3067
|
return;
|
|
3378
3068
|
}
|
|
3379
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.
|
|
3380
3073
|
let existing = "";
|
|
3381
3074
|
try {
|
|
3382
3075
|
existing = await this.readStorageSecureFile(filePath);
|
|
@@ -3395,10 +3088,6 @@ export class StorageManager {
|
|
|
3395
3088
|
private get entitySynthesisQueuePath(): string {
|
|
3396
3089
|
return path.join(this.stateDir, "entity-synthesis-queue.json");
|
|
3397
3090
|
}
|
|
3398
|
-
private get factHashIndexReadyPath(): string {
|
|
3399
|
-
return path.join(this.stateDir, "fact-hashes.ready");
|
|
3400
|
-
}
|
|
3401
|
-
|
|
3402
3091
|
// ── Tombstone store access (issue #1579) ─────────────────────────────────
|
|
3403
3092
|
/**
|
|
3404
3093
|
* The on-disk tombstone log path. Lives under `<stateDir>/tombstones.jsonl`
|
|
@@ -3596,101 +3285,200 @@ export class StorageManager {
|
|
|
3596
3285
|
return this.factHashIndexLoadPromise;
|
|
3597
3286
|
}
|
|
3598
3287
|
|
|
3599
|
-
|
|
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> {
|
|
3600
3319
|
if (this.factHashIndexAuthoritative === true) {
|
|
3601
|
-
|
|
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;
|
|
3602
3333
|
}
|
|
3603
3334
|
if (this.factHashIndexAuthoritativePromise) {
|
|
3604
|
-
|
|
3605
|
-
return;
|
|
3335
|
+
return this.factHashIndexAuthoritativePromise;
|
|
3606
3336
|
}
|
|
3607
3337
|
|
|
3338
|
+
this.factHashIndexAuthoritative = false;
|
|
3608
3339
|
this.factHashIndexAuthoritativePromise = (async () => {
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
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.
|
|
3617
3356
|
const factHashIndex = await this.getFactHashIndex();
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
continue;
|
|
3631
|
-
}
|
|
3632
|
-
// Legacy fact written before contentHash was introduced (Finding 1 —
|
|
3633
|
-
// Uhol). Apply nuanced handling based on whether the citation can be
|
|
3634
|
-
// reliably stripped:
|
|
3635
|
-
//
|
|
3636
|
-
// 1. Default citation present → strip it and index the raw body.
|
|
3637
|
-
// 2. No citation at all → index the raw body as-is.
|
|
3638
|
-
// 3. Unknown/custom citation template → skip with a warning.
|
|
3639
|
-
//
|
|
3640
|
-
// Rationale for (3): for facts annotated with a custom citation
|
|
3641
|
-
// template, stripCitationForTemplate cannot reliably detect the inline
|
|
3642
|
-
// marker and would hash the cited body — producing a hash that never
|
|
3643
|
-
// matches what hasFactContentHash(rawContent) computes. A
|
|
3644
|
-
// false-negative miss (the fact is not in the index) is preferable to
|
|
3645
|
-
// a wrong index entry that permanently suppresses legitimate duplicate
|
|
3646
|
-
// writes.
|
|
3647
|
-
//
|
|
3648
|
-
// Limitation (Thread 2 — stale hash): even when contentHash IS present
|
|
3649
|
-
// it may be stale if updateMemory() rewrote the body without updating
|
|
3650
|
-
// the frontmatter hash. The hash is trusted as-is here; a future
|
|
3651
|
-
// migration pass can recompute it from the current content.
|
|
3652
|
-
const content = memory.content;
|
|
3653
|
-
// Use the configured template (Thread 1 fix): citationTemplate is set
|
|
3654
|
-
// by the orchestrator to the active inlineSourceAttributionFormat so
|
|
3655
|
-
// the rebuild can strip both the default and any custom template.
|
|
3656
|
-
// Falls back to DEFAULT_CITATION_FORMAT when the orchestrator has not
|
|
3657
|
-
// configured a custom template (e.g. direct StorageManager construction
|
|
3658
|
-
// in tests).
|
|
3659
|
-
const stripped = stripCitationForTemplate(content, this.citationTemplate);
|
|
3660
|
-
if (stripped !== content) {
|
|
3661
|
-
// Citation was stripped — index the bare body.
|
|
3662
|
-
factHashIndex.addByHash(ContentHashIndex.computeHash(sanitizeMemoryContent(stripped).text));
|
|
3663
|
-
continue;
|
|
3664
|
-
}
|
|
3665
|
-
// No citation was removed. Decide whether to index or skip.
|
|
3666
|
-
// Thread 4 fix: use hasCitation() rather than the too-broad endsWith("]")
|
|
3667
|
-
// heuristic. Facts that legitimately end with "]" (e.g. "User prefers
|
|
3668
|
-
// [dark mode]") have no citation marker and should be indexed as-is.
|
|
3669
|
-
// Only skip when hasCitation() confirms a citation is present — that
|
|
3670
|
-
// means the citation is from an unknown/custom template we cannot strip.
|
|
3671
|
-
if (!hasCitation(content)) {
|
|
3672
|
-
// Content has no recognisable citation marker — index raw body.
|
|
3673
|
-
factHashIndex.addByHash(ContentHashIndex.computeHash(sanitizeMemoryContent(content).text));
|
|
3674
|
-
continue;
|
|
3675
|
-
}
|
|
3676
|
-
// Content carries a citation from an unknown/custom template
|
|
3677
|
-
// that we cannot safely strip. Skip rather than index a wrong hash.
|
|
3678
|
-
legacyRecovered++;
|
|
3679
|
-
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));
|
|
3680
3369
|
}
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
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)`,
|
|
3684
3375
|
);
|
|
3685
3376
|
}
|
|
3686
|
-
|
|
3687
|
-
await mkdir(path.dirname(this.factHashIndexReadyPath), { recursive: true });
|
|
3688
|
-
await writeFile(this.factHashIndexReadyPath, "v1\n", "utf-8");
|
|
3689
|
-
this.factHashIndexAuthoritative = true;
|
|
3377
|
+
return published;
|
|
3690
3378
|
})().finally(() => {
|
|
3691
3379
|
this.factHashIndexAuthoritativePromise = null;
|
|
3692
3380
|
});
|
|
3693
|
-
|
|
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;
|
|
3694
3482
|
}
|
|
3695
3483
|
private get questionsDir(): string {
|
|
3696
3484
|
return path.join(this.baseDir, "questions");
|
|
@@ -3743,6 +3531,27 @@ export class StorageManager {
|
|
|
3743
3531
|
private get behaviorSignalsPath(): string {
|
|
3744
3532
|
return path.join(this.stateDir, "behavior-signals.jsonl");
|
|
3745
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();
|
|
3746
3555
|
/**
|
|
3747
3556
|
* Buffer surprise telemetry ledger (issue #563 PR 3).
|
|
3748
3557
|
*
|
|
@@ -3870,71 +3679,7 @@ export class StorageManager {
|
|
|
3870
3679
|
async writeMemory(
|
|
3871
3680
|
category: MemoryCategory,
|
|
3872
3681
|
content: string,
|
|
3873
|
-
options: {
|
|
3874
|
-
actor?: string;
|
|
3875
|
-
confidence?: number;
|
|
3876
|
-
tags?: string[];
|
|
3877
|
-
entityRef?: string;
|
|
3878
|
-
source?: string;
|
|
3879
|
-
supersedes?: string;
|
|
3880
|
-
lineage?: string[];
|
|
3881
|
-
importance?: ImportanceScore;
|
|
3882
|
-
links?: MemoryLink[];
|
|
3883
|
-
intentGoal?: string;
|
|
3884
|
-
intentActionType?: string;
|
|
3885
|
-
intentEntityTypes?: string[];
|
|
3886
|
-
artifactType?: MemoryFrontmatter["artifactType"];
|
|
3887
|
-
sourceMemoryId?: string;
|
|
3888
|
-
sourceTurnId?: string;
|
|
3889
|
-
memoryKind?: MemoryFrontmatter["memoryKind"];
|
|
3890
|
-
expiresAt?: string;
|
|
3891
|
-
validAt?: string;
|
|
3892
|
-
// Issue #1578 — bi-temporal ingestion provenance. `observedAt` is the
|
|
3893
|
-
// ingestion time (when Remnic learned the fact); `eventTimeSource`
|
|
3894
|
-
// records whether `validAt` was resolved from an extracted expression
|
|
3895
|
-
// or assumed from the ingestion anchor. Both validate on serialize.
|
|
3896
|
-
observedAt?: string;
|
|
3897
|
-
eventTimeSource?: "extracted" | "assumed";
|
|
3898
|
-
invalidAt?: string;
|
|
3899
|
-
structuredAttributes?: Record<string, string>;
|
|
3900
|
-
/**
|
|
3901
|
-
* When provided, this string is used as the source for the fact-content
|
|
3902
|
-
* dedup hash index instead of the persisted body (`content`).
|
|
3903
|
-
*
|
|
3904
|
-
* Use this when the persisted body differs from the canonical fact text
|
|
3905
|
-
* — for example when `content` is a citation-annotated variant of a raw
|
|
3906
|
-
* fact. Passing the raw fact as `contentHashSource` ensures that
|
|
3907
|
-
* `hasFactContentHash(rawFact)` returns `true` after the write, so
|
|
3908
|
-
* subsequent extractions of the same logical fact are correctly deduped
|
|
3909
|
-
* even when their citation timestamp differs.
|
|
3910
|
-
*/
|
|
3911
|
-
contentHashSource?: string;
|
|
3912
|
-
status?: MemoryStatus;
|
|
3913
|
-
/**
|
|
3914
|
-
* Consolidation provenance (issue #561 PR 2). When the caller is a
|
|
3915
|
-
* consolidation / supersession / dedup-merge path, these fields wire
|
|
3916
|
-
* the page-version snapshots the new memory was derived from and the
|
|
3917
|
-
* operator that produced it. Persisted onto frontmatter as
|
|
3918
|
-
* `derived_from` + `derived_via`; validated at serialize time.
|
|
3919
|
-
*/
|
|
3920
|
-
derivedFrom?: string[];
|
|
3921
|
-
derivedVia?: ConsolidationOperator;
|
|
3922
|
-
/**
|
|
3923
|
-
* Faithfulness gate verdict (issue #1576). When provided, persisted to
|
|
3924
|
-
* frontmatter so downstream readers (TrustScore #1577, review queue)
|
|
3925
|
-
* can consume it. Absent = gate was off or fact predates #1576.
|
|
3926
|
-
*/
|
|
3927
|
-
faithfulness?: import("./types.js").FaithfulnessFrontmatter;
|
|
3928
|
-
/**
|
|
3929
|
-
* Claim-level provenance spans (issue #1575 PR 2). When provided,
|
|
3930
|
-
* persisted to frontmatter so downstream readers (memory_get, x-ray,
|
|
3931
|
-
* faithfulness gate #1576) can consume them. Absent = fact predates
|
|
3932
|
-
* #1575 or provenance was disabled.
|
|
3933
|
-
*/
|
|
3934
|
-
sources?: ProvenanceSource[];
|
|
3935
|
-
provenance?: "verified" | "unverified" | "none";
|
|
3936
|
-
sourceConnector?: string;
|
|
3937
|
-
} = {}
|
|
3682
|
+
options: WriteMemoryOptions = {}
|
|
3938
3683
|
): Promise<MemoryWriteResult> {
|
|
3939
3684
|
await this.ensureDirectories();
|
|
3940
3685
|
const now = new Date();
|
|
@@ -4017,17 +3762,10 @@ export class StorageManager {
|
|
|
4017
3762
|
fm.sourceConnector = options.sourceConnector;
|
|
4018
3763
|
}
|
|
4019
3764
|
|
|
4020
|
-
//
|
|
4021
|
-
//
|
|
4022
|
-
//
|
|
4023
|
-
|
|
4024
|
-
// orchestrator's hash-dedup path.
|
|
4025
|
-
let enrichedContent = content;
|
|
4026
|
-
if (options.structuredAttributes && Object.keys(options.structuredAttributes).length > 0) {
|
|
4027
|
-
enrichedContent = `${content}\n[Attributes: ${normalizeAttributePairs(options.structuredAttributes)}]`;
|
|
4028
|
-
}
|
|
4029
|
-
|
|
4030
|
-
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);
|
|
4031
3769
|
if (!sanitized.clean) {
|
|
4032
3770
|
log.warn(`memory content sanitized for ${id}; violations=${sanitized.violations.join(", ")}`);
|
|
4033
3771
|
}
|
|
@@ -4110,7 +3848,8 @@ export class StorageManager {
|
|
|
4110
3848
|
|
|
4111
3849
|
await this.snapshotBeforeWrite(filePath, "write");
|
|
4112
3850
|
await this.writeStorageSecureFile(filePath, fileContent);
|
|
4113
|
-
await this.patchHotMemoriesCache({ addedPath: filePath });
|
|
3851
|
+
await this.patchHotMemoriesCache({ addedPath: filePath }, "memory-create");
|
|
3852
|
+
this.notifyMemoryWrite(filePath);
|
|
4114
3853
|
await this.appendGeneratedMemoryLifecycleEventFailOpen("storage.writeMemory", {
|
|
4115
3854
|
memoryId: id,
|
|
4116
3855
|
eventType: "created",
|
|
@@ -4133,13 +3872,31 @@ export class StorageManager {
|
|
|
4133
3872
|
// fact text before citation annotation), index THAT string so that
|
|
4134
3873
|
// hasFactContentHash(rawFact) returns true on subsequent extractions.
|
|
4135
3874
|
// Otherwise fall back to the sanitized persisted body as before.
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
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();
|
|
4141
3899
|
}
|
|
4142
|
-
await factHashIndex.save();
|
|
4143
3900
|
} catch (err) {
|
|
4144
3901
|
log.warn(`storage.writeMemory completed but failed to update fact hash index: ${err}`);
|
|
4145
3902
|
}
|
|
@@ -4148,11 +3905,73 @@ export class StorageManager {
|
|
|
4148
3905
|
return { id, tombstoneBlocked, ...(fm.blockedBy ? { blockedBy: fm.blockedBy } : {}) };
|
|
4149
3906
|
}
|
|
4150
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
|
+
|
|
4151
3940
|
async hasFactContentHash(content: string): Promise<boolean> {
|
|
4152
|
-
await this.ensureFactHashIndexAuthoritative();
|
|
4153
|
-
const factHashIndex = await this.getFactHashIndex();
|
|
3941
|
+
const authoritative = await this.ensureFactHashIndexAuthoritative();
|
|
4154
3942
|
const sanitized = sanitizeMemoryContent(content);
|
|
4155
|
-
|
|
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;
|
|
4156
3975
|
}
|
|
4157
3976
|
|
|
4158
3977
|
private factContentHashForRemoval(memory: MemoryFile): string | null {
|
|
@@ -4177,7 +3996,22 @@ export class StorageManager {
|
|
|
4177
3996
|
await this.ensureFactHashIndexAuthoritative();
|
|
4178
3997
|
const factHashIndex = await this.getFactHashIndex();
|
|
4179
3998
|
factHashIndex.addByHash(hash);
|
|
4180
|
-
|
|
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();
|
|
4181
4015
|
}
|
|
4182
4016
|
|
|
4183
4017
|
/**
|
|
@@ -4246,14 +4080,38 @@ export class StorageManager {
|
|
|
4246
4080
|
for (const hash of removedHashes.values()) {
|
|
4247
4081
|
if (!remainingActiveHashes.has(hash)) {
|
|
4248
4082
|
factHashIndex.removeByHash(hash);
|
|
4083
|
+
this.factOnlyHashes.delete(hash);
|
|
4249
4084
|
}
|
|
4250
4085
|
}
|
|
4251
|
-
|
|
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);
|
|
4252
4111
|
}
|
|
4253
4112
|
|
|
4254
4113
|
async isFactContentHashAuthoritative(): Promise<boolean> {
|
|
4255
|
-
await this.ensureFactHashIndexAuthoritative();
|
|
4256
|
-
return true;
|
|
4114
|
+
return await this.ensureFactHashIndexAuthoritative();
|
|
4257
4115
|
}
|
|
4258
4116
|
|
|
4259
4117
|
async writeArtifact(
|
|
@@ -4559,6 +4417,10 @@ export class StorageManager {
|
|
|
4559
4417
|
StorageManager.hotMemoriesCacheDefault = true;
|
|
4560
4418
|
StorageManager.hotMemoriesCacheTtlMs = 60_000;
|
|
4561
4419
|
StorageManager.hotMemoriesCacheProcessDefaultSeeded = false;
|
|
4420
|
+
// Reset the #1904 scope-invalidation gate so a legacy-mode (=false) test
|
|
4421
|
+
// dir cannot leak its setting into a later test constructing over a reused
|
|
4422
|
+
// temp path.
|
|
4423
|
+
StorageManager.scopedCacheInvalidationByDir.clear();
|
|
4562
4424
|
}
|
|
4563
4425
|
|
|
4564
4426
|
/** Cancel any in-flight concurrent read so the next readAllMemories()
|
|
@@ -4579,13 +4441,21 @@ export class StorageManager {
|
|
|
4579
4441
|
// corpus sentinel too so PEER processes rescan instead of serving a warm
|
|
4580
4442
|
// pre-mutation corpus entry (issue #1902 cross-process coherence).
|
|
4581
4443
|
this.bumpMemoryCorpusVersion();
|
|
4582
|
-
// Invalidation chokepoint (issue #1535):
|
|
4583
|
-
//
|
|
4584
|
-
//
|
|
4585
|
-
//
|
|
4586
|
-
//
|
|
4587
|
-
//
|
|
4588
|
-
|
|
4444
|
+
// Invalidation chokepoint (issue #1535 / #1904): evict the layers a
|
|
4445
|
+
// memory-mutate can affect — hot, archive, derived episode/rule views, and
|
|
4446
|
+
// both QMD result caches (qmdSearchCache and qmdRecallCache). Before the QMD
|
|
4447
|
+
// caches were invalidated on mutations, recall served pre-edit bundles for
|
|
4448
|
+
// the remainder of its fresh/stale TTL window. The `entities` layer is NOT
|
|
4449
|
+
// in memory-mutate scope: these funnel callers (update/frontmatter/move/
|
|
4450
|
+
// archive/invalidate/bulk/offline-sync) never rewrite entity files, and the
|
|
4451
|
+
// ones that touch entity-derived state additionally bump the status version
|
|
4452
|
+
// (which does the full clear). scopedCacheInvalidationEnabled=false restores
|
|
4453
|
+
// the pre-#1904 full clear of every layer (rollback lever).
|
|
4454
|
+
if (StorageManager.scopedCacheInvalidationByDir.get(this.baseDir) ?? true) {
|
|
4455
|
+
invalidateForScope(this.baseDir, "memory-mutate");
|
|
4456
|
+
} else {
|
|
4457
|
+
invalidateAllForDir(this.baseDir);
|
|
4458
|
+
}
|
|
4589
4459
|
// Issue #1579 — tombstone store is a cache layer too (rule 25: clear ALL
|
|
4590
4460
|
// cache layers). A tombstone append from another path (supersession /
|
|
4591
4461
|
// correction) must be visible to the next writeMemory lookup on this
|
|
@@ -4614,7 +4484,10 @@ export class StorageManager {
|
|
|
4614
4484
|
* coherence is preserved. Bulk/ambiguous mutations keep the wholesale
|
|
4615
4485
|
* invalidateAllMemoriesCache() drop instead (issue #1902 Step 5).
|
|
4616
4486
|
*/
|
|
4617
|
-
private async patchHotMemoriesCache(
|
|
4487
|
+
private async patchHotMemoriesCache(
|
|
4488
|
+
opts: { addedPath?: string; removedPath?: string },
|
|
4489
|
+
scope: "memory-create" | "memory-mutate" = "memory-mutate",
|
|
4490
|
+
): Promise<void> {
|
|
4618
4491
|
const prevVersion = this.getMemoryCorpusVersion();
|
|
4619
4492
|
// Snapshot the key identity once (issue #1902, Codex Medium): a mid-await
|
|
4620
4493
|
// setSecureStoreKey change would otherwise let the post-await patch/re-key
|
|
@@ -4684,9 +4557,19 @@ export class StorageManager {
|
|
|
4684
4557
|
const patched = getCachedMemories(this.baseDir, prevVersion, keyId);
|
|
4685
4558
|
if (patched) setCachedMemories(this.baseDir, patched, produced, keyId, this.hotCacheTtlMs());
|
|
4686
4559
|
}
|
|
4687
|
-
// Evict
|
|
4688
|
-
// to be version-rejected) above, not dropped here.
|
|
4689
|
-
|
|
4560
|
+
// Evict the non-hot layers; the hot layer is patched (or intentionally left
|
|
4561
|
+
// to be version-rejected) above, not dropped here. Scope-aware (issue #1904):
|
|
4562
|
+
// a `memory-create` evicts ONLY the derived episode/rule views, keeping the
|
|
4563
|
+
// global QMD result caches and the version-keyed entity cache warm (a create
|
|
4564
|
+
// adds a doc that is not yet in the QMD index and changes no existing doc's
|
|
4565
|
+
// recall visibility). A `memory-mutate` (or scoped-off rollback) still evicts
|
|
4566
|
+
// the derived + global QMD views so a superseded/edited memory cannot resurface
|
|
4567
|
+
// from a stale recall bundle (issue #1535 correctness contract).
|
|
4568
|
+
if (scope === "memory-create" && (StorageManager.scopedCacheInvalidationByDir.get(this.baseDir) ?? true)) {
|
|
4569
|
+
invalidateForScopeExceptHot(this.baseDir, "memory-create");
|
|
4570
|
+
} else {
|
|
4571
|
+
invalidateDerivedAndGlobalForDir(this.baseDir);
|
|
4572
|
+
}
|
|
4690
4573
|
if (this.tombstoneStore) {
|
|
4691
4574
|
this.tombstoneStore.invalidate();
|
|
4692
4575
|
this.tombstoneStore = null;
|
|
@@ -5421,20 +5304,77 @@ export class StorageManager {
|
|
|
5421
5304
|
async appendMemoryLifecycleEvents(events: MemoryLifecycleEvent[]): Promise<number> {
|
|
5422
5305
|
if (events.length === 0) return 0;
|
|
5423
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
|
+
}
|
|
5424
5317
|
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
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
|
+
}
|
|
5435
5328
|
|
|
5436
|
-
|
|
5437
|
-
|
|
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);
|
|
5438
5378
|
}
|
|
5439
5379
|
|
|
5440
5380
|
async appendBufferSurpriseEvents(events: BufferSurpriseEvent[]): Promise<number> {
|
|
@@ -5446,46 +5386,113 @@ export class StorageManager {
|
|
|
5446
5386
|
}
|
|
5447
5387
|
|
|
5448
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> {
|
|
5449
5401
|
if (events.length === 0) return 0;
|
|
5450
5402
|
await this.ensureDirectories();
|
|
5451
5403
|
|
|
5452
|
-
let existingKeys
|
|
5404
|
+
let existingKeys: Set<string>;
|
|
5405
|
+
let identity: { size: number; mtimeMs: number } | null = null;
|
|
5453
5406
|
try {
|
|
5454
|
-
const
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
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).
|
|
5463
5432
|
}
|
|
5464
|
-
} catch {
|
|
5465
|
-
// Ignore malformed rows (fail-open).
|
|
5466
5433
|
}
|
|
5467
5434
|
}
|
|
5468
5435
|
} catch (err) {
|
|
5469
5436
|
if (err instanceof SecureStoreLockedError) throw err;
|
|
5470
5437
|
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
5471
5438
|
existingKeys = new Set<string>();
|
|
5439
|
+
identity = null;
|
|
5472
5440
|
}
|
|
5473
5441
|
|
|
5474
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>();
|
|
5475
5449
|
const deduped: BehaviorSignalEvent[] = [];
|
|
5476
5450
|
for (const event of events) {
|
|
5477
5451
|
const key = `${event.memoryId}:${event.signalHash}`;
|
|
5478
|
-
if (existingKeys.has(key)) continue;
|
|
5479
|
-
|
|
5452
|
+
if (existingKeys.has(key) || pending.has(key)) continue;
|
|
5453
|
+
pending.add(key);
|
|
5480
5454
|
deduped.push({
|
|
5481
5455
|
...event,
|
|
5482
5456
|
timestamp: event.timestamp && event.timestamp.length > 0 ? event.timestamp : nowIso,
|
|
5483
5457
|
});
|
|
5484
5458
|
}
|
|
5485
5459
|
|
|
5486
|
-
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
|
+
}
|
|
5487
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.
|
|
5488
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
|
+
}
|
|
5489
5496
|
return deduped.length;
|
|
5490
5497
|
}
|
|
5491
5498
|
|
|
@@ -5602,42 +5609,30 @@ export class StorageManager {
|
|
|
5602
5609
|
}
|
|
5603
5610
|
|
|
5604
5611
|
async readAllMemoryLifecycleEvents(): Promise<MemoryLifecycleEvent[]> {
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
typeof parsed.actor === "string" &&
|
|
5620
|
-
typeof parsed.ruleVersion === "string"
|
|
5621
|
-
) {
|
|
5622
|
-
out.push(parsed as MemoryLifecycleEvent);
|
|
5623
|
-
}
|
|
5624
|
-
} catch {
|
|
5625
|
-
// Ignore malformed rows (fail-open).
|
|
5626
|
-
}
|
|
5627
|
-
}
|
|
5628
|
-
return sortMemoryLifecycleEvents(out);
|
|
5629
|
-
} catch (err) {
|
|
5630
|
-
if (err instanceof SecureStoreLockedError) throw err;
|
|
5631
|
-
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
5632
|
-
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
|
+
);
|
|
5633
5626
|
}
|
|
5627
|
+
return readAllLifecycleEventsFromLedgerBuffer(
|
|
5628
|
+
this.memoryLifecycleLedgerPath,
|
|
5629
|
+
(p) => readMaybeEncryptedFileBuffer(p, this._secureStoreKey, this.baseDir),
|
|
5630
|
+
);
|
|
5634
5631
|
}
|
|
5635
5632
|
|
|
5636
5633
|
async readMemoryLifecycleEvents(limit: number = 200): Promise<MemoryLifecycleEvent[]> {
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
const events = await this.readAllMemoryLifecycleEvents();
|
|
5640
|
-
return events.slice(-cappedLimit);
|
|
5634
|
+
return readBoundedLifecycleEventsFromLedger(
|
|
5635
|
+
this.memoryLifecycleLedgerPath, (p) => this.readStorageSecureFile(p), limit);
|
|
5641
5636
|
}
|
|
5642
5637
|
|
|
5643
5638
|
async writeCompressionGuidelines(content: string): Promise<void> {
|
|
@@ -6365,6 +6360,7 @@ export class StorageManager {
|
|
|
6365
6360
|
|
|
6366
6361
|
const memories = await this.readAllMemories();
|
|
6367
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]));
|
|
6368
6364
|
let updated = 0;
|
|
6369
6365
|
// Capture the corpus version + warmth BEFORE writing so we can patch the hot
|
|
6370
6366
|
// entries in place and then re-key them to the version this flush produces
|
|
@@ -6384,7 +6380,9 @@ export class StorageManager {
|
|
|
6384
6380
|
const appliedPatches = new Map<string, { accessCount: number; lastAccessed: string }>();
|
|
6385
6381
|
|
|
6386
6382
|
for (const entry of entries) {
|
|
6387
|
-
const memory =
|
|
6383
|
+
const memory = entry.memoryPath
|
|
6384
|
+
? memoryPathMap.get(path.resolve(entry.memoryPath))
|
|
6385
|
+
: memoryMap.get(entry.memoryId);
|
|
6388
6386
|
if (!memory) continue;
|
|
6389
6387
|
|
|
6390
6388
|
const newFm: MemoryFrontmatter = {
|
|
@@ -6403,7 +6401,10 @@ export class StorageManager {
|
|
|
6403
6401
|
if (warm) {
|
|
6404
6402
|
updateCacheOnWrite(this.baseDir, { ...memory, frontmatter: newFm }, keyId);
|
|
6405
6403
|
}
|
|
6406
|
-
appliedPatches.set(
|
|
6404
|
+
appliedPatches.set(path.resolve(memory.path), {
|
|
6405
|
+
accessCount: entry.newCount,
|
|
6406
|
+
lastAccessed: entry.lastAccessed,
|
|
6407
|
+
});
|
|
6407
6408
|
updated++;
|
|
6408
6409
|
} catch (err) {
|
|
6409
6410
|
log.debug(`failed to update access tracking for ${entry.memoryId}: ${err}`);
|
|
@@ -6436,7 +6437,7 @@ export class StorageManager {
|
|
|
6436
6437
|
// re-key robust to that race; the publish guard blocks any scan
|
|
6437
6438
|
// finishing AFTER our bump, so this is the only window.
|
|
6438
6439
|
const reapplied = cur.map((m) => {
|
|
6439
|
-
const patch = appliedPatches.get(m.
|
|
6440
|
+
const patch = appliedPatches.get(path.resolve(m.path));
|
|
6440
6441
|
return patch
|
|
6441
6442
|
? { ...m, frontmatter: { ...m.frontmatter, accessCount: patch.accessCount, lastAccessed: patch.lastAccessed } }
|
|
6442
6443
|
: m;
|
|
@@ -6464,28 +6465,68 @@ export class StorageManager {
|
|
|
6464
6465
|
}
|
|
6465
6466
|
|
|
6466
6467
|
/**
|
|
6467
|
-
*
|
|
6468
|
+
* Resolve existing active memory IDs to their on-disk paths.
|
|
6468
6469
|
*
|
|
6469
6470
|
* Uses a lightweight directory scan (collectActiveMemoryPaths) that reads
|
|
6470
6471
|
* file names without parsing frontmatter — much cheaper than readAllMemories()
|
|
6471
|
-
* for
|
|
6472
|
-
*
|
|
6473
|
-
* Returns the subset of `ids` that correspond to real memory files.
|
|
6472
|
+
* for citation usage tracking and other existence checks.
|
|
6474
6473
|
*/
|
|
6475
|
-
async
|
|
6476
|
-
|
|
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();
|
|
6477
6479
|
const wantedIds = new Set(ids);
|
|
6478
6480
|
const filePaths = await this.collectActiveMemoryPaths();
|
|
6479
|
-
const
|
|
6481
|
+
const pathsById = new Map<string, string[]>();
|
|
6482
|
+
const filePathsById = new Map<string, string[]>();
|
|
6480
6483
|
for (const filePath of filePaths) {
|
|
6481
|
-
const
|
|
6482
|
-
if (wantedIds.has(
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
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);
|
|
6486
6520
|
}
|
|
6487
6521
|
}
|
|
6488
|
-
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());
|
|
6489
6530
|
}
|
|
6490
6531
|
|
|
6491
6532
|
async getProjectedMemoryState(id: string): Promise<MemoryProjectionCurrentState | null> {
|
|
@@ -6544,8 +6585,8 @@ export class StorageManager {
|
|
|
6544
6585
|
const projected = readProjectedMemoryTimeline(this.baseDir, memoryId, cappedLimit);
|
|
6545
6586
|
if (projected && projected.length > 0) return projected;
|
|
6546
6587
|
warnProjectionFallback(this.baseDir, "getMemoryTimeline");
|
|
6547
|
-
|
|
6548
|
-
|
|
6588
|
+
return readBoundedLifecycleEventsFromLedger(
|
|
6589
|
+
this.memoryLifecycleLedgerPath, (p) => this.readStorageSecureFile(p), cappedLimit, memoryId);
|
|
6549
6590
|
}
|
|
6550
6591
|
|
|
6551
6592
|
// ---------------------------------------------------------------------------
|
|
@@ -6648,7 +6689,7 @@ export class StorageManager {
|
|
|
6648
6689
|
await this.writeStorageSecureFile(filePath, fileContent);
|
|
6649
6690
|
// Keep the version-keyed hot-memories cache coherent with the new chunk
|
|
6650
6691
|
// file (issue #1902) — same single-file patch path writeMemory uses.
|
|
6651
|
-
await this.patchHotMemoriesCache({ addedPath: filePath });
|
|
6692
|
+
await this.patchHotMemoriesCache({ addedPath: filePath }, "memory-create");
|
|
6652
6693
|
log.debug(`wrote chunk ${id} (${chunkIndex + 1}/${chunkTotal}) to ${filePath}`);
|
|
6653
6694
|
return id;
|
|
6654
6695
|
}
|
|
@@ -6736,13 +6777,11 @@ export class StorageManager {
|
|
|
6736
6777
|
}
|
|
6737
6778
|
}
|
|
6738
6779
|
|
|
6739
|
-
//
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
lineage: [oldMemoryId, newMemoryId],
|
|
6745
|
-
});
|
|
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] });
|
|
6746
6785
|
|
|
6747
6786
|
return true;
|
|
6748
6787
|
} catch (err) {
|