@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
|
@@ -18,7 +18,8 @@ import test from "node:test";
|
|
|
18
18
|
import assert from "node:assert/strict";
|
|
19
19
|
import os from "node:os";
|
|
20
20
|
import path from "node:path";
|
|
21
|
-
import { mkdir, mkdtemp, rm, symlink } from "node:fs/promises";
|
|
21
|
+
import { lstat, mkdir, mkdtemp, readdir, readFile, rm, stat, symlink, writeFile } from "node:fs/promises";
|
|
22
|
+
import { execFileSync } from "node:child_process";
|
|
22
23
|
|
|
23
24
|
import { MaintenanceScheduler } from "./maintenance.js";
|
|
24
25
|
import type { PluginConfig } from "../types.js";
|
|
@@ -27,9 +28,22 @@ import type {
|
|
|
27
28
|
NamespaceSearchRouter,
|
|
28
29
|
NamespaceUpdateResult,
|
|
29
30
|
} from "../namespaces/search.js";
|
|
30
|
-
import type { NamespaceCatalog } from "../namespaces/catalog.js";
|
|
31
|
-
import { namespaceIdentityToken } from "../namespaces/identity.js";
|
|
31
|
+
import type { NamespaceCatalog, NamespaceRecord } from "../namespaces/catalog.js";
|
|
32
|
+
import { namespaceIdentityFromToken, namespaceIdentityToken } from "../namespaces/identity.js";
|
|
33
|
+
import { isSafeRouteNamespace } from "../routing/engine.js";
|
|
32
34
|
import { readNamespaceMaintenanceStatuses } from "../maintenance/namespace-planner.js";
|
|
35
|
+
import { StorageManager } from "../storage.js";
|
|
36
|
+
import {
|
|
37
|
+
decryptFileBody,
|
|
38
|
+
encryptFileBody,
|
|
39
|
+
filePathAad,
|
|
40
|
+
isEncryptedFile,
|
|
41
|
+
SECURE_STORE_ENVELOPE_OVERHEAD_BYTES,
|
|
42
|
+
} from "../secure-store/secure-fs.js";
|
|
43
|
+
import { pendingLifecycleLedgerDir } from "../storage/memory-lifecycle-ledger-access.js";
|
|
44
|
+
import { listContainedSpillFiles } from "../utils/path-containment.js";
|
|
45
|
+
import { rebuildMemoryLifecycleLedger } from "../maintenance/rebuild-memory-lifecycle-ledger.js";
|
|
46
|
+
import { resolveNamespaceStorageRoot } from "../namespaces/storage.js";
|
|
33
47
|
|
|
34
48
|
/** Build a fixture PluginConfig. Cast bridges a partial fixture to the full
|
|
35
49
|
* contract — only the fields the scheduler/planner read are populated. */
|
|
@@ -736,3 +750,2089 @@ test("MaintenanceScheduler reads a runtime-swapped qmd backend via getQmd (regre
|
|
|
736
750
|
scheduler.dispose();
|
|
737
751
|
}
|
|
738
752
|
});
|
|
753
|
+
|
|
754
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
755
|
+
// Lifecycle-ledger auto-compaction (issue #1910)
|
|
756
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
757
|
+
|
|
758
|
+
/** Access the private size-gated compaction trigger for focused testing. The
|
|
759
|
+
* `lifecycleLedgerMaxBytes` seam lets a test inject a tiny bound so the
|
|
760
|
+
* bounding + post-write verification path is exercised without a 400MB fixture
|
|
761
|
+
* (#2033). */
|
|
762
|
+
interface CompactableScheduler {
|
|
763
|
+
maybeCompactMemoryLifecycleLedger(): Promise<void>;
|
|
764
|
+
lifecycleLedgerMaxBytes: number;
|
|
765
|
+
// Arming this to `Date.now()` puts a focused test inside the min-interval
|
|
766
|
+
// window so it can prove the throttle path (#2033).
|
|
767
|
+
lastLifecycleCompactionAtMs: number;
|
|
768
|
+
dispose(): void;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
async function seedMemoryDirWithOversizedLedger(
|
|
772
|
+
padBytes: number,
|
|
773
|
+
): Promise<{ memoryDir: string; ledgerPath: string }> {
|
|
774
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-"));
|
|
775
|
+
await mkdir(path.join(memoryDir, "facts", "2026-03-08"), { recursive: true });
|
|
776
|
+
await writeFile(
|
|
777
|
+
path.join(memoryDir, "facts", "2026-03-08", "fact-1.md"),
|
|
778
|
+
`---
|
|
779
|
+
id: fact-1
|
|
780
|
+
category: fact
|
|
781
|
+
created: 2026-03-08T00:00:00.000Z
|
|
782
|
+
updated: 2026-03-08T01:00:00.000Z
|
|
783
|
+
source: test
|
|
784
|
+
confidence: 0.8
|
|
785
|
+
confidenceTier: implied
|
|
786
|
+
tags: ["alpha"]
|
|
787
|
+
---
|
|
788
|
+
|
|
789
|
+
alpha
|
|
790
|
+
`,
|
|
791
|
+
"utf-8",
|
|
792
|
+
);
|
|
793
|
+
const ledgerPath = path.join(memoryDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
794
|
+
await mkdir(path.dirname(ledgerPath), { recursive: true });
|
|
795
|
+
// Oversized legacy ledger: many junk rows the rebuild will discard (it
|
|
796
|
+
// reconstructs from frontmatter), so the compacted output is far smaller.
|
|
797
|
+
const line = '{"legacy":true,"pad":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}\n';
|
|
798
|
+
const rows = Math.ceil(padBytes / line.length);
|
|
799
|
+
await writeFile(ledgerPath, line.repeat(rows), "utf-8");
|
|
800
|
+
return { memoryDir, ledgerPath };
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function buildCompactionScheduler(config: PluginConfig): CompactableScheduler {
|
|
804
|
+
return new MaintenanceScheduler({
|
|
805
|
+
config,
|
|
806
|
+
getQmd: () => stubQmd(),
|
|
807
|
+
// The compaction path never touches the router/catalog.
|
|
808
|
+
namespaceSearchRouter: {} as unknown as NamespaceSearchRouter,
|
|
809
|
+
namespaceCatalog: {} as unknown as NamespaceCatalog,
|
|
810
|
+
}) as unknown as CompactableScheduler;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
test("auto-compaction shrinks an oversized ledger and writes a verbatim backup", async () => {
|
|
814
|
+
const { memoryDir, ledgerPath } = await seedMemoryDirWithOversizedLedger(4096);
|
|
815
|
+
try {
|
|
816
|
+
const before = await readFile(ledgerPath, "utf-8");
|
|
817
|
+
const beforeSize = (await stat(ledgerPath)).size;
|
|
818
|
+
const scheduler = buildCompactionScheduler(
|
|
819
|
+
fixtureConfig({
|
|
820
|
+
memoryDir,
|
|
821
|
+
memoryLifecycleLedgerCompactBytes: 1024,
|
|
822
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
823
|
+
}),
|
|
824
|
+
);
|
|
825
|
+
try {
|
|
826
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
827
|
+
} finally {
|
|
828
|
+
scheduler.dispose();
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const afterSize = (await stat(ledgerPath)).size;
|
|
832
|
+
assert.ok(afterSize < beforeSize, "compacted ledger must be smaller");
|
|
833
|
+
const rebuilt = (await readFile(ledgerPath, "utf-8")).trim().split("\n").map((l) => JSON.parse(l));
|
|
834
|
+
assert.deepEqual(rebuilt.map((r) => r.eventType), ["created", "updated"]);
|
|
835
|
+
|
|
836
|
+
// A verbatim backup of the original ledger must exist under archive/.
|
|
837
|
+
const archiveRoot = path.join(memoryDir, "archive", "memory-lifecycle-ledger");
|
|
838
|
+
const stamps = await readdir(archiveRoot);
|
|
839
|
+
assert.equal(stamps.length, 1);
|
|
840
|
+
const backup = await readFile(
|
|
841
|
+
path.join(archiveRoot, stamps[0]!, "state", "memory-lifecycle-ledger.jsonl"),
|
|
842
|
+
"utf-8",
|
|
843
|
+
);
|
|
844
|
+
assert.equal(backup, before, "backup must be the verbatim original ledger");
|
|
845
|
+
} finally {
|
|
846
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
test("auto-compaction preserves append-only lifecycle events with no frontmatter equivalent (#1910)", async () => {
|
|
851
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-preserve-"));
|
|
852
|
+
try {
|
|
853
|
+
await mkdir(path.join(memoryDir, "facts", "2026-03-08"), { recursive: true });
|
|
854
|
+
await writeFile(
|
|
855
|
+
path.join(memoryDir, "facts", "2026-03-08", "fact-1.md"),
|
|
856
|
+
`---
|
|
857
|
+
id: fact-1
|
|
858
|
+
category: fact
|
|
859
|
+
created: 2026-03-08T00:00:00.000Z
|
|
860
|
+
updated: 2026-03-08T01:00:00.000Z
|
|
861
|
+
source: test
|
|
862
|
+
confidence: 0.8
|
|
863
|
+
confidenceTier: implied
|
|
864
|
+
tags: ["alpha"]
|
|
865
|
+
---
|
|
866
|
+
|
|
867
|
+
alpha
|
|
868
|
+
`,
|
|
869
|
+
"utf-8",
|
|
870
|
+
);
|
|
871
|
+
const ledgerPath = path.join(memoryDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
872
|
+
await mkdir(path.dirname(ledgerPath), { recursive: true });
|
|
873
|
+
const appendOnly = {
|
|
874
|
+
eventId: "capture-1",
|
|
875
|
+
memoryId: "fact-1",
|
|
876
|
+
eventType: "explicit_capture_accepted",
|
|
877
|
+
timestamp: "2026-03-08T02:00:00.000Z",
|
|
878
|
+
actor: "explicit-capture",
|
|
879
|
+
ruleVersion: "memory-lifecycle-ledger.v1",
|
|
880
|
+
};
|
|
881
|
+
// Oversized: junk pad rows the rebuild discards + one valid append-only
|
|
882
|
+
// event frontmatter cannot reconstruct and MUST survive compaction.
|
|
883
|
+
const pad = '{"legacy":true,"pad":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}\n';
|
|
884
|
+
await writeFile(
|
|
885
|
+
ledgerPath,
|
|
886
|
+
pad.repeat(Math.ceil(4096 / pad.length)) + `${JSON.stringify(appendOnly)}\n`,
|
|
887
|
+
"utf-8",
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
const scheduler = buildCompactionScheduler(
|
|
891
|
+
fixtureConfig({
|
|
892
|
+
memoryDir,
|
|
893
|
+
memoryLifecycleLedgerCompactBytes: 1024,
|
|
894
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
895
|
+
}),
|
|
896
|
+
);
|
|
897
|
+
try {
|
|
898
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
899
|
+
} finally {
|
|
900
|
+
scheduler.dispose();
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const rebuilt = (await readFile(ledgerPath, "utf-8"))
|
|
904
|
+
.trim()
|
|
905
|
+
.split("\n")
|
|
906
|
+
.map((l) => JSON.parse(l) as { eventId: string; eventType: string; legacy?: boolean });
|
|
907
|
+
const eventTypes = rebuilt.map((r) => r.eventType);
|
|
908
|
+
// Frontmatter-derived rows are reconstructed AND the append-only capture row
|
|
909
|
+
// is carried over (a bare rebuild dropped it silently before this fix).
|
|
910
|
+
assert.ok(eventTypes.includes("created"));
|
|
911
|
+
assert.ok(eventTypes.includes("updated"));
|
|
912
|
+
assert.ok(
|
|
913
|
+
rebuilt.some((r) => r.eventId === "capture-1" && r.eventType === "explicit_capture_accepted"),
|
|
914
|
+
"append-only capture event must survive background compaction",
|
|
915
|
+
);
|
|
916
|
+
// Junk padding rows are still discarded — compaction shrank the ledger.
|
|
917
|
+
assert.ok(!rebuilt.some((r) => r.legacy === true), "junk rows discarded");
|
|
918
|
+
} finally {
|
|
919
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
test("auto-compaction is disabled when memoryLifecycleLedgerCompactBytes is 0", async () => {
|
|
924
|
+
const { memoryDir, ledgerPath } = await seedMemoryDirWithOversizedLedger(4096);
|
|
925
|
+
try {
|
|
926
|
+
const before = await readFile(ledgerPath, "utf-8");
|
|
927
|
+
const scheduler = buildCompactionScheduler(
|
|
928
|
+
fixtureConfig({
|
|
929
|
+
memoryDir,
|
|
930
|
+
memoryLifecycleLedgerCompactBytes: 0,
|
|
931
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
932
|
+
}),
|
|
933
|
+
);
|
|
934
|
+
try {
|
|
935
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
936
|
+
} finally {
|
|
937
|
+
scheduler.dispose();
|
|
938
|
+
}
|
|
939
|
+
assert.equal(await readFile(ledgerPath, "utf-8"), before, "disabled: ledger untouched");
|
|
940
|
+
await assert.rejects(() => readdir(path.join(memoryDir, "archive", "memory-lifecycle-ledger")));
|
|
941
|
+
} finally {
|
|
942
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
test("auto-compaction min-interval throttle prevents a second run within the window", async () => {
|
|
947
|
+
const { memoryDir, ledgerPath } = await seedMemoryDirWithOversizedLedger(4096);
|
|
948
|
+
try {
|
|
949
|
+
const scheduler = buildCompactionScheduler(
|
|
950
|
+
fixtureConfig({
|
|
951
|
+
memoryDir,
|
|
952
|
+
memoryLifecycleLedgerCompactBytes: 1024,
|
|
953
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
954
|
+
}),
|
|
955
|
+
);
|
|
956
|
+
try {
|
|
957
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
958
|
+
const compactedSize = (await stat(ledgerPath)).size;
|
|
959
|
+
|
|
960
|
+
// Re-grow the ledger past the threshold; a second immediate call is
|
|
961
|
+
// inside the min-interval window and must be throttled (no recompaction).
|
|
962
|
+
const line = '{"legacy":true,"pad":"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"}\n';
|
|
963
|
+
await writeFile(ledgerPath, line.repeat(Math.ceil(4096 / line.length)), "utf-8");
|
|
964
|
+
const regrownSize = (await stat(ledgerPath)).size;
|
|
965
|
+
|
|
966
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
967
|
+
assert.equal((await stat(ledgerPath)).size, regrownSize, "throttled: ledger not recompacted");
|
|
968
|
+
assert.ok(regrownSize > compactedSize);
|
|
969
|
+
} finally {
|
|
970
|
+
scheduler.dispose();
|
|
971
|
+
}
|
|
972
|
+
} finally {
|
|
973
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
test("auto-compaction is a no-op when the ledger is below the threshold", async () => {
|
|
978
|
+
const { memoryDir, ledgerPath } = await seedMemoryDirWithOversizedLedger(256);
|
|
979
|
+
try {
|
|
980
|
+
const before = await readFile(ledgerPath, "utf-8");
|
|
981
|
+
const scheduler = buildCompactionScheduler(
|
|
982
|
+
fixtureConfig({
|
|
983
|
+
memoryDir,
|
|
984
|
+
memoryLifecycleLedgerCompactBytes: 1024 * 1024,
|
|
985
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
986
|
+
}),
|
|
987
|
+
);
|
|
988
|
+
try {
|
|
989
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
990
|
+
} finally {
|
|
991
|
+
scheduler.dispose();
|
|
992
|
+
}
|
|
993
|
+
assert.equal(await readFile(ledgerPath, "utf-8"), before, "under threshold: ledger untouched");
|
|
994
|
+
} finally {
|
|
995
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
996
|
+
}
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
test("auto-compaction singleflight: two requests racing through the over-cap probe compact once (#2033)", async () => {
|
|
1000
|
+
const { memoryDir, ledgerPath } = await seedMemoryDirWithOversizedLedger(4096);
|
|
1001
|
+
try {
|
|
1002
|
+
const scheduler = buildCompactionScheduler(
|
|
1003
|
+
fixtureConfig({
|
|
1004
|
+
memoryDir,
|
|
1005
|
+
memoryLifecycleLedgerCompactBytes: 1024,
|
|
1006
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
1007
|
+
}),
|
|
1008
|
+
) as CompactableScheduler & {
|
|
1009
|
+
hasOverCapEncryptedLifecycleLedger: (targets: unknown) => Promise<boolean>;
|
|
1010
|
+
compactLifecycleLedgerTarget: (
|
|
1011
|
+
target: unknown,
|
|
1012
|
+
threshold: number,
|
|
1013
|
+
) => Promise<"skipped" | "compacted" | "failed" | "deferred">;
|
|
1014
|
+
};
|
|
1015
|
+
try {
|
|
1016
|
+
// Inside the min-interval window so the awaited over-cap probe — the ONLY
|
|
1017
|
+
// await between the singleflight guard and claiming it — is exercised.
|
|
1018
|
+
scheduler.lastLifecycleCompactionAtMs = Date.now();
|
|
1019
|
+
|
|
1020
|
+
// Deterministically open the race window the reviewer flagged: gate the
|
|
1021
|
+
// over-cap probe so BOTH callers pass the early `lifecycleCompactionInFlight`
|
|
1022
|
+
// guard and park inside the same await, then release them together. An
|
|
1023
|
+
// over-cap encrypted ledger (probe => true) bypasses the throttle, so both
|
|
1024
|
+
// fall through toward claiming the guard.
|
|
1025
|
+
const { promise: probeGate, resolve: releaseProbe } = Promise.withResolvers<void>();
|
|
1026
|
+
// Resolves the instant the SECOND caller enters the probe, so the test
|
|
1027
|
+
// releases the gate on a real signal rather than a timed poll.
|
|
1028
|
+
const { promise: bothArrived, resolve: signalBothArrived } =
|
|
1029
|
+
Promise.withResolvers<void>();
|
|
1030
|
+
let probeArrivals = 0;
|
|
1031
|
+
scheduler.hasOverCapEncryptedLifecycleLedger = async () => {
|
|
1032
|
+
probeArrivals += 1;
|
|
1033
|
+
if (probeArrivals === 2) signalBothArrived();
|
|
1034
|
+
await probeGate;
|
|
1035
|
+
return true;
|
|
1036
|
+
};
|
|
1037
|
+
|
|
1038
|
+
// Count real compaction attempts. Each one rewrites the ledger and writes
|
|
1039
|
+
// a verbatim (400MB-class in production) backup, so a second call here is
|
|
1040
|
+
// exactly the duplicate expensive work singleflight must prevent.
|
|
1041
|
+
let compactions = 0;
|
|
1042
|
+
const realCompact = scheduler.compactLifecycleLedgerTarget.bind(scheduler);
|
|
1043
|
+
scheduler.compactLifecycleLedgerTarget = async (target, threshold) => {
|
|
1044
|
+
compactions += 1;
|
|
1045
|
+
return realCompact(target, threshold);
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
const a = scheduler.maybeCompactMemoryLifecycleLedger();
|
|
1049
|
+
const b = scheduler.maybeCompactMemoryLifecycleLedger();
|
|
1050
|
+
// Both callers must reach the gated probe before either can claim the guard.
|
|
1051
|
+
await bothArrived;
|
|
1052
|
+
releaseProbe();
|
|
1053
|
+
await Promise.all([a, b]);
|
|
1054
|
+
|
|
1055
|
+
assert.equal(
|
|
1056
|
+
compactions,
|
|
1057
|
+
1,
|
|
1058
|
+
"exactly one caller compacts; the racing caller must re-observe the singleflight guard after the probe",
|
|
1059
|
+
);
|
|
1060
|
+
|
|
1061
|
+
// Exactly one verbatim backup — proof no duplicate backup ran back-to-back.
|
|
1062
|
+
const archiveRoot = path.join(memoryDir, "archive", "memory-lifecycle-ledger");
|
|
1063
|
+
const stamps = await readdir(archiveRoot);
|
|
1064
|
+
assert.equal(stamps.length, 1, "only one compaction backup exists");
|
|
1065
|
+
assert.ok((await stat(ledgerPath)).size < 4096, "ledger was compacted once");
|
|
1066
|
+
} finally {
|
|
1067
|
+
scheduler.dispose();
|
|
1068
|
+
}
|
|
1069
|
+
} finally {
|
|
1070
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
/** Scheduler wired with a live secure-store-configured root storage (issue
|
|
1075
|
+
* #1910, Cursor Medium / Codex P2). */
|
|
1076
|
+
function buildCompactionSchedulerWithStorage(
|
|
1077
|
+
config: PluginConfig,
|
|
1078
|
+
getStorage: () => StorageManager,
|
|
1079
|
+
storageForNamespace?: (namespace: string) => Promise<StorageManager>,
|
|
1080
|
+
namespaceCatalog?: NamespaceCatalog,
|
|
1081
|
+
): CompactableScheduler {
|
|
1082
|
+
return new MaintenanceScheduler({
|
|
1083
|
+
config,
|
|
1084
|
+
getQmd: () => stubQmd(),
|
|
1085
|
+
namespaceSearchRouter: {} as unknown as NamespaceSearchRouter,
|
|
1086
|
+
namespaceCatalog: (namespaceCatalog ?? ({} as unknown as NamespaceCatalog)),
|
|
1087
|
+
getStorage,
|
|
1088
|
+
storageForNamespace,
|
|
1089
|
+
}) as unknown as CompactableScheduler;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
test("auto-compaction leaves the throttle un-advanced after a failed run so it retries next pass", async () => {
|
|
1093
|
+
const { memoryDir, ledgerPath } = await seedMemoryDirWithOversizedLedger(4096);
|
|
1094
|
+
try {
|
|
1095
|
+
const beforeSize = (await stat(ledgerPath)).size;
|
|
1096
|
+
// A required-but-locked secure store makes the rewrite throw
|
|
1097
|
+
// SecureStoreLockedError, so the first compaction fails.
|
|
1098
|
+
const storage = new StorageManager(memoryDir);
|
|
1099
|
+
storage.setSecureStoreRequired(true);
|
|
1100
|
+
const scheduler = buildCompactionSchedulerWithStorage(
|
|
1101
|
+
fixtureConfig({
|
|
1102
|
+
memoryDir,
|
|
1103
|
+
memoryLifecycleLedgerCompactBytes: 1024,
|
|
1104
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
1105
|
+
}),
|
|
1106
|
+
() => storage,
|
|
1107
|
+
);
|
|
1108
|
+
try {
|
|
1109
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
1110
|
+
// Failed run must NOT shrink the ledger and must NOT arm the throttle.
|
|
1111
|
+
assert.equal((await stat(ledgerPath)).size, beforeSize, "failed run leaves ledger intact");
|
|
1112
|
+
|
|
1113
|
+
// Unlock the store, then call again INSIDE the min-interval window. If the
|
|
1114
|
+
// failed run had armed the throttle this second call would be skipped and
|
|
1115
|
+
// the ledger would stay oversized. Because it did not, the ledger compacts.
|
|
1116
|
+
storage.setSecureStoreKey(Buffer.alloc(32, 9));
|
|
1117
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
1118
|
+
assert.ok(
|
|
1119
|
+
(await stat(ledgerPath)).size < beforeSize,
|
|
1120
|
+
"still-eligible retry compacts once the failure clears",
|
|
1121
|
+
);
|
|
1122
|
+
} finally {
|
|
1123
|
+
scheduler.dispose();
|
|
1124
|
+
}
|
|
1125
|
+
} finally {
|
|
1126
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
test("auto-compaction rewrites the ledger encrypted at rest under a secure store", async () => {
|
|
1131
|
+
const { memoryDir, ledgerPath } = await seedMemoryDirWithOversizedLedger(4096);
|
|
1132
|
+
try {
|
|
1133
|
+
const before = await readFile(ledgerPath, "utf-8");
|
|
1134
|
+
const beforeSize = before.length;
|
|
1135
|
+
const key = Buffer.alloc(32, 5);
|
|
1136
|
+
const storage = new StorageManager(memoryDir);
|
|
1137
|
+
storage.setSecureStoreRequired(true);
|
|
1138
|
+
storage.setSecureStoreKey(key);
|
|
1139
|
+
const scheduler = buildCompactionSchedulerWithStorage(
|
|
1140
|
+
fixtureConfig({
|
|
1141
|
+
memoryDir,
|
|
1142
|
+
memoryLifecycleLedgerCompactBytes: 1024,
|
|
1143
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
1144
|
+
}),
|
|
1145
|
+
() => storage,
|
|
1146
|
+
);
|
|
1147
|
+
try {
|
|
1148
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
1149
|
+
} finally {
|
|
1150
|
+
scheduler.dispose();
|
|
1151
|
+
}
|
|
1152
|
+
// The rewritten ledger is encrypted at rest and smaller than the plaintext
|
|
1153
|
+
// original, and decrypts back to the rebuilt events via the same key.
|
|
1154
|
+
assert.ok(isEncryptedFile(await readFile(ledgerPath)), "compacted ledger must be encrypted");
|
|
1155
|
+
assert.ok((await stat(ledgerPath)).size < beforeSize);
|
|
1156
|
+
const rebuilt = await storage.readAllMemoryLifecycleEvents();
|
|
1157
|
+
assert.deepEqual(rebuilt.map((e) => e.eventType), ["created", "updated"]);
|
|
1158
|
+
// The existing ledger here is PLAINTEXT (only the rewrite encrypts), so its
|
|
1159
|
+
// backup is a directly-readable verbatim copy — the source-AAD re-encrypt only
|
|
1160
|
+
// applies when the EXISTING ledger is itself encrypted (#2033).
|
|
1161
|
+
const archiveRoot = path.join(memoryDir, "archive", "memory-lifecycle-ledger");
|
|
1162
|
+
const stamps = await readdir(archiveRoot);
|
|
1163
|
+
assert.equal(stamps.length, 1);
|
|
1164
|
+
assert.equal(
|
|
1165
|
+
await readFile(
|
|
1166
|
+
path.join(archiveRoot, stamps[0]!, "state", "memory-lifecycle-ledger.jsonl"),
|
|
1167
|
+
"utf-8",
|
|
1168
|
+
),
|
|
1169
|
+
before,
|
|
1170
|
+
);
|
|
1171
|
+
} finally {
|
|
1172
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
1173
|
+
}
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
test("auto-compaction re-encrypts an ENCRYPTED ledger's backup for the archive path so it decrypts there, not with the source AAD (#2033)", async () => {
|
|
1177
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-enc-backup-"));
|
|
1178
|
+
try {
|
|
1179
|
+
// A memory file so the rebuild reconstructs a small active ledger.
|
|
1180
|
+
await mkdir(path.join(memoryDir, "facts", "2026-03-08"), { recursive: true });
|
|
1181
|
+
await writeFile(
|
|
1182
|
+
path.join(memoryDir, "facts", "2026-03-08", "fact-1.md"),
|
|
1183
|
+
`---\nid: fact-1\ncategory: fact\ncreated: 2026-03-08T00:00:00.000Z\n`
|
|
1184
|
+
+ `updated: 2026-03-08T01:00:00.000Z\nsource: test\nconfidence: 0.8\n`
|
|
1185
|
+
+ `confidenceTier: implied\ntags: ["alpha"]\n---\n\nalpha\n`,
|
|
1186
|
+
"utf-8",
|
|
1187
|
+
);
|
|
1188
|
+
const key = Buffer.alloc(32, 6);
|
|
1189
|
+
const ledgerPath = path.join(memoryDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
1190
|
+
await mkdir(path.dirname(ledgerPath), { recursive: true });
|
|
1191
|
+
// An oversized ENCRYPTED existing ledger of valid legacy events. Its bytes
|
|
1192
|
+
// are path-bound to the SOURCE via AAD, so a byte copy to the archive path
|
|
1193
|
+
// would not decrypt there — the exact orphaning the fix prevents.
|
|
1194
|
+
const legacyIds = Array.from({ length: 80 }, (_, i) => `evt-legacy-${i}`);
|
|
1195
|
+
const plaintextLedger = legacyIds
|
|
1196
|
+
.map((id) => JSON.stringify({
|
|
1197
|
+
schemaVersion: 1, eventId: id, memoryId: "m-legacy", eventType: "note",
|
|
1198
|
+
timestamp: "2026-03-08T00:00:00.000Z", actor: "t", ruleVersion: "1",
|
|
1199
|
+
}))
|
|
1200
|
+
.join("\n") + "\n";
|
|
1201
|
+
await writeFile(ledgerPath, encryptFileBody(plaintextLedger, key, filePathAad(ledgerPath, memoryDir)));
|
|
1202
|
+
assert.ok(isEncryptedFile(await readFile(ledgerPath)), "precondition: source ledger encrypted");
|
|
1203
|
+
|
|
1204
|
+
const storage = new StorageManager(memoryDir);
|
|
1205
|
+
storage.setSecureStoreRequired(true);
|
|
1206
|
+
storage.setSecureStoreKey(key);
|
|
1207
|
+
const scheduler = buildCompactionSchedulerWithStorage(
|
|
1208
|
+
fixtureConfig({
|
|
1209
|
+
memoryDir,
|
|
1210
|
+
memoryLifecycleLedgerCompactBytes: 1024,
|
|
1211
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
1212
|
+
}),
|
|
1213
|
+
() => storage,
|
|
1214
|
+
);
|
|
1215
|
+
try {
|
|
1216
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
1217
|
+
} finally {
|
|
1218
|
+
scheduler.dispose();
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
assert.ok(isEncryptedFile(await readFile(ledgerPath)), "compacted ledger stays encrypted at rest");
|
|
1222
|
+
|
|
1223
|
+
const archiveRoot = path.join(memoryDir, "archive", "memory-lifecycle-ledger");
|
|
1224
|
+
const stamps = await readdir(archiveRoot);
|
|
1225
|
+
assert.equal(stamps.length, 1);
|
|
1226
|
+
const backupPath = path.join(archiveRoot, stamps[0]!, "state", "memory-lifecycle-ledger.jsonl");
|
|
1227
|
+
const backupBytes = await readFile(backupPath);
|
|
1228
|
+
assert.ok(isEncryptedFile(backupBytes), "backup encrypted at rest (no plaintext leak)");
|
|
1229
|
+
// Decrypts AT THE BACKUP PATH — a directly readable recovery ledger.
|
|
1230
|
+
const decrypted = decryptFileBody(backupBytes, key, filePathAad(backupPath, memoryDir)).toString("utf8");
|
|
1231
|
+
const decryptedIds = decrypted
|
|
1232
|
+
.trim().split("\n").filter(Boolean)
|
|
1233
|
+
.map((l) => JSON.parse(l).eventId as string)
|
|
1234
|
+
.sort();
|
|
1235
|
+
assert.deepEqual(
|
|
1236
|
+
decryptedIds,
|
|
1237
|
+
[...legacyIds].sort(),
|
|
1238
|
+
"backup preserves exactly the prior ledger's events, decryptable at the archive path",
|
|
1239
|
+
);
|
|
1240
|
+
// A byte-for-byte copy would carry the SOURCE path's AAD and fail here.
|
|
1241
|
+
assert.throws(
|
|
1242
|
+
() => decryptFileBody(backupBytes, key, filePathAad(ledgerPath, memoryDir)),
|
|
1243
|
+
"backup must NOT decrypt under the SOURCE-path AAD (proves re-encrypt, not byte copy)",
|
|
1244
|
+
);
|
|
1245
|
+
} finally {
|
|
1246
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
test("mixed pass: a deferred encrypted target keeps the throttle un-armed so untouched ledgers retry (#2033)", async () => {
|
|
1251
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-mixed-"));
|
|
1252
|
+
try {
|
|
1253
|
+
const junk = '{"legacy":true,"pad":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}\n';
|
|
1254
|
+
const junkBlob = junk.repeat(Math.ceil(4096 / junk.length));
|
|
1255
|
+
|
|
1256
|
+
// Root: plaintext oversized ledger with a reconstructable memory → compacts.
|
|
1257
|
+
await mkdir(path.join(root, "facts", "2026-03-08"), { recursive: true });
|
|
1258
|
+
await writeFile(
|
|
1259
|
+
path.join(root, "facts", "2026-03-08", "fact-root.md"),
|
|
1260
|
+
`---\nid: fact-root\ncategory: fact\ncreated: 2026-03-08T00:00:00.000Z\n`
|
|
1261
|
+
+ `updated: 2026-03-08T01:00:00.000Z\nsource: test\nconfidence: 0.8\n`
|
|
1262
|
+
+ `confidenceTier: implied\ntags: ["alpha"]\n---\n\nalpha\n`,
|
|
1263
|
+
"utf-8",
|
|
1264
|
+
);
|
|
1265
|
+
const rootLedger = path.join(root, "state", "memory-lifecycle-ledger.jsonl");
|
|
1266
|
+
await mkdir(path.dirname(rootLedger), { recursive: true });
|
|
1267
|
+
await writeFile(rootLedger, junkBlob, "utf-8");
|
|
1268
|
+
|
|
1269
|
+
// Namespace: encrypted-at-rest oversized ledger whose secure store is LOCKED
|
|
1270
|
+
// (required, no key). Compaction must DEFER it (not skip) so this untouched,
|
|
1271
|
+
// still-oversized ledger keeps its retry eligibility.
|
|
1272
|
+
const key = Buffer.alloc(32, 5);
|
|
1273
|
+
const nsDir = path.join(root, "namespaces", namespaceIdentityToken("locked-ns"));
|
|
1274
|
+
const nsLedger = path.join(nsDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
1275
|
+
await mkdir(path.dirname(nsLedger), { recursive: true });
|
|
1276
|
+
const encrypted = encryptFileBody(junkBlob, key, filePathAad(nsLedger, nsDir));
|
|
1277
|
+
await writeFile(nsLedger, encrypted);
|
|
1278
|
+
|
|
1279
|
+
const catalog = {
|
|
1280
|
+
enabled: true,
|
|
1281
|
+
listNamespaces: async (): Promise<NamespaceRecord[]> =>
|
|
1282
|
+
[{ namespace: "locked-ns", storageDir: nsDir } as unknown as NamespaceRecord],
|
|
1283
|
+
} as unknown as NamespaceCatalog;
|
|
1284
|
+
const nsStorage = new StorageManager(nsDir);
|
|
1285
|
+
nsStorage.setSecureStoreRequired(true); // required + no key → locked (deferred).
|
|
1286
|
+
const scheduler = buildCompactionSchedulerWithStorage(
|
|
1287
|
+
fixtureConfig({
|
|
1288
|
+
memoryDir: root,
|
|
1289
|
+
memoryLifecycleLedgerCompactBytes: 1024,
|
|
1290
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
1291
|
+
}),
|
|
1292
|
+
() => new StorageManager(root),
|
|
1293
|
+
async () => nsStorage,
|
|
1294
|
+
catalog,
|
|
1295
|
+
);
|
|
1296
|
+
try {
|
|
1297
|
+
const rootBefore = (await stat(rootLedger)).size;
|
|
1298
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
1299
|
+
assert.ok((await stat(rootLedger)).size < rootBefore, "root ledger compacted");
|
|
1300
|
+
assert.deepEqual(
|
|
1301
|
+
await readFile(nsLedger),
|
|
1302
|
+
encrypted,
|
|
1303
|
+
"encrypted+locked namespace ledger deferred, left byte-for-byte intact",
|
|
1304
|
+
);
|
|
1305
|
+
|
|
1306
|
+
// Re-grow the root within the min-interval window. Because a target was
|
|
1307
|
+
// DEFERRED (not a genuine no-op), the throttle must NOT have armed, so this
|
|
1308
|
+
// second in-window pass still recompacts — the #2033 throttle fix.
|
|
1309
|
+
await writeFile(rootLedger, junkBlob, "utf-8");
|
|
1310
|
+
const regrown = (await stat(rootLedger)).size;
|
|
1311
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
1312
|
+
assert.ok(
|
|
1313
|
+
(await stat(rootLedger)).size < regrown,
|
|
1314
|
+
"deferred target left the throttle un-armed: the second in-window pass recompacts",
|
|
1315
|
+
);
|
|
1316
|
+
} finally {
|
|
1317
|
+
scheduler.dispose();
|
|
1318
|
+
}
|
|
1319
|
+
} finally {
|
|
1320
|
+
await rm(root, { recursive: true, force: true });
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
test("maintenance drains a pending lifecycle-append spill even when compaction is disabled (#2033)", async () => {
|
|
1325
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-drain-"));
|
|
1326
|
+
try {
|
|
1327
|
+
const key = Buffer.alloc(32, 7);
|
|
1328
|
+
const storage = new StorageManager(memoryDir);
|
|
1329
|
+
storage.setSecureStoreKey(key);
|
|
1330
|
+
const ledgerPath = path.join(memoryDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
1331
|
+
await mkdir(path.dirname(ledgerPath), { recursive: true });
|
|
1332
|
+
// An encrypted pending spill, exactly as a lock-timed-out append leaves it.
|
|
1333
|
+
const spillDir = pendingLifecycleLedgerDir(ledgerPath);
|
|
1334
|
+
await mkdir(spillDir, { recursive: true });
|
|
1335
|
+
const spillPath = path.join(spillDir, "spill.jsonl");
|
|
1336
|
+
const row =
|
|
1337
|
+
`{"eventId":"evt-spilled","memoryId":"m","eventType":"created",`
|
|
1338
|
+
+ `"timestamp":"2026-03-08T00:00:00.000Z","actor":"t","ruleVersion":"1"}\n`;
|
|
1339
|
+
await writeFile(spillPath, encryptFileBody(row, key, filePathAad(spillPath, memoryDir)));
|
|
1340
|
+
|
|
1341
|
+
// Compaction disabled (threshold 0): the unconditional root drain must still
|
|
1342
|
+
// fold the spill back into the ledger so no lifecycle row is lost.
|
|
1343
|
+
const scheduler = buildCompactionSchedulerWithStorage(
|
|
1344
|
+
fixtureConfig({
|
|
1345
|
+
memoryDir,
|
|
1346
|
+
memoryLifecycleLedgerCompactBytes: 0,
|
|
1347
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 1,
|
|
1348
|
+
}),
|
|
1349
|
+
() => storage,
|
|
1350
|
+
);
|
|
1351
|
+
try {
|
|
1352
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
1353
|
+
} finally {
|
|
1354
|
+
scheduler.dispose();
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
const ids = (await storage.readAllMemoryLifecycleEvents()).map((e) => e.eventId);
|
|
1358
|
+
assert.ok(ids.includes("evt-spilled"), "spill drained into the ledger with compaction disabled");
|
|
1359
|
+
await assert.rejects(() => stat(spillPath), /ENOENT/, "spill file removed after drain");
|
|
1360
|
+
} finally {
|
|
1361
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
1362
|
+
}
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
test("maintenance drains a namespace pending spill created after the throttle armed, before returning on the min-interval throttle (#2033)", async () => {
|
|
1366
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-ns-drain-throttled-"));
|
|
1367
|
+
try {
|
|
1368
|
+
// Root: oversized plaintext ledger with a reconstructable memory so the
|
|
1369
|
+
// FIRST pass compacts and arms the min-interval throttle.
|
|
1370
|
+
const junk = '{"legacy":true,"pad":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}\n';
|
|
1371
|
+
const junkBlob = junk.repeat(Math.ceil(4096 / junk.length));
|
|
1372
|
+
await mkdir(path.join(root, "facts", "2026-03-08"), { recursive: true });
|
|
1373
|
+
await writeFile(
|
|
1374
|
+
path.join(root, "facts", "2026-03-08", "fact-root.md"),
|
|
1375
|
+
`---\nid: fact-root\ncategory: fact\ncreated: 2026-03-08T00:00:00.000Z\n`
|
|
1376
|
+
+ `updated: 2026-03-08T01:00:00.000Z\nsource: test\nconfidence: 0.8\n`
|
|
1377
|
+
+ `confidenceTier: implied\ntags: ["alpha"]\n---\n\nalpha\n`,
|
|
1378
|
+
"utf-8",
|
|
1379
|
+
);
|
|
1380
|
+
const rootLedger = path.join(root, "state", "memory-lifecycle-ledger.jsonl");
|
|
1381
|
+
await mkdir(path.dirname(rootLedger), { recursive: true });
|
|
1382
|
+
await writeFile(rootLedger, junkBlob, "utf-8");
|
|
1383
|
+
|
|
1384
|
+
// Namespace: a keyed (unlocked) secure store, no oversized ledger — so the
|
|
1385
|
+
// first pass SKIPS it (skip never blocks arming the throttle).
|
|
1386
|
+
const key = Buffer.alloc(32, 7);
|
|
1387
|
+
const token = namespaceIdentityToken("project-alpha");
|
|
1388
|
+
const nsDir = path.join(root, "namespaces", token);
|
|
1389
|
+
const nsLedger = path.join(nsDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
1390
|
+
await mkdir(path.dirname(nsLedger), { recursive: true });
|
|
1391
|
+
const nsStorage = new StorageManager(nsDir);
|
|
1392
|
+
nsStorage.setSecureStoreKey(key);
|
|
1393
|
+
const catalog = {
|
|
1394
|
+
enabled: true,
|
|
1395
|
+
listNamespaces: async (): Promise<NamespaceRecord[]> =>
|
|
1396
|
+
[{ namespace: "project-alpha", storageDir: nsDir } as unknown as NamespaceRecord],
|
|
1397
|
+
} as unknown as NamespaceCatalog;
|
|
1398
|
+
const scheduler = buildCompactionSchedulerWithStorage(
|
|
1399
|
+
fixtureConfig({
|
|
1400
|
+
memoryDir: root,
|
|
1401
|
+
memoryLifecycleLedgerCompactBytes: 1024,
|
|
1402
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
1403
|
+
}),
|
|
1404
|
+
() => new StorageManager(root),
|
|
1405
|
+
async () => nsStorage,
|
|
1406
|
+
catalog,
|
|
1407
|
+
);
|
|
1408
|
+
try {
|
|
1409
|
+
// Pass 1 compacts the root and arms the throttle for the whole interval.
|
|
1410
|
+
const rootBefore = (await stat(rootLedger)).size;
|
|
1411
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
1412
|
+
assert.ok((await stat(rootLedger)).size < rootBefore, "root ledger compacted on the first pass");
|
|
1413
|
+
|
|
1414
|
+
// AFTER the throttle armed, a namespace impression spills (exactly as a
|
|
1415
|
+
// lock-timed-out append leaves it) — encrypted for the namespace store.
|
|
1416
|
+
const spillDir = pendingLifecycleLedgerDir(nsLedger);
|
|
1417
|
+
await mkdir(spillDir, { recursive: true });
|
|
1418
|
+
const spillPath = path.join(spillDir, "spill.jsonl");
|
|
1419
|
+
const row =
|
|
1420
|
+
`{"eventId":"evt-ns-spilled","memoryId":"m","eventType":"created",`
|
|
1421
|
+
+ `"timestamp":"2026-03-08T00:00:00.000Z","actor":"t","ruleVersion":"1"}\n`;
|
|
1422
|
+
await writeFile(spillPath, encryptFileBody(row, key, filePathAad(spillPath, nsDir)));
|
|
1423
|
+
|
|
1424
|
+
// Pass 2 is inside the min-interval window: it WILL return on the throttle,
|
|
1425
|
+
// but it must first drain the namespace pending spill. Before the #2033
|
|
1426
|
+
// fix the throttle return preceded namespace draining and the spill was
|
|
1427
|
+
// stranded for the whole interval.
|
|
1428
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
1429
|
+
const ids = (await nsStorage.readAllMemoryLifecycleEvents()).map((e) => e.eventId);
|
|
1430
|
+
assert.ok(
|
|
1431
|
+
ids.includes("evt-ns-spilled"),
|
|
1432
|
+
"namespace spill drained before the throttle return, not stranded until the interval elapses",
|
|
1433
|
+
);
|
|
1434
|
+
await assert.rejects(() => stat(spillPath), /ENOENT/, "namespace spill file removed after drain");
|
|
1435
|
+
} finally {
|
|
1436
|
+
scheduler.dispose();
|
|
1437
|
+
}
|
|
1438
|
+
} finally {
|
|
1439
|
+
await rm(root, { recursive: true, force: true });
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
test("auto-compaction bounds per-namespace ledgers, not just the root state path", async () => {
|
|
1444
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-ns-"));
|
|
1445
|
+
try {
|
|
1446
|
+
// Root ledger stays absent (nothing to compact); a namespace under
|
|
1447
|
+
// memoryDir/namespaces/<token>/ carries the oversized ledger.
|
|
1448
|
+
const token = namespaceIdentityToken("project-alpha");
|
|
1449
|
+
const nsDir = path.join(root, "namespaces", token);
|
|
1450
|
+
await mkdir(path.join(nsDir, "facts", "2026-03-08"), { recursive: true });
|
|
1451
|
+
await writeFile(
|
|
1452
|
+
path.join(nsDir, "facts", "2026-03-08", "fact-ns.md"),
|
|
1453
|
+
`---\nid: fact-ns\ncategory: fact\ncreated: 2026-03-08T00:00:00.000Z\n`
|
|
1454
|
+
+ `updated: 2026-03-08T01:00:00.000Z\nsource: test\nconfidence: 0.8\n`
|
|
1455
|
+
+ `confidenceTier: implied\ntags: ["alpha"]\n---\n\nalpha\n`,
|
|
1456
|
+
"utf-8",
|
|
1457
|
+
);
|
|
1458
|
+
const nsLedger = path.join(nsDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
1459
|
+
await mkdir(path.dirname(nsLedger), { recursive: true });
|
|
1460
|
+
const line = '{"legacy":true,"pad":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"}\n';
|
|
1461
|
+
await writeFile(nsLedger, line.repeat(Math.ceil(4096 / line.length)), "utf-8");
|
|
1462
|
+
const beforeSize = (await stat(nsLedger)).size;
|
|
1463
|
+
|
|
1464
|
+
const catalog = {
|
|
1465
|
+
enabled: true,
|
|
1466
|
+
listNamespaces: async (): Promise<NamespaceRecord[]> =>
|
|
1467
|
+
[{ namespace: "project-alpha", storageDir: nsDir } as unknown as NamespaceRecord],
|
|
1468
|
+
} as unknown as NamespaceCatalog;
|
|
1469
|
+
const nsStorages = new Map<string, StorageManager>();
|
|
1470
|
+
const scheduler = buildCompactionSchedulerWithStorage(
|
|
1471
|
+
fixtureConfig({
|
|
1472
|
+
memoryDir: root,
|
|
1473
|
+
memoryLifecycleLedgerCompactBytes: 1024,
|
|
1474
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
1475
|
+
}),
|
|
1476
|
+
() => new StorageManager(root),
|
|
1477
|
+
async (namespace) => {
|
|
1478
|
+
let sm = nsStorages.get(namespace);
|
|
1479
|
+
if (!sm) {
|
|
1480
|
+
sm = new StorageManager(nsDir);
|
|
1481
|
+
nsStorages.set(namespace, sm);
|
|
1482
|
+
}
|
|
1483
|
+
return sm;
|
|
1484
|
+
},
|
|
1485
|
+
catalog,
|
|
1486
|
+
);
|
|
1487
|
+
try {
|
|
1488
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
1489
|
+
} finally {
|
|
1490
|
+
scheduler.dispose();
|
|
1491
|
+
}
|
|
1492
|
+
assert.ok((await stat(nsLedger)).size < beforeSize, "namespace ledger must be compacted");
|
|
1493
|
+
const rebuilt = (await readFile(nsLedger, "utf-8")).trim().split("\n").map((l) => JSON.parse(l));
|
|
1494
|
+
assert.deepEqual(rebuilt.map((r) => r.eventType), ["created", "updated"]);
|
|
1495
|
+
} finally {
|
|
1496
|
+
await rm(root, { recursive: true, force: true });
|
|
1497
|
+
}
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
test("catalog-disabled fallback never downgrades an encrypted namespace ledger to plaintext (#2033)", async () => {
|
|
1501
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-ns-secure-"));
|
|
1502
|
+
try {
|
|
1503
|
+
// Namespaces enabled but the catalog is inactive, so only the filesystem
|
|
1504
|
+
// fallback finds this per-namespace ledger. It is encrypted at rest and
|
|
1505
|
+
// oversized, but the fallback has no secure StorageManager for a
|
|
1506
|
+
// catalog-disabled namespace. The rebuild-chokepoint guard must REFUSE to
|
|
1507
|
+
// rebuild it through a plaintext StorageManager — leaving it untouched and
|
|
1508
|
+
// still encrypted — rather than downgrade it to plaintext.
|
|
1509
|
+
const key = Buffer.alloc(32, 5);
|
|
1510
|
+
const token = namespaceIdentityToken("project-secure");
|
|
1511
|
+
const nsDir = path.join(root, "namespaces", token);
|
|
1512
|
+
const nsLedger = path.join(nsDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
1513
|
+
await mkdir(path.dirname(nsLedger), { recursive: true });
|
|
1514
|
+
const junk = '{"legacy":true,"pad":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"}\n';
|
|
1515
|
+
const encrypted = encryptFileBody(
|
|
1516
|
+
junk.repeat(Math.ceil(4096 / junk.length)), key, filePathAad(nsLedger, nsDir),
|
|
1517
|
+
);
|
|
1518
|
+
await writeFile(nsLedger, encrypted);
|
|
1519
|
+
assert.ok(isEncryptedFile(await readFile(nsLedger)), "precondition: ledger encrypted");
|
|
1520
|
+
|
|
1521
|
+
const scheduler = buildCompactionScheduler(
|
|
1522
|
+
fixtureConfig({
|
|
1523
|
+
memoryDir: root,
|
|
1524
|
+
namespacesEnabled: true,
|
|
1525
|
+
memoryLifecycleLedgerCompactBytes: 1024,
|
|
1526
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
1527
|
+
}),
|
|
1528
|
+
);
|
|
1529
|
+
try {
|
|
1530
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
1531
|
+
} finally {
|
|
1532
|
+
scheduler.dispose();
|
|
1533
|
+
}
|
|
1534
|
+
// Skipped, not rewritten: the encrypted ledger must be byte-for-byte intact.
|
|
1535
|
+
assert.deepEqual(
|
|
1536
|
+
await readFile(nsLedger),
|
|
1537
|
+
encrypted,
|
|
1538
|
+
"encrypted namespace ledger must be left untouched, never plaintext-rewritten",
|
|
1539
|
+
);
|
|
1540
|
+
} finally {
|
|
1541
|
+
await rm(root, { recursive: true, force: true });
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
|
|
1545
|
+
test("catalog-disabled fallback compacts an encrypted over-cap namespace ledger through the keyed store (#2033 thread PRRT_kwDORJXyws6SEvWo)", async () => {
|
|
1546
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-ns-secure-keyed-"));
|
|
1547
|
+
try {
|
|
1548
|
+
// Catalog disabled but namespaces + secure-store enabled: only the
|
|
1549
|
+
// filesystem fallback finds this per-namespace ledger. With a namespace
|
|
1550
|
+
// storage resolver wired (the production orchestrator wiring), the fallback
|
|
1551
|
+
// must resolve the namespace's root-keyed secure StorageManager so the
|
|
1552
|
+
// encrypted, over-cap ledger COMPACTS — instead of deferring forever behind
|
|
1553
|
+
// a keyless plaintext target (the pre-fix behavior asserted by the sibling
|
|
1554
|
+
// "never downgrades" test, which wires no resolver).
|
|
1555
|
+
const key = Buffer.alloc(32, 5);
|
|
1556
|
+
const nsName = "project-secure-keyed";
|
|
1557
|
+
const token = namespaceIdentityToken(nsName);
|
|
1558
|
+
const nsDir = path.join(root, "namespaces", token);
|
|
1559
|
+
// A plaintext frontmatter memory the rebuild reconstructs into created +
|
|
1560
|
+
// updated events (read fine under a keyed store — the reader probes per
|
|
1561
|
+
// file), so the compacted ledger has real, bounded content.
|
|
1562
|
+
await mkdir(path.join(nsDir, "facts", "2026-03-08"), { recursive: true });
|
|
1563
|
+
await writeFile(
|
|
1564
|
+
path.join(nsDir, "facts", "2026-03-08", "fact-ns.md"),
|
|
1565
|
+
`---\nid: fact-ns\ncategory: fact\ncreated: 2026-03-08T00:00:00.000Z\n`
|
|
1566
|
+
+ `updated: 2026-03-08T01:00:00.000Z\nsource: test\nconfidence: 0.8\n`
|
|
1567
|
+
+ `confidenceTier: implied\ntags: ["secure"]\n---\n\nsecure\n`,
|
|
1568
|
+
"utf-8",
|
|
1569
|
+
);
|
|
1570
|
+
const nsLedger = path.join(nsDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
1571
|
+
await mkdir(path.dirname(nsLedger), { recursive: true });
|
|
1572
|
+
const junk = '{"legacy":true,"pad":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"}\n';
|
|
1573
|
+
const encrypted = encryptFileBody(
|
|
1574
|
+
junk.repeat(Math.ceil(4096 / junk.length)), key, filePathAad(nsLedger, nsDir),
|
|
1575
|
+
);
|
|
1576
|
+
await writeFile(nsLedger, encrypted);
|
|
1577
|
+
assert.ok(isEncryptedFile(await readFile(nsLedger)), "precondition: ledger encrypted");
|
|
1578
|
+
const beforeSize = (await stat(nsLedger)).size;
|
|
1579
|
+
|
|
1580
|
+
// The resolver is consulted with the DECODED namespace (the on-disk token is
|
|
1581
|
+
// decoded back to its canonical namespace) and returns the namespace's
|
|
1582
|
+
// root-keyed, unlocked secure storage rooted AT the fallback dir — exactly
|
|
1583
|
+
// what the live router's storageFor(namespace) yields.
|
|
1584
|
+
const nsStorage = new StorageManager(nsDir);
|
|
1585
|
+
nsStorage.setSecureStoreRequired(true);
|
|
1586
|
+
nsStorage.setSecureStoreKey(key); // unlocked + encrypt-on-write ⇒ rewrite stays encrypted.
|
|
1587
|
+
let resolverCalls = 0;
|
|
1588
|
+
const scheduler = buildCompactionSchedulerWithStorage(
|
|
1589
|
+
fixtureConfig({
|
|
1590
|
+
memoryDir: root,
|
|
1591
|
+
namespacesEnabled: true,
|
|
1592
|
+
secureStoreEnabled: true,
|
|
1593
|
+
secureStoreEncryptOnWrite: true,
|
|
1594
|
+
memoryLifecycleLedgerCompactBytes: 1024,
|
|
1595
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
1596
|
+
}),
|
|
1597
|
+
() => new StorageManager(root), // root has no ledger ⇒ skipped.
|
|
1598
|
+
async (namespace) => {
|
|
1599
|
+
resolverCalls += 1;
|
|
1600
|
+
assert.equal(namespace, nsName, "fallback resolves by the DECODED namespace, not the raw token");
|
|
1601
|
+
return nsStorage;
|
|
1602
|
+
},
|
|
1603
|
+
undefined, // catalog disabled.
|
|
1604
|
+
);
|
|
1605
|
+
// Named seam cast (reason: assert the resolved target and its compaction
|
|
1606
|
+
// outcome directly), mirroring the extended casts used by the singleflight
|
|
1607
|
+
// and request-wiring tests above.
|
|
1608
|
+
const seam = scheduler as unknown as CompactableScheduler & {
|
|
1609
|
+
resolveLifecycleCompactionTargets: () => Promise<
|
|
1610
|
+
Array<{ memoryDir: string; storage?: StorageManager }>
|
|
1611
|
+
>;
|
|
1612
|
+
compactLifecycleLedgerTarget: (
|
|
1613
|
+
target: unknown,
|
|
1614
|
+
threshold: number,
|
|
1615
|
+
) => Promise<"skipped" | "compacted" | "failed" | "deferred">;
|
|
1616
|
+
};
|
|
1617
|
+
try {
|
|
1618
|
+
const targets = await seam.resolveLifecycleCompactionTargets();
|
|
1619
|
+
assert.ok(resolverCalls > 0, "the fallback consulted the namespace storage resolver");
|
|
1620
|
+
const nsTarget = targets.find(
|
|
1621
|
+
(t) => path.resolve(t.memoryDir) === path.resolve(nsDir),
|
|
1622
|
+
);
|
|
1623
|
+
assert.ok(nsTarget, "fallback discovered the per-namespace ledger dir");
|
|
1624
|
+
assert.ok(nsTarget?.storage, "fallback target carries a keyed StorageManager, not a keyless dir");
|
|
1625
|
+
assert.ok(
|
|
1626
|
+
nsTarget?.storage?.isSecureStoreUnlocked(),
|
|
1627
|
+
"fallback target storage is unlocked so the encrypted ledger can be rewritten",
|
|
1628
|
+
);
|
|
1629
|
+
|
|
1630
|
+
// The core proof: the encrypted over-cap ledger COMPACTS, it does not defer.
|
|
1631
|
+
const outcome = await seam.compactLifecycleLedgerTarget(nsTarget, 1024);
|
|
1632
|
+
assert.equal(
|
|
1633
|
+
outcome,
|
|
1634
|
+
"compacted",
|
|
1635
|
+
"encrypted fallback ledger compacts through the keyed store, never deferred forever",
|
|
1636
|
+
);
|
|
1637
|
+
} finally {
|
|
1638
|
+
scheduler.dispose();
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// On disk: rewritten, still encrypted at rest (no plaintext downgrade), and
|
|
1642
|
+
// decrypts back to the reconstructed events via the same key.
|
|
1643
|
+
assert.notDeepEqual(
|
|
1644
|
+
await readFile(nsLedger),
|
|
1645
|
+
encrypted,
|
|
1646
|
+
"ledger was rewritten (not left byte-for-byte intact, i.e. not deferred)",
|
|
1647
|
+
);
|
|
1648
|
+
assert.ok((await stat(nsLedger)).size < beforeSize, "compacted ledger is smaller");
|
|
1649
|
+
assert.ok(
|
|
1650
|
+
isEncryptedFile(await readFile(nsLedger)),
|
|
1651
|
+
"compacted ledger stays encrypted at rest (namespace isolation + at-rest format preserved)",
|
|
1652
|
+
);
|
|
1653
|
+
const rebuilt = await nsStorage.readAllMemoryLifecycleEvents();
|
|
1654
|
+
assert.deepEqual(rebuilt.map((e) => e.eventType), ["created", "updated"]);
|
|
1655
|
+
} finally {
|
|
1656
|
+
await rm(root, { recursive: true, force: true });
|
|
1657
|
+
}
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
test("catalog-disabled fallback decodes a long tokenized namespace dir so an encrypted over-cap ledger compacts instead of deferring forever (#2033 thread PRRT_kwDORJXyws6SE5fv)", async () => {
|
|
1661
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-ns-longtoken-"));
|
|
1662
|
+
try {
|
|
1663
|
+
// A VALID namespace (passes isSafeRouteNamespace) that is long enough that
|
|
1664
|
+
// its identity token — the on-disk dir name — exceeds the router's 64-char
|
|
1665
|
+
// route-namespace limit. Passing that raw token back to the resolver (the
|
|
1666
|
+
// pre-fix behavior) makes the router reject it, dropping the fallback to a
|
|
1667
|
+
// keyless target that defers the encrypted over-cap ledger forever. The fix
|
|
1668
|
+
// decodes the token to the canonical namespace before resolving.
|
|
1669
|
+
const nsName = "project-origin-with-a-verylong-namespace";
|
|
1670
|
+
const token = namespaceIdentityToken(nsName);
|
|
1671
|
+
assert.ok(nsName.length >= 31, "precondition: namespace long enough to overflow the routed token");
|
|
1672
|
+
assert.ok(isSafeRouteNamespace(nsName), "precondition: the namespace itself is a valid route namespace");
|
|
1673
|
+
assert.ok(token.length > 64, "precondition: the tokenized dir name exceeds the 64-char route limit");
|
|
1674
|
+
assert.ok(!isSafeRouteNamespace(token), "precondition: the raw token would be REJECTED as a route namespace");
|
|
1675
|
+
assert.equal(namespaceIdentityFromToken(token), nsName, "precondition: the token decodes back to the namespace");
|
|
1676
|
+
|
|
1677
|
+
const key = Buffer.alloc(32, 7);
|
|
1678
|
+
const nsDir = path.join(root, "namespaces", token);
|
|
1679
|
+
// Plaintext frontmatter memory the rebuild reconstructs into created+updated.
|
|
1680
|
+
await mkdir(path.join(nsDir, "facts", "2026-03-08"), { recursive: true });
|
|
1681
|
+
await writeFile(
|
|
1682
|
+
path.join(nsDir, "facts", "2026-03-08", "fact-ns.md"),
|
|
1683
|
+
`---\nid: fact-ns\ncategory: fact\ncreated: 2026-03-08T00:00:00.000Z\n`
|
|
1684
|
+
+ `updated: 2026-03-08T01:00:00.000Z\nsource: test\nconfidence: 0.8\n`
|
|
1685
|
+
+ `confidenceTier: implied\ntags: ["long"]\n---\n\nlong\n`,
|
|
1686
|
+
"utf-8",
|
|
1687
|
+
);
|
|
1688
|
+
const nsLedger = path.join(nsDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
1689
|
+
await mkdir(path.dirname(nsLedger), { recursive: true });
|
|
1690
|
+
const junk = '{"legacy":true,"pad":"wwwwwwwwwwwwwwwwwwwwwwwwwwwwww"}\n';
|
|
1691
|
+
const encrypted = encryptFileBody(
|
|
1692
|
+
junk.repeat(Math.ceil(4096 / junk.length)), key, filePathAad(nsLedger, nsDir),
|
|
1693
|
+
);
|
|
1694
|
+
await writeFile(nsLedger, encrypted);
|
|
1695
|
+
assert.ok(isEncryptedFile(await readFile(nsLedger)), "precondition: ledger encrypted");
|
|
1696
|
+
const beforeSize = (await stat(nsLedger)).size;
|
|
1697
|
+
|
|
1698
|
+
const nsStorage = new StorageManager(nsDir);
|
|
1699
|
+
nsStorage.setSecureStoreRequired(true);
|
|
1700
|
+
nsStorage.setSecureStoreKey(key); // unlocked + encrypt-on-write ⇒ rewrite stays encrypted.
|
|
1701
|
+
let resolvedWith: string | null = null;
|
|
1702
|
+
const scheduler = buildCompactionSchedulerWithStorage(
|
|
1703
|
+
fixtureConfig({
|
|
1704
|
+
memoryDir: root,
|
|
1705
|
+
namespacesEnabled: true,
|
|
1706
|
+
secureStoreEnabled: true,
|
|
1707
|
+
secureStoreEncryptOnWrite: true,
|
|
1708
|
+
memoryLifecycleLedgerCompactBytes: 1024,
|
|
1709
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
1710
|
+
}),
|
|
1711
|
+
() => new StorageManager(root), // root has no ledger ⇒ skipped.
|
|
1712
|
+
// Mirror the production router: reject anything that is not a safe route
|
|
1713
|
+
// namespace, exactly how getWritableStorageForNamespace throws. With the
|
|
1714
|
+
// pre-fix behavior the fallback passed the over-64 token and this resolver
|
|
1715
|
+
// would throw, stranding the ledger on a keyless deferred target.
|
|
1716
|
+
async (namespace) => {
|
|
1717
|
+
resolvedWith = namespace;
|
|
1718
|
+
if (!isSafeRouteNamespace(namespace)) {
|
|
1719
|
+
throw new Error(`invalid namespace: ${namespace}`);
|
|
1720
|
+
}
|
|
1721
|
+
return nsStorage;
|
|
1722
|
+
},
|
|
1723
|
+
undefined, // catalog disabled.
|
|
1724
|
+
);
|
|
1725
|
+
const seam = scheduler as unknown as CompactableScheduler & {
|
|
1726
|
+
resolveLifecycleCompactionTargets: () => Promise<
|
|
1727
|
+
Array<{ memoryDir: string; storage?: StorageManager }>
|
|
1728
|
+
>;
|
|
1729
|
+
compactLifecycleLedgerTarget: (
|
|
1730
|
+
target: unknown,
|
|
1731
|
+
threshold: number,
|
|
1732
|
+
) => Promise<"skipped" | "compacted" | "failed" | "deferred">;
|
|
1733
|
+
};
|
|
1734
|
+
try {
|
|
1735
|
+
const targets = await seam.resolveLifecycleCompactionTargets();
|
|
1736
|
+
assert.equal(resolvedWith, nsName, "fallback resolves by the DECODED namespace, never the over-64 token");
|
|
1737
|
+
const nsTarget = targets.find(
|
|
1738
|
+
(t) => path.resolve(t.memoryDir) === path.resolve(nsDir),
|
|
1739
|
+
);
|
|
1740
|
+
assert.ok(nsTarget, "fallback discovered the per-namespace ledger dir");
|
|
1741
|
+
assert.ok(
|
|
1742
|
+
nsTarget?.storage,
|
|
1743
|
+
"long-namespace fallback target carries a keyed StorageManager, not a keyless dir",
|
|
1744
|
+
);
|
|
1745
|
+
assert.ok(
|
|
1746
|
+
nsTarget?.storage?.isSecureStoreUnlocked(),
|
|
1747
|
+
"keyed target is unlocked so the encrypted ledger can be rewritten",
|
|
1748
|
+
);
|
|
1749
|
+
|
|
1750
|
+
// The core proof: the encrypted over-cap ledger COMPACTS, it does not defer.
|
|
1751
|
+
const outcome = await seam.compactLifecycleLedgerTarget(nsTarget, 1024);
|
|
1752
|
+
assert.equal(
|
|
1753
|
+
outcome,
|
|
1754
|
+
"compacted",
|
|
1755
|
+
"encrypted over-cap ledger for a long namespace compacts through the keyed store, never deferred forever",
|
|
1756
|
+
);
|
|
1757
|
+
} finally {
|
|
1758
|
+
scheduler.dispose();
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
assert.notDeepEqual(
|
|
1762
|
+
await readFile(nsLedger),
|
|
1763
|
+
encrypted,
|
|
1764
|
+
"ledger was rewritten (not left byte-for-byte intact, i.e. not deferred)",
|
|
1765
|
+
);
|
|
1766
|
+
assert.ok((await stat(nsLedger)).size < beforeSize, "compacted ledger is smaller");
|
|
1767
|
+
assert.ok(
|
|
1768
|
+
isEncryptedFile(await readFile(nsLedger)),
|
|
1769
|
+
"compacted ledger stays encrypted at rest (namespace isolation + at-rest format preserved)",
|
|
1770
|
+
);
|
|
1771
|
+
const rebuilt = await nsStorage.readAllMemoryLifecycleEvents();
|
|
1772
|
+
assert.deepEqual(rebuilt.map((e) => e.eventType), ["created", "updated"]);
|
|
1773
|
+
} finally {
|
|
1774
|
+
await rm(root, { recursive: true, force: true });
|
|
1775
|
+
}
|
|
1776
|
+
});
|
|
1777
|
+
|
|
1778
|
+
test("catalog-disabled fallback resolves a LEGACY RAW namespace dir (not a ns- token) through the keyed store so its encrypted over-cap ledger compacts (#2033)", async () => {
|
|
1779
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-ns-legacyraw-"));
|
|
1780
|
+
try {
|
|
1781
|
+
// A legacy deployment wrote per-namespace data under the RAW namespace name
|
|
1782
|
+
// (namespaces/<name>/), before identity tokenization existed. Such a dir name
|
|
1783
|
+
// is NOT a ns-<hex> token, so namespaceIdentityFromToken returns null; the
|
|
1784
|
+
// pre-fix fallback then only offered ns- token dirs to the keyed resolver and
|
|
1785
|
+
// left this legacy dir on a keyless target, deferring its encrypted over-cap
|
|
1786
|
+
// ledger forever. The fix treats a non-token dir as a raw namespace when it
|
|
1787
|
+
// is a safe route namespace and the keyed store roots back at THIS dir.
|
|
1788
|
+
const nsName = "legacy-raw-ns";
|
|
1789
|
+
assert.equal(namespaceIdentityFromToken(nsName), null, "precondition: dir name is NOT a ns- identity token");
|
|
1790
|
+
assert.ok(isSafeRouteNamespace(nsName), "precondition: dir name is a safe route namespace");
|
|
1791
|
+
|
|
1792
|
+
const key = Buffer.alloc(32, 5);
|
|
1793
|
+
const nsDir = path.join(root, "namespaces", nsName);
|
|
1794
|
+
await mkdir(path.join(nsDir, "facts", "2026-03-08"), { recursive: true });
|
|
1795
|
+
await writeFile(
|
|
1796
|
+
path.join(nsDir, "facts", "2026-03-08", "fact-legacy.md"),
|
|
1797
|
+
`---\nid: fact-legacy\ncategory: fact\ncreated: 2026-03-08T00:00:00.000Z\n`
|
|
1798
|
+
+ `updated: 2026-03-08T01:00:00.000Z\nsource: test\nconfidence: 0.8\n`
|
|
1799
|
+
+ `confidenceTier: implied\ntags: ["legacy"]\n---\n\nlegacy\n`,
|
|
1800
|
+
"utf-8",
|
|
1801
|
+
);
|
|
1802
|
+
const nsLedger = path.join(nsDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
1803
|
+
await mkdir(path.dirname(nsLedger), { recursive: true });
|
|
1804
|
+
const junk = '{"legacy":true,"pad":"wwwwwwwwwwwwwwwwwwwwwwwwwwwwww"}\n';
|
|
1805
|
+
const encrypted = encryptFileBody(
|
|
1806
|
+
junk.repeat(Math.ceil(4096 / junk.length)), key, filePathAad(nsLedger, nsDir),
|
|
1807
|
+
);
|
|
1808
|
+
await writeFile(nsLedger, encrypted);
|
|
1809
|
+
assert.ok(isEncryptedFile(await readFile(nsLedger)), "precondition: ledger encrypted");
|
|
1810
|
+
const beforeSize = (await stat(nsLedger)).size;
|
|
1811
|
+
|
|
1812
|
+
const nsStorage = new StorageManager(nsDir);
|
|
1813
|
+
nsStorage.setSecureStoreRequired(true);
|
|
1814
|
+
nsStorage.setSecureStoreKey(key); // unlocked + encrypt-on-write ⇒ rewrite stays encrypted.
|
|
1815
|
+
let resolvedWith: string | null = null;
|
|
1816
|
+
const scheduler = buildCompactionSchedulerWithStorage(
|
|
1817
|
+
fixtureConfig({
|
|
1818
|
+
memoryDir: root,
|
|
1819
|
+
namespacesEnabled: true,
|
|
1820
|
+
secureStoreEnabled: true,
|
|
1821
|
+
secureStoreEncryptOnWrite: true,
|
|
1822
|
+
memoryLifecycleLedgerCompactBytes: 1024,
|
|
1823
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
1824
|
+
}),
|
|
1825
|
+
() => new StorageManager(root), // root has no ledger ⇒ skipped.
|
|
1826
|
+
// The production router routes an existing legacy raw-name dir back to its
|
|
1827
|
+
// namespaces/<name>/ root; mirror that by returning the keyed nsStorage for
|
|
1828
|
+
// the raw name and rejecting anything unsafe.
|
|
1829
|
+
async (namespace) => {
|
|
1830
|
+
resolvedWith = namespace;
|
|
1831
|
+
if (!isSafeRouteNamespace(namespace)) {
|
|
1832
|
+
throw new Error(`invalid namespace: ${namespace}`);
|
|
1833
|
+
}
|
|
1834
|
+
return nsStorage;
|
|
1835
|
+
},
|
|
1836
|
+
undefined, // catalog disabled.
|
|
1837
|
+
);
|
|
1838
|
+
const seam = scheduler as unknown as CompactableScheduler & {
|
|
1839
|
+
resolveLifecycleCompactionTargets: () => Promise<
|
|
1840
|
+
Array<{ memoryDir: string; storage?: StorageManager }>
|
|
1841
|
+
>;
|
|
1842
|
+
compactLifecycleLedgerTarget: (
|
|
1843
|
+
target: unknown,
|
|
1844
|
+
threshold: number,
|
|
1845
|
+
) => Promise<"skipped" | "compacted" | "failed" | "deferred">;
|
|
1846
|
+
};
|
|
1847
|
+
try {
|
|
1848
|
+
const targets = await seam.resolveLifecycleCompactionTargets();
|
|
1849
|
+
assert.equal(resolvedWith, nsName, "fallback resolves by the RAW legacy namespace name");
|
|
1850
|
+
const nsTarget = targets.find(
|
|
1851
|
+
(t) => path.resolve(t.memoryDir) === path.resolve(nsDir),
|
|
1852
|
+
);
|
|
1853
|
+
assert.ok(nsTarget, "fallback discovered the legacy raw namespace ledger dir");
|
|
1854
|
+
assert.ok(
|
|
1855
|
+
nsTarget?.storage,
|
|
1856
|
+
"legacy raw fallback target carries a keyed StorageManager, not a keyless dir",
|
|
1857
|
+
);
|
|
1858
|
+
assert.ok(
|
|
1859
|
+
nsTarget?.storage?.isSecureStoreUnlocked(),
|
|
1860
|
+
"keyed target is unlocked so the encrypted ledger can be rewritten",
|
|
1861
|
+
);
|
|
1862
|
+
|
|
1863
|
+
const outcome = await seam.compactLifecycleLedgerTarget(nsTarget, 1024);
|
|
1864
|
+
assert.equal(
|
|
1865
|
+
outcome,
|
|
1866
|
+
"compacted",
|
|
1867
|
+
"encrypted over-cap ledger under a legacy raw namespace dir compacts through the keyed store, never deferred forever",
|
|
1868
|
+
);
|
|
1869
|
+
} finally {
|
|
1870
|
+
scheduler.dispose();
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
assert.notDeepEqual(await readFile(nsLedger), encrypted, "ledger was rewritten (not deferred)");
|
|
1874
|
+
assert.ok((await stat(nsLedger)).size < beforeSize, "compacted ledger is smaller");
|
|
1875
|
+
assert.ok(
|
|
1876
|
+
isEncryptedFile(await readFile(nsLedger)),
|
|
1877
|
+
"compacted ledger stays encrypted at rest (namespace isolation + at-rest format preserved)",
|
|
1878
|
+
);
|
|
1879
|
+
const rebuilt = await nsStorage.readAllMemoryLifecycleEvents();
|
|
1880
|
+
assert.deepEqual(rebuilt.map((e) => e.eventType), ["created", "updated"]);
|
|
1881
|
+
} finally {
|
|
1882
|
+
await rm(root, { recursive: true, force: true });
|
|
1883
|
+
}
|
|
1884
|
+
});
|
|
1885
|
+
|
|
1886
|
+
test("catalog-disabled fallback routes a LEGACY RAW namespace dir through the PRODUCTION resolver (resolveNamespaceStorageRoot) so its encrypted over-cap ledger compacts, not defers (#2033)", async () => {
|
|
1887
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-ns-legacyraw-prod-"));
|
|
1888
|
+
try {
|
|
1889
|
+
// Higher-fidelity companion to the hand-rolled-mock legacy-raw test above.
|
|
1890
|
+
// The resolver here is the LIVE production routing helper
|
|
1891
|
+
// `resolveNamespaceStorageRoot` (exactly the root NamespaceStorageRouter.
|
|
1892
|
+
// storageFor resolves through), not a stub returning a fixed StorageManager.
|
|
1893
|
+
// A legacy deployment wrote per-namespace data under the RAW namespace name
|
|
1894
|
+
// (namespaces/<name>/) before tokenization, with NO catalog. The fallback
|
|
1895
|
+
// must (1) offer the raw dir name to the keyed resolver AND (2) the production
|
|
1896
|
+
// resolver must route that raw name BACK to its legacy namespaces/<name>/ root
|
|
1897
|
+
// (returning the legacy dir when the tokenized dir has no marker), so the keyed
|
|
1898
|
+
// guard `storage.dir === childPath` holds and the encrypted over-cap ledger
|
|
1899
|
+
// compacts through the keyed store. Were resolveNamespaceStorageRoot ever to
|
|
1900
|
+
// route a legacy raw name to the tokenized dir, the guard would fail and the
|
|
1901
|
+
// ledger would defer forever — a regression the mock-based test cannot catch.
|
|
1902
|
+
const nsName = "legacy-raw-prod-ns";
|
|
1903
|
+
assert.equal(namespaceIdentityFromToken(nsName), null, "precondition: dir name is NOT a ns- identity token");
|
|
1904
|
+
assert.ok(isSafeRouteNamespace(nsName), "precondition: dir name is a safe route namespace");
|
|
1905
|
+
|
|
1906
|
+
const config = fixtureConfig({
|
|
1907
|
+
memoryDir: root,
|
|
1908
|
+
defaultNamespace: "primary",
|
|
1909
|
+
namespacesEnabled: true,
|
|
1910
|
+
secureStoreEnabled: true,
|
|
1911
|
+
secureStoreEncryptOnWrite: true,
|
|
1912
|
+
memoryLifecycleLedgerCompactBytes: 1024,
|
|
1913
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
1914
|
+
});
|
|
1915
|
+
|
|
1916
|
+
const key = Buffer.alloc(32, 7);
|
|
1917
|
+
const nsDir = path.join(root, "namespaces", nsName);
|
|
1918
|
+
await mkdir(path.join(nsDir, "facts", "2026-03-08"), { recursive: true });
|
|
1919
|
+
await writeFile(
|
|
1920
|
+
path.join(nsDir, "facts", "2026-03-08", "fact-legacy.md"),
|
|
1921
|
+
`---\nid: fact-legacy\ncategory: fact\ncreated: 2026-03-08T00:00:00.000Z\n`
|
|
1922
|
+
+ `updated: 2026-03-08T01:00:00.000Z\nsource: test\nconfidence: 0.8\n`
|
|
1923
|
+
+ `confidenceTier: implied\ntags: ["legacy"]\n---\n\nlegacy\n`,
|
|
1924
|
+
"utf-8",
|
|
1925
|
+
);
|
|
1926
|
+
const nsLedger = path.join(nsDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
1927
|
+
await mkdir(path.dirname(nsLedger), { recursive: true });
|
|
1928
|
+
const junk = '{"legacy":true,"pad":"vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv"}\n';
|
|
1929
|
+
const encrypted = encryptFileBody(
|
|
1930
|
+
junk.repeat(Math.ceil(4096 / junk.length)), key, filePathAad(nsLedger, nsDir),
|
|
1931
|
+
);
|
|
1932
|
+
await writeFile(nsLedger, encrypted);
|
|
1933
|
+
assert.ok(isEncryptedFile(await readFile(nsLedger)), "precondition: ledger encrypted");
|
|
1934
|
+
const beforeSize = (await stat(nsLedger)).size;
|
|
1935
|
+
|
|
1936
|
+
// Precondition on the PRODUCTION resolver: the live routing helper resolves
|
|
1937
|
+
// the legacy raw name back to the legacy namespaces/<name>/ dir (no tokenized
|
|
1938
|
+
// dir with markers exists), so the keyed target roots at THIS childPath.
|
|
1939
|
+
assert.equal(
|
|
1940
|
+
path.resolve(await resolveNamespaceStorageRoot(config, nsName)),
|
|
1941
|
+
path.resolve(nsDir),
|
|
1942
|
+
"production resolver routes the legacy raw namespace name to its legacy dir",
|
|
1943
|
+
);
|
|
1944
|
+
|
|
1945
|
+
let resolvedWith: string | null = null;
|
|
1946
|
+
const scheduler = buildCompactionSchedulerWithStorage(
|
|
1947
|
+
config,
|
|
1948
|
+
() => new StorageManager(root), // root has no ledger, so it is skipped.
|
|
1949
|
+
// Mirror NamespaceStorageRouter.storageFor: resolve the root through the
|
|
1950
|
+
// LIVE resolver, then build the root-keyed secure StorageManager the router
|
|
1951
|
+
// would hand back (applySecureStoreConfig sets the required flag + key).
|
|
1952
|
+
async (namespace) => {
|
|
1953
|
+
resolvedWith = namespace;
|
|
1954
|
+
const resolvedRoot = await resolveNamespaceStorageRoot(config, namespace);
|
|
1955
|
+
const sm = new StorageManager(resolvedRoot);
|
|
1956
|
+
sm.setSecureStoreRequired(true);
|
|
1957
|
+
sm.setSecureStoreKey(key);
|
|
1958
|
+
return sm;
|
|
1959
|
+
},
|
|
1960
|
+
undefined, // catalog disabled.
|
|
1961
|
+
);
|
|
1962
|
+
const seam = scheduler as unknown as CompactableScheduler & {
|
|
1963
|
+
resolveLifecycleCompactionTargets: () => Promise<
|
|
1964
|
+
Array<{ memoryDir: string; storage?: StorageManager }>
|
|
1965
|
+
>;
|
|
1966
|
+
compactLifecycleLedgerTarget: (
|
|
1967
|
+
target: unknown,
|
|
1968
|
+
threshold: number,
|
|
1969
|
+
) => Promise<"skipped" | "compacted" | "failed" | "deferred">;
|
|
1970
|
+
};
|
|
1971
|
+
try {
|
|
1972
|
+
const targets = await seam.resolveLifecycleCompactionTargets();
|
|
1973
|
+
assert.equal(resolvedWith, nsName, "fallback resolves by the RAW legacy namespace name");
|
|
1974
|
+
const nsTarget = targets.find(
|
|
1975
|
+
(t) => path.resolve(t.memoryDir) === path.resolve(nsDir),
|
|
1976
|
+
);
|
|
1977
|
+
assert.ok(nsTarget, "fallback discovered the legacy raw namespace ledger dir");
|
|
1978
|
+
assert.ok(
|
|
1979
|
+
nsTarget?.storage,
|
|
1980
|
+
"legacy raw fallback target carries a keyed StorageManager (production resolver), not a keyless dir",
|
|
1981
|
+
);
|
|
1982
|
+
assert.equal(
|
|
1983
|
+
path.resolve(nsTarget!.storage!.dir),
|
|
1984
|
+
path.resolve(nsDir),
|
|
1985
|
+
"the keyed target roots at the legacy raw dir, so the keyed guard held",
|
|
1986
|
+
);
|
|
1987
|
+
assert.ok(
|
|
1988
|
+
nsTarget?.storage?.isSecureStoreUnlocked(),
|
|
1989
|
+
"keyed target is unlocked so the encrypted ledger can be rewritten",
|
|
1990
|
+
);
|
|
1991
|
+
|
|
1992
|
+
const outcome = await seam.compactLifecycleLedgerTarget(nsTarget, 1024);
|
|
1993
|
+
assert.equal(
|
|
1994
|
+
outcome,
|
|
1995
|
+
"compacted",
|
|
1996
|
+
"encrypted over-cap ledger under a legacy raw namespace dir compacts through the production-resolved keyed store, never deferred forever",
|
|
1997
|
+
);
|
|
1998
|
+
} finally {
|
|
1999
|
+
scheduler.dispose();
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
assert.notDeepEqual(await readFile(nsLedger), encrypted, "ledger was rewritten (not deferred)");
|
|
2003
|
+
assert.ok((await stat(nsLedger)).size < beforeSize, "compacted ledger is smaller");
|
|
2004
|
+
assert.ok(
|
|
2005
|
+
isEncryptedFile(await readFile(nsLedger)),
|
|
2006
|
+
"compacted ledger stays encrypted at rest (namespace isolation + at-rest format preserved)",
|
|
2007
|
+
);
|
|
2008
|
+
const verifyStorage = new StorageManager(nsDir);
|
|
2009
|
+
verifyStorage.setSecureStoreRequired(true);
|
|
2010
|
+
verifyStorage.setSecureStoreKey(key);
|
|
2011
|
+
const rebuilt = await verifyStorage.readAllMemoryLifecycleEvents();
|
|
2012
|
+
assert.deepEqual(rebuilt.map((e) => e.eventType), ["created", "updated"]);
|
|
2013
|
+
} finally {
|
|
2014
|
+
await rm(root, { recursive: true, force: true });
|
|
2015
|
+
}
|
|
2016
|
+
});
|
|
2017
|
+
|
|
2018
|
+
test("preserving rebuild skips the backup+rewrite for an already-compact ledger, ending the periodic re-archive churn (#2033 thread PRRT_kwDORJXyws6SExst)", async () => {
|
|
2019
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-noop-"));
|
|
2020
|
+
try {
|
|
2021
|
+
const ledgerPath = path.join(memoryDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
2022
|
+
await mkdir(path.dirname(ledgerPath), { recursive: true });
|
|
2023
|
+
// Valid append-only history (no frontmatter equivalent) that stays well below
|
|
2024
|
+
// the read/decrypt cap, plus junk rows the first rebuild discards. The first
|
|
2025
|
+
// rebuild therefore genuinely rewrites (drops the junk) and archives a backup;
|
|
2026
|
+
// every later rebuild would reproduce the now-canonical ledger byte-for-byte.
|
|
2027
|
+
// Pre-fix that reproduced identical content and archived a NEW backup every
|
|
2028
|
+
// interval, growing archive/memory-lifecycle-ledger without reducing the
|
|
2029
|
+
// active file. The no-op skip must leave both the ledger and the archive
|
|
2030
|
+
// untouched on the second pass.
|
|
2031
|
+
const events = Array.from({ length: 30 }, (_unused, i) => ({
|
|
2032
|
+
eventId: `noop-${String(i).padStart(3, "0")}`,
|
|
2033
|
+
memoryId: "mem-a",
|
|
2034
|
+
eventType: "explicit_capture_accepted",
|
|
2035
|
+
timestamp: new Date(Date.UTC(2026, 2, 8, 0, 0, 0, 0) + i * 1000).toISOString(),
|
|
2036
|
+
actor: "explicit-capture",
|
|
2037
|
+
ruleVersion: "memory-lifecycle-ledger.v1",
|
|
2038
|
+
}));
|
|
2039
|
+
const junk = '{"legacy":true,"pad":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}\n';
|
|
2040
|
+
await writeFile(
|
|
2041
|
+
ledgerPath,
|
|
2042
|
+
junk.repeat(4) + `${events.map((e) => JSON.stringify(e)).join("\n")}\n`,
|
|
2043
|
+
"utf-8",
|
|
2044
|
+
);
|
|
2045
|
+
|
|
2046
|
+
const storage = new StorageManager(memoryDir);
|
|
2047
|
+
const bigCap = 10 * 1024 * 1024; // far above the tiny ledger: nothing is bounded away.
|
|
2048
|
+
const archiveRoot = path.join(memoryDir, "archive", "memory-lifecycle-ledger");
|
|
2049
|
+
|
|
2050
|
+
// First rebuild: discards the junk, canonicalizes, and archives one backup.
|
|
2051
|
+
const first = await rebuildMemoryLifecycleLedger({
|
|
2052
|
+
memoryDir,
|
|
2053
|
+
dryRun: false,
|
|
2054
|
+
storage,
|
|
2055
|
+
preserveExistingEvents: true,
|
|
2056
|
+
maxLedgerBytes: bigCap,
|
|
2057
|
+
now: new Date("2026-03-08T00:00:00.000Z"),
|
|
2058
|
+
});
|
|
2059
|
+
assert.equal(first.rewritten, true, "first rebuild rewrites (junk discarded)");
|
|
2060
|
+
assert.ok(first.backupPath, "first rebuild archives a backup");
|
|
2061
|
+
const afterFirst = await readFile(ledgerPath);
|
|
2062
|
+
assert.equal(
|
|
2063
|
+
(await readdir(archiveRoot)).length,
|
|
2064
|
+
1,
|
|
2065
|
+
"exactly one backup after the first (effective) rebuild",
|
|
2066
|
+
);
|
|
2067
|
+
|
|
2068
|
+
// Second rebuild at a DISTINCT timestamp: a real re-archive would create a
|
|
2069
|
+
// second stamped backup dir. The no-op skip must create none and leave the
|
|
2070
|
+
// ledger byte-for-byte.
|
|
2071
|
+
const second = await rebuildMemoryLifecycleLedger({
|
|
2072
|
+
memoryDir,
|
|
2073
|
+
dryRun: false,
|
|
2074
|
+
storage,
|
|
2075
|
+
preserveExistingEvents: true,
|
|
2076
|
+
maxLedgerBytes: bigCap,
|
|
2077
|
+
now: new Date("2026-03-08T01:00:00.000Z"),
|
|
2078
|
+
});
|
|
2079
|
+
assert.equal(second.rewritten, false, "already-compact ledger: rebuild is a no-op");
|
|
2080
|
+
assert.equal(second.backupPath, undefined, "no-op rebuild archives no backup");
|
|
2081
|
+
assert.deepEqual(await readFile(ledgerPath), afterFirst, "no-op leaves the ledger byte-for-byte");
|
|
2082
|
+
assert.equal(
|
|
2083
|
+
(await readdir(archiveRoot)).length,
|
|
2084
|
+
1,
|
|
2085
|
+
"no second backup archived across intervals (churn eliminated)",
|
|
2086
|
+
);
|
|
2087
|
+
|
|
2088
|
+
// A third pass at yet another timestamp stays a no-op: the archive never grows.
|
|
2089
|
+
const third = await rebuildMemoryLifecycleLedger({
|
|
2090
|
+
memoryDir,
|
|
2091
|
+
dryRun: false,
|
|
2092
|
+
storage,
|
|
2093
|
+
preserveExistingEvents: true,
|
|
2094
|
+
maxLedgerBytes: bigCap,
|
|
2095
|
+
now: new Date("2026-03-08T02:00:00.000Z"),
|
|
2096
|
+
});
|
|
2097
|
+
assert.equal(third.rewritten, false, "still a no-op on the third interval");
|
|
2098
|
+
assert.equal((await readdir(archiveRoot)).length, 1, "archive still holds exactly one backup");
|
|
2099
|
+
} finally {
|
|
2100
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
2101
|
+
}
|
|
2102
|
+
});
|
|
2103
|
+
|
|
2104
|
+
test("catalog-disabled fallback refuses a symlinked namespaces scan root (#2033)", async () => {
|
|
2105
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-ns-symlink-"));
|
|
2106
|
+
const outside = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-ns-outside-"));
|
|
2107
|
+
try {
|
|
2108
|
+
// <root>/namespaces is a symlink pointing OUTSIDE the memory dir. Following
|
|
2109
|
+
// it would let compaction read/rewrite ledgers outside memoryDir, so the
|
|
2110
|
+
// scan root must be rejected and the external ledger left untouched.
|
|
2111
|
+
const nsLedger = path.join(
|
|
2112
|
+
outside, namespaceIdentityToken("evil"), "state", "memory-lifecycle-ledger.jsonl",
|
|
2113
|
+
);
|
|
2114
|
+
await mkdir(path.dirname(nsLedger), { recursive: true });
|
|
2115
|
+
const junk = '{"legacy":true,"pad":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"}\n';
|
|
2116
|
+
const original = junk.repeat(Math.ceil(4096 / junk.length));
|
|
2117
|
+
await writeFile(nsLedger, original, "utf-8");
|
|
2118
|
+
await symlink(outside, path.join(root, "namespaces"));
|
|
2119
|
+
|
|
2120
|
+
const scheduler = buildCompactionScheduler(
|
|
2121
|
+
fixtureConfig({
|
|
2122
|
+
memoryDir: root,
|
|
2123
|
+
namespacesEnabled: true,
|
|
2124
|
+
memoryLifecycleLedgerCompactBytes: 1024,
|
|
2125
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
2126
|
+
}),
|
|
2127
|
+
);
|
|
2128
|
+
try {
|
|
2129
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
2130
|
+
} finally {
|
|
2131
|
+
scheduler.dispose();
|
|
2132
|
+
}
|
|
2133
|
+
assert.equal(
|
|
2134
|
+
await readFile(nsLedger, "utf-8"),
|
|
2135
|
+
original,
|
|
2136
|
+
"ledger behind a symlinked namespaces root must be untouched",
|
|
2137
|
+
);
|
|
2138
|
+
} finally {
|
|
2139
|
+
await rm(root, { recursive: true, force: true });
|
|
2140
|
+
await rm(outside, { recursive: true, force: true });
|
|
2141
|
+
}
|
|
2142
|
+
});
|
|
2143
|
+
|
|
2144
|
+
test("maintenance request triggers lifecycle compaction even when QMD is unavailable (#1910)", async () => {
|
|
2145
|
+
const { memoryDir } = await seedMemoryDirWithOversizedLedger(4096);
|
|
2146
|
+
try {
|
|
2147
|
+
// QMD reports unavailable, so requestQmdMaintenance() short-circuits and
|
|
2148
|
+
// runQmdMaintenance() never fires. The compaction check must still be wired
|
|
2149
|
+
// off the request entrypoint so a QMD-off deployment keeps bounding the
|
|
2150
|
+
// ledger. requestQmdMaintenanceForTool invokes the check synchronously
|
|
2151
|
+
// (fire-and-forget), so a spy on the private method observes the wiring
|
|
2152
|
+
// deterministically without waiting on the async filesystem work.
|
|
2153
|
+
const scheduler = new MaintenanceScheduler({
|
|
2154
|
+
config: fixtureConfig({
|
|
2155
|
+
memoryDir,
|
|
2156
|
+
memoryLifecycleLedgerCompactBytes: 1024,
|
|
2157
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
2158
|
+
}),
|
|
2159
|
+
getQmd: () => ({ isAvailable: () => false }) as unknown as SearchBackend,
|
|
2160
|
+
namespaceSearchRouter: {} as unknown as NamespaceSearchRouter,
|
|
2161
|
+
namespaceCatalog: {} as unknown as NamespaceCatalog,
|
|
2162
|
+
});
|
|
2163
|
+
let compactionInvoked = false;
|
|
2164
|
+
// Named seam cast (reason: reach the private compaction method to observe
|
|
2165
|
+
// the request→compaction wiring), mirroring the CompactableScheduler cast
|
|
2166
|
+
// used by the direct compaction tests above.
|
|
2167
|
+
const seam = scheduler as unknown as CompactableScheduler;
|
|
2168
|
+
seam.maybeCompactMemoryLifecycleLedger = async () => {
|
|
2169
|
+
compactionInvoked = true;
|
|
2170
|
+
};
|
|
2171
|
+
try {
|
|
2172
|
+
scheduler.requestQmdMaintenanceForTool("test");
|
|
2173
|
+
assert.ok(
|
|
2174
|
+
compactionInvoked,
|
|
2175
|
+
"lifecycle compaction must be triggered from the maintenance request when QMD is unavailable",
|
|
2176
|
+
);
|
|
2177
|
+
} finally {
|
|
2178
|
+
scheduler.dispose();
|
|
2179
|
+
}
|
|
2180
|
+
} finally {
|
|
2181
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
2182
|
+
}
|
|
2183
|
+
});
|
|
2184
|
+
|
|
2185
|
+
test("auto-compaction bounds per-namespace ledgers via filesystem fallback when the catalog is disabled (#1910)", async () => {
|
|
2186
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-ns-nocatalog-"));
|
|
2187
|
+
try {
|
|
2188
|
+
const token = namespaceIdentityToken("project-beta");
|
|
2189
|
+
const nsDir = path.join(root, "namespaces", token);
|
|
2190
|
+
await mkdir(path.join(nsDir, "facts", "2026-03-08"), { recursive: true });
|
|
2191
|
+
await writeFile(
|
|
2192
|
+
path.join(nsDir, "facts", "2026-03-08", "fact-ns.md"),
|
|
2193
|
+
`---\nid: fact-ns\ncategory: fact\ncreated: 2026-03-08T00:00:00.000Z\n`
|
|
2194
|
+
+ `updated: 2026-03-08T01:00:00.000Z\nsource: test\nconfidence: 0.8\n`
|
|
2195
|
+
+ `confidenceTier: implied\ntags: ["beta"]\n---\n\nbeta\n`,
|
|
2196
|
+
"utf-8",
|
|
2197
|
+
);
|
|
2198
|
+
const nsLedger = path.join(nsDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
2199
|
+
await mkdir(path.dirname(nsLedger), { recursive: true });
|
|
2200
|
+
const line = '{"legacy":true,"pad":"wwwwwwwwwwwwwwwwwwwwwwwwwwwwww"}\n';
|
|
2201
|
+
await writeFile(nsLedger, line.repeat(Math.ceil(4096 / line.length)), "utf-8");
|
|
2202
|
+
const beforeSize = (await stat(nsLedger)).size;
|
|
2203
|
+
|
|
2204
|
+
// namespacesEnabled=true but NO catalog wired (the namespaceCatalogEnabled=false
|
|
2205
|
+
// deployment). The catalog walk finds nothing; the filesystem fallback must
|
|
2206
|
+
// still discover and bound the per-namespace ledger (codex P2).
|
|
2207
|
+
const scheduler = buildCompactionScheduler(
|
|
2208
|
+
fixtureConfig({
|
|
2209
|
+
memoryDir: root,
|
|
2210
|
+
namespacesEnabled: true,
|
|
2211
|
+
memoryLifecycleLedgerCompactBytes: 1024,
|
|
2212
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
2213
|
+
}),
|
|
2214
|
+
);
|
|
2215
|
+
try {
|
|
2216
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
2217
|
+
} finally {
|
|
2218
|
+
scheduler.dispose();
|
|
2219
|
+
}
|
|
2220
|
+
assert.ok(
|
|
2221
|
+
(await stat(nsLedger)).size < beforeSize,
|
|
2222
|
+
"namespace ledger must be compacted via the filesystem fallback",
|
|
2223
|
+
);
|
|
2224
|
+
const rebuilt = (await readFile(nsLedger, "utf-8")).trim().split("\n").map((l) => JSON.parse(l));
|
|
2225
|
+
assert.deepEqual(rebuilt.map((r) => r.eventType), ["created", "updated"]);
|
|
2226
|
+
} finally {
|
|
2227
|
+
await rm(root, { recursive: true, force: true });
|
|
2228
|
+
}
|
|
2229
|
+
});
|
|
2230
|
+
|
|
2231
|
+
test("auto-compaction bounds a large append-only history under the read/decrypt cap and arms the throttle (#2033)", async () => {
|
|
2232
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-bound-"));
|
|
2233
|
+
try {
|
|
2234
|
+
const ledgerPath = path.join(memoryDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
2235
|
+
await mkdir(path.dirname(ledgerPath), { recursive: true });
|
|
2236
|
+
// Many VALID append-only events with no frontmatter equivalent: a preserving
|
|
2237
|
+
// compaction must keep them, so bounding — not junk-discard — is what keeps
|
|
2238
|
+
// the ledger under the cap. Distinct ascending timestamps make "newest"
|
|
2239
|
+
// precise.
|
|
2240
|
+
const total = 200;
|
|
2241
|
+
const events = Array.from({ length: total }, (_unused, i) => ({
|
|
2242
|
+
eventId: `cap-${String(i).padStart(3, "0")}`,
|
|
2243
|
+
memoryId: "mem-a",
|
|
2244
|
+
eventType: "explicit_capture_accepted",
|
|
2245
|
+
timestamp: new Date(Date.UTC(2026, 2, 8, 0, 0, 0, 0) + i * 1000).toISOString(),
|
|
2246
|
+
actor: "explicit-capture",
|
|
2247
|
+
ruleVersion: "memory-lifecycle-ledger.v1",
|
|
2248
|
+
}));
|
|
2249
|
+
const original = `${events.map((e) => JSON.stringify(e)).join("\n")}\n`;
|
|
2250
|
+
await writeFile(ledgerPath, original, "utf-8");
|
|
2251
|
+
const rowBytes = Buffer.byteLength(`${JSON.stringify(events[0])}\n`, "utf8");
|
|
2252
|
+
const cap = rowBytes * 20 + Math.floor(rowBytes / 2); // admits ~20 rows.
|
|
2253
|
+
|
|
2254
|
+
const scheduler = buildCompactionScheduler(
|
|
2255
|
+
fixtureConfig({
|
|
2256
|
+
memoryDir,
|
|
2257
|
+
memoryLifecycleLedgerCompactBytes: rowBytes * 5, // threshold << ledger size.
|
|
2258
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
2259
|
+
}),
|
|
2260
|
+
);
|
|
2261
|
+
scheduler.lifecycleLedgerMaxBytes = cap;
|
|
2262
|
+
try {
|
|
2263
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
2264
|
+
|
|
2265
|
+
const rewritten = await readFile(ledgerPath, "utf-8");
|
|
2266
|
+
assert.ok(
|
|
2267
|
+
Buffer.byteLength(rewritten, "utf8") < cap,
|
|
2268
|
+
"compacted ledger must be bounded under the read/decrypt cap",
|
|
2269
|
+
);
|
|
2270
|
+
const keptIds = rewritten.trim().split("\n").map((l) => JSON.parse(l).eventId);
|
|
2271
|
+
assert.ok(keptIds.includes(`cap-${String(total - 1).padStart(3, "0")}`), "newest append-only event survives");
|
|
2272
|
+
assert.ok(!keptIds.includes("cap-000"), "oldest append-only event archived out of the active ledger");
|
|
2273
|
+
|
|
2274
|
+
// The bounding archive lives in the timestamped backup, so nothing is lost.
|
|
2275
|
+
const backups = await readdir(path.join(memoryDir, "archive", "memory-lifecycle-ledger")).catch(() => []);
|
|
2276
|
+
assert.ok(backups.length > 0, "a backup archive was written for the overflow");
|
|
2277
|
+
|
|
2278
|
+
// Effective (under cap) compaction armed the throttle: a second in-window
|
|
2279
|
+
// pass after re-growing the ledger must NOT recompact.
|
|
2280
|
+
await writeFile(ledgerPath, original, "utf-8");
|
|
2281
|
+
const regrown = (await stat(ledgerPath)).size;
|
|
2282
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
2283
|
+
assert.equal(
|
|
2284
|
+
(await stat(ledgerPath)).size,
|
|
2285
|
+
regrown,
|
|
2286
|
+
"throttle armed after an effective bounded compaction: second in-window pass is a no-op",
|
|
2287
|
+
);
|
|
2288
|
+
} finally {
|
|
2289
|
+
scheduler.dispose();
|
|
2290
|
+
}
|
|
2291
|
+
} finally {
|
|
2292
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
2293
|
+
}
|
|
2294
|
+
});
|
|
2295
|
+
|
|
2296
|
+
test("filesystem-fallback compaction drains a pending lifecycle append even when no storage is wired (#2033)", async () => {
|
|
2297
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-fallback-drain-"));
|
|
2298
|
+
try {
|
|
2299
|
+
// Namespaces enabled but NO catalog storage wired: only the filesystem
|
|
2300
|
+
// fallback finds this per-namespace ledger, so target.storage is absent.
|
|
2301
|
+
const token = namespaceIdentityToken("project-fallback");
|
|
2302
|
+
const nsDir = path.join(root, "namespaces", token);
|
|
2303
|
+
const nsLedger = path.join(nsDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
2304
|
+
await mkdir(path.dirname(nsLedger), { recursive: true });
|
|
2305
|
+
await writeFile(nsLedger, "", "utf-8");
|
|
2306
|
+
// A plaintext pending spill, exactly as a lock-timed-out append leaves it.
|
|
2307
|
+
const spillDir = pendingLifecycleLedgerDir(nsLedger);
|
|
2308
|
+
await mkdir(spillDir, { recursive: true });
|
|
2309
|
+
const spillPath = path.join(spillDir, "spill.jsonl");
|
|
2310
|
+
const spilled = {
|
|
2311
|
+
eventId: "evt-fallback-spilled",
|
|
2312
|
+
memoryId: "mem-ns",
|
|
2313
|
+
eventType: "explicit_capture_accepted",
|
|
2314
|
+
timestamp: "2026-03-08T00:00:00.000Z",
|
|
2315
|
+
actor: "explicit-capture",
|
|
2316
|
+
ruleVersion: "memory-lifecycle-ledger.v1",
|
|
2317
|
+
};
|
|
2318
|
+
await writeFile(spillPath, `${JSON.stringify(spilled)}\n`, "utf-8");
|
|
2319
|
+
|
|
2320
|
+
const scheduler = buildCompactionScheduler(
|
|
2321
|
+
fixtureConfig({
|
|
2322
|
+
memoryDir: root,
|
|
2323
|
+
namespacesEnabled: true,
|
|
2324
|
+
memoryLifecycleLedgerCompactBytes: 1024,
|
|
2325
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
2326
|
+
}),
|
|
2327
|
+
);
|
|
2328
|
+
try {
|
|
2329
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
2330
|
+
} finally {
|
|
2331
|
+
scheduler.dispose();
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
const ledger = await readFile(nsLedger, "utf-8");
|
|
2335
|
+
assert.ok(
|
|
2336
|
+
ledger.includes("evt-fallback-spilled"),
|
|
2337
|
+
"the pending spill must be drained into the namespace ledger via the fallback path",
|
|
2338
|
+
);
|
|
2339
|
+
await assert.rejects(() => stat(spillPath), /ENOENT/, "spill file removed after the fallback drain");
|
|
2340
|
+
} finally {
|
|
2341
|
+
await rm(root, { recursive: true, force: true });
|
|
2342
|
+
}
|
|
2343
|
+
});
|
|
2344
|
+
|
|
2345
|
+
test("filesystem-fallback compaction recovers a crash-orphaned *.claimed spill when no live spill remains (#2033)", async () => {
|
|
2346
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-fallback-claimed-"));
|
|
2347
|
+
try {
|
|
2348
|
+
// Namespaces enabled but NO catalog storage wired: only the filesystem
|
|
2349
|
+
// fallback finds this per-namespace ledger, so target.storage is absent.
|
|
2350
|
+
const token = namespaceIdentityToken("project-fallback-claimed");
|
|
2351
|
+
const nsDir = path.join(root, "namespaces", token);
|
|
2352
|
+
const nsLedger = path.join(nsDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
2353
|
+
await mkdir(path.dirname(nsLedger), { recursive: true });
|
|
2354
|
+
await writeFile(nsLedger, "", "utf-8");
|
|
2355
|
+
// A drain that died between claiming a spill (rename to `.claimed`) and
|
|
2356
|
+
// committing it leaves the ONLY copy of those rows as a crash-orphaned
|
|
2357
|
+
// `*.jsonl.claimed` file — NO live `*.jsonl` spill remains. The pre-scan
|
|
2358
|
+
// must still reach the recovery drain so recoverOrphanedClaims restores and
|
|
2359
|
+
// re-commits it; gating solely on live spills stranded these rows forever.
|
|
2360
|
+
const spillDir = pendingLifecycleLedgerDir(nsLedger);
|
|
2361
|
+
await mkdir(spillDir, { recursive: true });
|
|
2362
|
+
const claimedPath = path.join(spillDir, "spill.jsonl.claimed");
|
|
2363
|
+
const orphaned = {
|
|
2364
|
+
eventId: "evt-claimed-orphan",
|
|
2365
|
+
memoryId: "mem-ns",
|
|
2366
|
+
eventType: "explicit_capture_accepted",
|
|
2367
|
+
timestamp: "2026-03-08T00:00:00.000Z",
|
|
2368
|
+
actor: "explicit-capture",
|
|
2369
|
+
ruleVersion: "memory-lifecycle-ledger.v1",
|
|
2370
|
+
};
|
|
2371
|
+
await writeFile(claimedPath, `${JSON.stringify(orphaned)}\n`, "utf-8");
|
|
2372
|
+
// Precondition: no live spill exists — only the claimed orphan.
|
|
2373
|
+
assert.equal((await listContainedSpillFiles(spillDir)).length, 0, "precondition: no live *.jsonl spill");
|
|
2374
|
+
assert.equal(
|
|
2375
|
+
(await listContainedSpillFiles(spillDir, ".jsonl.claimed")).length,
|
|
2376
|
+
1,
|
|
2377
|
+
"precondition: exactly one crash-orphaned *.jsonl.claimed file",
|
|
2378
|
+
);
|
|
2379
|
+
|
|
2380
|
+
const scheduler = buildCompactionScheduler(
|
|
2381
|
+
fixtureConfig({
|
|
2382
|
+
memoryDir: root,
|
|
2383
|
+
namespacesEnabled: true,
|
|
2384
|
+
memoryLifecycleLedgerCompactBytes: 1024,
|
|
2385
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
2386
|
+
}),
|
|
2387
|
+
);
|
|
2388
|
+
try {
|
|
2389
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
2390
|
+
} finally {
|
|
2391
|
+
scheduler.dispose();
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
const ledger = await readFile(nsLedger, "utf-8");
|
|
2395
|
+
assert.ok(
|
|
2396
|
+
ledger.includes("evt-claimed-orphan"),
|
|
2397
|
+
"the crash-orphaned claimed spill must be recovered and folded into the namespace ledger",
|
|
2398
|
+
);
|
|
2399
|
+
await assert.rejects(() => stat(claimedPath), /ENOENT/, "claimed orphan removed after recovery + commit");
|
|
2400
|
+
assert.equal(
|
|
2401
|
+
(await listContainedSpillFiles(spillDir, ".jsonl.claimed")).length,
|
|
2402
|
+
0,
|
|
2403
|
+
"no claimed orphan left stranded",
|
|
2404
|
+
);
|
|
2405
|
+
} finally {
|
|
2406
|
+
await rm(root, { recursive: true, force: true });
|
|
2407
|
+
}
|
|
2408
|
+
});
|
|
2409
|
+
|
|
2410
|
+
test("catalog-disabled fallback drain re-probes encryption UNDER the lock and defers ciphertext instead of plaintext-corrupting it (#2033)", async () => {
|
|
2411
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-fallback-secure-drain-"));
|
|
2412
|
+
try {
|
|
2413
|
+
const key = Buffer.alloc(32, 4);
|
|
2414
|
+
const validRow = (id: string): string =>
|
|
2415
|
+
`{"eventId":"${id}","memoryId":"m","eventType":"created",`
|
|
2416
|
+
+ `"timestamp":"2026-03-08T00:00:00.000Z","actor":"t","ruleVersion":"1"}\n`;
|
|
2417
|
+
|
|
2418
|
+
// nsA: plaintext (empty) ledger + an ENCRYPTED pending spill, exactly as a
|
|
2419
|
+
// secure-store writer's lock-timeout leaves it. A keyless plaintext readFile
|
|
2420
|
+
// would fold ciphertext into the ledger; the under-lock probe must refuse it.
|
|
2421
|
+
const tokenA = namespaceIdentityToken("ns-enc-spill");
|
|
2422
|
+
const nsDirA = path.join(root, "namespaces", tokenA);
|
|
2423
|
+
const nsLedgerA = path.join(nsDirA, "state", "memory-lifecycle-ledger.jsonl");
|
|
2424
|
+
await mkdir(path.dirname(nsLedgerA), { recursive: true });
|
|
2425
|
+
await writeFile(nsLedgerA, "", "utf-8");
|
|
2426
|
+
const spillDirA = pendingLifecycleLedgerDir(nsLedgerA);
|
|
2427
|
+
await mkdir(spillDirA, { recursive: true });
|
|
2428
|
+
const spillPathA = path.join(spillDirA, "spill.jsonl");
|
|
2429
|
+
const encryptedSpillA = encryptFileBody(validRow("evt-enc-spill"), key, filePathAad(spillPathA, nsDirA));
|
|
2430
|
+
await writeFile(spillPathA, encryptedSpillA);
|
|
2431
|
+
|
|
2432
|
+
// nsB: ENCRYPTED ledger + a PLAINTEXT pending spill. A keyless plaintext
|
|
2433
|
+
// appendFile would append plaintext onto the encrypted ledger; the under-lock
|
|
2434
|
+
// append probe must refuse it and roll the claim back.
|
|
2435
|
+
const tokenB = namespaceIdentityToken("ns-enc-ledger");
|
|
2436
|
+
const nsDirB = path.join(root, "namespaces", tokenB);
|
|
2437
|
+
const nsLedgerB = path.join(nsDirB, "state", "memory-lifecycle-ledger.jsonl");
|
|
2438
|
+
await mkdir(path.dirname(nsLedgerB), { recursive: true });
|
|
2439
|
+
const encryptedLedgerB = encryptFileBody(validRow("evt-existing"), key, filePathAad(nsLedgerB, nsDirB));
|
|
2440
|
+
await writeFile(nsLedgerB, encryptedLedgerB);
|
|
2441
|
+
const spillDirB = pendingLifecycleLedgerDir(nsLedgerB);
|
|
2442
|
+
await mkdir(spillDirB, { recursive: true });
|
|
2443
|
+
const spillPathB = path.join(spillDirB, "spill.jsonl");
|
|
2444
|
+
await writeFile(spillPathB, validRow("evt-plain-spill"), "utf-8");
|
|
2445
|
+
|
|
2446
|
+
const scheduler = buildCompactionScheduler(
|
|
2447
|
+
fixtureConfig({
|
|
2448
|
+
memoryDir: root,
|
|
2449
|
+
namespacesEnabled: true,
|
|
2450
|
+
memoryLifecycleLedgerCompactBytes: 0, // drain still runs unconditionally
|
|
2451
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
2452
|
+
}),
|
|
2453
|
+
);
|
|
2454
|
+
try {
|
|
2455
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
2456
|
+
} finally {
|
|
2457
|
+
scheduler.dispose();
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
// nsA: encrypted spill deferred — byte-for-byte intact, the ledger never got
|
|
2461
|
+
// ciphertext folded into it.
|
|
2462
|
+
assert.deepEqual(await readFile(spillPathA), encryptedSpillA, "encrypted spill left intact for the keyed path");
|
|
2463
|
+
assert.equal(await readFile(nsLedgerA, "utf-8"), "", "plaintext ledger never received ciphertext");
|
|
2464
|
+
assert.ok(isEncryptedFile(await readFile(spillPathA)), "spill stays encrypted at rest");
|
|
2465
|
+
|
|
2466
|
+
// nsB: plaintext spill deferred (not appended onto the encrypted ledger), and
|
|
2467
|
+
// the encrypted ledger left byte-for-byte intact.
|
|
2468
|
+
assert.deepEqual(await readFile(nsLedgerB), encryptedLedgerB, "encrypted ledger left byte-for-byte intact");
|
|
2469
|
+
assert.equal(
|
|
2470
|
+
await readFile(spillPathB, "utf-8"),
|
|
2471
|
+
validRow("evt-plain-spill"),
|
|
2472
|
+
"plaintext spill rolled back to unclaimed for a later keyed pass",
|
|
2473
|
+
);
|
|
2474
|
+
} finally {
|
|
2475
|
+
await rm(root, { recursive: true, force: true });
|
|
2476
|
+
}
|
|
2477
|
+
});
|
|
2478
|
+
|
|
2479
|
+
test("auto-compaction triggers on an encrypted ledger at/above the decrypt cap even when the configured threshold is larger (#2033)", async () => {
|
|
2480
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-cap-clamp-"));
|
|
2481
|
+
try {
|
|
2482
|
+
await mkdir(path.join(memoryDir, "facts", "2026-03-08"), { recursive: true });
|
|
2483
|
+
await writeFile(
|
|
2484
|
+
path.join(memoryDir, "facts", "2026-03-08", "fact-1.md"),
|
|
2485
|
+
`---\nid: fact-1\ncategory: fact\ncreated: 2026-03-08T00:00:00.000Z\n`
|
|
2486
|
+
+ `updated: 2026-03-08T01:00:00.000Z\nsource: test\nconfidence: 0.8\n`
|
|
2487
|
+
+ `confidenceTier: implied\ntags: ["alpha"]\n---\n\nalpha\n`,
|
|
2488
|
+
"utf-8",
|
|
2489
|
+
);
|
|
2490
|
+
const ledgerPath = path.join(memoryDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
2491
|
+
await mkdir(path.dirname(ledgerPath), { recursive: true });
|
|
2492
|
+
// Encrypted-at-rest oversized ledger of junk rows the rebuild discards.
|
|
2493
|
+
const key = Buffer.alloc(32, 7);
|
|
2494
|
+
const junk = '{"legacy":true,"pad":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}\n';
|
|
2495
|
+
const junkBlob = junk.repeat(Math.ceil(4096 / junk.length));
|
|
2496
|
+
await writeFile(ledgerPath, encryptFileBody(junkBlob, key, filePathAad(ledgerPath, memoryDir)));
|
|
2497
|
+
const beforeSize = (await stat(ledgerPath)).size;
|
|
2498
|
+
|
|
2499
|
+
const storage = new StorageManager(memoryDir);
|
|
2500
|
+
storage.setSecureStoreRequired(true);
|
|
2501
|
+
storage.setSecureStoreKey(key); // unlocked, so an encrypted target is not deferred.
|
|
2502
|
+
const scheduler = buildCompactionSchedulerWithStorage(
|
|
2503
|
+
fixtureConfig({
|
|
2504
|
+
memoryDir,
|
|
2505
|
+
// Threshold FAR above both the ledger AND the (seam) decrypt cap. A
|
|
2506
|
+
// pre-clamp trigger keyed purely off this threshold would skip the ledger
|
|
2507
|
+
// forever and let it grow permanently unreadable past the cap.
|
|
2508
|
+
memoryLifecycleLedgerCompactBytes: 10 * 1024 * 1024,
|
|
2509
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
2510
|
+
}),
|
|
2511
|
+
() => storage,
|
|
2512
|
+
);
|
|
2513
|
+
scheduler.lifecycleLedgerMaxBytes = 2048; // tiny cap: the ~4KB ledger is "at/above" it.
|
|
2514
|
+
try {
|
|
2515
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
2516
|
+
} finally {
|
|
2517
|
+
scheduler.dispose();
|
|
2518
|
+
}
|
|
2519
|
+
const afterSize = (await stat(ledgerPath)).size;
|
|
2520
|
+
assert.ok(afterSize < beforeSize, "over-cap encrypted ledger must compact despite the larger threshold");
|
|
2521
|
+
assert.ok(afterSize < 2048, "compacted encrypted ledger must land below the decrypt cap");
|
|
2522
|
+
assert.ok(isEncryptedFile(await readFile(ledgerPath)), "rewritten ledger stays encrypted at rest");
|
|
2523
|
+
const rebuilt = await storage.readAllMemoryLifecycleEvents();
|
|
2524
|
+
assert.deepEqual(rebuilt.map((e) => e.eventType), ["created", "updated"]);
|
|
2525
|
+
} finally {
|
|
2526
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
2527
|
+
}
|
|
2528
|
+
});
|
|
2529
|
+
|
|
2530
|
+
test("auto-compaction bypasses the min-interval throttle for an over-cap encrypted ledger, but still throttles a below-cap target (#2033)", async () => {
|
|
2531
|
+
// Bypass arm: an encrypted, over-cap ledger with the throttle freshly armed.
|
|
2532
|
+
// Without the bypass the min-interval return fires before the compaction path
|
|
2533
|
+
// can clamp it, leaving the unreadable ledger to wait out the whole interval.
|
|
2534
|
+
const bypassDir = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-throttle-bypass-"));
|
|
2535
|
+
// Throttle arm: an ordinary below-cap target with the throttle armed must NOT
|
|
2536
|
+
// recompact — the bypass is scoped to over-cap encrypted ledgers only.
|
|
2537
|
+
const throttledDir = (await seedMemoryDirWithOversizedLedger(4096)).memoryDir;
|
|
2538
|
+
try {
|
|
2539
|
+
await mkdir(path.join(bypassDir, "facts", "2026-03-08"), { recursive: true });
|
|
2540
|
+
await writeFile(
|
|
2541
|
+
path.join(bypassDir, "facts", "2026-03-08", "fact-1.md"),
|
|
2542
|
+
`---\nid: fact-1\ncategory: fact\ncreated: 2026-03-08T00:00:00.000Z\n`
|
|
2543
|
+
+ `updated: 2026-03-08T01:00:00.000Z\nsource: test\nconfidence: 0.8\n`
|
|
2544
|
+
+ `confidenceTier: implied\ntags: ["alpha"]\n---\n\nalpha\n`,
|
|
2545
|
+
"utf-8",
|
|
2546
|
+
);
|
|
2547
|
+
const bypassLedger = path.join(bypassDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
2548
|
+
await mkdir(path.dirname(bypassLedger), { recursive: true });
|
|
2549
|
+
const key = Buffer.alloc(32, 7);
|
|
2550
|
+
const junk = '{"legacy":true,"pad":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}\n';
|
|
2551
|
+
const junkBlob = junk.repeat(Math.ceil(4096 / junk.length));
|
|
2552
|
+
await writeFile(bypassLedger, encryptFileBody(junkBlob, key, filePathAad(bypassLedger, bypassDir)));
|
|
2553
|
+
const bypassBefore = (await stat(bypassLedger)).size;
|
|
2554
|
+
|
|
2555
|
+
const storage = new StorageManager(bypassDir);
|
|
2556
|
+
storage.setSecureStoreRequired(true);
|
|
2557
|
+
storage.setSecureStoreKey(key); // unlocked, so the encrypted target is not deferred.
|
|
2558
|
+
const bypassScheduler = buildCompactionSchedulerWithStorage(
|
|
2559
|
+
fixtureConfig({
|
|
2560
|
+
memoryDir: bypassDir,
|
|
2561
|
+
// Threshold far above both the ledger and the (seam) cap: only the
|
|
2562
|
+
// cap-clamp — not the configured threshold — makes this target eligible.
|
|
2563
|
+
memoryLifecycleLedgerCompactBytes: 10 * 1024 * 1024,
|
|
2564
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
2565
|
+
}),
|
|
2566
|
+
() => storage,
|
|
2567
|
+
);
|
|
2568
|
+
bypassScheduler.lifecycleLedgerMaxBytes = 2048; // tiny cap: the ~4KB ledger is over it.
|
|
2569
|
+
// Arm the throttle: WITHOUT the #2033 bypass this in-window pass returns
|
|
2570
|
+
// before compaction and the over-cap encrypted ledger is never clamped.
|
|
2571
|
+
bypassScheduler.lastLifecycleCompactionAtMs = Date.now();
|
|
2572
|
+
try {
|
|
2573
|
+
await bypassScheduler.maybeCompactMemoryLifecycleLedger();
|
|
2574
|
+
} finally {
|
|
2575
|
+
bypassScheduler.dispose();
|
|
2576
|
+
}
|
|
2577
|
+
const bypassAfter = (await stat(bypassLedger)).size;
|
|
2578
|
+
assert.ok(
|
|
2579
|
+
bypassAfter < bypassBefore,
|
|
2580
|
+
"over-cap encrypted ledger must compact despite the armed throttle (#2033 bypass)",
|
|
2581
|
+
);
|
|
2582
|
+
assert.ok(bypassAfter < 2048, "compacted encrypted ledger must land below the decrypt cap");
|
|
2583
|
+
assert.ok(isEncryptedFile(await readFile(bypassLedger)), "rewritten ledger stays encrypted at rest");
|
|
2584
|
+
|
|
2585
|
+
// Below-cap control: same armed throttle, ordinary plaintext oversized
|
|
2586
|
+
// ledger (over threshold, under the default cap). The bypass must NOT fire,
|
|
2587
|
+
// so the throttle still holds and the ledger is left untouched.
|
|
2588
|
+
const throttledLedger = path.join(throttledDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
2589
|
+
const throttledBefore = (await stat(throttledLedger)).size;
|
|
2590
|
+
const throttledScheduler = buildCompactionScheduler(
|
|
2591
|
+
fixtureConfig({
|
|
2592
|
+
memoryDir: throttledDir,
|
|
2593
|
+
memoryLifecycleLedgerCompactBytes: 1024, // ledger is over threshold — eligible if unthrottled.
|
|
2594
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
2595
|
+
}),
|
|
2596
|
+
);
|
|
2597
|
+
throttledScheduler.lastLifecycleCompactionAtMs = Date.now();
|
|
2598
|
+
try {
|
|
2599
|
+
await throttledScheduler.maybeCompactMemoryLifecycleLedger();
|
|
2600
|
+
} finally {
|
|
2601
|
+
throttledScheduler.dispose();
|
|
2602
|
+
}
|
|
2603
|
+
assert.equal(
|
|
2604
|
+
(await stat(throttledLedger)).size,
|
|
2605
|
+
throttledBefore,
|
|
2606
|
+
"below-cap target stays throttled: the armed throttle prevents recompaction",
|
|
2607
|
+
);
|
|
2608
|
+
} finally {
|
|
2609
|
+
await rm(bypassDir, { recursive: true, force: true });
|
|
2610
|
+
await rm(throttledDir, { recursive: true, force: true });
|
|
2611
|
+
}
|
|
2612
|
+
});
|
|
2613
|
+
|
|
2614
|
+
test("auto-compaction reserves the secure-store envelope so an encrypted rewrite lands below the cap and does not retry forever (#2033)", async () => {
|
|
2615
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-envelope-"));
|
|
2616
|
+
try {
|
|
2617
|
+
const ledgerPath = path.join(memoryDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
2618
|
+
await mkdir(path.dirname(ledgerPath), { recursive: true });
|
|
2619
|
+
const total = 200;
|
|
2620
|
+
const events = Array.from({ length: total }, (_unused, i) => ({
|
|
2621
|
+
eventId: `cap-${String(i).padStart(3, "0")}`,
|
|
2622
|
+
memoryId: "mem-a",
|
|
2623
|
+
eventType: "explicit_capture_accepted",
|
|
2624
|
+
timestamp: new Date(Date.UTC(2026, 2, 8, 0, 0, 0, 0) + i * 1000).toISOString(),
|
|
2625
|
+
actor: "explicit-capture",
|
|
2626
|
+
ruleVersion: "memory-lifecycle-ledger.v1",
|
|
2627
|
+
}));
|
|
2628
|
+
const plaintext = `${events.map((e) => JSON.stringify(e)).join("\n")}\n`;
|
|
2629
|
+
const key = Buffer.alloc(32, 3);
|
|
2630
|
+
await writeFile(ledgerPath, encryptFileBody(plaintext, key, filePathAad(ledgerPath, memoryDir)));
|
|
2631
|
+
const rowBytes = Buffer.byteLength(`${JSON.stringify(events[0])}\n`, "utf8");
|
|
2632
|
+
// Cap == exactly 20 plaintext rows. The reserving budget (cap − envelope − 1)
|
|
2633
|
+
// admits 19 rows, so the encrypted output (19 rows + envelope) is below the
|
|
2634
|
+
// cap. WITHOUT the reserve the old budget (== cap) would keep 20 rows and the
|
|
2635
|
+
// encrypted file (20 rows + envelope) would be at/over the cap — the endless
|
|
2636
|
+
// "ineffective compaction" retry this fix prevents.
|
|
2637
|
+
const cap = rowBytes * 20;
|
|
2638
|
+
assert.ok(
|
|
2639
|
+
rowBytes * 20 + SECURE_STORE_ENVELOPE_OVERHEAD_BYTES > cap,
|
|
2640
|
+
"sanity: cap chosen so an unreserved budget would push the encrypted file over the cap",
|
|
2641
|
+
);
|
|
2642
|
+
|
|
2643
|
+
const storage = new StorageManager(memoryDir);
|
|
2644
|
+
storage.setSecureStoreRequired(true);
|
|
2645
|
+
storage.setSecureStoreKey(key);
|
|
2646
|
+
const scheduler = buildCompactionSchedulerWithStorage(
|
|
2647
|
+
fixtureConfig({
|
|
2648
|
+
memoryDir,
|
|
2649
|
+
memoryLifecycleLedgerCompactBytes: rowBytes * 5,
|
|
2650
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
2651
|
+
}),
|
|
2652
|
+
() => storage,
|
|
2653
|
+
);
|
|
2654
|
+
scheduler.lifecycleLedgerMaxBytes = cap;
|
|
2655
|
+
try {
|
|
2656
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
2657
|
+
const onDisk = (await stat(ledgerPath)).size;
|
|
2658
|
+
assert.ok(isEncryptedFile(await readFile(ledgerPath)), "rewritten ledger encrypted at rest");
|
|
2659
|
+
assert.ok(onDisk < cap, `encrypted on-disk ledger (${onDisk}B) must be strictly below the cap (${cap}B)`);
|
|
2660
|
+
const keptIds = (await storage.readAllMemoryLifecycleEvents()).map((e) => e.eventId);
|
|
2661
|
+
assert.ok(keptIds.includes(`cap-${String(total - 1).padStart(3, "0")}`), "newest event survives");
|
|
2662
|
+
assert.ok(!keptIds.includes("cap-000"), "oldest event archived out of the active ledger");
|
|
2663
|
+
|
|
2664
|
+
const backups = await readdir(path.join(memoryDir, "archive", "memory-lifecycle-ledger")).catch(() => []);
|
|
2665
|
+
assert.ok(backups.length > 0, "overflow archived to a verbatim backup");
|
|
2666
|
+
|
|
2667
|
+
// The compaction was judged effective (throttle armed): re-grow within the
|
|
2668
|
+
// window; a broken budget would have reported "failed" (throttle un-armed)
|
|
2669
|
+
// and this second pass would recompact.
|
|
2670
|
+
// Re-grow OVER the threshold but strictly BELOW the decrypt cap, so this
|
|
2671
|
+
// pass is governed by the armed throttle — not the over-cap encrypted
|
|
2672
|
+
// bypass (#2033), which only fires for a ledger at/over the cap.
|
|
2673
|
+
const regrownPlain = `${events.slice(0, 10).map((e) => JSON.stringify(e)).join("\n")}\n`;
|
|
2674
|
+
await writeFile(ledgerPath, encryptFileBody(regrownPlain, key, filePathAad(ledgerPath, memoryDir)));
|
|
2675
|
+
const regrown = (await stat(ledgerPath)).size;
|
|
2676
|
+
assert.ok(
|
|
2677
|
+
regrown > rowBytes * 5 && regrown < cap,
|
|
2678
|
+
"regrown ledger is over threshold but below the cap (throttle, not bypass, applies)",
|
|
2679
|
+
);
|
|
2680
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
2681
|
+
assert.equal(
|
|
2682
|
+
(await stat(ledgerPath)).size,
|
|
2683
|
+
regrown,
|
|
2684
|
+
"throttle armed after an effective encrypted compaction: second in-window pass is a no-op",
|
|
2685
|
+
);
|
|
2686
|
+
} finally {
|
|
2687
|
+
scheduler.dispose();
|
|
2688
|
+
}
|
|
2689
|
+
} finally {
|
|
2690
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
2691
|
+
}
|
|
2692
|
+
});
|
|
2693
|
+
|
|
2694
|
+
test("auto-compaction reserves the envelope when a PLAINTEXT ledger will be rewritten encrypted (write-mode budget, #2033)", async () => {
|
|
2695
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-plaintext-encrypt-"));
|
|
2696
|
+
try {
|
|
2697
|
+
const ledgerPath = path.join(memoryDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
2698
|
+
await mkdir(path.dirname(ledgerPath), { recursive: true });
|
|
2699
|
+
const total = 200;
|
|
2700
|
+
const events = Array.from({ length: total }, (_unused, i) => ({
|
|
2701
|
+
eventId: `cap-${String(i).padStart(3, "0")}`,
|
|
2702
|
+
memoryId: "mem-a",
|
|
2703
|
+
eventType: "explicit_capture_accepted",
|
|
2704
|
+
timestamp: new Date(Date.UTC(2026, 2, 8, 0, 0, 0, 0) + i * 1000).toISOString(),
|
|
2705
|
+
actor: "explicit-capture",
|
|
2706
|
+
ruleVersion: "memory-lifecycle-ledger.v1",
|
|
2707
|
+
}));
|
|
2708
|
+
const plaintext = `${events.map((e) => JSON.stringify(e)).join("\n")}\n`;
|
|
2709
|
+
// The on-disk ledger is PLAINTEXT (header not encrypted). The pre-fix budget
|
|
2710
|
+
// keyed off this header and skipped the reserve, but the rewrite below
|
|
2711
|
+
// encrypts (secureStoreEncryptOnWrite + unlocked key), so the envelope must
|
|
2712
|
+
// still be reserved.
|
|
2713
|
+
await writeFile(ledgerPath, plaintext, "utf-8");
|
|
2714
|
+
assert.ok(!isEncryptedFile(await readFile(ledgerPath)), "precondition: existing ledger is plaintext");
|
|
2715
|
+
|
|
2716
|
+
const rowBytes = Buffer.byteLength(`${JSON.stringify(events[0])}\n`, "utf8");
|
|
2717
|
+
// Cap so the UNRESERVED budget (cap − 1) keeps 20 rows whose encrypted size
|
|
2718
|
+
// (20 rows + envelope) reaches the cap, while the reserving budget
|
|
2719
|
+
// (cap − envelope − 1) keeps 19 rows that land strictly below it.
|
|
2720
|
+
const cap = rowBytes * 20 + SECURE_STORE_ENVELOPE_OVERHEAD_BYTES;
|
|
2721
|
+
assert.ok(
|
|
2722
|
+
20 * rowBytes + SECURE_STORE_ENVELOPE_OVERHEAD_BYTES >= cap,
|
|
2723
|
+
"sanity: an unreserved 20-row plaintext budget encrypts to at/over the cap",
|
|
2724
|
+
);
|
|
2725
|
+
assert.ok(
|
|
2726
|
+
19 * rowBytes + SECURE_STORE_ENVELOPE_OVERHEAD_BYTES < cap,
|
|
2727
|
+
"sanity: a reserved 19-row budget encrypts to strictly below the cap",
|
|
2728
|
+
);
|
|
2729
|
+
|
|
2730
|
+
const key = Buffer.alloc(32, 5);
|
|
2731
|
+
const storage = new StorageManager(memoryDir);
|
|
2732
|
+
storage.setSecureStoreRequired(true);
|
|
2733
|
+
storage.setSecureStoreKey(key); // unlocked + encrypt-on-write ⇒ the rewrite encrypts.
|
|
2734
|
+
assert.ok(storage.willEncryptStateWrites(), "precondition: writes will be encrypted at rest");
|
|
2735
|
+
|
|
2736
|
+
const scheduler = buildCompactionSchedulerWithStorage(
|
|
2737
|
+
fixtureConfig({
|
|
2738
|
+
memoryDir,
|
|
2739
|
+
memoryLifecycleLedgerCompactBytes: rowBytes * 5,
|
|
2740
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
2741
|
+
}),
|
|
2742
|
+
() => storage,
|
|
2743
|
+
);
|
|
2744
|
+
scheduler.lifecycleLedgerMaxBytes = cap;
|
|
2745
|
+
try {
|
|
2746
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
2747
|
+
const onDisk = (await stat(ledgerPath)).size;
|
|
2748
|
+
assert.ok(isEncryptedFile(await readFile(ledgerPath)), "rewritten ledger encrypted at rest");
|
|
2749
|
+
assert.ok(onDisk < cap, `encrypted on-disk ledger (${onDisk}B) must be strictly below the cap (${cap}B)`);
|
|
2750
|
+
const keptIds = (await storage.readAllMemoryLifecycleEvents()).map((e) => e.eventId);
|
|
2751
|
+
assert.ok(keptIds.includes(`cap-${String(total - 1).padStart(3, "0")}`), "newest event survives");
|
|
2752
|
+
assert.ok(!keptIds.includes("cap-000"), "oldest event archived out of the active ledger");
|
|
2753
|
+
|
|
2754
|
+
// Throttle armed only after an EFFECTIVE compaction: an unreserved budget
|
|
2755
|
+
// would have reported "failed" (over-cap) and recompacted on this pass.
|
|
2756
|
+
await writeFile(ledgerPath, plaintext, "utf-8");
|
|
2757
|
+
const regrown = (await stat(ledgerPath)).size;
|
|
2758
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
2759
|
+
assert.equal(
|
|
2760
|
+
(await stat(ledgerPath)).size,
|
|
2761
|
+
regrown,
|
|
2762
|
+
"throttle armed after an effective compaction: second in-window pass is a no-op",
|
|
2763
|
+
);
|
|
2764
|
+
} finally {
|
|
2765
|
+
scheduler.dispose();
|
|
2766
|
+
}
|
|
2767
|
+
} finally {
|
|
2768
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
2769
|
+
}
|
|
2770
|
+
});
|
|
2771
|
+
|
|
2772
|
+
test("catalog-disabled fallback pending drain skips unsafe spill entries (symlink/FIFO/dir) without following or blocking (#2033)", { timeout: 30_000 }, async () => {
|
|
2773
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-fallback-unsafe-"));
|
|
2774
|
+
const outside = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-fallback-outside-"));
|
|
2775
|
+
try {
|
|
2776
|
+
const token = namespaceIdentityToken("project-unsafe");
|
|
2777
|
+
const nsDir = path.join(root, "namespaces", token);
|
|
2778
|
+
const nsLedger = path.join(nsDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
2779
|
+
await mkdir(path.dirname(nsLedger), { recursive: true });
|
|
2780
|
+
await writeFile(nsLedger, "", "utf-8");
|
|
2781
|
+
const spillDir = pendingLifecycleLedgerDir(nsLedger);
|
|
2782
|
+
await mkdir(spillDir, { recursive: true });
|
|
2783
|
+
|
|
2784
|
+
// A legitimate plaintext spill that MUST drain.
|
|
2785
|
+
const goodPath = path.join(spillDir, "good.jsonl");
|
|
2786
|
+
const spilled = {
|
|
2787
|
+
eventId: "evt-good-spilled",
|
|
2788
|
+
memoryId: "mem-ns",
|
|
2789
|
+
eventType: "explicit_capture_accepted",
|
|
2790
|
+
timestamp: "2026-03-08T00:00:00.000Z",
|
|
2791
|
+
actor: "explicit-capture",
|
|
2792
|
+
ruleVersion: "memory-lifecycle-ledger.v1",
|
|
2793
|
+
};
|
|
2794
|
+
await writeFile(goodPath, `${JSON.stringify(spilled)}\n`, "utf-8");
|
|
2795
|
+
|
|
2796
|
+
// Unsafe entries the guarded lister MUST skip WITHOUT opening/following:
|
|
2797
|
+
// - a symlink to an OUTSIDE file (following it would escape the spill dir),
|
|
2798
|
+
// - a FIFO (opening it for read would block until a writer appears),
|
|
2799
|
+
// - a subdirectory (not a regular file).
|
|
2800
|
+
const outsideFile = path.join(outside, "target.jsonl");
|
|
2801
|
+
await writeFile(outsideFile, '{"eventId":"evt-outside"}\n', "utf-8");
|
|
2802
|
+
const linkPath = path.join(spillDir, "link.jsonl");
|
|
2803
|
+
await symlink(outsideFile, linkPath);
|
|
2804
|
+
const fifoPath = path.join(spillDir, "pipe.jsonl");
|
|
2805
|
+
execFileSync("mkfifo", [fifoPath]);
|
|
2806
|
+
const dirPath = path.join(spillDir, "dir.jsonl");
|
|
2807
|
+
await mkdir(dirPath);
|
|
2808
|
+
|
|
2809
|
+
const scheduler = buildCompactionScheduler(
|
|
2810
|
+
fixtureConfig({
|
|
2811
|
+
memoryDir: root,
|
|
2812
|
+
namespacesEnabled: true,
|
|
2813
|
+
memoryLifecycleLedgerCompactBytes: 1024,
|
|
2814
|
+
memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
|
|
2815
|
+
}),
|
|
2816
|
+
);
|
|
2817
|
+
try {
|
|
2818
|
+
// Completing at all is part of the assertion: opening the FIFO would hang
|
|
2819
|
+
// (caught by the test timeout) and following the symlink would escape.
|
|
2820
|
+
await scheduler.maybeCompactMemoryLifecycleLedger();
|
|
2821
|
+
} finally {
|
|
2822
|
+
scheduler.dispose();
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
const ledger = await readFile(nsLedger, "utf-8");
|
|
2826
|
+
assert.ok(ledger.includes("evt-good-spilled"), "the safe plaintext spill drains into the ledger");
|
|
2827
|
+
assert.ok(!ledger.includes("evt-outside"), "the symlink target outside the spill dir is never followed");
|
|
2828
|
+
await assert.rejects(() => stat(goodPath), /ENOENT/, "drained safe spill removed");
|
|
2829
|
+
// Unsafe entries are left untouched (skipped, not read or deleted).
|
|
2830
|
+
assert.ok((await lstat(linkPath)).isSymbolicLink(), "symlink entry left in place");
|
|
2831
|
+
assert.ok((await lstat(fifoPath)).isFIFO(), "FIFO entry left in place");
|
|
2832
|
+
assert.ok((await lstat(dirPath)).isDirectory(), "directory entry left in place");
|
|
2833
|
+
assert.equal(await readFile(outsideFile, "utf-8"), '{"eventId":"evt-outside"}\n', "outside target intact and unread");
|
|
2834
|
+
} finally {
|
|
2835
|
+
await rm(root, { recursive: true, force: true });
|
|
2836
|
+
await rm(outside, { recursive: true, force: true });
|
|
2837
|
+
}
|
|
2838
|
+
});
|