@remnic/core 10.0.36 → 10.0.38
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.js +84 -84
- package/dist/access-authorization-probe.js +85 -85
- package/dist/access-boundary.js +84 -84
- package/dist/access-citation-namespace.js +2 -2
- package/dist/access-cli.js +178 -176
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.js +84 -84
- package/dist/access-http-briefing.js +85 -85
- package/dist/access-http-query.js +86 -86
- package/dist/access-http.js +108 -108
- package/dist/access-identity-continuity-surface.js +84 -84
- package/dist/access-lcm-surface.js +84 -84
- package/dist/access-mcp.js +97 -97
- package/dist/access-namespace-preflight.js +84 -84
- package/dist/access-observe-write-category.js +13 -13
- package/dist/access-observe-write-surface.js +84 -84
- package/dist/access-offline-manifest.js +12 -12
- package/dist/access-operations-batch.js +88 -88
- package/dist/access-operations.js +95 -95
- package/dist/access-recall-concurrency.js +84 -84
- package/dist/access-recall-diagnostics.js +2 -2
- package/dist/access-recall-response.js +84 -84
- package/dist/access-recall-surface.js +84 -84
- package/dist/access-schema.js +8 -8
- package/dist/access-service.js +84 -84
- package/dist/access-surface-catalog.js +86 -86
- package/dist/active-recall.js +43 -43
- package/dist/adapters/index.js +7 -7
- package/dist/adapters/registry.js +3 -3
- package/dist/{auto-sync-VPHR3FBI.js → auto-sync-432USQDH.js} +9 -9
- package/dist/briefing.js +32 -32
- package/dist/buffer-eviction.d.ts +16 -0
- package/dist/buffer-eviction.d.ts.map +1 -0
- package/dist/buffer-eviction.js +8 -0
- package/dist/buffer-persistence.d.ts +45 -0
- package/dist/buffer-persistence.d.ts.map +1 -0
- package/dist/buffer-persistence.js +8 -0
- package/dist/buffer.d.ts +5 -2
- package/dist/buffer.d.ts.map +1 -1
- package/dist/buffer.js +13 -11
- package/dist/calibration.js +1 -1
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.js +35 -35
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory-graph.js +2 -2
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-FL6SLVAJ.js → chunk-2AKIVSHP.js} +2 -2
- package/dist/{chunk-ORWDCX5Q.js → chunk-3AODCBKX.js} +11 -11
- package/dist/{chunk-XFDHL252.js → chunk-3K5IEJTK.js} +4 -4
- package/dist/{chunk-6FIFEXFF.js → chunk-3PLAL2KP.js} +21 -7
- package/dist/chunk-3PLAL2KP.js.map +1 -0
- package/dist/{chunk-XVZA6VXC.js → chunk-3SL52JKD.js} +4 -4
- package/dist/{chunk-ZSAQSLKL.js → chunk-3XVBGGF3.js} +2 -2
- package/dist/{chunk-XO4HOBM2.js → chunk-4B37KNMP.js} +2 -2
- package/dist/{chunk-NIANATGL.js → chunk-4IZ6AC2U.js} +2 -2
- package/dist/chunk-4OFXLQJD.js +58 -0
- package/dist/chunk-4OFXLQJD.js.map +1 -0
- package/dist/{chunk-S2PDFSXS.js → chunk-4P4VLPGB.js} +1 -1
- package/dist/{chunk-G5UR4DXQ.js → chunk-53BLBDV6.js} +2 -2
- package/dist/{chunk-PDYP7SUC.js → chunk-5ANUG4JZ.js} +2 -2
- package/dist/{chunk-2ZYIAUYU.js → chunk-5CWH2LKL.js} +4 -4
- package/dist/{chunk-S2S4U4G3.js → chunk-5DCS4C4S.js} +4 -4
- package/dist/{chunk-7WTTE5FW.js → chunk-5K23NQRI.js} +2 -2
- package/dist/{chunk-V3JZZJ2E.js → chunk-5UPOKIFP.js} +12 -12
- package/dist/{chunk-K567EIW3.js → chunk-63YPKHAU.js} +2 -2
- package/dist/{chunk-G3T6YMBO.js → chunk-64S6X4IU.js} +7 -7
- package/dist/{chunk-HJM23E65.js → chunk-6DTIYAAN.js} +2 -2
- package/dist/{chunk-G7XAZU2L.js → chunk-6F5BGIAF.js} +3 -3
- package/dist/{chunk-IHTKBQBS.js → chunk-6JEEY42N.js} +2 -2
- package/dist/{chunk-NHB4RC3G.js → chunk-6QOL4G76.js} +2 -2
- package/dist/{chunk-AD3ZB72W.js → chunk-6SYS5YX3.js} +2 -2
- package/dist/{chunk-DFP37GUN.js → chunk-7HXZOWTR.js} +2 -2
- package/dist/{chunk-DGBWPSM6.js → chunk-7VFNGQ4V.js} +2 -2
- package/dist/{chunk-77ZK533I.js → chunk-7ZRRJ6RG.js} +2 -2
- package/dist/{chunk-GMMZ45C4.js → chunk-AVVCPA7U.js} +6 -8
- package/dist/chunk-AVVCPA7U.js.map +1 -0
- package/dist/{chunk-QISYYK4L.js → chunk-AYBAC2N4.js} +3 -3
- package/dist/{chunk-V44DWTFA.js → chunk-AYXRK264.js} +6 -6
- package/dist/{chunk-LJSBF6PW.js → chunk-C2KOW2UQ.js} +2 -2
- package/dist/{chunk-GY4R4P2A.js → chunk-CUWOSRCS.js} +2 -2
- package/dist/{chunk-VNAIUNXI.js → chunk-CVMAOPAH.js} +10 -10
- package/dist/{chunk-CJKHLACF.js → chunk-DG4H56SK.js} +2 -2
- package/dist/{chunk-BQ6DGL6A.js → chunk-DKHX2FHI.js} +2 -2
- package/dist/{chunk-77IXBNV6.js → chunk-E24RUQEN.js} +2 -2
- package/dist/{chunk-HRT2SJPC.js → chunk-E3NBMTTX.js} +2 -2
- package/dist/{chunk-PTQZPVRQ.js → chunk-E5PVMB3I.js} +2 -2
- package/dist/{chunk-AHS3QEEA.js → chunk-EEOYF3IB.js} +2 -2
- package/dist/{chunk-5XTSIZ3L.js → chunk-EGNONTVT.js} +6 -6
- package/dist/{chunk-PH33R7GN.js → chunk-EHIKY2IA.js} +2 -2
- package/dist/{chunk-4ISHIPOC.js → chunk-FGEY2AGV.js} +2 -2
- package/dist/{chunk-O6YJI5VX.js → chunk-FKTMWDN2.js} +2 -2
- package/dist/{chunk-KQUN4RSX.js → chunk-FQFMLOAC.js} +18 -18
- package/dist/{chunk-5LWODP5R.js → chunk-FRAL3EU4.js} +2 -2
- package/dist/{chunk-AAANTXCQ.js → chunk-FZVZYU5C.js} +2 -2
- package/dist/{chunk-TGAXRT7N.js → chunk-GFYFKXWZ.js} +2 -2
- package/dist/{chunk-ID2PLENW.js → chunk-GGI6FLO6.js} +2 -2
- package/dist/{chunk-GDYLZ6FR.js → chunk-HJ3UTGIW.js} +3 -3
- package/dist/{chunk-X3IFRQKS.js → chunk-I73ZSORJ.js} +4 -4
- package/dist/{chunk-X73SCHKR.js → chunk-I75BWXRZ.js} +2 -2
- package/dist/{chunk-M3GKSCFN.js → chunk-J7G656MY.js} +4 -4
- package/dist/{chunk-EH6KLYIE.js → chunk-JF3MEIHO.js} +4 -4
- package/dist/{chunk-XUB4I2N6.js → chunk-JFI3ATK4.js} +2 -2
- package/dist/{chunk-R4SATHCV.js → chunk-JH2ALMPR.js} +2 -2
- package/dist/{chunk-TKBW4YLS.js → chunk-JH4FCVWJ.js} +2 -2
- package/dist/{chunk-DG5UKN2Y.js → chunk-JIA435P3.js} +2 -2
- package/dist/chunk-KASJJGRG.js +29 -0
- package/dist/chunk-KASJJGRG.js.map +1 -0
- package/dist/{chunk-4Z7O2QVJ.js → chunk-KOAVX2IT.js} +2 -2
- package/dist/{chunk-6NXHX6MI.js → chunk-KOY25HUB.js} +2 -2
- package/dist/{chunk-TRW7N4FS.js → chunk-L2JQSG5M.js} +2 -2
- package/dist/{chunk-QQ4T622E.js → chunk-L7PGPH4E.js} +2 -2
- package/dist/{chunk-TYT4TEQV.js → chunk-LDWE7OZS.js} +6 -2
- package/dist/chunk-LDWE7OZS.js.map +1 -0
- package/dist/{chunk-23VGZ65V.js → chunk-LHBOFROT.js} +2 -2
- package/dist/{chunk-VKB2FGNW.js → chunk-LOMSBYNS.js} +90 -90
- package/dist/{chunk-NDJ42GKB.js → chunk-MCPSMWUC.js} +6 -6
- package/dist/{chunk-ERMT5WQM.js → chunk-MKE5DP3S.js} +2 -2
- package/dist/{chunk-64IO6ZEE.js → chunk-MZWHIAOL.js} +2 -2
- package/dist/{chunk-LDXS746V.js → chunk-N7DVBHU3.js} +3 -3
- package/dist/{chunk-F2JLBXMG.js → chunk-NH6GZEQN.js} +8 -8
- package/dist/{chunk-7FG2JSU6.js → chunk-NMKETOAX.js} +2 -2
- package/dist/{chunk-GAARPWMN.js → chunk-OKKBOTRA.js} +2 -2
- package/dist/{chunk-PCIIKQXW.js → chunk-OLH4XCLU.js} +2 -2
- package/dist/{chunk-XA32GZ6B.js → chunk-OLHN4ZNT.js} +3 -3
- package/dist/{chunk-UR25IQVT.js → chunk-P5CUEB2Q.js} +8 -8
- package/dist/{chunk-LO775P3S.js → chunk-POGPI5GR.js} +2 -2
- package/dist/{chunk-FXO43NBW.js → chunk-PSQ2P2T3.js} +3 -3
- package/dist/{chunk-KRAMCBIW.js → chunk-QKXPOLQD.js} +4 -4
- package/dist/{chunk-65JLKIVA.js → chunk-QO5KDQGG.js} +8 -8
- package/dist/{chunk-A4B7WL5N.js → chunk-QOUKZ2N3.js} +2 -2
- package/dist/{chunk-ZABEDRGQ.js → chunk-RGKXF4W3.js} +2 -2
- package/dist/{chunk-IKCGNTGS.js → chunk-RWRHGE75.js} +2 -2
- package/dist/{chunk-NBH7IRME.js → chunk-SWZOAQUM.js} +54 -54
- package/dist/{chunk-FQ52AJXX.js → chunk-SZPPCEYT.js} +2 -2
- package/dist/{chunk-4YYIZ475.js → chunk-TAEJ2MRR.js} +52 -52
- package/dist/{chunk-3ODEGTVV.js → chunk-TDCR6PQL.js} +2 -2
- package/dist/{chunk-QCP75JGO.js → chunk-TFA6HPOA.js} +5 -5
- package/dist/{chunk-BBDASWNT.js → chunk-TJ5YGCPV.js} +81 -76
- package/dist/chunk-TJ5YGCPV.js.map +1 -0
- package/dist/{chunk-SE3JAD22.js → chunk-TPKB7T6Z.js} +2 -2
- package/dist/{chunk-6VYY2XFK.js → chunk-TVIGN3XZ.js} +19 -19
- package/dist/{chunk-OHMGR6QN.js → chunk-TXP7Q7T5.js} +2 -2
- package/dist/{chunk-ZG3HZZ6K.js → chunk-TYGLV6ZG.js} +5 -1
- package/dist/{chunk-ZG3HZZ6K.js.map → chunk-TYGLV6ZG.js.map} +1 -1
- package/dist/{chunk-AI2VXOSO.js → chunk-TYLNWE4D.js} +2 -2
- package/dist/{chunk-WI6MIJYK.js → chunk-U7HJBODU.js} +5 -5
- package/dist/{chunk-IS4HKNWP.js → chunk-UBN5AOLT.js} +2 -2
- package/dist/{chunk-A6PKEU7T.js → chunk-UFM3PJDT.js} +1 -1
- package/dist/chunk-UFM3PJDT.js.map +1 -0
- package/dist/{chunk-JNELS2TW.js → chunk-UJDTXWMT.js} +2 -2
- package/dist/{chunk-DJMC4W6N.js → chunk-VC3X7XP3.js} +2 -2
- package/dist/{chunk-A2D3WICT.js → chunk-VGFNTY3Y.js} +2 -2
- package/dist/{chunk-X544TW54.js → chunk-VGRM43X6.js} +2 -2
- package/dist/{chunk-CSGFGQBI.js → chunk-VPO53QMG.js} +2 -2
- package/dist/{chunk-BRZRP5WD.js → chunk-VSSQIBVU.js} +2 -2
- package/dist/{chunk-O3VKQMWC.js → chunk-WCBLPKG3.js} +366 -168
- package/dist/chunk-WCBLPKG3.js.map +1 -0
- package/dist/{chunk-GKNX4QO3.js → chunk-WCIDCJOQ.js} +3 -3
- package/dist/{chunk-7VJEZQYF.js → chunk-WEK3OUGO.js} +3 -3
- package/dist/{chunk-IVYAPAHG.js → chunk-WRDFYSUC.js} +2 -2
- package/dist/{chunk-5EILJ6W2.js → chunk-WXPBDEMW.js} +2 -2
- package/dist/{chunk-QZP3RMYP.js → chunk-X247CYL6.js} +4 -4
- package/dist/{chunk-UAYA4NJN.js → chunk-XHVO3CGX.js} +28 -36
- package/dist/chunk-XHVO3CGX.js.map +1 -0
- package/dist/{chunk-YLF42JQ7.js → chunk-XHX67LX6.js} +2 -2
- package/dist/{chunk-C6S2Y7HE.js → chunk-XKDQTDXN.js} +4 -4
- package/dist/{chunk-4AHKNZW7.js → chunk-XLEP7SS3.js} +2 -2
- package/dist/{chunk-B6L7BKA7.js → chunk-XRFLR3XF.js} +4 -4
- package/dist/{chunk-H7QC7WCU.js → chunk-XTBEXZFN.js} +4 -4
- package/dist/{chunk-CIJRM6SQ.js → chunk-Y5ZHZKZD.js} +2 -2
- package/dist/{chunk-H6SKBCNI.js → chunk-YC2C67F7.js} +2 -2
- package/dist/{chunk-EZ25VE3G.js → chunk-YNDLCWXS.js} +4 -4
- package/dist/{chunk-YOL56HY5.js → chunk-YQ6SLFNZ.js} +3 -3
- package/dist/{chunk-SIIWHIBK.js → chunk-YYGZ4EIY.js} +2 -2
- package/dist/{chunk-G22A6WJQ.js → chunk-Z2T5BPOZ.js} +2 -2
- package/dist/{chunk-RYUSWAAU.js → chunk-ZAP7BKTZ.js} +6 -6
- package/dist/cli.js +136 -136
- package/dist/coding/export-okf-codegraph.js +33 -33
- package/dist/coding/pre-action-gate.js +2 -2
- package/dist/compounding/engine.js +34 -34
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +43 -43
- package/dist/connectors/codex-materialize-runner.js +32 -32
- package/dist/connectors/index.js +32 -32
- package/dist/contradiction/index.js +5 -5
- package/dist/convergence-refresh.js +34 -34
- package/dist/conversation-index/backend.js +2 -2
- package/dist/corpus-watermark.js +34 -34
- package/dist/dashboard-runtime.js +2 -2
- package/dist/deep-recall-http-glue.js +9 -9
- package/dist/deep-recall-policy-llm.js +2 -2
- package/dist/entity-retrieval.js +34 -34
- package/dist/explicit-capture.js +7 -7
- package/dist/external-wiki-access.js +85 -85
- package/dist/extraction-faithfulness.js +2 -2
- package/dist/extraction-judge.d.ts +42 -2
- package/dist/extraction-judge.d.ts.map +1 -1
- package/dist/extraction-judge.js +12 -2
- package/dist/extraction-span-materialize.js +2 -2
- package/dist/extraction.js +47 -47
- package/dist/fallback-llm.d.ts +1 -0
- package/dist/fallback-llm.d.ts.map +1 -1
- package/dist/fallback-llm.js +1 -1
- package/dist/{graph-edge-decay-IU75NV42.js → graph-edge-decay-2BZTY2IM.js} +5 -5
- package/dist/graph-edge-decay-2BZTY2IM.js.map +1 -0
- package/dist/graph-jsonl.js +3 -3
- package/dist/graph-snapshot.js +3 -3
- package/dist/graph.js +2 -2
- package/dist/harmonic-retrieval.js +33 -33
- package/dist/harmonic-source-lookup.js +2 -2
- package/dist/index.js +236 -234
- package/dist/lcm/index.js +3 -3
- package/dist/location/index.js +35 -35
- package/dist/maintenance/memory-governance.js +32 -32
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +32 -32
- package/dist/maintenance/rebuild-memory-projection-runner.js +34 -34
- package/dist/maintenance/rebuild-memory-projection.js +33 -33
- package/dist/memory-browse-operations.js +85 -85
- package/dist/memory-browse.js +5 -5
- package/dist/memory-subject.js +3 -3
- package/dist/namespaces/migrate.js +33 -33
- package/dist/namespaces/search.js +11 -11
- package/dist/namespaces/storage.js +32 -32
- package/dist/offline-sync-changeset-generations.js +3 -3
- package/dist/offline-sync-embedding-generation.js +2 -2
- package/dist/offline-sync-file-io.js +2 -2
- package/dist/offline-sync-impression-drain.js +3 -3
- package/dist/offline-sync-runtime-state.d.ts.map +1 -1
- package/dist/offline-sync-runtime-state.js +1 -1
- package/dist/offline-sync-snapshot-stream.js +8 -8
- package/dist/offline-sync-upload-staging.js +3 -3
- package/dist/offline-sync.js +8 -8
- package/dist/operator-doctor-corpus.js +35 -35
- package/dist/operator-toolkit.js +54 -54
- package/dist/orchestration/maintenance.js +44 -44
- package/dist/orchestrator.js +183 -181
- package/dist/qmd.js +4 -4
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-navigate-access.js +2 -2
- package/dist/recall-navigate-disclosure.js +2 -2
- package/dist/recall-navigate.js +2 -2
- package/dist/recall-navigation-operations.js +85 -85
- package/dist/recall-navigation.js +84 -84
- package/dist/recall-planner-llm.js +1 -1
- package/dist/recall-state-view-admission.js +5 -5
- package/dist/recall-why-service.js +5 -5
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.js +2 -2
- package/dist/reconcile/manifest.js +3 -3
- package/dist/{resolution-V6JBMW7U.js → resolution-4ZEQPXUZ.js} +6 -6
- package/dist/resolution-4ZEQPXUZ.js.map +1 -0
- package/dist/resume-bundles.js +44 -44
- package/dist/retrieval-agents.js +2 -2
- package/dist/salvage-envelope.js +5 -5
- package/dist/search/factory.js +10 -10
- package/dist/search/index.js +14 -14
- package/dist/search/lancedb-backend.js +2 -2
- package/dist/search/meilisearch-backend.js +2 -2
- package/dist/search/orama-backend.js +2 -2
- package/dist/secure-store/index.js +2 -2
- package/dist/semantic-consolidation.js +33 -33
- package/dist/semantic-rule-promotion.js +32 -32
- package/dist/semantic-rule-verifier.js +32 -32
- package/dist/source-agent-qualifier.js +37 -37
- package/dist/standup/build.js +32 -32
- package/dist/storage/buffer-session-files.d.ts +69 -0
- package/dist/storage/buffer-session-files.d.ts.map +1 -0
- package/dist/storage.d.ts +5 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +31 -31
- package/dist/subject-config.js +4 -4
- package/dist/summarizer.js +2 -2
- package/dist/support-passport/index.js +87 -87
- package/dist/transfer/export-okf.js +33 -33
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/verified-recall.js +32 -32
- package/dist/who-knows-cli.js +33 -33
- package/dist/who-knows.js +32 -32
- package/dist/write-envelope.js +4 -4
- package/package.json +2 -2
- package/src/buffer-eviction.ts +51 -0
- package/src/buffer-incremental-persistence.test.ts +256 -0
- package/src/buffer-persistence.ts +89 -0
- package/src/buffer.ts +32 -61
- package/src/config.test.ts +16 -0
- package/src/config.ts +14 -0
- package/src/extraction-judge-chain.test.ts +298 -2
- package/src/extraction-judge.ts +147 -82
- package/src/fallback-llm.test.ts +94 -0
- package/src/fallback-llm.ts +7 -7
- package/src/offline-sync-runtime-state.ts +24 -2
- package/src/offline-sync.test.ts +9 -0
- package/src/secure-store/secure-fs.ts +11 -0
- package/src/secure-store/secure-store.test.ts +22 -0
- package/src/storage/buffer-session-files.ts +249 -0
- package/src/storage-buffer-sessions.test.ts +369 -0
- package/src/storage.ts +11 -16
- package/src/types.ts +12 -0
- package/dist/chunk-6FIFEXFF.js.map +0 -1
- package/dist/chunk-A6PKEU7T.js.map +0 -1
- package/dist/chunk-BBDASWNT.js.map +0 -1
- package/dist/chunk-GMMZ45C4.js.map +0 -1
- package/dist/chunk-O3VKQMWC.js.map +0 -1
- package/dist/chunk-TYT4TEQV.js.map +0 -1
- package/dist/chunk-UAYA4NJN.js.map +0 -1
- /package/dist/{auto-sync-VPHR3FBI.js.map → auto-sync-432USQDH.js.map} +0 -0
- /package/dist/{graph-edge-decay-IU75NV42.js.map → buffer-eviction.js.map} +0 -0
- /package/dist/{resolution-V6JBMW7U.js.map → buffer-persistence.js.map} +0 -0
- /package/dist/{chunk-FL6SLVAJ.js.map → chunk-2AKIVSHP.js.map} +0 -0
- /package/dist/{chunk-ORWDCX5Q.js.map → chunk-3AODCBKX.js.map} +0 -0
- /package/dist/{chunk-XFDHL252.js.map → chunk-3K5IEJTK.js.map} +0 -0
- /package/dist/{chunk-XVZA6VXC.js.map → chunk-3SL52JKD.js.map} +0 -0
- /package/dist/{chunk-ZSAQSLKL.js.map → chunk-3XVBGGF3.js.map} +0 -0
- /package/dist/{chunk-XO4HOBM2.js.map → chunk-4B37KNMP.js.map} +0 -0
- /package/dist/{chunk-NIANATGL.js.map → chunk-4IZ6AC2U.js.map} +0 -0
- /package/dist/{chunk-S2PDFSXS.js.map → chunk-4P4VLPGB.js.map} +0 -0
- /package/dist/{chunk-G5UR4DXQ.js.map → chunk-53BLBDV6.js.map} +0 -0
- /package/dist/{chunk-PDYP7SUC.js.map → chunk-5ANUG4JZ.js.map} +0 -0
- /package/dist/{chunk-2ZYIAUYU.js.map → chunk-5CWH2LKL.js.map} +0 -0
- /package/dist/{chunk-S2S4U4G3.js.map → chunk-5DCS4C4S.js.map} +0 -0
- /package/dist/{chunk-7WTTE5FW.js.map → chunk-5K23NQRI.js.map} +0 -0
- /package/dist/{chunk-V3JZZJ2E.js.map → chunk-5UPOKIFP.js.map} +0 -0
- /package/dist/{chunk-K567EIW3.js.map → chunk-63YPKHAU.js.map} +0 -0
- /package/dist/{chunk-G3T6YMBO.js.map → chunk-64S6X4IU.js.map} +0 -0
- /package/dist/{chunk-HJM23E65.js.map → chunk-6DTIYAAN.js.map} +0 -0
- /package/dist/{chunk-G7XAZU2L.js.map → chunk-6F5BGIAF.js.map} +0 -0
- /package/dist/{chunk-IHTKBQBS.js.map → chunk-6JEEY42N.js.map} +0 -0
- /package/dist/{chunk-NHB4RC3G.js.map → chunk-6QOL4G76.js.map} +0 -0
- /package/dist/{chunk-AD3ZB72W.js.map → chunk-6SYS5YX3.js.map} +0 -0
- /package/dist/{chunk-DFP37GUN.js.map → chunk-7HXZOWTR.js.map} +0 -0
- /package/dist/{chunk-DGBWPSM6.js.map → chunk-7VFNGQ4V.js.map} +0 -0
- /package/dist/{chunk-77ZK533I.js.map → chunk-7ZRRJ6RG.js.map} +0 -0
- /package/dist/{chunk-QISYYK4L.js.map → chunk-AYBAC2N4.js.map} +0 -0
- /package/dist/{chunk-V44DWTFA.js.map → chunk-AYXRK264.js.map} +0 -0
- /package/dist/{chunk-LJSBF6PW.js.map → chunk-C2KOW2UQ.js.map} +0 -0
- /package/dist/{chunk-GY4R4P2A.js.map → chunk-CUWOSRCS.js.map} +0 -0
- /package/dist/{chunk-VNAIUNXI.js.map → chunk-CVMAOPAH.js.map} +0 -0
- /package/dist/{chunk-CJKHLACF.js.map → chunk-DG4H56SK.js.map} +0 -0
- /package/dist/{chunk-BQ6DGL6A.js.map → chunk-DKHX2FHI.js.map} +0 -0
- /package/dist/{chunk-77IXBNV6.js.map → chunk-E24RUQEN.js.map} +0 -0
- /package/dist/{chunk-HRT2SJPC.js.map → chunk-E3NBMTTX.js.map} +0 -0
- /package/dist/{chunk-PTQZPVRQ.js.map → chunk-E5PVMB3I.js.map} +0 -0
- /package/dist/{chunk-AHS3QEEA.js.map → chunk-EEOYF3IB.js.map} +0 -0
- /package/dist/{chunk-5XTSIZ3L.js.map → chunk-EGNONTVT.js.map} +0 -0
- /package/dist/{chunk-PH33R7GN.js.map → chunk-EHIKY2IA.js.map} +0 -0
- /package/dist/{chunk-4ISHIPOC.js.map → chunk-FGEY2AGV.js.map} +0 -0
- /package/dist/{chunk-O6YJI5VX.js.map → chunk-FKTMWDN2.js.map} +0 -0
- /package/dist/{chunk-KQUN4RSX.js.map → chunk-FQFMLOAC.js.map} +0 -0
- /package/dist/{chunk-5LWODP5R.js.map → chunk-FRAL3EU4.js.map} +0 -0
- /package/dist/{chunk-AAANTXCQ.js.map → chunk-FZVZYU5C.js.map} +0 -0
- /package/dist/{chunk-TGAXRT7N.js.map → chunk-GFYFKXWZ.js.map} +0 -0
- /package/dist/{chunk-ID2PLENW.js.map → chunk-GGI6FLO6.js.map} +0 -0
- /package/dist/{chunk-GDYLZ6FR.js.map → chunk-HJ3UTGIW.js.map} +0 -0
- /package/dist/{chunk-X3IFRQKS.js.map → chunk-I73ZSORJ.js.map} +0 -0
- /package/dist/{chunk-X73SCHKR.js.map → chunk-I75BWXRZ.js.map} +0 -0
- /package/dist/{chunk-M3GKSCFN.js.map → chunk-J7G656MY.js.map} +0 -0
- /package/dist/{chunk-EH6KLYIE.js.map → chunk-JF3MEIHO.js.map} +0 -0
- /package/dist/{chunk-XUB4I2N6.js.map → chunk-JFI3ATK4.js.map} +0 -0
- /package/dist/{chunk-R4SATHCV.js.map → chunk-JH2ALMPR.js.map} +0 -0
- /package/dist/{chunk-TKBW4YLS.js.map → chunk-JH4FCVWJ.js.map} +0 -0
- /package/dist/{chunk-DG5UKN2Y.js.map → chunk-JIA435P3.js.map} +0 -0
- /package/dist/{chunk-4Z7O2QVJ.js.map → chunk-KOAVX2IT.js.map} +0 -0
- /package/dist/{chunk-6NXHX6MI.js.map → chunk-KOY25HUB.js.map} +0 -0
- /package/dist/{chunk-TRW7N4FS.js.map → chunk-L2JQSG5M.js.map} +0 -0
- /package/dist/{chunk-QQ4T622E.js.map → chunk-L7PGPH4E.js.map} +0 -0
- /package/dist/{chunk-23VGZ65V.js.map → chunk-LHBOFROT.js.map} +0 -0
- /package/dist/{chunk-VKB2FGNW.js.map → chunk-LOMSBYNS.js.map} +0 -0
- /package/dist/{chunk-NDJ42GKB.js.map → chunk-MCPSMWUC.js.map} +0 -0
- /package/dist/{chunk-ERMT5WQM.js.map → chunk-MKE5DP3S.js.map} +0 -0
- /package/dist/{chunk-64IO6ZEE.js.map → chunk-MZWHIAOL.js.map} +0 -0
- /package/dist/{chunk-LDXS746V.js.map → chunk-N7DVBHU3.js.map} +0 -0
- /package/dist/{chunk-F2JLBXMG.js.map → chunk-NH6GZEQN.js.map} +0 -0
- /package/dist/{chunk-7FG2JSU6.js.map → chunk-NMKETOAX.js.map} +0 -0
- /package/dist/{chunk-GAARPWMN.js.map → chunk-OKKBOTRA.js.map} +0 -0
- /package/dist/{chunk-PCIIKQXW.js.map → chunk-OLH4XCLU.js.map} +0 -0
- /package/dist/{chunk-XA32GZ6B.js.map → chunk-OLHN4ZNT.js.map} +0 -0
- /package/dist/{chunk-UR25IQVT.js.map → chunk-P5CUEB2Q.js.map} +0 -0
- /package/dist/{chunk-LO775P3S.js.map → chunk-POGPI5GR.js.map} +0 -0
- /package/dist/{chunk-FXO43NBW.js.map → chunk-PSQ2P2T3.js.map} +0 -0
- /package/dist/{chunk-KRAMCBIW.js.map → chunk-QKXPOLQD.js.map} +0 -0
- /package/dist/{chunk-65JLKIVA.js.map → chunk-QO5KDQGG.js.map} +0 -0
- /package/dist/{chunk-A4B7WL5N.js.map → chunk-QOUKZ2N3.js.map} +0 -0
- /package/dist/{chunk-ZABEDRGQ.js.map → chunk-RGKXF4W3.js.map} +0 -0
- /package/dist/{chunk-IKCGNTGS.js.map → chunk-RWRHGE75.js.map} +0 -0
- /package/dist/{chunk-NBH7IRME.js.map → chunk-SWZOAQUM.js.map} +0 -0
- /package/dist/{chunk-FQ52AJXX.js.map → chunk-SZPPCEYT.js.map} +0 -0
- /package/dist/{chunk-4YYIZ475.js.map → chunk-TAEJ2MRR.js.map} +0 -0
- /package/dist/{chunk-3ODEGTVV.js.map → chunk-TDCR6PQL.js.map} +0 -0
- /package/dist/{chunk-QCP75JGO.js.map → chunk-TFA6HPOA.js.map} +0 -0
- /package/dist/{chunk-SE3JAD22.js.map → chunk-TPKB7T6Z.js.map} +0 -0
- /package/dist/{chunk-6VYY2XFK.js.map → chunk-TVIGN3XZ.js.map} +0 -0
- /package/dist/{chunk-OHMGR6QN.js.map → chunk-TXP7Q7T5.js.map} +0 -0
- /package/dist/{chunk-AI2VXOSO.js.map → chunk-TYLNWE4D.js.map} +0 -0
- /package/dist/{chunk-WI6MIJYK.js.map → chunk-U7HJBODU.js.map} +0 -0
- /package/dist/{chunk-IS4HKNWP.js.map → chunk-UBN5AOLT.js.map} +0 -0
- /package/dist/{chunk-JNELS2TW.js.map → chunk-UJDTXWMT.js.map} +0 -0
- /package/dist/{chunk-DJMC4W6N.js.map → chunk-VC3X7XP3.js.map} +0 -0
- /package/dist/{chunk-A2D3WICT.js.map → chunk-VGFNTY3Y.js.map} +0 -0
- /package/dist/{chunk-X544TW54.js.map → chunk-VGRM43X6.js.map} +0 -0
- /package/dist/{chunk-CSGFGQBI.js.map → chunk-VPO53QMG.js.map} +0 -0
- /package/dist/{chunk-BRZRP5WD.js.map → chunk-VSSQIBVU.js.map} +0 -0
- /package/dist/{chunk-GKNX4QO3.js.map → chunk-WCIDCJOQ.js.map} +0 -0
- /package/dist/{chunk-7VJEZQYF.js.map → chunk-WEK3OUGO.js.map} +0 -0
- /package/dist/{chunk-IVYAPAHG.js.map → chunk-WRDFYSUC.js.map} +0 -0
- /package/dist/{chunk-5EILJ6W2.js.map → chunk-WXPBDEMW.js.map} +0 -0
- /package/dist/{chunk-QZP3RMYP.js.map → chunk-X247CYL6.js.map} +0 -0
- /package/dist/{chunk-YLF42JQ7.js.map → chunk-XHX67LX6.js.map} +0 -0
- /package/dist/{chunk-C6S2Y7HE.js.map → chunk-XKDQTDXN.js.map} +0 -0
- /package/dist/{chunk-4AHKNZW7.js.map → chunk-XLEP7SS3.js.map} +0 -0
- /package/dist/{chunk-B6L7BKA7.js.map → chunk-XRFLR3XF.js.map} +0 -0
- /package/dist/{chunk-H7QC7WCU.js.map → chunk-XTBEXZFN.js.map} +0 -0
- /package/dist/{chunk-CIJRM6SQ.js.map → chunk-Y5ZHZKZD.js.map} +0 -0
- /package/dist/{chunk-H6SKBCNI.js.map → chunk-YC2C67F7.js.map} +0 -0
- /package/dist/{chunk-EZ25VE3G.js.map → chunk-YNDLCWXS.js.map} +0 -0
- /package/dist/{chunk-YOL56HY5.js.map → chunk-YQ6SLFNZ.js.map} +0 -0
- /package/dist/{chunk-SIIWHIBK.js.map → chunk-YYGZ4EIY.js.map} +0 -0
- /package/dist/{chunk-G22A6WJQ.js.map → chunk-Z2T5BPOZ.js.map} +0 -0
- /package/dist/{chunk-RYUSWAAU.js.map → chunk-ZAP7BKTZ.js.map} +0 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { Stats } from "node:fs";
|
|
3
|
+
import { lstat, readdir, unlink } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { log } from "../logger.js";
|
|
6
|
+
import { SecureStoreDecryptError, SecureStoreLockedError } from "../secure-store/secure-fs.js";
|
|
7
|
+
import type { BufferEntryState, BufferState, BufferTurn } from "../types.js";
|
|
8
|
+
|
|
9
|
+
/** The secure-store file I/O the owning StorageManager already provides. */
|
|
10
|
+
export interface BufferFileIo {
|
|
11
|
+
read(filePath: string): Promise<string>;
|
|
12
|
+
write(filePath: string, content: string): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type FsError = NodeJS.ErrnoException;
|
|
16
|
+
|
|
17
|
+
function isMissing(err: FsError | null): boolean {
|
|
18
|
+
return err?.code === "ENOENT";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function asFsError(err: Error): FsError {
|
|
22
|
+
return err;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Case-independent, collision-free file name for a buffer key: the lowercase
|
|
27
|
+
* hex SHA-256 of the key under a fixed prefix. Hex has no case and no path
|
|
28
|
+
* characters, so `Session-A` and `session-a` get different names even on
|
|
29
|
+
* case-insensitive filesystems, every key (short or long) lives in the same
|
|
30
|
+
* naming domain, and the key itself is stored inside the file (loading never
|
|
31
|
+
* inverts the hash).
|
|
32
|
+
*/
|
|
33
|
+
export function bufferSessionFileName(key: string): string {
|
|
34
|
+
return `s-${createHash("sha256").update(key).digest("hex")}.json`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Flatten a legacy BufferState into per-session entries (top-level fields
|
|
38
|
+
* are the legacy mirror of the "default" entry). */
|
|
39
|
+
export function legacyStateToEntries(state: BufferState): Map<string, BufferEntryState> {
|
|
40
|
+
const entries = new Map<string, BufferEntryState>();
|
|
41
|
+
for (const [key, value] of Object.entries(state.entries ?? {})) {
|
|
42
|
+
if (value && Array.isArray(value.turns)) entries.set(key, value);
|
|
43
|
+
}
|
|
44
|
+
if (!entries.has("default")) {
|
|
45
|
+
entries.set("default", {
|
|
46
|
+
turns: Array.isArray(state.turns) ? state.turns : [],
|
|
47
|
+
lastExtractionAt: state.lastExtractionAt ?? null,
|
|
48
|
+
extractionCount: Number.isFinite(state.extractionCount) ? state.extractionCount : 0,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return entries;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** What a per-session file contains once parsed (written only by this module). */
|
|
55
|
+
interface SessionFileBody {
|
|
56
|
+
key?: string;
|
|
57
|
+
turns?: BufferTurn[];
|
|
58
|
+
retainedTurns?: BufferTurn[];
|
|
59
|
+
lastExtractionAt?: string | null;
|
|
60
|
+
extractionCount?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function entryFromBody(body: SessionFileBody): { key: string; entry: BufferEntryState } | null {
|
|
64
|
+
if (!body || !Array.isArray(body.turns) || (body.key?.length ?? 0) === 0) return null;
|
|
65
|
+
const entry: BufferEntryState = {
|
|
66
|
+
turns: body.turns,
|
|
67
|
+
lastExtractionAt: body.lastExtractionAt ?? null,
|
|
68
|
+
extractionCount: Number.isFinite(body.extractionCount) ? Number(body.extractionCount) : 0,
|
|
69
|
+
};
|
|
70
|
+
if (Array.isArray(body.retainedTurns)) entry.retainedTurns = body.retainedTurns;
|
|
71
|
+
return { key: String(body.key), entry };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* One file per buffer session under `<stateDir>/buffer/`, so the observe hot
|
|
76
|
+
* path persists only the session that changed. All writes happen under
|
|
77
|
+
* SmartBuffer's serialized mutation chain (one writer per process).
|
|
78
|
+
*/
|
|
79
|
+
export class BufferSessionFiles {
|
|
80
|
+
private dirVerified = false;
|
|
81
|
+
|
|
82
|
+
constructor(
|
|
83
|
+
private readonly stateDir: string,
|
|
84
|
+
private readonly io: BufferFileIo,
|
|
85
|
+
) {}
|
|
86
|
+
|
|
87
|
+
private get dir(): string {
|
|
88
|
+
return path.join(this.stateDir, "buffer");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private fileFor(key: string): string {
|
|
92
|
+
return path.join(this.dir, bufferSessionFileName(key));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Refuse a symlinked buffer directory: following it would send buffered
|
|
97
|
+
* conversation data (and deletions) outside the memory root. One lstat of
|
|
98
|
+
* one directory, cached after it passes, so the observe path pays nothing
|
|
99
|
+
* per write; the cache is dropped on any failure.
|
|
100
|
+
*/
|
|
101
|
+
private async assertDirSafe(): Promise<void> {
|
|
102
|
+
if (this.dirVerified) return;
|
|
103
|
+
let stat: Stats | null = null;
|
|
104
|
+
try {
|
|
105
|
+
stat = await lstat(this.dir);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
if (!(err instanceof Error) || !isMissing(asFsError(err))) throw err;
|
|
108
|
+
}
|
|
109
|
+
if (stat && (stat.isSymbolicLink() || !stat.isDirectory())) {
|
|
110
|
+
throw new Error(`buffer session directory must be a real directory, not a symlink or file: ${this.dir}`);
|
|
111
|
+
}
|
|
112
|
+
this.dirVerified = stat !== null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Persist ONE session atomically. Compact JSON: machine-read state, and
|
|
116
|
+
* pretty-printing a large buffer costs CPU and bytes on every observe. */
|
|
117
|
+
async save(key: string, entry: BufferEntryState): Promise<void> {
|
|
118
|
+
await this.assertDirSafe();
|
|
119
|
+
await this.io.write(this.fileFor(key), JSON.stringify({ key, ...entry }));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Remove one session's file. Idempotent: an absent file is fine. */
|
|
123
|
+
async remove(key: string): Promise<void> {
|
|
124
|
+
await this.assertDirSafe();
|
|
125
|
+
try {
|
|
126
|
+
await unlink(this.fileFor(key));
|
|
127
|
+
} catch (err) {
|
|
128
|
+
if (!(err instanceof Error) || !isMissing(asFsError(err))) throw err;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private async listFiles(): Promise<string[]> {
|
|
133
|
+
await this.assertDirSafe();
|
|
134
|
+
try {
|
|
135
|
+
const dirents = await readdir(this.dir, { withFileTypes: true });
|
|
136
|
+
// Regular files only: a symlink inside the directory is never followed.
|
|
137
|
+
return dirents.filter((d) => d.isFile() && d.name.endsWith(".json")).map((d) => d.name);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
if (err instanceof Error && isMissing(asFsError(err))) return [];
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Read every per-session file. Corrupt files are skipped (and logged) so
|
|
145
|
+
* one bad file cannot nuke the other sessions' buffered turns. */
|
|
146
|
+
private async readAll(): Promise<{ entries: Map<string, BufferEntryState>; fileCount: number }> {
|
|
147
|
+
const names = await this.listFiles();
|
|
148
|
+
const entries = new Map<string, BufferEntryState>();
|
|
149
|
+
for (const name of names) {
|
|
150
|
+
try {
|
|
151
|
+
const body: SessionFileBody = JSON.parse(await this.io.read(path.join(this.dir, name)));
|
|
152
|
+
const parsed = entryFromBody(body);
|
|
153
|
+
if (!parsed) throw new Error("invalid buffer session file shape");
|
|
154
|
+
if (entries.has(parsed.key)) {
|
|
155
|
+
log.warn(`buffer: duplicate session file for key ${parsed.key} (${name}); keeping the first`);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
entries.set(parsed.key, parsed.entry);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
// A locked store or a wrong key is not a corrupt file: reading it as empty would let
|
|
161
|
+
// later saves replace the real encrypted session.
|
|
162
|
+
if (err instanceof SecureStoreLockedError || err instanceof SecureStoreDecryptError) throw err;
|
|
163
|
+
log.warn(`buffer: skipping unreadable per-session buffer file ${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return { entries, fileCount: names.length };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Read the legacy single-file buffer.json for migration. Null when absent,
|
|
171
|
+
* or unreadable while session files exist (they are authoritative; a
|
|
172
|
+
* corrupt leftover must not block startup). With no session files a corrupt
|
|
173
|
+
* legacy file THROWS, preserving the old contract so callers can reset it.
|
|
174
|
+
*/
|
|
175
|
+
private async readLegacy(hasSessionFiles: boolean): Promise<BufferState | null> {
|
|
176
|
+
let raw: string;
|
|
177
|
+
try {
|
|
178
|
+
raw = await this.io.read(path.join(this.stateDir, "buffer.json"));
|
|
179
|
+
} catch (err) {
|
|
180
|
+
if (err instanceof SecureStoreLockedError) throw err;
|
|
181
|
+
if (err instanceof Error && isMissing(asFsError(err))) return null;
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const legacy: BufferState = JSON.parse(raw);
|
|
186
|
+
return legacy;
|
|
187
|
+
} catch (err) {
|
|
188
|
+
if (!hasSessionFiles) throw err;
|
|
189
|
+
log.warn(
|
|
190
|
+
`buffer: legacy buffer.json is unreadable but per-session buffer files exist; ` +
|
|
191
|
+
`ignoring the legacy file (${err instanceof Error ? err.message : String(err)})`,
|
|
192
|
+
);
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Load the whole buffer, migrating a legacy buffer.json once. A per-session
|
|
199
|
+
* file that parsed wins over the legacy snapshot (a crash mid-migration
|
|
200
|
+
* leaves exactly that pair); legacy entries without a usable file are
|
|
201
|
+
* written out, then the legacy file is retired. Both steps are idempotent.
|
|
202
|
+
*/
|
|
203
|
+
async load(): Promise<BufferState> {
|
|
204
|
+
const { entries, fileCount } = await this.readAll();
|
|
205
|
+
const legacy = await this.readLegacy(fileCount > 0);
|
|
206
|
+
if (legacy !== null) {
|
|
207
|
+
for (const [key, entry] of legacyStateToEntries(legacy)) {
|
|
208
|
+
if (entries.has(key)) continue;
|
|
209
|
+
entries.set(key, entry);
|
|
210
|
+
await this.save(key, entry);
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
await unlink(path.join(this.stateDir, "buffer.json"));
|
|
214
|
+
} catch (err) {
|
|
215
|
+
if (!(err instanceof Error) || !isMissing(asFsError(err))) throw err;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const out: NonNullable<BufferState["entries"]> = Object.create(null);
|
|
219
|
+
for (const [key, entry] of entries) out[key] = entry;
|
|
220
|
+
const def = entries.get("default");
|
|
221
|
+
return {
|
|
222
|
+
turns: def?.turns ?? [],
|
|
223
|
+
lastExtractionAt: def?.lastExtractionAt ?? null,
|
|
224
|
+
extractionCount: def?.extractionCount ?? 0,
|
|
225
|
+
entries: out,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Reconcile the files with a WHOLE buffer state: write every entry, then
|
|
231
|
+
* delete files for keys no longer present. Writes first, deletes last: a
|
|
232
|
+
* crash leaves stale extra files (removed by the next reconcile) but never
|
|
233
|
+
* loses sessions the state says exist. Not on the observe hot path.
|
|
234
|
+
*/
|
|
235
|
+
async reconcile(state: BufferState): Promise<void> {
|
|
236
|
+
const entries = legacyStateToEntries(state);
|
|
237
|
+
const existing = await this.listFiles();
|
|
238
|
+
for (const [key, entry] of entries) await this.save(key, entry);
|
|
239
|
+
const live = new Set([...entries.keys()].map((key) => bufferSessionFileName(key)));
|
|
240
|
+
for (const name of existing) {
|
|
241
|
+
if (live.has(name)) continue;
|
|
242
|
+
try {
|
|
243
|
+
await unlink(path.join(this.dir, name));
|
|
244
|
+
} catch (err) {
|
|
245
|
+
if (!(err instanceof Error) || !isMissing(asFsError(err))) throw err;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-session buffer persistence (storage layer).
|
|
3
|
+
*
|
|
4
|
+
* The legacy buffer persistence rewrote the WHOLE state/buffer.json — every
|
|
5
|
+
* session's turns — on every buffer save, which on long-running daemons meant
|
|
6
|
+
* serializing 100+ MB per observe. These tests pin the replacement contract:
|
|
7
|
+
*
|
|
8
|
+
* - one file per session under `<memoryDir>/state/buffer/`,
|
|
9
|
+
* - incremental `saveBufferSession` / `deleteBufferSession` for the hot path,
|
|
10
|
+
* - whole-state `saveBuffer` as a full reconcile (used at correctness
|
|
11
|
+
* boundaries and by legacy callers),
|
|
12
|
+
* - one-time migration from the legacy single-file format, safe when a crash
|
|
13
|
+
* lands mid-migration (per-session file wins over the legacy snapshot),
|
|
14
|
+
* - per-session writes do NOT run the heavyweight `ensureDirectories()`
|
|
15
|
+
* pass (the realpath/lstat storm) — they mkdir only what the write needs.
|
|
16
|
+
*/
|
|
17
|
+
import assert from "node:assert/strict";
|
|
18
|
+
import { mkdir, mkdtemp, readdir, readFile, rm, stat, symlink, writeFile } from "node:fs/promises";
|
|
19
|
+
import os from "node:os";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import test from "node:test";
|
|
22
|
+
|
|
23
|
+
import { SecureStoreDecryptError } from "./secure-store/secure-fs.js";
|
|
24
|
+
import { StorageManager } from "./storage.js";
|
|
25
|
+
import { bufferSessionFileName } from "./storage/buffer-session-files.js";
|
|
26
|
+
import type { BufferEntryState, BufferState } from "./types.js";
|
|
27
|
+
|
|
28
|
+
async function tempRoot(prefix: string): Promise<string> {
|
|
29
|
+
return mkdtemp(path.join(os.tmpdir(), `${prefix}-`));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function makeEntry(turnContent: string, extractionCount = 0): BufferEntryState {
|
|
33
|
+
return {
|
|
34
|
+
turns: [
|
|
35
|
+
{
|
|
36
|
+
role: "user",
|
|
37
|
+
content: turnContent,
|
|
38
|
+
timestamp: "2026-10-29T12:00:00.000Z",
|
|
39
|
+
sessionKey: "session-a",
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
lastExtractionAt: extractionCount > 0 ? "2026-10-29T12:01:00.000Z" : null,
|
|
43
|
+
extractionCount,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
test("saveBufferSession writes one file per session and loadBuffer reads it back", async () => {
|
|
48
|
+
const root = await tempRoot("bufio-sessions");
|
|
49
|
+
try {
|
|
50
|
+
const storage = new StorageManager(root);
|
|
51
|
+
await storage.saveBufferSession("session-a", makeEntry("alpha fact"));
|
|
52
|
+
|
|
53
|
+
const bufferDir = path.join(root, "state", "buffer");
|
|
54
|
+
const names = await readdir(bufferDir);
|
|
55
|
+
assert.equal(names.length, 1, `expected exactly one session file, got ${names.join(",")}`);
|
|
56
|
+
|
|
57
|
+
const state = await storage.loadBuffer();
|
|
58
|
+
assert.equal(state.entries?.["session-a"]?.turns.length, 1);
|
|
59
|
+
assert.equal(state.entries?.["session-a"]?.turns[0]?.content, "alpha fact");
|
|
60
|
+
} finally {
|
|
61
|
+
await rm(root, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("saving session B leaves session A's file byte-identical", async () => {
|
|
66
|
+
const root = await tempRoot("bufio-isolation");
|
|
67
|
+
try {
|
|
68
|
+
const storage = new StorageManager(root);
|
|
69
|
+
await storage.saveBufferSession("session-a", makeEntry("alpha fact"));
|
|
70
|
+
const dir = path.join(root, "state", "buffer");
|
|
71
|
+
const [aName] = await readdir(dir);
|
|
72
|
+
const before = await readFile(path.join(dir, aName));
|
|
73
|
+
const beforeStat = await stat(path.join(dir, aName));
|
|
74
|
+
|
|
75
|
+
await storage.saveBufferSession("session-b", makeEntry("beta fact"));
|
|
76
|
+
const after = await readFile(path.join(dir, aName));
|
|
77
|
+
const afterStat = await stat(path.join(dir, aName));
|
|
78
|
+
assert.deepEqual(after, before, "session A file content changed when session B was saved");
|
|
79
|
+
assert.equal(afterStat.mtimeMs, beforeStat.mtimeMs, "session A file was rewritten when session B was saved");
|
|
80
|
+
} finally {
|
|
81
|
+
await rm(root, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("deleteBufferSession removes the file and loadBuffer drops the session", async () => {
|
|
86
|
+
const root = await tempRoot("bufio-delete");
|
|
87
|
+
try {
|
|
88
|
+
const storage = new StorageManager(root);
|
|
89
|
+
await storage.saveBufferSession("session-a", makeEntry("alpha fact"));
|
|
90
|
+
await storage.deleteBufferSession("session-a");
|
|
91
|
+
const state = await storage.loadBuffer();
|
|
92
|
+
assert.equal(state.entries?.["session-a"], undefined);
|
|
93
|
+
// Deleting an absent session is a no-op, not a throw (idempotent cleanup).
|
|
94
|
+
await storage.deleteBufferSession("session-a");
|
|
95
|
+
} finally {
|
|
96
|
+
await rm(root, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("legacy state/buffer.json migrates to per-session files exactly once", async () => {
|
|
101
|
+
const root = await tempRoot("bufio-migrate");
|
|
102
|
+
try {
|
|
103
|
+
const stateDir = path.join(root, "state");
|
|
104
|
+
await mkdir(stateDir, { recursive: true });
|
|
105
|
+
const legacy: BufferState = {
|
|
106
|
+
turns: [{ role: "user", content: "default turn", timestamp: "2026-10-29T12:00:00.000Z", sessionKey: "default" }],
|
|
107
|
+
lastExtractionAt: null,
|
|
108
|
+
extractionCount: 2,
|
|
109
|
+
entries: {
|
|
110
|
+
default: {
|
|
111
|
+
turns: [{ role: "user", content: "default turn", timestamp: "2026-10-29T12:00:00.000Z", sessionKey: "default" }],
|
|
112
|
+
lastExtractionAt: null,
|
|
113
|
+
extractionCount: 2,
|
|
114
|
+
},
|
|
115
|
+
"session-a": makeEntry("alpha fact"),
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
await writeFile(path.join(stateDir, "buffer.json"), JSON.stringify(legacy), "utf8");
|
|
119
|
+
|
|
120
|
+
const storage = new StorageManager(root);
|
|
121
|
+
const loaded = await storage.loadBuffer();
|
|
122
|
+
assert.equal(loaded.entries?.["session-a"]?.turns[0]?.content, "alpha fact");
|
|
123
|
+
assert.equal(loaded.entries?.default?.extractionCount, 2);
|
|
124
|
+
|
|
125
|
+
// Migration is materialized: per-session files exist, the legacy file is gone.
|
|
126
|
+
const names = await readdir(path.join(stateDir, "buffer"));
|
|
127
|
+
assert.equal(names.filter((n) => n.endsWith(".json")).length, 2, "expected one file per legacy entry");
|
|
128
|
+
await assert.rejects(stat(path.join(stateDir, "buffer.json")), { code: "ENOENT" });
|
|
129
|
+
|
|
130
|
+
// Second load reads purely from the per-session files.
|
|
131
|
+
const reloaded = await storage.loadBuffer();
|
|
132
|
+
assert.equal(reloaded.entries?.["session-a"]?.turns[0]?.content, "alpha fact");
|
|
133
|
+
} finally {
|
|
134
|
+
await rm(root, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("crash mid-migration: existing per-session file wins over the legacy snapshot", async () => {
|
|
139
|
+
const root = await tempRoot("bufio-migrate-crash");
|
|
140
|
+
try {
|
|
141
|
+
const stateDir = path.join(root, "state");
|
|
142
|
+
await mkdir(path.join(stateDir, "buffer"), { recursive: true });
|
|
143
|
+
// Session A was already migrated (with a NEWER turn) before the crash;
|
|
144
|
+
// the legacy file still holds the older snapshot for both sessions.
|
|
145
|
+
await storage_writeSession(stateDir, "session-a", makeEntry("alpha fact NEW"));
|
|
146
|
+
const legacy: BufferState = {
|
|
147
|
+
turns: [],
|
|
148
|
+
lastExtractionAt: null,
|
|
149
|
+
extractionCount: 0,
|
|
150
|
+
entries: {
|
|
151
|
+
"session-a": makeEntry("alpha fact OLD"),
|
|
152
|
+
"session-b": makeEntry("beta fact"),
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
await writeFile(path.join(stateDir, "buffer.json"), JSON.stringify(legacy), "utf8");
|
|
156
|
+
|
|
157
|
+
const storage = new StorageManager(root);
|
|
158
|
+
const loaded = await storage.loadBuffer();
|
|
159
|
+
assert.equal(loaded.entries?.["session-a"]?.turns[0]?.content, "alpha fact NEW");
|
|
160
|
+
assert.equal(loaded.entries?.["session-b"]?.turns[0]?.content, "beta fact");
|
|
161
|
+
await assert.rejects(stat(path.join(stateDir, "buffer.json")), { code: "ENOENT" });
|
|
162
|
+
} finally {
|
|
163
|
+
await rm(root, { recursive: true, force: true });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
async function storage_writeSession(stateDir: string, key: string, entry: BufferEntryState): Promise<void> {
|
|
168
|
+
// Written directly (pre-migration crash simulation) using the same file naming
|
|
169
|
+
// the storage layer derives; the storage instance is created after setup.
|
|
170
|
+
await writeFile(
|
|
171
|
+
path.join(stateDir, "buffer", bufferSessionFileName(key)),
|
|
172
|
+
JSON.stringify({ key, ...entry }),
|
|
173
|
+
"utf8",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
test("whole-state saveBuffer reconciles per-session files (writes, deletes stale)", async () => {
|
|
178
|
+
const root = await tempRoot("bufio-reconcile");
|
|
179
|
+
try {
|
|
180
|
+
const storage = new StorageManager(root);
|
|
181
|
+
await storage.saveBufferSession("session-a", makeEntry("alpha fact"));
|
|
182
|
+
await storage.saveBufferSession("session-gone", makeEntry("stale fact"));
|
|
183
|
+
|
|
184
|
+
await storage.saveBuffer({
|
|
185
|
+
turns: [],
|
|
186
|
+
lastExtractionAt: null,
|
|
187
|
+
extractionCount: 0,
|
|
188
|
+
entries: { "session-a": makeEntry("alpha fact v2") },
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const state = await storage.loadBuffer();
|
|
192
|
+
assert.equal(state.entries?.["session-a"]?.turns[0]?.content, "alpha fact v2");
|
|
193
|
+
assert.equal(state.entries?.["session-gone"], undefined, "stale session file survived a whole-state reconcile");
|
|
194
|
+
const names = await readdir(path.join(root, "state", "buffer"));
|
|
195
|
+
// Reconcile materializes the "default" entry too (legacy semantics: the
|
|
196
|
+
// top-level state IS the default session), so expect exactly those two.
|
|
197
|
+
assert.deepEqual(
|
|
198
|
+
names.filter((n) => n.endsWith(".json")).sort(),
|
|
199
|
+
[bufferSessionFileName("session-a"), bufferSessionFileName("default")].sort(),
|
|
200
|
+
);
|
|
201
|
+
} finally {
|
|
202
|
+
await rm(root, { recursive: true, force: true });
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("a corrupt per-session file is skipped; the rest of the buffer loads", async () => {
|
|
207
|
+
const root = await tempRoot("bufio-corrupt-file");
|
|
208
|
+
try {
|
|
209
|
+
const storage = new StorageManager(root);
|
|
210
|
+
await storage.saveBufferSession("session-a", makeEntry("alpha fact"));
|
|
211
|
+
await storage.saveBufferSession("session-b", makeEntry("beta fact"));
|
|
212
|
+
const dir = path.join(root, "state", "buffer");
|
|
213
|
+
const names = (await readdir(dir)).filter((n) => n === bufferSessionFileName("session-b"));
|
|
214
|
+
await writeFile(path.join(dir, names[0]), "{not json", "utf8");
|
|
215
|
+
|
|
216
|
+
const state = await storage.loadBuffer();
|
|
217
|
+
assert.equal(state.entries?.["session-a"]?.turns[0]?.content, "alpha fact");
|
|
218
|
+
assert.equal(state.entries?.["session-b"], undefined);
|
|
219
|
+
} finally {
|
|
220
|
+
await rm(root, { recursive: true, force: true });
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("a corrupt session file is replaced by the legacy snapshot during migration", async () => {
|
|
225
|
+
const root = await tempRoot("bufio-corrupt-plus-legacy");
|
|
226
|
+
try {
|
|
227
|
+
const stateDir = path.join(root, "state");
|
|
228
|
+
await mkdir(path.join(stateDir, "buffer"), { recursive: true });
|
|
229
|
+
await writeFile(
|
|
230
|
+
path.join(stateDir, "buffer", bufferSessionFileName("session-a")),
|
|
231
|
+
"{not json",
|
|
232
|
+
"utf8",
|
|
233
|
+
);
|
|
234
|
+
const legacy: BufferState = {
|
|
235
|
+
turns: [],
|
|
236
|
+
lastExtractionAt: null,
|
|
237
|
+
extractionCount: 0,
|
|
238
|
+
entries: { "session-a": makeEntry("alpha fact") },
|
|
239
|
+
};
|
|
240
|
+
await writeFile(path.join(stateDir, "buffer.json"), JSON.stringify(legacy), "utf8");
|
|
241
|
+
|
|
242
|
+
const storage = new StorageManager(root);
|
|
243
|
+
const loaded = await storage.loadBuffer();
|
|
244
|
+
assert.equal(loaded.entries?.["session-a"]?.turns[0]?.content, "alpha fact");
|
|
245
|
+
await assert.rejects(stat(path.join(stateDir, "buffer.json")), { code: "ENOENT" });
|
|
246
|
+
} finally {
|
|
247
|
+
await rm(root, { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("corrupt legacy buffer.json with no session files still throws (reset path preserved)", async () => {
|
|
252
|
+
const root = await tempRoot("bufio-corrupt-legacy");
|
|
253
|
+
try {
|
|
254
|
+
const stateDir = path.join(root, "state");
|
|
255
|
+
await mkdir(stateDir, { recursive: true });
|
|
256
|
+
await writeFile(path.join(stateDir, "buffer.json"), "{not json", "utf8");
|
|
257
|
+
const storage = new StorageManager(root);
|
|
258
|
+
await assert.rejects(() => storage.loadBuffer());
|
|
259
|
+
} finally {
|
|
260
|
+
await rm(root, { recursive: true, force: true });
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("per-session save does not run the heavyweight ensureDirectories pass", async () => {
|
|
265
|
+
const root = await tempRoot("bufio-light-save");
|
|
266
|
+
try {
|
|
267
|
+
const storage = new StorageManager(root);
|
|
268
|
+
await storage.saveBufferSession("session-a", makeEntry("alpha fact"));
|
|
269
|
+
// ensureDirectories() materializes facts/, entities/, questions/, ... — a
|
|
270
|
+
// per-session buffer write must only need state/buffer/.
|
|
271
|
+
assert.equal(await exists(path.join(root, "entities")), false, "entities dir created by a buffer session save");
|
|
272
|
+
assert.equal(await exists(path.join(root, "questions")), false, "questions dir created by a buffer session save");
|
|
273
|
+
const state = await storage.loadBuffer();
|
|
274
|
+
assert.equal(state.entries?.["session-a"]?.turns[0]?.content, "alpha fact");
|
|
275
|
+
} finally {
|
|
276
|
+
await rm(root, { recursive: true, force: true });
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
async function exists(p: string): Promise<boolean> {
|
|
281
|
+
try {
|
|
282
|
+
await stat(p);
|
|
283
|
+
return true;
|
|
284
|
+
} catch {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
test("session file names round-trip awkward buffer keys", async () => {
|
|
290
|
+
const root = await tempRoot("bufio-awkward-keys");
|
|
291
|
+
try {
|
|
292
|
+
const storage = new StorageManager(root);
|
|
293
|
+
const keys = ["default", "agent:main:abc123", "weird /slash/key", "__proto__", "unicode-ключ-🔑"];
|
|
294
|
+
for (const [i, key] of keys.entries()) {
|
|
295
|
+
await storage.saveBufferSession(key, makeEntry(`fact-${i}`));
|
|
296
|
+
}
|
|
297
|
+
const state = await storage.loadBuffer();
|
|
298
|
+
for (const [i, key] of keys.entries()) {
|
|
299
|
+
assert.equal(state.entries?.[key]?.turns[0]?.content, `fact-${i}`, `key ${key} did not round-trip`);
|
|
300
|
+
}
|
|
301
|
+
const names = await readdir(path.join(root, "state", "buffer"));
|
|
302
|
+
assert.equal(names.filter((n) => n.endsWith(".json")).length, keys.length, "two keys collided into one file");
|
|
303
|
+
} finally {
|
|
304
|
+
await rm(root, { recursive: true, force: true });
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("session file names do not collide on case-insensitive filesystems", () => {
|
|
309
|
+
const keys = ["Session-A", "session-a", "SESSION-A", "h" + "0".repeat(64), "x".repeat(400), "x".repeat(401)];
|
|
310
|
+
const folded = new Set(keys.map((key) => bufferSessionFileName(key).toLowerCase()));
|
|
311
|
+
assert.equal(folded.size, keys.length, "two keys fold to the same file name");
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("a symlinked buffer directory is refused and nothing is written through it", async () => {
|
|
315
|
+
const root = await tempRoot("bufio-symlink-dir");
|
|
316
|
+
const outside = await tempRoot("bufio-outside");
|
|
317
|
+
try {
|
|
318
|
+
const storage = new StorageManager(root);
|
|
319
|
+
await mkdir(path.join(root, "state"), { recursive: true });
|
|
320
|
+
await symlink(outside, path.join(root, "state", "buffer"));
|
|
321
|
+
|
|
322
|
+
await assert.rejects(storage.saveBufferSession("session-a", makeEntry("secret")), /symlink/);
|
|
323
|
+
await assert.rejects(storage.deleteBufferSession("session-a"), /symlink/);
|
|
324
|
+
await assert.rejects(storage.loadBuffer(), /symlink/);
|
|
325
|
+
assert.deepEqual(await readdir(outside), [], "nothing may be written outside the memory root");
|
|
326
|
+
} finally {
|
|
327
|
+
await rm(root, { recursive: true, force: true });
|
|
328
|
+
await rm(outside, { recursive: true, force: true });
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("a symlinked session file is never followed when loading", async () => {
|
|
333
|
+
const root = await tempRoot("bufio-symlink-file");
|
|
334
|
+
const outside = await tempRoot("bufio-outside-file");
|
|
335
|
+
try {
|
|
336
|
+
const storage = new StorageManager(root);
|
|
337
|
+
await storage.saveBufferSession("session-a", makeEntry("alpha fact"));
|
|
338
|
+
const target = path.join(outside, "stolen.json");
|
|
339
|
+
await writeFile(target, JSON.stringify({ key: "session-evil", ...makeEntry("evil fact") }), "utf8");
|
|
340
|
+
await symlink(target, path.join(root, "state", "buffer", "s-planted.json"));
|
|
341
|
+
|
|
342
|
+
const state = await storage.loadBuffer();
|
|
343
|
+
|
|
344
|
+
assert.equal(state.entries?.["session-a"]?.turns[0]?.content, "alpha fact");
|
|
345
|
+
assert.equal(state.entries?.["session-evil"], undefined, "the symlink target must not be read");
|
|
346
|
+
} finally {
|
|
347
|
+
await rm(root, { recursive: true, force: true });
|
|
348
|
+
await rm(outside, { recursive: true, force: true });
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("loading with the wrong secure-store key rejects instead of reading an empty buffer", async () => {
|
|
353
|
+
const root = await tempRoot("bufio-wrong-key");
|
|
354
|
+
try {
|
|
355
|
+
const writer = new StorageManager(root);
|
|
356
|
+
writer.setSecureStoreKey(Buffer.alloc(32, 1), true);
|
|
357
|
+
await writer.saveBufferSession("session-a", makeEntry("sealed turn"));
|
|
358
|
+
|
|
359
|
+
const reader = new StorageManager(root);
|
|
360
|
+
reader.setSecureStoreKey(Buffer.alloc(32, 2), true);
|
|
361
|
+
|
|
362
|
+
await assert.rejects(reader.loadBuffer(), SecureStoreDecryptError);
|
|
363
|
+
// The encrypted file must survive: a rejected load must not let later saves replace it.
|
|
364
|
+
const names = await readdir(path.join(root, "state", "buffer"));
|
|
365
|
+
assert.deepEqual(names, [bufferSessionFileName("session-a")]);
|
|
366
|
+
} finally {
|
|
367
|
+
await rm(root, { recursive: true, force: true });
|
|
368
|
+
}
|
|
369
|
+
});
|
package/src/storage.ts
CHANGED
|
@@ -142,9 +142,11 @@ import {
|
|
|
142
142
|
type TombstoneStats,
|
|
143
143
|
} from "./lifecycle/tombstones.js";
|
|
144
144
|
import { supersessionKeysForFact } from "./temporal-supersession.js";
|
|
145
|
+
import { BufferSessionFiles } from "./storage/buffer-session-files.js";
|
|
145
146
|
import { runSupersessionSideEffects, type SupersessionAuditOptions } from "./storage/supersession-side-effects.js";
|
|
146
147
|
import type {
|
|
147
148
|
AccessTrackingEntry,
|
|
149
|
+
BufferEntryState,
|
|
148
150
|
BufferState,
|
|
149
151
|
ConfidenceTier,
|
|
150
152
|
ContinuityIncidentCloseInput,
|
|
@@ -5508,23 +5510,16 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
5508
5510
|
return deleted;
|
|
5509
5511
|
}
|
|
5510
5512
|
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
} catch (err) {
|
|
5517
|
-
if (err instanceof SecureStoreLockedError) throw err;
|
|
5518
|
-
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
5519
|
-
return { turns: [], lastExtractionAt: null, extractionCount: 0 };
|
|
5520
|
-
}
|
|
5521
|
-
}
|
|
5522
|
-
|
|
5523
|
-
async saveBuffer(state: BufferState): Promise<void> {
|
|
5524
|
-
await this.ensureDirectories();
|
|
5525
|
-
const bufferPath = path.join(this.stateDir, "buffer.json");
|
|
5526
|
-
await this.writeStorageSecureFile(bufferPath, JSON.stringify(state, null, 2));
|
|
5513
|
+
// Buffer persistence: one file per session (storage/buffer-session-files.ts).
|
|
5514
|
+
private bufferFiles?: BufferSessionFiles;
|
|
5515
|
+
private bufferSessionFiles(): BufferSessionFiles {
|
|
5516
|
+
const io = { read: (p: string) => this.readStorageSecureFile(p), write: (p: string, c: string) => this.writeStorageSecureFile(p, c) };
|
|
5517
|
+
return (this.bufferFiles ??= new BufferSessionFiles(this.stateDir, io));
|
|
5527
5518
|
}
|
|
5519
|
+
async loadBuffer(): Promise<BufferState> { return this.bufferSessionFiles().load(); }
|
|
5520
|
+
async saveBuffer(state: BufferState): Promise<void> { await this.bufferSessionFiles().reconcile(state); }
|
|
5521
|
+
async saveBufferSession(key: string, entry: BufferEntryState): Promise<void> { await this.bufferSessionFiles().save(key, entry); }
|
|
5522
|
+
async deleteBufferSession(key: string): Promise<void> { await this.bufferSessionFiles().remove(key); }
|
|
5528
5523
|
|
|
5529
5524
|
async loadMeta(): Promise<MetaState> {
|
|
5530
5525
|
const metaPath = path.join(this.stateDir, "meta.json");
|
package/src/types.ts
CHANGED
|
@@ -1299,6 +1299,18 @@ export interface PluginConfig
|
|
|
1299
1299
|
extractionJudgeModel: string;
|
|
1300
1300
|
/** Maximum number of candidate facts per judge LLM batch call. */
|
|
1301
1301
|
extractionJudgeBatchSize: number;
|
|
1302
|
+
/**
|
|
1303
|
+
* Per-call timeout for the judge LLM (ms). Default 20000: measured 2.1-2.5 s
|
|
1304
|
+
* per compact verdict call (completion budget 24 tokens per candidate, up
|
|
1305
|
+
* to 2048) on a small fast judge model; the big shared core model measured
|
|
1306
|
+
* 13-22 s per compact call, 41 s verbose — raise it there. The earlier 150s
|
|
1307
|
+
* default only papered over 59-305s verbose replies; sub-second budgets
|
|
1308
|
+
* abort every call. On the local LLM path each attempt lasts
|
|
1309
|
+
* min(localLlmTimeoutMs, extractionJudgeTimeoutMs) (localLlmTimeoutMs
|
|
1310
|
+
* defaults to 180000), so a value above localLlmTimeoutMs does not lengthen
|
|
1311
|
+
* an attempt: raise both settings for a slower judge model.
|
|
1312
|
+
*/
|
|
1313
|
+
extractionJudgeTimeoutMs: number;
|
|
1302
1314
|
/** Shadow mode: log judge verdicts but do not filter facts. Default false. */
|
|
1303
1315
|
extractionJudgeShadow: boolean;
|
|
1304
1316
|
/**
|