@remnic/core 10.0.36 → 10.0.37
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 +77 -77
- package/dist/access-authorization-probe.js +78 -78
- package/dist/access-boundary.js +77 -77
- package/dist/access-citation-namespace.js +2 -2
- package/dist/access-cli.js +168 -166
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.js +77 -77
- package/dist/access-http-briefing.js +78 -78
- package/dist/access-http-query.js +79 -79
- package/dist/access-http.js +100 -100
- package/dist/access-identity-continuity-surface.js +77 -77
- package/dist/access-lcm-surface.js +77 -77
- package/dist/access-mcp.js +89 -89
- package/dist/access-namespace-preflight.js +77 -77
- package/dist/access-observe-write-category.js +13 -13
- package/dist/access-observe-write-surface.js +77 -77
- package/dist/access-offline-manifest.js +12 -12
- package/dist/access-operations-batch.js +81 -81
- package/dist/access-operations.js +88 -88
- package/dist/access-recall-concurrency.js +77 -77
- package/dist/access-recall-response.js +77 -77
- package/dist/access-recall-surface.js +77 -77
- package/dist/access-schema.js +8 -8
- package/dist/access-service.js +77 -77
- package/dist/access-surface-catalog.js +79 -79
- package/dist/active-recall.js +40 -40
- package/dist/adapters/index.js +7 -7
- package/dist/adapters/registry.js +3 -3
- package/dist/{auto-sync-VPHR3FBI.js → auto-sync-R3XGJL5I.js} +7 -7
- package/dist/briefing.js +29 -29
- 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/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.js +32 -32
- 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-NIANATGL.js → chunk-2RJO7VS4.js} +2 -2
- package/dist/{chunk-YLF42JQ7.js → chunk-3BPMYMPX.js} +2 -2
- package/dist/{chunk-GAARPWMN.js → chunk-3L5WH6CV.js} +2 -2
- package/dist/{chunk-XVZA6VXC.js → chunk-3SL52JKD.js} +4 -4
- 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-2ZYIAUYU.js → chunk-5CWH2LKL.js} +4 -4
- package/dist/{chunk-VKB2FGNW.js → chunk-5U3NUHMA.js} +85 -85
- package/dist/{chunk-PDYP7SUC.js → chunk-5UWP5X3T.js} +2 -2
- package/dist/{chunk-G3T6YMBO.js → chunk-64S6X4IU.js} +7 -7
- package/dist/{chunk-NHB4RC3G.js → chunk-6QOL4G76.js} +2 -2
- package/dist/{chunk-NBH7IRME.js → chunk-6R6CU3MR.js} +51 -51
- package/dist/{chunk-4ISHIPOC.js → chunk-7PG7QBWD.js} +2 -2
- package/dist/{chunk-A4B7WL5N.js → chunk-7ZBANI3M.js} +2 -2
- package/dist/{chunk-DG5UKN2Y.js → chunk-A23DPFFI.js} +2 -2
- package/dist/{chunk-77ZK533I.js → chunk-ARFT6ZBE.js} +2 -2
- package/dist/{chunk-V44DWTFA.js → chunk-AYXRK264.js} +6 -6
- package/dist/{chunk-XO4HOBM2.js → chunk-BPBUUGAO.js} +2 -2
- package/dist/{chunk-DFP37GUN.js → chunk-CCYXCT4B.js} +2 -2
- package/dist/{chunk-IKCGNTGS.js → chunk-CNQQNYJ2.js} +2 -2
- package/dist/{chunk-QZP3RMYP.js → chunk-CU7WDDPV.js} +4 -4
- package/dist/{chunk-GY4R4P2A.js → chunk-CUWOSRCS.js} +2 -2
- package/dist/{chunk-CJKHLACF.js → chunk-D2AKUKAQ.js} +2 -2
- package/dist/{chunk-BRZRP5WD.js → chunk-DK5VMJL3.js} +2 -2
- package/dist/{chunk-77IXBNV6.js → chunk-E24RUQEN.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-PH33R7GN.js → chunk-EHIKY2IA.js} +2 -2
- package/dist/{chunk-LJSBF6PW.js → chunk-EVVCE7S2.js} +2 -2
- package/dist/{chunk-FQ52AJXX.js → chunk-FBAHBFT5.js} +2 -2
- package/dist/{chunk-NDJ42GKB.js → chunk-FO5AFCRI.js} +5 -5
- package/dist/{chunk-5LWODP5R.js → chunk-FRAL3EU4.js} +2 -2
- package/dist/{chunk-AAANTXCQ.js → chunk-FZVZYU5C.js} +2 -2
- package/dist/{chunk-OHMGR6QN.js → chunk-G2SRB62M.js} +2 -2
- package/dist/{chunk-ORWDCX5Q.js → chunk-G6PPA4TH.js} +11 -11
- package/dist/{chunk-VNAIUNXI.js → chunk-GCVUBVEN.js} +10 -10
- package/dist/{chunk-TGAXRT7N.js → chunk-GFYFKXWZ.js} +2 -2
- package/dist/{chunk-ZABEDRGQ.js → chunk-HIMXDRSQ.js} +2 -2
- package/dist/{chunk-GDYLZ6FR.js → chunk-HJ3UTGIW.js} +3 -3
- package/dist/{chunk-AD3ZB72W.js → chunk-HKEK27M4.js} +2 -2
- package/dist/{chunk-X3IFRQKS.js → chunk-I73ZSORJ.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-UAYA4NJN.js → chunk-JWTUJVIF.js} +28 -36
- package/dist/chunk-JWTUJVIF.js.map +1 -0
- package/dist/chunk-KASJJGRG.js +29 -0
- package/dist/chunk-KASJJGRG.js.map +1 -0
- package/dist/{chunk-X73SCHKR.js → chunk-KC5SVCUJ.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-SIIWHIBK.js → chunk-L46Y5FPE.js} +2 -2
- package/dist/{chunk-CIJRM6SQ.js → chunk-LAQS5HYU.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-LO775P3S.js → chunk-LRJKHXU4.js} +2 -2
- package/dist/{chunk-QISYYK4L.js → chunk-LSSCMZQ2.js} +3 -3
- package/dist/{chunk-PCIIKQXW.js → chunk-MBMT5FUQ.js} +2 -2
- package/dist/{chunk-XFDHL252.js → chunk-MGXFJOUR.js} +3 -3
- package/dist/{chunk-7WTTE5FW.js → chunk-MH3LC7KB.js} +2 -2
- package/dist/{chunk-ERMT5WQM.js → chunk-MKE5DP3S.js} +2 -2
- package/dist/{chunk-DJMC4W6N.js → chunk-MNDHKZZQ.js} +2 -2
- package/dist/{chunk-7FG2JSU6.js → chunk-NMKETOAX.js} +2 -2
- package/dist/{chunk-RYUSWAAU.js → chunk-NQOM6PHO.js} +6 -6
- package/dist/{chunk-AI2VXOSO.js → chunk-NSQSOLCE.js} +2 -2
- package/dist/{chunk-UR25IQVT.js → chunk-P5CUEB2Q.js} +8 -8
- package/dist/{chunk-FXO43NBW.js → chunk-PSQ2P2T3.js} +3 -3
- package/dist/{chunk-X544TW54.js → chunk-PTRPOJ7U.js} +2 -2
- package/dist/{chunk-4YYIZ475.js → chunk-QEGE5QCL.js} +45 -45
- package/dist/{chunk-KQUN4RSX.js → chunk-QIC7YZ3D.js} +16 -16
- package/dist/{chunk-KRAMCBIW.js → chunk-QKXPOLQD.js} +4 -4
- package/dist/{chunk-O3VKQMWC.js → chunk-R6TCM47S.js} +364 -166
- package/dist/chunk-R6TCM47S.js.map +1 -0
- package/dist/{chunk-G5UR4DXQ.js → chunk-RBC4GIZU.js} +2 -2
- package/dist/{chunk-65JLKIVA.js → chunk-RVYXSS3Q.js} +7 -7
- package/dist/{chunk-G7XAZU2L.js → chunk-RWWYBE7E.js} +3 -3
- package/dist/{chunk-HJM23E65.js → chunk-S53WZWGL.js} +2 -2
- package/dist/{chunk-3ODEGTVV.js → chunk-TDCR6PQL.js} +2 -2
- package/dist/{chunk-QCP75JGO.js → chunk-TFA6HPOA.js} +5 -5
- package/dist/{chunk-SE3JAD22.js → chunk-TPKB7T6Z.js} +2 -2
- package/dist/{chunk-6VYY2XFK.js → chunk-TVIGN3XZ.js} +19 -19
- 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-K567EIW3.js → chunk-U4JXQCJ3.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-7VJEZQYF.js → chunk-UFBIOF7C.js} +3 -3
- package/dist/{chunk-JNELS2TW.js → chunk-UJDTXWMT.js} +2 -2
- package/dist/{chunk-M3GKSCFN.js → chunk-ULUX6Q7R.js} +4 -4
- package/dist/{chunk-V3JZZJ2E.js → chunk-UMFBHYPW.js} +11 -11
- package/dist/{chunk-GKNX4QO3.js → chunk-UQWA5QDT.js} +3 -3
- package/dist/{chunk-4Z7O2QVJ.js → chunk-USOXPN5H.js} +2 -2
- package/dist/{chunk-XA32GZ6B.js → chunk-UXXVOA4D.js} +3 -3
- package/dist/{chunk-5XTSIZ3L.js → chunk-V5RJCXLG.js} +6 -6
- package/dist/{chunk-A2D3WICT.js → chunk-VGFNTY3Y.js} +2 -2
- package/dist/{chunk-EH6KLYIE.js → chunk-VKTP3TZB.js} +4 -4
- package/dist/{chunk-CSGFGQBI.js → chunk-VPO53QMG.js} +2 -2
- package/dist/{chunk-ZSAQSLKL.js → chunk-WADPRAZE.js} +2 -2
- package/dist/{chunk-IVYAPAHG.js → chunk-WRDFYSUC.js} +2 -2
- package/dist/{chunk-F2JLBXMG.js → chunk-WSLNC5AL.js} +8 -8
- 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-S2S4U4G3.js → chunk-Y6Y3TRBN.js} +4 -4
- package/dist/{chunk-H6SKBCNI.js → chunk-YC2C67F7.js} +2 -2
- package/dist/{chunk-6FIFEXFF.js → chunk-YGYT7S3G.js} +7 -7
- package/dist/{chunk-EZ25VE3G.js → chunk-YNDLCWXS.js} +4 -4
- package/dist/{chunk-YOL56HY5.js → chunk-YQ6SLFNZ.js} +3 -3
- package/dist/{chunk-G22A6WJQ.js → chunk-Z2T5BPOZ.js} +2 -2
- package/dist/{chunk-IHTKBQBS.js → chunk-Z5AQZRHF.js} +2 -2
- package/dist/{chunk-QQ4T622E.js → chunk-Z5QWUR3Y.js} +2 -2
- package/dist/cli.js +127 -127
- package/dist/coding/export-okf-codegraph.js +30 -30
- package/dist/coding/pre-action-gate.js +2 -2
- package/dist/compounding/engine.js +31 -31
- package/dist/config.js +40 -40
- package/dist/connectors/codex-materialize-runner.js +29 -29
- package/dist/connectors/index.js +29 -29
- package/dist/contradiction/index.js +5 -5
- package/dist/convergence-refresh.js +31 -31
- package/dist/conversation-index/backend.js +2 -2
- package/dist/corpus-watermark.js +31 -31
- package/dist/dashboard-runtime.js +2 -2
- package/dist/deep-recall-http-glue.js +9 -9
- package/dist/entity-retrieval.js +31 -31
- package/dist/explicit-capture.js +7 -7
- package/dist/external-wiki-access.js +78 -78
- package/dist/extraction-span-materialize.js +2 -2
- package/dist/extraction.js +44 -44
- 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 +30 -30
- package/dist/harmonic-source-lookup.js +2 -2
- package/dist/index.js +225 -223
- package/dist/lcm/index.js +3 -3
- package/dist/location/index.js +32 -32
- package/dist/maintenance/memory-governance.js +29 -29
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +29 -29
- package/dist/maintenance/rebuild-memory-projection-runner.js +31 -31
- package/dist/maintenance/rebuild-memory-projection.js +30 -30
- package/dist/memory-browse-operations.js +78 -78
- package/dist/memory-browse.js +5 -5
- package/dist/memory-subject.js +3 -3
- package/dist/namespaces/migrate.js +30 -30
- package/dist/namespaces/search.js +11 -11
- package/dist/namespaces/storage.js +29 -29
- 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 +32 -32
- package/dist/operator-toolkit.js +51 -51
- package/dist/orchestration/maintenance.js +41 -41
- package/dist/orchestrator.js +173 -171
- package/dist/qmd.js +4 -4
- package/dist/recall-navigation-operations.js +78 -78
- package/dist/recall-navigation.js +77 -77
- package/dist/recall-state-view-admission.js +5 -5
- package/dist/recall-why-service.js +5 -5
- 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 +41 -41
- 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 +30 -30
- package/dist/semantic-rule-promotion.js +29 -29
- package/dist/semantic-rule-verifier.js +29 -29
- package/dist/source-agent-qualifier.js +34 -34
- package/dist/standup/build.js +29 -29
- 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 +28 -28
- package/dist/subject-config.js +4 -4
- package/dist/support-passport/index.js +80 -80
- package/dist/transfer/export-okf.js +30 -30
- package/dist/verified-recall.js +29 -29
- package/dist/who-knows-cli.js +30 -30
- package/dist/who-knows.js +29 -29
- 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/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/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-R3XGJL5I.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-NIANATGL.js.map → chunk-2RJO7VS4.js.map} +0 -0
- /package/dist/{chunk-YLF42JQ7.js.map → chunk-3BPMYMPX.js.map} +0 -0
- /package/dist/{chunk-GAARPWMN.js.map → chunk-3L5WH6CV.js.map} +0 -0
- /package/dist/{chunk-XVZA6VXC.js.map → chunk-3SL52JKD.js.map} +0 -0
- /package/dist/{chunk-S2PDFSXS.js.map → chunk-4P4VLPGB.js.map} +0 -0
- /package/dist/{chunk-2ZYIAUYU.js.map → chunk-5CWH2LKL.js.map} +0 -0
- /package/dist/{chunk-VKB2FGNW.js.map → chunk-5U3NUHMA.js.map} +0 -0
- /package/dist/{chunk-PDYP7SUC.js.map → chunk-5UWP5X3T.js.map} +0 -0
- /package/dist/{chunk-G3T6YMBO.js.map → chunk-64S6X4IU.js.map} +0 -0
- /package/dist/{chunk-NHB4RC3G.js.map → chunk-6QOL4G76.js.map} +0 -0
- /package/dist/{chunk-NBH7IRME.js.map → chunk-6R6CU3MR.js.map} +0 -0
- /package/dist/{chunk-4ISHIPOC.js.map → chunk-7PG7QBWD.js.map} +0 -0
- /package/dist/{chunk-A4B7WL5N.js.map → chunk-7ZBANI3M.js.map} +0 -0
- /package/dist/{chunk-DG5UKN2Y.js.map → chunk-A23DPFFI.js.map} +0 -0
- /package/dist/{chunk-77ZK533I.js.map → chunk-ARFT6ZBE.js.map} +0 -0
- /package/dist/{chunk-V44DWTFA.js.map → chunk-AYXRK264.js.map} +0 -0
- /package/dist/{chunk-XO4HOBM2.js.map → chunk-BPBUUGAO.js.map} +0 -0
- /package/dist/{chunk-DFP37GUN.js.map → chunk-CCYXCT4B.js.map} +0 -0
- /package/dist/{chunk-IKCGNTGS.js.map → chunk-CNQQNYJ2.js.map} +0 -0
- /package/dist/{chunk-QZP3RMYP.js.map → chunk-CU7WDDPV.js.map} +0 -0
- /package/dist/{chunk-GY4R4P2A.js.map → chunk-CUWOSRCS.js.map} +0 -0
- /package/dist/{chunk-CJKHLACF.js.map → chunk-D2AKUKAQ.js.map} +0 -0
- /package/dist/{chunk-BRZRP5WD.js.map → chunk-DK5VMJL3.js.map} +0 -0
- /package/dist/{chunk-77IXBNV6.js.map → chunk-E24RUQEN.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-PH33R7GN.js.map → chunk-EHIKY2IA.js.map} +0 -0
- /package/dist/{chunk-LJSBF6PW.js.map → chunk-EVVCE7S2.js.map} +0 -0
- /package/dist/{chunk-FQ52AJXX.js.map → chunk-FBAHBFT5.js.map} +0 -0
- /package/dist/{chunk-NDJ42GKB.js.map → chunk-FO5AFCRI.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-OHMGR6QN.js.map → chunk-G2SRB62M.js.map} +0 -0
- /package/dist/{chunk-ORWDCX5Q.js.map → chunk-G6PPA4TH.js.map} +0 -0
- /package/dist/{chunk-VNAIUNXI.js.map → chunk-GCVUBVEN.js.map} +0 -0
- /package/dist/{chunk-TGAXRT7N.js.map → chunk-GFYFKXWZ.js.map} +0 -0
- /package/dist/{chunk-ZABEDRGQ.js.map → chunk-HIMXDRSQ.js.map} +0 -0
- /package/dist/{chunk-GDYLZ6FR.js.map → chunk-HJ3UTGIW.js.map} +0 -0
- /package/dist/{chunk-AD3ZB72W.js.map → chunk-HKEK27M4.js.map} +0 -0
- /package/dist/{chunk-X3IFRQKS.js.map → chunk-I73ZSORJ.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-X73SCHKR.js.map → chunk-KC5SVCUJ.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-SIIWHIBK.js.map → chunk-L46Y5FPE.js.map} +0 -0
- /package/dist/{chunk-CIJRM6SQ.js.map → chunk-LAQS5HYU.js.map} +0 -0
- /package/dist/{chunk-23VGZ65V.js.map → chunk-LHBOFROT.js.map} +0 -0
- /package/dist/{chunk-LO775P3S.js.map → chunk-LRJKHXU4.js.map} +0 -0
- /package/dist/{chunk-QISYYK4L.js.map → chunk-LSSCMZQ2.js.map} +0 -0
- /package/dist/{chunk-PCIIKQXW.js.map → chunk-MBMT5FUQ.js.map} +0 -0
- /package/dist/{chunk-XFDHL252.js.map → chunk-MGXFJOUR.js.map} +0 -0
- /package/dist/{chunk-7WTTE5FW.js.map → chunk-MH3LC7KB.js.map} +0 -0
- /package/dist/{chunk-ERMT5WQM.js.map → chunk-MKE5DP3S.js.map} +0 -0
- /package/dist/{chunk-DJMC4W6N.js.map → chunk-MNDHKZZQ.js.map} +0 -0
- /package/dist/{chunk-7FG2JSU6.js.map → chunk-NMKETOAX.js.map} +0 -0
- /package/dist/{chunk-RYUSWAAU.js.map → chunk-NQOM6PHO.js.map} +0 -0
- /package/dist/{chunk-AI2VXOSO.js.map → chunk-NSQSOLCE.js.map} +0 -0
- /package/dist/{chunk-UR25IQVT.js.map → chunk-P5CUEB2Q.js.map} +0 -0
- /package/dist/{chunk-FXO43NBW.js.map → chunk-PSQ2P2T3.js.map} +0 -0
- /package/dist/{chunk-X544TW54.js.map → chunk-PTRPOJ7U.js.map} +0 -0
- /package/dist/{chunk-4YYIZ475.js.map → chunk-QEGE5QCL.js.map} +0 -0
- /package/dist/{chunk-KQUN4RSX.js.map → chunk-QIC7YZ3D.js.map} +0 -0
- /package/dist/{chunk-KRAMCBIW.js.map → chunk-QKXPOLQD.js.map} +0 -0
- /package/dist/{chunk-G5UR4DXQ.js.map → chunk-RBC4GIZU.js.map} +0 -0
- /package/dist/{chunk-65JLKIVA.js.map → chunk-RVYXSS3Q.js.map} +0 -0
- /package/dist/{chunk-G7XAZU2L.js.map → chunk-RWWYBE7E.js.map} +0 -0
- /package/dist/{chunk-HJM23E65.js.map → chunk-S53WZWGL.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-K567EIW3.js.map → chunk-U4JXQCJ3.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-7VJEZQYF.js.map → chunk-UFBIOF7C.js.map} +0 -0
- /package/dist/{chunk-JNELS2TW.js.map → chunk-UJDTXWMT.js.map} +0 -0
- /package/dist/{chunk-M3GKSCFN.js.map → chunk-ULUX6Q7R.js.map} +0 -0
- /package/dist/{chunk-V3JZZJ2E.js.map → chunk-UMFBHYPW.js.map} +0 -0
- /package/dist/{chunk-GKNX4QO3.js.map → chunk-UQWA5QDT.js.map} +0 -0
- /package/dist/{chunk-4Z7O2QVJ.js.map → chunk-USOXPN5H.js.map} +0 -0
- /package/dist/{chunk-XA32GZ6B.js.map → chunk-UXXVOA4D.js.map} +0 -0
- /package/dist/{chunk-5XTSIZ3L.js.map → chunk-V5RJCXLG.js.map} +0 -0
- /package/dist/{chunk-A2D3WICT.js.map → chunk-VGFNTY3Y.js.map} +0 -0
- /package/dist/{chunk-EH6KLYIE.js.map → chunk-VKTP3TZB.js.map} +0 -0
- /package/dist/{chunk-CSGFGQBI.js.map → chunk-VPO53QMG.js.map} +0 -0
- /package/dist/{chunk-ZSAQSLKL.js.map → chunk-WADPRAZE.js.map} +0 -0
- /package/dist/{chunk-IVYAPAHG.js.map → chunk-WRDFYSUC.js.map} +0 -0
- /package/dist/{chunk-F2JLBXMG.js.map → chunk-WSLNC5AL.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-S2S4U4G3.js.map → chunk-Y6Y3TRBN.js.map} +0 -0
- /package/dist/{chunk-H6SKBCNI.js.map → chunk-YC2C67F7.js.map} +0 -0
- /package/dist/{chunk-6FIFEXFF.js.map → chunk-YGYT7S3G.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-G22A6WJQ.js.map → chunk-Z2T5BPOZ.js.map} +0 -0
- /package/dist/{chunk-IHTKBQBS.js.map → chunk-Z5AQZRHF.js.map} +0 -0
- /package/dist/{chunk-QQ4T622E.js.map → chunk-Z5QWUR3Y.js.map} +0 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-session incremental buffer persistence (SmartBuffer layer).
|
|
3
|
+
*
|
|
4
|
+
* Pins the hot-path contract on top of the storage primitives tested in
|
|
5
|
+
* storage-buffer-sessions.test.ts:
|
|
6
|
+
*
|
|
7
|
+
* - a storage that exposes saveBufferSession/deleteBufferSession gets
|
|
8
|
+
* INCREMENTAL saves: only the sessions whose state actually changed are
|
|
9
|
+
* written, only on boundaries that need durability;
|
|
10
|
+
* - sessions whose state did not change are never rewritten;
|
|
11
|
+
* - eviction and resetToEmpty delete the corresponding session files;
|
|
12
|
+
* - a legacy storage (only loadBuffer/saveBuffer) keeps the exact old
|
|
13
|
+
* whole-state behavior — existing deployments and test doubles are
|
|
14
|
+
* unaffected.
|
|
15
|
+
*
|
|
16
|
+
* All saves flow through SmartBuffer's mutation chain, so there is exactly
|
|
17
|
+
* one writer per session file.
|
|
18
|
+
*/
|
|
19
|
+
import test from "node:test";
|
|
20
|
+
import assert from "node:assert/strict";
|
|
21
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
22
|
+
import os from "node:os";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
|
|
25
|
+
import { SmartBuffer } from "./buffer.js";
|
|
26
|
+
import { parseConfig } from "./config.js";
|
|
27
|
+
import { StorageManager } from "./storage.js";
|
|
28
|
+
import type { BufferEntryState, BufferState, BufferTurn } from "./types.js";
|
|
29
|
+
|
|
30
|
+
interface SessionFileRecord {
|
|
31
|
+
entry: BufferEntryState;
|
|
32
|
+
writeCount: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Storage double implementing the incremental per-session surface. Records
|
|
37
|
+
* per-session writes/deletes and whole-state saves separately so tests can
|
|
38
|
+
* assert exactly which files the hot path touched.
|
|
39
|
+
*/
|
|
40
|
+
class IncrementalStorage {
|
|
41
|
+
readonly sessions = new Map<string, SessionFileRecord>();
|
|
42
|
+
readonly deleted: string[] = [];
|
|
43
|
+
wholeStateSaves = 0;
|
|
44
|
+
private initial: BufferState;
|
|
45
|
+
|
|
46
|
+
constructor(initial: BufferState) {
|
|
47
|
+
this.initial = initial;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async loadBuffer(): Promise<BufferState> {
|
|
51
|
+
return structuredClone(this.initial);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async saveBuffer(state: BufferState): Promise<void> {
|
|
55
|
+
this.wholeStateSaves += 1;
|
|
56
|
+
this.initial = structuredClone(state);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async saveBufferSession(key: string, entry: BufferEntryState): Promise<void> {
|
|
60
|
+
const record = this.sessions.get(key) ?? { entry, writeCount: 0 };
|
|
61
|
+
record.entry = structuredClone(entry);
|
|
62
|
+
record.writeCount += 1;
|
|
63
|
+
this.sessions.set(key, record);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async deleteBufferSession(key: string): Promise<void> {
|
|
67
|
+
this.deleted.push(key);
|
|
68
|
+
this.sessions.delete(key);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Storage double implementing ONLY the legacy whole-state surface — every
|
|
74
|
+
* pre-existing test double in the repo looks like this from SmartBuffer's
|
|
75
|
+
* point of view, and must keep working unchanged.
|
|
76
|
+
*/
|
|
77
|
+
class LegacyOnlyStorage {
|
|
78
|
+
wholeStateSaves = 0;
|
|
79
|
+
lastSaved: BufferState | null = null;
|
|
80
|
+
|
|
81
|
+
constructor(private initial: BufferState) {}
|
|
82
|
+
|
|
83
|
+
async loadBuffer(): Promise<BufferState> {
|
|
84
|
+
return structuredClone(this.initial);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async saveBuffer(state: BufferState): Promise<void> {
|
|
88
|
+
this.wholeStateSaves += 1;
|
|
89
|
+
this.lastSaved = structuredClone(state);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function makeTurn(sessionKey: string, content: string): BufferTurn {
|
|
94
|
+
return {
|
|
95
|
+
role: "user",
|
|
96
|
+
content,
|
|
97
|
+
timestamp: "2026-10-29T12:00:00.000Z",
|
|
98
|
+
sessionKey,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function emptyState(): BufferState {
|
|
103
|
+
return { turns: [], lastExtractionAt: null, extractionCount: 0 };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
test("steady-state observe persists only the active session's file", async () => {
|
|
107
|
+
const storage = new IncrementalStorage(emptyState());
|
|
108
|
+
const buffer = new SmartBuffer(parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100 }), storage);
|
|
109
|
+
|
|
110
|
+
await buffer.addTurn("thread-a", makeTurn("thread-a", "alpha"));
|
|
111
|
+
await buffer.flushPendingSave();
|
|
112
|
+
assert.equal(storage.wholeStateSaves, 0, "hot path fell back to whole-state save");
|
|
113
|
+
assert.equal(storage.sessions.get("thread-a")?.writeCount, 1);
|
|
114
|
+
assert.equal(storage.sessions.get("thread-a")?.entry.turns[0]?.content, "alpha");
|
|
115
|
+
|
|
116
|
+
await buffer.addTurn("thread-b", makeTurn("thread-b", "beta"));
|
|
117
|
+
await buffer.flushPendingSave();
|
|
118
|
+
assert.equal(storage.sessions.get("thread-b")?.writeCount, 1);
|
|
119
|
+
// Session A's turn did NOT touch session A's file again.
|
|
120
|
+
assert.equal(storage.sessions.get("thread-a")?.writeCount, 1, "unchanged session file was rewritten");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("a second turn in the same session rewrites only that session's file", async () => {
|
|
124
|
+
const storage = new IncrementalStorage(emptyState());
|
|
125
|
+
const buffer = new SmartBuffer(parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100 }), storage);
|
|
126
|
+
|
|
127
|
+
await buffer.addTurn("thread-a", makeTurn("thread-a", "one"));
|
|
128
|
+
await buffer.flushPendingSave();
|
|
129
|
+
await buffer.addTurn("thread-a", makeTurn("thread-a", "two"));
|
|
130
|
+
await buffer.flushPendingSave();
|
|
131
|
+
|
|
132
|
+
const record = storage.sessions.get("thread-a");
|
|
133
|
+
assert.equal(record?.writeCount, 2);
|
|
134
|
+
assert.equal(record?.entry.turns.length, 2);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("extraction-boundary turn persists inline and durably", async () => {
|
|
138
|
+
const storage = new IncrementalStorage(emptyState());
|
|
139
|
+
// bufferMaxTurns: 1 → the very first turn triggers extract_batch.
|
|
140
|
+
const buffer = new SmartBuffer(parseConfig({ triggerMode: "every_n", bufferMaxTurns: 1 }), storage);
|
|
141
|
+
|
|
142
|
+
const outcome = await buffer.addTurnWithOutcome("thread-a", makeTurn("thread-a", "trigger"));
|
|
143
|
+
assert.equal(outcome.decision, "extract_batch");
|
|
144
|
+
// Inline save already ran inside the mutation — no flush needed.
|
|
145
|
+
assert.equal(storage.sessions.get("thread-a")?.writeCount, 1);
|
|
146
|
+
assert.equal(storage.wholeStateSaves, 0, "extraction boundary fell back to whole-state save");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("clearAfterExtraction persists the cleared session state", async () => {
|
|
150
|
+
const storage = new IncrementalStorage(emptyState());
|
|
151
|
+
const buffer = new SmartBuffer(parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100 }), storage);
|
|
152
|
+
|
|
153
|
+
await buffer.addTurn("thread-a", makeTurn("thread-a", "alpha"));
|
|
154
|
+
await buffer.flushPendingSave();
|
|
155
|
+
const before = storage.sessions.get("thread-a")?.writeCount ?? 0;
|
|
156
|
+
|
|
157
|
+
await buffer.clearAfterExtraction("thread-a");
|
|
158
|
+
const record = storage.sessions.get("thread-a");
|
|
159
|
+
assert.equal(record?.writeCount, before + 1, "clearAfterExtraction did not persist the session");
|
|
160
|
+
assert.equal(record?.entry.turns.length, 0);
|
|
161
|
+
assert.equal(record?.entry.extractionCount, 1);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("evicted sessions get their files deleted", async () => {
|
|
165
|
+
const storage = new IncrementalStorage(emptyState());
|
|
166
|
+
const buffer = new SmartBuffer(parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100 }), storage);
|
|
167
|
+
|
|
168
|
+
// normalizeState always materializes a "default" entry, so 198 seeded
|
|
169
|
+
// sessions + default = 199; new-session reaches exactly 200 (the
|
|
170
|
+
// addTurnBounded cap). One more UNBOUNDED addTurn pushes to 201 and
|
|
171
|
+
// pruneEntries evicts the oldest non-default session (s0).
|
|
172
|
+
for (let i = 0; i < 198; i++) {
|
|
173
|
+
const key = `s${i}`;
|
|
174
|
+
await buffer.addTurnBounded(key, makeTurn(key, `content ${i}`));
|
|
175
|
+
}
|
|
176
|
+
await buffer.flushPendingSave();
|
|
177
|
+
|
|
178
|
+
const outcome = await buffer.addTurnBounded("new-session", makeTurn("new-session", "fresh"));
|
|
179
|
+
assert.notEqual(outcome, null, "addTurnBounded refused below capacity");
|
|
180
|
+
await buffer.addTurn("overflow", makeTurn("overflow", "one too many"));
|
|
181
|
+
// The evicted key's file deletion lands on the next persist, like any
|
|
182
|
+
// other buffered state change.
|
|
183
|
+
await buffer.flushPendingSave();
|
|
184
|
+
assert.ok(storage.deleted.includes("s0"), "oldest session was not evicted under capacity pressure");
|
|
185
|
+
assert.ok(
|
|
186
|
+
storage.deleted.length > 0,
|
|
187
|
+
"capacity pressure evicted entries without deleting their session files",
|
|
188
|
+
);
|
|
189
|
+
for (const key of storage.deleted) {
|
|
190
|
+
assert.equal(storage.sessions.has(key), false, `evicted session ${key} still has a file record`);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("resetToEmpty drops every session file on the next persist", async () => {
|
|
195
|
+
const storage = new IncrementalStorage(emptyState());
|
|
196
|
+
const buffer = new SmartBuffer(parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100 }), storage);
|
|
197
|
+
|
|
198
|
+
await buffer.addTurn("thread-a", makeTurn("thread-a", "alpha"));
|
|
199
|
+
await buffer.flushPendingSave();
|
|
200
|
+
buffer.resetToEmpty();
|
|
201
|
+
await buffer.flushPendingSave();
|
|
202
|
+
// resetToEmpty marks every known key for deletion; deleting a key whose
|
|
203
|
+
// file never existed is an ENOENT no-op on real storage (covered in the
|
|
204
|
+
// storage contract tests). The session that HAD a file must be gone.
|
|
205
|
+
assert.ok(storage.deleted.includes("thread-a"));
|
|
206
|
+
assert.equal(storage.sessions.size, 0);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("retained turns survive the per-session persist", async () => {
|
|
210
|
+
const storage = new IncrementalStorage(emptyState());
|
|
211
|
+
const buffer = new SmartBuffer(parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100 }), storage);
|
|
212
|
+
|
|
213
|
+
await buffer.addTurn("thread-a", makeTurn("thread-a", "alpha"));
|
|
214
|
+
await buffer.flushPendingSave();
|
|
215
|
+
await buffer.retainDeferredTurns("thread-a", [makeTurn("thread-a", "alpha")], 5);
|
|
216
|
+
await buffer.flushPendingSave();
|
|
217
|
+
|
|
218
|
+
const record = storage.sessions.get("thread-a");
|
|
219
|
+
assert.equal(record?.entry.retainedTurns?.length, 1, "retained turns were not persisted");
|
|
220
|
+
assert.equal(record?.entry.retainedTurns?.[0]?.content, "alpha");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("buffer state survives a restart through per-session files", async () => {
|
|
224
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-bufio-restart-"));
|
|
225
|
+
try {
|
|
226
|
+
const storage = new StorageManager(root);
|
|
227
|
+
const config = parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100 });
|
|
228
|
+
const first = new SmartBuffer(config, storage);
|
|
229
|
+
await first.addTurn("thread-a", makeTurn("thread-a", "alpha"));
|
|
230
|
+
await first.addTurn("thread-b", makeTurn("thread-b", "beta"));
|
|
231
|
+
await first.flushPendingSave();
|
|
232
|
+
|
|
233
|
+
// A fresh process reads the buffer back from the per-session files.
|
|
234
|
+
const second = new SmartBuffer(config, storage);
|
|
235
|
+
await second.load();
|
|
236
|
+
assert.equal(second.getTurns("thread-a").length, 1);
|
|
237
|
+
assert.equal(second.getTurns("thread-a")[0]?.content, "alpha");
|
|
238
|
+
assert.equal(second.getTurns("thread-b")[0]?.content, "beta");
|
|
239
|
+
} finally {
|
|
240
|
+
await rm(root, { recursive: true, force: true });
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("legacy-only storage keeps the whole-state saveBuffer behavior", async () => {
|
|
245
|
+
const storage = new LegacyOnlyStorage(emptyState());
|
|
246
|
+
const buffer = new SmartBuffer(parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100 }), storage);
|
|
247
|
+
|
|
248
|
+
await buffer.addTurn("thread-a", makeTurn("thread-a", "alpha"));
|
|
249
|
+
await buffer.flushPendingSave();
|
|
250
|
+
await buffer.addTurn("thread-b", makeTurn("thread-b", "beta"));
|
|
251
|
+
await buffer.flushPendingSave();
|
|
252
|
+
|
|
253
|
+
assert.ok(storage.wholeStateSaves >= 2, "legacy storage did not receive whole-state saves");
|
|
254
|
+
assert.equal(storage.lastSaved?.entries?.["thread-a"]?.turns[0]?.content, "alpha");
|
|
255
|
+
assert.equal(storage.lastSaved?.entries?.["thread-b"]?.turns[0]?.content, "beta");
|
|
256
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { BufferEntryState, BufferState, BufferSurpriseEvent } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The buffer persistence surface SmartBuffer depends on.
|
|
5
|
+
*
|
|
6
|
+
* `saveBufferSession` / `deleteBufferSession` are the per-session incremental
|
|
7
|
+
* writes (one file per session); when a storage backend does not provide
|
|
8
|
+
* them, the buffer falls back to the whole-state `saveBuffer`. Both are
|
|
9
|
+
* optional in the type so a storage double only has to implement what it
|
|
10
|
+
* actually supports.
|
|
11
|
+
*/
|
|
12
|
+
export interface BufferStorage {
|
|
13
|
+
loadBuffer(): Promise<BufferState>;
|
|
14
|
+
saveBuffer(state: BufferState): Promise<void>;
|
|
15
|
+
saveBufferSession?(key: string, entry: BufferEntryState): Promise<void>;
|
|
16
|
+
deleteBufferSession?(key: string): Promise<void>;
|
|
17
|
+
/** Optional surprise-telemetry sink; older storage and test doubles omit it. */
|
|
18
|
+
appendBufferSurpriseEvents?(events: BufferSurpriseEvent[]): Promise<number>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Tracks which sessions' persisted files are stale and writes only those.
|
|
23
|
+
*
|
|
24
|
+
* Marks are cleared only after a durable write of that session. All calls
|
|
25
|
+
* happen under SmartBuffer's serialized mutation chain: one writer.
|
|
26
|
+
*/
|
|
27
|
+
export class BufferSessionPersistence {
|
|
28
|
+
private readonly dirty = new Set<string>();
|
|
29
|
+
private readonly deleted = new Set<string>();
|
|
30
|
+
|
|
31
|
+
markDirty(key: string): void {
|
|
32
|
+
this.dirty.add(key);
|
|
33
|
+
this.deleted.delete(key);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
markDeleted(key: string): void {
|
|
37
|
+
this.deleted.add(key);
|
|
38
|
+
this.dirty.delete(key);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Eviction removed `key` from memory: its session file must go, any pending write is moot. */
|
|
42
|
+
markEvicted(keys: readonly string[]): void {
|
|
43
|
+
for (const key of keys) this.markDeleted(key);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
hasPending(): boolean {
|
|
47
|
+
return this.dirty.size > 0 || this.deleted.size > 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Persist buffer state. On session-capable storage this writes ONLY the
|
|
52
|
+
* sessions whose state changed since the last successful persist and
|
|
53
|
+
* deletes files for evicted/reset sessions, so the observe path never
|
|
54
|
+
* re-serializes other sessions' turns. On legacy storage it falls back to
|
|
55
|
+
* the whole-state saveBuffer. Files are deleted before dirty writes: a
|
|
56
|
+
* crash mid-persist can then only resurrect already-evicted data (the same
|
|
57
|
+
* exposure as a whole-file rewrite), never drop sessions the state says
|
|
58
|
+
* exist.
|
|
59
|
+
*/
|
|
60
|
+
async persist(
|
|
61
|
+
storage: BufferStorage,
|
|
62
|
+
state: BufferState,
|
|
63
|
+
peekEntry: (key: string) => BufferEntryState | undefined,
|
|
64
|
+
): Promise<void> {
|
|
65
|
+
const saveSession = storage.saveBufferSession?.bind(storage);
|
|
66
|
+
const deleteSession = storage.deleteBufferSession?.bind(storage);
|
|
67
|
+
if (!saveSession || !deleteSession) {
|
|
68
|
+
await storage.saveBuffer(state);
|
|
69
|
+
this.dirty.clear();
|
|
70
|
+
this.deleted.clear();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
for (const key of [...this.deleted]) {
|
|
74
|
+
await deleteSession(key);
|
|
75
|
+
this.deleted.delete(key);
|
|
76
|
+
}
|
|
77
|
+
for (const key of [...this.dirty]) {
|
|
78
|
+
const entry = peekEntry(key);
|
|
79
|
+
if (!entry) {
|
|
80
|
+
// A dirty key without state must not be written from a fabricated
|
|
81
|
+
// empty entry: treat it as a deletion instead.
|
|
82
|
+
this.markDeleted(key);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
await saveSession(key, entry);
|
|
86
|
+
this.dirty.delete(key);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/buffer.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { log } from "./logger.js";
|
|
2
2
|
import { throwIfAborted } from "./abort-error.js";
|
|
3
3
|
import { scanSignals } from "./signal.js";
|
|
4
|
-
import type
|
|
4
|
+
import { BufferSessionPersistence, type BufferStorage } from "./buffer-persistence.js";
|
|
5
|
+
import { selectEvictedBufferKeys } from "./buffer-eviction.js";
|
|
5
6
|
import type {
|
|
6
7
|
BufferEntryState,
|
|
7
8
|
BufferState,
|
|
@@ -175,9 +176,12 @@ export class SmartBuffer {
|
|
|
175
176
|
* activity (see BUFFER_SAVE_MAX_DEFER_MULTIPLIER). */
|
|
176
177
|
private firstPendingAtMs: number | null = null;
|
|
177
178
|
|
|
179
|
+
/** Which sessions' persisted files are stale (per-session incremental saves). */
|
|
180
|
+
private readonly persistence = new BufferSessionPersistence();
|
|
181
|
+
|
|
178
182
|
constructor(
|
|
179
183
|
private readonly config: PluginConfig,
|
|
180
|
-
private readonly storage:
|
|
184
|
+
private readonly storage: BufferStorage,
|
|
181
185
|
surpriseProbe: BufferSurpriseProbe | null = null,
|
|
182
186
|
) {
|
|
183
187
|
this.state = { turns: [], lastExtractionAt: null, extractionCount: 0 };
|
|
@@ -261,60 +265,20 @@ export class SmartBuffer {
|
|
|
261
265
|
);
|
|
262
266
|
}
|
|
263
267
|
|
|
268
|
+
/** Evict oldest entries past MAX_BUFFER_ENTRY_COUNT and queue their session files for deletion. */
|
|
264
269
|
private pruneEntries(retainKeys: string[]): void {
|
|
265
270
|
const entries = this.state.entries;
|
|
266
271
|
if (!entries) return;
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
turns: [],
|
|
274
|
-
lastExtractionAt: null,
|
|
275
|
-
extractionCount: 0,
|
|
276
|
-
});
|
|
277
|
-
const rightAt = this.entryActivityAt(entries[right] ?? {
|
|
278
|
-
turns: [],
|
|
279
|
-
lastExtractionAt: null,
|
|
280
|
-
extractionCount: 0,
|
|
281
|
-
});
|
|
282
|
-
if (leftAt !== rightAt) return leftAt - rightAt;
|
|
283
|
-
return (insertionOrder.get(left) ?? 0) - (insertionOrder.get(right) ?? 0);
|
|
284
|
-
};
|
|
285
|
-
const candidates = keys.filter(
|
|
286
|
-
(key) =>
|
|
287
|
-
key !== "default" &&
|
|
288
|
-
!retainKeys.includes(key) &&
|
|
289
|
-
// #3251: accepted captures exit via refusal/backpressure, never eviction.
|
|
290
|
-
!this.entryHoldsAcceptedCapture(entries[key]),
|
|
291
|
-
);
|
|
292
|
-
const emptyRemovable = candidates
|
|
293
|
-
.filter((key) => (entries[key]?.turns.length ?? 0) === 0)
|
|
294
|
-
.sort(byOldestActivity);
|
|
295
|
-
|
|
296
|
-
const removableCount = Math.max(0, keys.length - MAX_BUFFER_ENTRY_COUNT);
|
|
297
|
-
let evicted = emptyRemovable.slice(0, removableCount);
|
|
298
|
-
if (evicted.length < removableCount) {
|
|
299
|
-
// Retained failed extractions (#1908 retry retention) keep their turns in
|
|
300
|
-
// the buffer, so empty entries alone may not bring the map back under the
|
|
301
|
-
// cap during a provider outage. The cap is the documented bound — evict
|
|
302
|
-
// the oldest NON-empty sessions too rather than growing unboundedly
|
|
303
|
-
// (codex review).
|
|
304
|
-
const nonEmptyRemovable = candidates
|
|
305
|
-
.filter((key) => (entries[key]?.turns.length ?? 0) > 0)
|
|
306
|
-
.sort(byOldestActivity);
|
|
307
|
-
evicted = evicted.concat(nonEmptyRemovable.slice(0, removableCount - evicted.length));
|
|
308
|
-
}
|
|
309
|
-
for (const key of evicted) {
|
|
310
|
-
delete entries[key];
|
|
311
|
-
}
|
|
272
|
+
const evicted = selectEvictedBufferKeys(entries, retainKeys, MAX_BUFFER_ENTRY_COUNT, {
|
|
273
|
+
activityAt: (entry) => this.entryActivityAt(entry),
|
|
274
|
+
holdsAcceptedCapture: (entry) => this.entryHoldsAcceptedCapture(entry),
|
|
275
|
+
});
|
|
276
|
+
for (const key of evicted) delete entries[key];
|
|
277
|
+
this.persistence.markEvicted(evicted);
|
|
312
278
|
if (evicted.length > 0) {
|
|
313
|
-
// Loud degradation (extraction hot-loop hardening, Step 7):
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
// MAX_BUFFER_ENTRY_COUNT is bounded and expected, but must be observable —
|
|
317
|
-
// not silent — so operators can see buffer pressure.
|
|
279
|
+
// Loud degradation (extraction hot-loop hardening, Step 7): buffer
|
|
280
|
+
// pressure during a provider outage is bounded and expected, but must
|
|
281
|
+
// be observable, not silent.
|
|
318
282
|
log.warn(
|
|
319
283
|
`buffer: pruned ${evicted.length} oldest session entr${evicted.length === 1 ? "y" : "ies"} ` +
|
|
320
284
|
`past MAX_BUFFER_ENTRY_COUNT=${MAX_BUFFER_ENTRY_COUNT}; buffered turn data for those sessions was dropped`,
|
|
@@ -376,12 +340,15 @@ export class SmartBuffer {
|
|
|
376
340
|
* so the buffer can still accept new turns for the rest of the session.
|
|
377
341
|
*/
|
|
378
342
|
resetToEmpty(): void {
|
|
343
|
+
for (const key of Object.keys(this.state.entries ?? {})) {
|
|
344
|
+
this.persistence.markDeleted(key);
|
|
345
|
+
}
|
|
379
346
|
this.state = { turns: [], lastExtractionAt: null, extractionCount: 0 };
|
|
380
347
|
this.loaded = true;
|
|
381
348
|
}
|
|
382
349
|
|
|
383
350
|
private async saveUnlocked(): Promise<void> {
|
|
384
|
-
await this.
|
|
351
|
+
await this.persistence.persist(this.storage, this.state, (key) => this.peekEntry(key) ?? undefined);
|
|
385
352
|
}
|
|
386
353
|
|
|
387
354
|
async save(): Promise<void> {
|
|
@@ -495,13 +462,16 @@ export class SmartBuffer {
|
|
|
495
462
|
clearTimeout(this.saveTimer);
|
|
496
463
|
this.saveTimer = null;
|
|
497
464
|
}
|
|
498
|
-
|
|
465
|
+
// resetToEmpty-only deletions are pending work too, even with no
|
|
466
|
+
// pending-save flag set.
|
|
467
|
+
if (!this.pendingSave && !this.persistence.hasPending()) return;
|
|
499
468
|
try {
|
|
500
469
|
await this.enqueueMutation(async () => {
|
|
501
470
|
// Re-check under the serializer: a concurrent flush may have already
|
|
502
471
|
// persisted this pending save. Skip the redundant write (no phantom
|
|
503
|
-
// writes).
|
|
504
|
-
|
|
472
|
+
// writes). resetToEmpty-only deletions also land here without a
|
|
473
|
+
// pending-save flag.
|
|
474
|
+
if (!this.pendingSave && !this.persistence.hasPending()) return;
|
|
505
475
|
await this.saveUnlocked();
|
|
506
476
|
// Clear the pending flag INSIDE the same serialized mutation as the
|
|
507
477
|
// write (issue #1909 review). Clearing it after the enqueue await
|
|
@@ -579,6 +549,7 @@ export class SmartBuffer {
|
|
|
579
549
|
for (const key of emptyRemovable) {
|
|
580
550
|
if (Object.keys(entries).length < MAX_BUFFER_ENTRY_COUNT) break;
|
|
581
551
|
delete entries[key];
|
|
552
|
+
this.persistence.markDeleted(key);
|
|
582
553
|
}
|
|
583
554
|
}
|
|
584
555
|
|
|
@@ -714,6 +685,7 @@ export class SmartBuffer {
|
|
|
714
685
|
const decision = this.evaluate(entry, signal.level);
|
|
715
686
|
const turnCountInWindow = entry.turns.length;
|
|
716
687
|
|
|
688
|
+
this.persistence.markDirty(bufferKey);
|
|
717
689
|
this.pruneEntries([bufferKey]);
|
|
718
690
|
if (decision === "keep_buffering" && this.config.bufferSaveDebounceMs > 0) {
|
|
719
691
|
// Steady-state buffering: coalesce the whole-state serialize onto a
|
|
@@ -792,11 +764,7 @@ export class SmartBuffer {
|
|
|
792
764
|
private async emitSurpriseEventSafe(
|
|
793
765
|
params: SurpriseTelemetryQueueEntry,
|
|
794
766
|
): Promise<void> {
|
|
795
|
-
const storage = this.storage
|
|
796
|
-
appendBufferSurpriseEvents?: (
|
|
797
|
-
events: BufferSurpriseEvent[],
|
|
798
|
-
) => Promise<number>;
|
|
799
|
-
};
|
|
767
|
+
const storage = this.storage;
|
|
800
768
|
if (typeof storage.appendBufferSurpriseEvents !== "function") {
|
|
801
769
|
// Older StorageManager / test double without the telemetry sink.
|
|
802
770
|
// Silently skip — core path is still covered by the log line above.
|
|
@@ -955,6 +923,7 @@ export class SmartBuffer {
|
|
|
955
923
|
);
|
|
956
924
|
if (retained.length > 0) entry.retainedTurns = retained;
|
|
957
925
|
else delete entry.retainedTurns;
|
|
926
|
+
this.persistence.markDirty(bufferKey);
|
|
958
927
|
await this.saveUnlocked();
|
|
959
928
|
});
|
|
960
929
|
}
|
|
@@ -1062,6 +1031,7 @@ export class SmartBuffer {
|
|
|
1062
1031
|
if (remainingTurns.length > 0) entry.retainedTurns = remainingTurns;
|
|
1063
1032
|
else delete entry.retainedTurns;
|
|
1064
1033
|
if (bufferKey === "default") this.state.turns = entry.turns;
|
|
1034
|
+
this.persistence.markDirty(bufferKey);
|
|
1065
1035
|
}
|
|
1066
1036
|
if (changed || hadPendingSave) {
|
|
1067
1037
|
assertLifecycle();
|
|
@@ -1172,6 +1142,7 @@ export class SmartBuffer {
|
|
|
1172
1142
|
this.state.lastExtractionAt = entry.lastExtractionAt;
|
|
1173
1143
|
this.state.extractionCount = entry.extractionCount;
|
|
1174
1144
|
}
|
|
1145
|
+
this.persistence.markDirty(bufferKey);
|
|
1175
1146
|
this.pruneEntries([bufferKey]);
|
|
1176
1147
|
await this.saveNowRetainingPendingOnFailure("clearAfterExtraction");
|
|
1177
1148
|
});
|
|
@@ -34,11 +34,33 @@ function isRemoteAuthoritativeRuntimeShard(basename: string, parts: string[]): b
|
|
|
34
34
|
return EMBEDDING_SHARD_FILE_PATTERN.test(basename) && parts[parts.length - 2] === "embeddings";
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Per-session buffer state files (`state/buffer/<session>.json`) are
|
|
39
|
+
* remote-authoritative like the legacy single `state/buffer.json` they
|
|
40
|
+
* replaced: the incoming copy wins so two nodes do not each keep their own
|
|
41
|
+
* version of the same session's buffered turns.
|
|
42
|
+
*/
|
|
43
|
+
function isRemoteAuthoritativeBufferSessionFile(parts: string[]): boolean {
|
|
44
|
+
const basename = parts[parts.length - 1] ?? "";
|
|
45
|
+
// Matches both `state/buffer/<session>.json` and the namespace-scoped
|
|
46
|
+
// `namespaces/<ns>/state/buffer/<session>.json`.
|
|
47
|
+
return (
|
|
48
|
+
parts.length >= 3 &&
|
|
49
|
+
parts[parts.length - 2] === "buffer" &&
|
|
50
|
+
parts[parts.length - 3] === "state" &&
|
|
51
|
+
basename.endsWith(".json")
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
37
55
|
export function shouldPreferIncomingOfflineRuntimeFile(relPosix: string): boolean {
|
|
38
56
|
const parts = relPosix.split("/");
|
|
39
57
|
const basename = parts[parts.length - 1] ?? "";
|
|
40
|
-
return
|
|
41
|
-
(
|
|
58
|
+
return (
|
|
59
|
+
isCanonicalRuntimeStatePath(parts) &&
|
|
60
|
+
(REMOTE_AUTHORITATIVE_RUNTIME_STATE_FILES.has(basename) ||
|
|
61
|
+
isRemoteAuthoritativeRuntimeShard(basename, parts) ||
|
|
62
|
+
isRemoteAuthoritativeBufferSessionFile(parts))
|
|
63
|
+
);
|
|
42
64
|
}
|
|
43
65
|
|
|
44
66
|
export function shouldDeleteAbsentIncomingOfflineRuntimeFile(relPosix: string): boolean {
|
package/src/offline-sync.test.ts
CHANGED
|
@@ -506,6 +506,15 @@ test("offline sync push-side default excludes live LCM sqlite but apply-side sti
|
|
|
506
506
|
assert.equal(shouldPreferIncomingOfflineRuntimeFile("state/embeddings/other.json"), false);
|
|
507
507
|
// Shard preference is scoped to the embedding index directory.
|
|
508
508
|
assert.equal(shouldPreferIncomingOfflineRuntimeFile("state/other/shard-0000.json"), false);
|
|
509
|
+
// Per-session buffer files are remote-authoritative like the legacy
|
|
510
|
+
// single state/buffer.json they replaced.
|
|
511
|
+
assert.equal(shouldPreferIncomingOfflineRuntimeFile("state/buffer/session-a.json"), true);
|
|
512
|
+
assert.equal(
|
|
513
|
+
shouldPreferIncomingOfflineRuntimeFile("namespaces/team/state/buffer/session-a.json"),
|
|
514
|
+
true,
|
|
515
|
+
);
|
|
516
|
+
assert.equal(shouldPreferIncomingOfflineRuntimeFile("state/buffer/notes.txt"), false);
|
|
517
|
+
assert.equal(shouldPreferIncomingOfflineRuntimeFile("state/buffer-other/session-a.json"), false);
|
|
509
518
|
|
|
510
519
|
// buildOfflineSyncSnapshotForPaths still rejects explicitly excluded
|
|
511
520
|
// paths so callers cannot bypass the default exclude via path lists.
|
|
@@ -836,6 +836,7 @@ function isEncryptableStoragePath(filePath: string, rootDir: string): boolean {
|
|
|
836
836
|
const normalized = normalizeStorageRelativePath(rel);
|
|
837
837
|
if (normalized === "profile.md") return true;
|
|
838
838
|
if (isEncryptableStateSidecar(normalized)) return true;
|
|
839
|
+
if (isEncryptableBufferSessionFile(normalized)) return true;
|
|
839
840
|
if (isEncryptableSummarySidecar(normalized)) return true;
|
|
840
841
|
const firstSegment = normalized.split("/", 1)[0];
|
|
841
842
|
return ENCRYPTABLE_MARKDOWN_STORAGE_ROOTS.has(firstSegment) && normalized.endsWith(".md");
|
|
@@ -904,6 +905,16 @@ function isEncryptableStateSidecar(normalized: string): boolean {
|
|
|
904
905
|
return ENCRYPTABLE_STATE_SIDECARS.has(normalized);
|
|
905
906
|
}
|
|
906
907
|
|
|
908
|
+
/**
|
|
909
|
+
* Per-session buffer state files (`state/buffer/<session>.json`) hold the same
|
|
910
|
+
* buffered turns the legacy `state/buffer.json` held, so they are encryptable
|
|
911
|
+
* state sidecars too — otherwise enabling the secure store would leave the
|
|
912
|
+
* daemon's buffered turns in plaintext on disk.
|
|
913
|
+
*/
|
|
914
|
+
function isEncryptableBufferSessionFile(normalized: string): boolean {
|
|
915
|
+
return normalized.startsWith("state/buffer/") && normalized.endsWith(".json");
|
|
916
|
+
}
|
|
917
|
+
|
|
907
918
|
function isEncryptableSummarySidecar(normalized: string): boolean {
|
|
908
919
|
return normalized.startsWith("summaries/") && normalized.endsWith(".json");
|
|
909
920
|
}
|
|
@@ -620,6 +620,28 @@ test("secure-store migration keeps namespaced file AAD readable through memory r
|
|
|
620
620
|
}
|
|
621
621
|
});
|
|
622
622
|
|
|
623
|
+
test("secure-store migration encrypts per-session buffer state files", async () => {
|
|
624
|
+
const memoryDir = await mkdtemp(path.join(tmpdir(), "remnic-secure-store-buffer-session-"));
|
|
625
|
+
try {
|
|
626
|
+
const sessionPath = path.join(memoryDir, "state", "buffer", "session-a.json");
|
|
627
|
+
await mkdir(path.dirname(sessionPath), { recursive: true });
|
|
628
|
+
await writeFile(sessionPath, JSON.stringify({ key: "session-a", turns: [] }), "utf8");
|
|
629
|
+
|
|
630
|
+
const key = deriveKeyScrypt("buffer-session", Buffer.alloc(KDF_SALT_LENGTH, 0x66), FAST_SCRYPT);
|
|
631
|
+
const migrated = await migrateMemoryDirToEncrypted(memoryDir, key);
|
|
632
|
+
|
|
633
|
+
assert.equal(migrated.encrypted, 1);
|
|
634
|
+
assert.deepEqual(migrated.errors, []);
|
|
635
|
+
assert.equal(isEncryptedFile(await readFile(sessionPath)), true);
|
|
636
|
+
assert.equal(
|
|
637
|
+
await readMaybeEncryptedFile(sessionPath, key, memoryDir),
|
|
638
|
+
JSON.stringify({ key: "session-a", turns: [] }),
|
|
639
|
+
);
|
|
640
|
+
} finally {
|
|
641
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
|
|
623
645
|
test("secure-store migration treats missing memory roots as empty", async () => {
|
|
624
646
|
const tempRoot = await mkdtemp(path.join(tmpdir(), "remnic-secure-store-missing-root-"));
|
|
625
647
|
try {
|