@remnic/core 10.0.35 → 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 +81 -81
- package/dist/access-authorization-probe.js +82 -82
- package/dist/access-boundary.js +81 -81
- package/dist/access-citation-namespace.js +2 -2
- package/dist/access-cli.js +172 -170
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.js +81 -81
- package/dist/access-http-briefing.js +82 -82
- package/dist/access-http-query.js +83 -83
- package/dist/access-http.js +104 -104
- package/dist/access-identity-continuity-surface.js +81 -81
- package/dist/access-lcm-surface.js +81 -81
- package/dist/access-mcp.js +93 -93
- package/dist/access-namespace-preflight.js +81 -81
- package/dist/access-observe-write-category.js +13 -13
- package/dist/access-observe-write-surface.js +81 -81
- package/dist/access-offline-manifest.js +12 -12
- package/dist/access-operations-batch.js +85 -85
- package/dist/access-operations.js +92 -92
- package/dist/access-recall-concurrency.js +81 -81
- package/dist/access-recall-response.js +81 -81
- package/dist/access-recall-surface.js +81 -81
- package/dist/access-schema.js +8 -8
- package/dist/access-service.js +81 -81
- package/dist/access-surface-catalog.js +83 -83
- 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/calibration.js +3 -3
- 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-HT6IEMQ2.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-G2PC6IZB.js → chunk-5U3NUHMA.js} +86 -86
- package/dist/{chunk-4LOMATQA.js → chunk-5UWP5X3T.js} +2 -2
- package/dist/{chunk-JUXOTY6J.js → chunk-5YQXWVCP.js} +9 -2
- package/dist/chunk-5YQXWVCP.js.map +1 -0
- package/dist/{chunk-G3T6YMBO.js → chunk-64S6X4IU.js} +7 -7
- package/dist/{chunk-NHB4RC3G.js → chunk-6QOL4G76.js} +2 -2
- package/dist/{chunk-GRBQO56D.js → chunk-6R6CU3MR.js} +51 -51
- package/dist/{chunk-SQ554O3K.js → chunk-74AY3S7U.js} +3 -3
- 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-GRZX3RHY.js → chunk-BPBUUGAO.js} +2 -2
- package/dist/{chunk-DFP37GUN.js → chunk-CCYXCT4B.js} +2 -2
- package/dist/{chunk-JJWOKRZA.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-NLQBTHBV.js → chunk-EVVCE7S2.js} +2 -2
- package/dist/{chunk-FQ52AJXX.js → chunk-FBAHBFT5.js} +2 -2
- package/dist/{chunk-D75ET36O.js → chunk-FL6SLVAJ.js} +2 -2
- package/dist/{chunk-6NAN4WFY.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-GQVTALUJ.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-MGXLLL5L.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-TVM47JQB.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-KVNOHMVE.js → chunk-PTRPOJ7U.js} +2 -2
- package/dist/{chunk-SXRX6K35.js → chunk-QEGE5QCL.js} +47 -47
- package/dist/{chunk-CM6JDFCN.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-GK56GNNS.js → chunk-RVYXSS3Q.js} +8 -8
- package/dist/{chunk-G7XAZU2L.js → chunk-RWWYBE7E.js} +3 -3
- package/dist/{chunk-HJM23E65.js → chunk-S53WZWGL.js} +2 -2
- package/dist/{chunk-MLED254J.js → chunk-T6OZW2WR.js} +8 -2
- package/dist/chunk-T6OZW2WR.js.map +1 -0
- 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-ZRU7636R.js → chunk-UMFBHYPW.js} +12 -12
- 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-OLAQWYNF.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-P2RCFQA6.js → chunk-WADPRAZE.js} +2 -2
- package/dist/{chunk-IVYAPAHG.js → chunk-WRDFYSUC.js} +2 -2
- package/dist/{chunk-FW7KWZBI.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 +131 -131
- 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 +82 -82
- 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 +226 -224
- package/dist/lcm/index.js +3 -3
- package/dist/local-llm-helpers.d.ts.map +1 -1
- package/dist/local-llm-helpers.js +2 -1
- package/dist/local-llm-transport.d.ts +1 -0
- package/dist/local-llm-transport.d.ts.map +1 -1
- package/dist/local-llm-transport.js +5 -3
- package/dist/local-llm.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 +82 -82
- 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 +44 -44
- package/dist/orchestrator.js +174 -172
- package/dist/qmd.js +4 -4
- package/dist/recall-navigation-operations.js +82 -82
- package/dist/recall-navigation.js +81 -81
- 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/summarizer.js +4 -4
- package/dist/support-passport/index.js +84 -84
- 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/local-llm-headers-timeout.test.ts +64 -0
- package/src/local-llm-helpers.ts +6 -1
- package/src/local-llm-transport.ts +14 -1
- 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-JUXOTY6J.js.map +0 -1
- package/dist/chunk-MLED254J.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-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-HT6IEMQ2.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-G2PC6IZB.js.map → chunk-5U3NUHMA.js.map} +0 -0
- /package/dist/{chunk-4LOMATQA.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-GRBQO56D.js.map → chunk-6R6CU3MR.js.map} +0 -0
- /package/dist/{chunk-SQ554O3K.js.map → chunk-74AY3S7U.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-GRZX3RHY.js.map → chunk-BPBUUGAO.js.map} +0 -0
- /package/dist/{chunk-DFP37GUN.js.map → chunk-CCYXCT4B.js.map} +0 -0
- /package/dist/{chunk-JJWOKRZA.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-NLQBTHBV.js.map → chunk-EVVCE7S2.js.map} +0 -0
- /package/dist/{chunk-FQ52AJXX.js.map → chunk-FBAHBFT5.js.map} +0 -0
- /package/dist/{chunk-D75ET36O.js.map → chunk-FL6SLVAJ.js.map} +0 -0
- /package/dist/{chunk-6NAN4WFY.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-GQVTALUJ.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-MGXLLL5L.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-TVM47JQB.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-KVNOHMVE.js.map → chunk-PTRPOJ7U.js.map} +0 -0
- /package/dist/{chunk-SXRX6K35.js.map → chunk-QEGE5QCL.js.map} +0 -0
- /package/dist/{chunk-CM6JDFCN.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-GK56GNNS.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-ZRU7636R.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-OLAQWYNF.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-P2RCFQA6.js.map → chunk-WADPRAZE.js.map} +0 -0
- /package/dist/{chunk-IVYAPAHG.js.map → chunk-WRDFYSUC.js.map} +0 -0
- /package/dist/{chunk-FW7KWZBI.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
|
});
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "undici";
|
|
13
13
|
|
|
14
14
|
import { LocalLlmClient } from "./local-llm.js";
|
|
15
|
+
import { probeFetch } from "./local-llm-helpers.js";
|
|
15
16
|
import { classifyAnalysisProviderError } from "./activity/timeline/analysis-provider.js";
|
|
16
17
|
import { ChatTransport } from "./local-llm-transport.js";
|
|
17
18
|
import { initLogger, resetLogger } from "./logger.js";
|
|
@@ -466,6 +467,69 @@ test("a process-wide custom dispatcher is left in place", async () => {
|
|
|
466
467
|
await restored.close();
|
|
467
468
|
});
|
|
468
469
|
|
|
470
|
+
test("an explicit opt-in widens the pool even when a custom global dispatcher is installed", async () => {
|
|
471
|
+
// Some hosts install a process-wide dispatcher (for example an agent runtime's
|
|
472
|
+
// managed proxy) that is unrelated to the LAN model server. There the 300s
|
|
473
|
+
// header cap stood silently and every slow completion died with `fetch failed`
|
|
474
|
+
// at 301s. REMNIC_LOCAL_LLM_DIRECT=1 declares that the local endpoint is
|
|
475
|
+
// reached directly, so the transport uses its own pool. Without the flag the
|
|
476
|
+
// previous behavior (leave the custom dispatcher alone) is unchanged.
|
|
477
|
+
const original = getGlobalDispatcher();
|
|
478
|
+
const proxy = new ProxyAgent("http://127.0.0.1:9");
|
|
479
|
+
const priorFlag = process.env.REMNIC_LOCAL_LLM_DIRECT;
|
|
480
|
+
try {
|
|
481
|
+
setGlobalDispatcher(proxy);
|
|
482
|
+
delete process.env.REMNIC_LOCAL_LLM_DIRECT;
|
|
483
|
+
assert.equal(new ChatTransport().dispatcherFor(900_000), undefined, "no flag: custom dispatcher stays");
|
|
484
|
+
|
|
485
|
+
process.env.REMNIC_LOCAL_LLM_DIRECT = "1";
|
|
486
|
+
const transport = new ChatTransport();
|
|
487
|
+
const agent = transport.dispatcherFor(900_000);
|
|
488
|
+
assert.ok(agent instanceof Agent, "flag set: the widened pool must be used");
|
|
489
|
+
assert.ok(!(agent instanceof ProxyAgent), "flag set: the proxy dispatcher must not be used");
|
|
490
|
+
await agent.close();
|
|
491
|
+
|
|
492
|
+
process.env.REMNIC_LOCAL_LLM_DIRECT = "0";
|
|
493
|
+
assert.equal(new ChatTransport().dispatcherFor(900_000), undefined, "flag 0: custom dispatcher stays");
|
|
494
|
+
} finally {
|
|
495
|
+
if (priorFlag === undefined) delete process.env.REMNIC_LOCAL_LLM_DIRECT;
|
|
496
|
+
else process.env.REMNIC_LOCAL_LLM_DIRECT = priorFlag;
|
|
497
|
+
setGlobalDispatcher(original);
|
|
498
|
+
await proxy.close();
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test("REMNIC_LOCAL_LLM_DIRECT=1 also sends availability probes around a global proxy dispatcher", async () => {
|
|
503
|
+
// A host whose global dispatcher cannot reach the LAN model server would fail
|
|
504
|
+
// checkAvailability() before any chat request runs, so direct mode must cover
|
|
505
|
+
// the probe as well as ChatTransport.post.
|
|
506
|
+
const server = http.createServer((_req, res) => {
|
|
507
|
+
res.setHeader("content-type", "application/json");
|
|
508
|
+
res.end('{"data":[]}');
|
|
509
|
+
});
|
|
510
|
+
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
511
|
+
// SAFETY: listen(0, "127.0.0.1") above binds a TCP port, so address() is an AddressInfo, never a pipe string or null.
|
|
512
|
+
const { port } = server.address() as AddressInfo;
|
|
513
|
+
const url = `http://127.0.0.1:${port}/v1/models`;
|
|
514
|
+
const original = getGlobalDispatcher();
|
|
515
|
+
const priorFlag = process.env.REMNIC_LOCAL_LLM_DIRECT;
|
|
516
|
+
try {
|
|
517
|
+
setGlobalDispatcher(new ProxyAgent("http://127.0.0.1:9"));
|
|
518
|
+
delete process.env.REMNIC_LOCAL_LLM_DIRECT;
|
|
519
|
+
const viaProxy = await probeFetch(url, { timeoutMs: 2_000, headers: {} });
|
|
520
|
+
assert.equal(viaProxy.ok, false, "no flag: the probe goes through the (unreachable) proxy");
|
|
521
|
+
|
|
522
|
+
process.env.REMNIC_LOCAL_LLM_DIRECT = "1";
|
|
523
|
+
const direct = await probeFetch(url, { timeoutMs: 2_000, headers: {} });
|
|
524
|
+
assert.equal(direct.ok, true, "flag set: the probe reaches the endpoint directly");
|
|
525
|
+
} finally {
|
|
526
|
+
if (priorFlag === undefined) delete process.env.REMNIC_LOCAL_LLM_DIRECT;
|
|
527
|
+
else process.env.REMNIC_LOCAL_LLM_DIRECT = priorFlag;
|
|
528
|
+
setGlobalDispatcher(original);
|
|
529
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
|
|
469
533
|
test("aborting during a retry backoff stops the local request lane", async () => {
|
|
470
534
|
const original = globalThis.fetch;
|
|
471
535
|
const abortController = new AbortController();
|
package/src/local-llm-helpers.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ResponseFormatJSONSchema } from "openai/resources/shared";
|
|
2
|
+
import type { Agent } from "undici";
|
|
2
3
|
import { log } from "./logger.js";
|
|
4
|
+
import { directProbeDispatcher } from "./local-llm-transport.js";
|
|
3
5
|
import type { LocalLlmRequestPriority } from "./local-llm.js";
|
|
4
6
|
|
|
5
7
|
export interface LocalLlmChatCompletionOptions {
|
|
@@ -167,7 +169,10 @@ export async function probeFetch(
|
|
|
167
169
|
: controller.signal;
|
|
168
170
|
const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
169
171
|
try {
|
|
170
|
-
const
|
|
172
|
+
const init: RequestInit & { dispatcher?: Agent } = { signal: requestSignal, headers: options.headers };
|
|
173
|
+
const dispatcher = directProbeDispatcher();
|
|
174
|
+
if (dispatcher) init.dispatcher = dispatcher;
|
|
175
|
+
const response = await fetch(url, init);
|
|
171
176
|
clearTimeout(timeout);
|
|
172
177
|
if (!response.ok) {
|
|
173
178
|
log.debug(`local LLM probe: ${url} returned HTTP ${response.status}`);
|
|
@@ -40,6 +40,18 @@ function isPlainPoolingAgent(dispatcher: unknown): boolean {
|
|
|
40
40
|
return ctor.name === "Agent";
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* REMNIC_LOCAL_LLM_DIRECT=1 declares that the local endpoint is reached
|
|
45
|
+
* directly. Availability probes use this shared plain pool in that mode so they
|
|
46
|
+
* take the same route as chat requests; `undefined` keeps the global dispatcher.
|
|
47
|
+
*/
|
|
48
|
+
let directProbeAgent: Agent | null = null;
|
|
49
|
+
export function directProbeDispatcher(): Agent | undefined {
|
|
50
|
+
if (process.env.REMNIC_LOCAL_LLM_DIRECT !== "1") return undefined;
|
|
51
|
+
directProbeAgent ??= new Agent();
|
|
52
|
+
return directProbeAgent;
|
|
53
|
+
}
|
|
54
|
+
|
|
43
55
|
/**
|
|
44
56
|
* Connection pool whose header/body inactivity budgets track the configured
|
|
45
57
|
* request budget.
|
|
@@ -72,7 +84,8 @@ export class ChatTransport {
|
|
|
72
84
|
* because only the dispatcher's owner knows how to rebuild it.
|
|
73
85
|
*/
|
|
74
86
|
dispatcherFor(budgetMs: number): Agent | undefined {
|
|
75
|
-
|
|
87
|
+
// REMNIC_LOCAL_LLM_DIRECT=1: the operator declares the local endpoint is reached directly even though the process installed a global dispatcher for other traffic (an agent runtime's managed proxy). Without it the 300s header cap stands silently on such hosts and every completion slower than five minutes dies with `fetch failed`. Off by default so deployments that must use a proxy keep it.
|
|
88
|
+
if (!isPlainPoolingAgent(getGlobalDispatcher()) && process.env.REMNIC_LOCAL_LLM_DIRECT !== "1") {
|
|
76
89
|
if (!this.warnedAboutCustomDispatcher) {
|
|
77
90
|
this.warnedAboutCustomDispatcher = true;
|
|
78
91
|
log.debug(
|