@remnic/core 10.0.60 → 10.0.62
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 +25 -24
- package/dist/access-authorization-probe.js +26 -25
- package/dist/access-boundary.js +25 -24
- package/dist/access-cli.js +73 -72
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.js +25 -24
- package/dist/access-http-briefing.js +26 -25
- package/dist/access-http-query.js +27 -26
- package/dist/access-http.js +44 -43
- package/dist/access-identity-continuity-surface.js +25 -24
- package/dist/access-lcm-surface.js +25 -24
- package/dist/access-mcp.js +36 -35
- package/dist/access-namespace-preflight.js +25 -24
- package/dist/access-observe-write-category.js +4 -4
- package/dist/access-observe-write-surface.js +25 -24
- package/dist/access-operations-batch.js +27 -26
- package/dist/access-operations.js +34 -33
- package/dist/access-recall-concurrency.js +25 -24
- package/dist/access-recall-diagnostics.js +2 -2
- package/dist/access-recall-response.js +25 -24
- package/dist/access-recall-surface.js +25 -24
- package/dist/access-schema.js +3 -3
- package/dist/access-service.js +25 -24
- package/dist/access-surface-catalog.js +27 -26
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/active-recall.js +8 -6
- package/dist/active-recall.js.map +1 -1
- package/dist/briefing.js +8 -6
- package/dist/buffer-persistence.d.ts +14 -3
- package/dist/buffer-persistence.d.ts.map +1 -1
- package/dist/buffer-persistence.js +1 -1
- package/dist/buffer-turn-helpers.d.ts +7 -0
- package/dist/buffer-turn-helpers.d.ts.map +1 -1
- package/dist/buffer-turn-helpers.js +5 -3
- package/dist/buffer.d.ts +5 -1
- package/dist/buffer.d.ts.map +1 -1
- package/dist/buffer.js +3 -3
- package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-GWVG7LGC.js} +2 -2
- package/dist/causal-consolidation.js +9 -7
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/{chunk-VHBGJAYC.js → chunk-25QK3LR2.js} +2 -2
- package/dist/{chunk-X2TZWVCC.js → chunk-3HLWLYRD.js} +2 -2
- package/dist/{chunk-TIS6T6UP.js → chunk-3VNRDRBM.js} +2 -2
- package/dist/{chunk-4O4VAEWH.js → chunk-4NLHEE2Z.js} +2 -2
- package/dist/{chunk-3DG2YAAX.js → chunk-6EZPYZT3.js} +62 -35
- package/dist/chunk-6EZPYZT3.js.map +1 -0
- package/dist/{chunk-I6UTOUBK.js → chunk-6VBKU3FY.js} +150 -345
- package/dist/chunk-6VBKU3FY.js.map +1 -0
- package/dist/{chunk-66CC6L6R.js → chunk-76LKZ42K.js} +2 -2
- package/dist/{chunk-LR7RW4T6.js → chunk-7ETWIBTO.js} +3 -3
- package/dist/{chunk-JM2WJBP7.js → chunk-7JFXXA57.js} +2 -2
- package/dist/{chunk-PIVSUPF2.js → chunk-A6JV3WHI.js} +2 -2
- package/dist/{chunk-MOB4JHG6.js → chunk-BDNPW3KM.js} +2 -2
- package/dist/{chunk-IJFX4JRI.js → chunk-BQOOUKEN.js} +7 -7
- package/dist/{chunk-PRSF7DWT.js → chunk-BU4W2ISH.js} +2 -2
- package/dist/{chunk-KQAFEZQX.js → chunk-BUKK5SWA.js} +2 -2
- package/dist/{chunk-JRY55I6Z.js → chunk-C2AYUXEF.js} +2 -2
- package/dist/{chunk-DF4SYU4Z.js → chunk-C5OW2FFB.js} +2 -2
- package/dist/{chunk-74Z2BYEK.js → chunk-CYEEVWQ6.js} +2 -2
- package/dist/{chunk-M7KAJBO2.js → chunk-EADUOZWH.js} +3 -3
- package/dist/{chunk-N5H22V75.js → chunk-ECT5ME6X.js} +2 -2
- package/dist/{chunk-M3GOWG2P.js → chunk-FMOUUWQN.js} +2 -2
- package/dist/{chunk-2EJAPJ5G.js → chunk-FYPKSR6Z.js} +2 -2
- package/dist/{chunk-L5RMURVI.js → chunk-GDCKOER4.js} +41 -13
- package/dist/chunk-GDCKOER4.js.map +1 -0
- package/dist/{chunk-LGLLCWA2.js → chunk-GNEB3WXR.js} +2 -2
- package/dist/{chunk-Z4EH5TSO.js → chunk-HNZH57JK.js} +2 -2
- package/dist/{chunk-PTJWUN4H.js → chunk-IJKBIOWL.js} +30 -30
- package/dist/{chunk-C5GTIRIO.js → chunk-IMNJJKHW.js} +22 -22
- package/dist/{chunk-ADK5TMQL.js → chunk-IVJI5FDA.js} +2 -2
- package/dist/{chunk-F3GX76LV.js → chunk-IWAB7JVH.js} +2 -2
- package/dist/{chunk-JFRO5HJY.js → chunk-J62O2UW5.js} +2 -2
- package/dist/chunk-JCL7RO2G.js +674 -0
- package/dist/chunk-JCL7RO2G.js.map +1 -0
- package/dist/{chunk-A3UFV4K3.js → chunk-JIYJIH5N.js} +2 -2
- package/dist/{chunk-UIX4JLVV.js → chunk-JVFYP2LD.js} +2 -2
- package/dist/{chunk-KJ3MDX4V.js → chunk-KXY4HW2Y.js} +2 -2
- package/dist/{chunk-VQ6H4DL6.js → chunk-L334PSLW.js} +4 -4
- package/dist/{chunk-IA3XZG4V.js → chunk-L74VVEJA.js} +2 -2
- package/dist/{chunk-UY67YP6Q.js → chunk-LIUI46BX.js} +2 -2
- package/dist/{chunk-4OFXLQJD.js → chunk-MPDLLLOG.js} +8 -3
- package/dist/chunk-MPDLLLOG.js.map +1 -0
- package/dist/{chunk-UP2Q7U4G.js → chunk-MTVC5S5N.js} +2 -2
- package/dist/{chunk-QWURVT5Z.js → chunk-N7CATBYH.js} +2 -2
- package/dist/{chunk-JEDS2QYV.js → chunk-OH55F76C.js} +2 -2
- package/dist/{chunk-2NLLXCJG.js → chunk-OWHERGF2.js} +2 -2
- package/dist/{chunk-G6GMZJQ4.js → chunk-PA6ZNFB7.js} +2 -2
- package/dist/{chunk-6BKSUA6P.js → chunk-PWEIZCPS.js} +2 -2
- package/dist/{chunk-T4SMMTGY.js → chunk-Q3DAWUAV.js} +2 -2
- package/dist/{chunk-AWKADEMO.js → chunk-QCAJMIAE.js} +3 -3
- package/dist/{chunk-ZRDEOC4X.js → chunk-QI7LLSWX.js} +2 -2
- package/dist/{chunk-7DHNBABU.js → chunk-QJ6IR35B.js} +3 -3
- package/dist/{chunk-J53IBVMF.js → chunk-R25JMO5O.js} +2 -2
- package/dist/{chunk-PARM6OYL.js → chunk-SEA56G6S.js} +1 -1
- package/dist/chunk-SEA56G6S.js.map +1 -0
- package/dist/{chunk-HL643H2F.js → chunk-TMWSCE2L.js} +8 -8
- package/dist/{chunk-FLDXEJFX.js → chunk-TPCRDPUY.js} +2 -2
- package/dist/{chunk-KZMRMKC7.js → chunk-U7MIMS2Y.js} +11 -1
- package/dist/chunk-U7MIMS2Y.js.map +1 -0
- package/dist/{chunk-WJC5KGYC.js → chunk-UFQB5VQ5.js} +6 -6
- package/dist/{chunk-64WHWIPL.js → chunk-UVHXLVFJ.js} +2 -2
- package/dist/{chunk-X7Y7WX73.js → chunk-UVYI6VIX.js} +1 -1
- package/dist/{chunk-5XY46Z2W.js → chunk-UXRCORZX.js} +3 -3
- package/dist/{chunk-WUL6LDJ4.js → chunk-VPEA63FC.js} +2 -2
- package/dist/{chunk-MHSUEA32.js → chunk-WA6XRNWE.js} +3 -3
- package/dist/{chunk-GGHX4YHM.js → chunk-WB3OFPM2.js} +2 -2
- package/dist/{chunk-RSYFSCKL.js → chunk-WZEDFDJ6.js} +14 -14
- package/dist/{chunk-2HVCTFKG.js → chunk-X2ZLHUEO.js} +2 -2
- package/dist/{chunk-PJLJN23N.js → chunk-XICJL56Z.js} +2 -2
- package/dist/{chunk-ZXEPLZUQ.js → chunk-YGM47S7M.js} +7 -7
- package/dist/{chunk-XWXBXBOX.js → chunk-YLB7TW2B.js} +2 -1
- package/dist/chunk-YLB7TW2B.js.map +1 -0
- package/dist/{chunk-KXDUSGPM.js → chunk-YRL4PGPP.js} +2 -2
- package/dist/{chunk-GJF337YM.js → chunk-Z45353ML.js} +2 -2
- package/dist/{chunk-NJZY5LOC.js → chunk-ZARHK7PT.js} +2 -2
- package/dist/{chunk-NF5EHQTJ.js → chunk-ZFU2B2GM.js} +5 -5
- package/dist/{chunk-W37MS6SX.js → chunk-ZRSXXWIX.js} +2 -2
- package/dist/cli.js +60 -59
- package/dist/coding/export-okf-codegraph.js +7 -5
- package/dist/coding/export-okf-codegraph.js.map +1 -1
- package/dist/compounding/engine.js +9 -7
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -6
- package/dist/connectors/codex-materialize-runner.js +8 -6
- package/dist/connectors/index.js +8 -6
- package/dist/convergence-refresh.js +10 -8
- package/dist/corpus-watermark.js +10 -8
- package/dist/deep-recall-http-glue.js +4 -4
- package/dist/entity-retrieval.js +8 -6
- package/dist/external-wiki-access.js +26 -25
- package/dist/extraction.js +8 -6
- package/dist/harmonic-retrieval.js +8 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +75 -72
- package/dist/location/index.js +10 -8
- package/dist/maintenance/memory-governance.js +8 -6
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +8 -6
- package/dist/maintenance/rebuild-memory-projection-runner.js +10 -8
- package/dist/maintenance/rebuild-memory-projection.js +9 -7
- package/dist/memory-browse-operations.js +26 -25
- package/dist/namespaces/migrate.js +9 -7
- package/dist/namespaces/search.js +3 -3
- package/dist/namespaces/storage.js +8 -6
- package/dist/operator-doctor-corpus.js +11 -9
- package/dist/operator-toolkit.js +17 -15
- package/dist/orchestration/buffer-drain-gate.d.ts +21 -0
- package/dist/orchestration/buffer-drain-gate.d.ts.map +1 -0
- package/dist/orchestration/buffer-drain-runner.d.ts +79 -0
- package/dist/orchestration/buffer-drain-runner.d.ts.map +1 -0
- package/dist/orchestration/buffer-drain-runner.js +780 -0
- package/dist/orchestration/buffer-drain-runner.js.map +1 -0
- package/dist/orchestration/buffer-swap.d.ts +12 -0
- package/dist/orchestration/buffer-swap.d.ts.map +1 -0
- package/dist/orchestration/maintenance.js +18 -16
- package/dist/orchestration/turn-ingestion.d.ts.map +1 -1
- package/dist/orchestrator.d.ts +4 -1
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +73 -72
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-navigate-access.js +2 -2
- package/dist/recall-navigate-disclosure.js +2 -2
- package/dist/recall-navigate.js +2 -2
- package/dist/recall-navigation-operations.js +26 -25
- package/dist/recall-navigation.js +25 -24
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.js +2 -2
- package/dist/resume-bundles.js +9 -7
- package/dist/search/factory.js +2 -2
- package/dist/search/index.js +4 -4
- package/dist/semantic-consolidation.js +9 -7
- package/dist/semantic-rule-promotion.js +8 -6
- package/dist/semantic-rule-verifier.js +8 -6
- package/dist/source-agent-qualifier.js +8 -6
- package/dist/standup/build.js +8 -6
- package/dist/storage/buffer-drain-claims.d.ts +159 -0
- package/dist/storage/buffer-drain-claims.d.ts.map +1 -0
- package/dist/storage/buffer-drain-claims.js +18 -0
- package/dist/storage/buffer-drain-claims.js.map +1 -0
- package/dist/storage/buffer-session-files.d.ts +14 -2
- package/dist/storage/buffer-session-files.d.ts.map +1 -1
- package/dist/storage/buffer-session-files.js +22 -0
- package/dist/storage/buffer-session-files.js.map +1 -0
- package/dist/storage.d.ts +6 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +7 -5
- package/dist/support-passport/index.js +28 -27
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +2 -2
- package/dist/transfer/export-okf.js +8 -6
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/verified-recall.js +8 -6
- package/dist/who-knows-cli.js +9 -7
- package/dist/who-knows.js +8 -6
- package/package.json +32 -2
- package/src/access-observe-replay-durability.test.ts +1 -0
- package/src/buffer-drain-handover.test.ts +307 -0
- package/src/buffer-incremental-persistence.test.ts +19 -1
- package/src/buffer-persistence.ts +27 -5
- package/src/buffer-session.test.ts +3 -3
- package/src/buffer-turn-helpers.ts +16 -0
- package/src/buffer.ts +50 -10
- package/src/config.ts +1 -0
- package/src/index.ts +1 -1
- package/src/orchestration/buffer-drain-gate.test.ts +40 -0
- package/src/orchestration/buffer-drain-gate.ts +38 -0
- package/src/orchestration/buffer-drain-runner.ts +312 -0
- package/src/orchestration/buffer-swap.ts +27 -0
- package/src/orchestration/turn-ingestion.ts +2 -0
- package/src/orchestrator.ts +9 -6
- package/src/storage/buffer-drain-claims.test.ts +448 -0
- package/src/storage/buffer-drain-claims.ts +593 -0
- package/src/storage/buffer-session-files.ts +50 -3
- package/src/storage.ts +8 -11
- package/src/testing/subjects/extraction-lifecycle.test.ts +34 -0
- package/src/types.ts +2 -0
- package/dist/chunk-3DG2YAAX.js.map +0 -1
- package/dist/chunk-4OFXLQJD.js.map +0 -1
- package/dist/chunk-I6UTOUBK.js.map +0 -1
- package/dist/chunk-KZMRMKC7.js.map +0 -1
- package/dist/chunk-L5RMURVI.js.map +0 -1
- package/dist/chunk-PARM6OYL.js.map +0 -1
- package/dist/chunk-XWXBXBOX.js.map +0 -1
- /package/dist/{capsule-crypto-7FJQINUR.js.map → capsule-crypto-GWVG7LGC.js.map} +0 -0
- /package/dist/{chunk-VHBGJAYC.js.map → chunk-25QK3LR2.js.map} +0 -0
- /package/dist/{chunk-X2TZWVCC.js.map → chunk-3HLWLYRD.js.map} +0 -0
- /package/dist/{chunk-TIS6T6UP.js.map → chunk-3VNRDRBM.js.map} +0 -0
- /package/dist/{chunk-4O4VAEWH.js.map → chunk-4NLHEE2Z.js.map} +0 -0
- /package/dist/{chunk-66CC6L6R.js.map → chunk-76LKZ42K.js.map} +0 -0
- /package/dist/{chunk-LR7RW4T6.js.map → chunk-7ETWIBTO.js.map} +0 -0
- /package/dist/{chunk-JM2WJBP7.js.map → chunk-7JFXXA57.js.map} +0 -0
- /package/dist/{chunk-PIVSUPF2.js.map → chunk-A6JV3WHI.js.map} +0 -0
- /package/dist/{chunk-MOB4JHG6.js.map → chunk-BDNPW3KM.js.map} +0 -0
- /package/dist/{chunk-IJFX4JRI.js.map → chunk-BQOOUKEN.js.map} +0 -0
- /package/dist/{chunk-PRSF7DWT.js.map → chunk-BU4W2ISH.js.map} +0 -0
- /package/dist/{chunk-KQAFEZQX.js.map → chunk-BUKK5SWA.js.map} +0 -0
- /package/dist/{chunk-JRY55I6Z.js.map → chunk-C2AYUXEF.js.map} +0 -0
- /package/dist/{chunk-DF4SYU4Z.js.map → chunk-C5OW2FFB.js.map} +0 -0
- /package/dist/{chunk-74Z2BYEK.js.map → chunk-CYEEVWQ6.js.map} +0 -0
- /package/dist/{chunk-M7KAJBO2.js.map → chunk-EADUOZWH.js.map} +0 -0
- /package/dist/{chunk-N5H22V75.js.map → chunk-ECT5ME6X.js.map} +0 -0
- /package/dist/{chunk-M3GOWG2P.js.map → chunk-FMOUUWQN.js.map} +0 -0
- /package/dist/{chunk-2EJAPJ5G.js.map → chunk-FYPKSR6Z.js.map} +0 -0
- /package/dist/{chunk-LGLLCWA2.js.map → chunk-GNEB3WXR.js.map} +0 -0
- /package/dist/{chunk-Z4EH5TSO.js.map → chunk-HNZH57JK.js.map} +0 -0
- /package/dist/{chunk-PTJWUN4H.js.map → chunk-IJKBIOWL.js.map} +0 -0
- /package/dist/{chunk-C5GTIRIO.js.map → chunk-IMNJJKHW.js.map} +0 -0
- /package/dist/{chunk-ADK5TMQL.js.map → chunk-IVJI5FDA.js.map} +0 -0
- /package/dist/{chunk-F3GX76LV.js.map → chunk-IWAB7JVH.js.map} +0 -0
- /package/dist/{chunk-JFRO5HJY.js.map → chunk-J62O2UW5.js.map} +0 -0
- /package/dist/{chunk-A3UFV4K3.js.map → chunk-JIYJIH5N.js.map} +0 -0
- /package/dist/{chunk-UIX4JLVV.js.map → chunk-JVFYP2LD.js.map} +0 -0
- /package/dist/{chunk-KJ3MDX4V.js.map → chunk-KXY4HW2Y.js.map} +0 -0
- /package/dist/{chunk-VQ6H4DL6.js.map → chunk-L334PSLW.js.map} +0 -0
- /package/dist/{chunk-IA3XZG4V.js.map → chunk-L74VVEJA.js.map} +0 -0
- /package/dist/{chunk-UY67YP6Q.js.map → chunk-LIUI46BX.js.map} +0 -0
- /package/dist/{chunk-UP2Q7U4G.js.map → chunk-MTVC5S5N.js.map} +0 -0
- /package/dist/{chunk-QWURVT5Z.js.map → chunk-N7CATBYH.js.map} +0 -0
- /package/dist/{chunk-JEDS2QYV.js.map → chunk-OH55F76C.js.map} +0 -0
- /package/dist/{chunk-2NLLXCJG.js.map → chunk-OWHERGF2.js.map} +0 -0
- /package/dist/{chunk-G6GMZJQ4.js.map → chunk-PA6ZNFB7.js.map} +0 -0
- /package/dist/{chunk-6BKSUA6P.js.map → chunk-PWEIZCPS.js.map} +0 -0
- /package/dist/{chunk-T4SMMTGY.js.map → chunk-Q3DAWUAV.js.map} +0 -0
- /package/dist/{chunk-AWKADEMO.js.map → chunk-QCAJMIAE.js.map} +0 -0
- /package/dist/{chunk-ZRDEOC4X.js.map → chunk-QI7LLSWX.js.map} +0 -0
- /package/dist/{chunk-7DHNBABU.js.map → chunk-QJ6IR35B.js.map} +0 -0
- /package/dist/{chunk-J53IBVMF.js.map → chunk-R25JMO5O.js.map} +0 -0
- /package/dist/{chunk-HL643H2F.js.map → chunk-TMWSCE2L.js.map} +0 -0
- /package/dist/{chunk-FLDXEJFX.js.map → chunk-TPCRDPUY.js.map} +0 -0
- /package/dist/{chunk-WJC5KGYC.js.map → chunk-UFQB5VQ5.js.map} +0 -0
- /package/dist/{chunk-64WHWIPL.js.map → chunk-UVHXLVFJ.js.map} +0 -0
- /package/dist/{chunk-X7Y7WX73.js.map → chunk-UVYI6VIX.js.map} +0 -0
- /package/dist/{chunk-5XY46Z2W.js.map → chunk-UXRCORZX.js.map} +0 -0
- /package/dist/{chunk-WUL6LDJ4.js.map → chunk-VPEA63FC.js.map} +0 -0
- /package/dist/{chunk-MHSUEA32.js.map → chunk-WA6XRNWE.js.map} +0 -0
- /package/dist/{chunk-GGHX4YHM.js.map → chunk-WB3OFPM2.js.map} +0 -0
- /package/dist/{chunk-RSYFSCKL.js.map → chunk-WZEDFDJ6.js.map} +0 -0
- /package/dist/{chunk-2HVCTFKG.js.map → chunk-X2ZLHUEO.js.map} +0 -0
- /package/dist/{chunk-PJLJN23N.js.map → chunk-XICJL56Z.js.map} +0 -0
- /package/dist/{chunk-ZXEPLZUQ.js.map → chunk-YGM47S7M.js.map} +0 -0
- /package/dist/{chunk-KXDUSGPM.js.map → chunk-YRL4PGPP.js.map} +0 -0
- /package/dist/{chunk-GJF337YM.js.map → chunk-Z45353ML.js.map} +0 -0
- /package/dist/{chunk-NJZY5LOC.js.map → chunk-ZARHK7PT.js.map} +0 -0
- /package/dist/{chunk-NF5EHQTJ.js.map → chunk-ZFU2B2GM.js.map} +0 -0
- /package/dist/{chunk-W37MS6SX.js.map → chunk-ZRSXXWIX.js.map} +0 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// Buffer-drain handover protocol: a live buffer owner (the daemon) must hand
|
|
2
|
+
// a claimed session's turns INTO the drain claim instead of writing a session
|
|
3
|
+
// file, so the out-of-process `remnic buffer drain` can extract them without
|
|
4
|
+
// the daemon resurrecting or losing content. Covers:
|
|
5
|
+
// 1. persist path: claimed key saves hand over + evict the in-memory entry,
|
|
6
|
+
// merging the owner's newest turns into the claim (identity-deduped).
|
|
7
|
+
// 2. no resurrection: after handover the session file stays gone.
|
|
8
|
+
// 3. extraction gate: a claimed session's queued extraction settles skipped
|
|
9
|
+
// (claimed_by_buffer_drain) without touching the LLM; turns stay in the
|
|
10
|
+
// claim for the drain worker.
|
|
11
|
+
// 4. default-key mirror: handing over "default" clears the legacy mirror.
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { existsSync, mkdtempSync, readdirSync, readFileSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import { hostname } from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import test from "node:test";
|
|
18
|
+
|
|
19
|
+
import { SmartBuffer } from "./buffer.js";
|
|
20
|
+
import { BufferSessionFiles } from "./storage/buffer-session-files.js";
|
|
21
|
+
import { BufferDrainClaims, type DrainLease } from "./storage/buffer-drain-claims.js";
|
|
22
|
+
import type { BufferStorage } from "./buffer-persistence.js";
|
|
23
|
+
import type { BufferState } from "./types.js";
|
|
24
|
+
import { TurnIngestionCoordinator, type TurnIngestionDeps } from "./orchestration/turn-ingestion.js";
|
|
25
|
+
import type { BufferEntryState, BufferTurn, PluginConfig } from "./types.js";
|
|
26
|
+
import { parseConfig } from "./config.js";
|
|
27
|
+
import type { ExtractionRunResult } from "./orchestration/extraction-run.js";
|
|
28
|
+
|
|
29
|
+
function plainIo() {
|
|
30
|
+
return {
|
|
31
|
+
read: async (filePath: string) => readFileSync(filePath, "utf8"),
|
|
32
|
+
write: async (filePath: string, content: string) => {
|
|
33
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
34
|
+
writeFileSync(filePath, content);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function turn(content: string, sessionKey = "sess-1", timestamp = "2026-11-01T00:00:00.000Z"): BufferTurn {
|
|
40
|
+
return { role: "user", content, timestamp, sessionKey };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function entry(turns: ReturnType<typeof turn>[]): BufferEntryState {
|
|
44
|
+
return { turns, lastExtractionAt: null, extractionCount: 0 };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function makeConfig(): PluginConfig {
|
|
48
|
+
return parseConfig({
|
|
49
|
+
bufferSaveDebounceMs: 0,
|
|
50
|
+
triggerMode: "smart",
|
|
51
|
+
bufferMaxTurns: 1000,
|
|
52
|
+
bufferMaxMinutes: 100000,
|
|
53
|
+
highSignalPatterns: [],
|
|
54
|
+
extractionMinUserTurns: 1,
|
|
55
|
+
extractionMinChars: 0,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface Harness {
|
|
60
|
+
stateDir: string;
|
|
61
|
+
buffer: SmartBuffer;
|
|
62
|
+
claims: BufferDrainClaims;
|
|
63
|
+
storage: BufferStorage;
|
|
64
|
+
lease: DrainLease;
|
|
65
|
+
cleanup(): void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A SmartBuffer wired exactly like the daemon wires it: storage methods backed by BufferSessionFiles. */
|
|
69
|
+
function makeHarness(): Harness {
|
|
70
|
+
const stateDir = mkdtempSync(path.join(os.tmpdir(), "remnic-handover-"));
|
|
71
|
+
const files = new BufferSessionFiles(stateDir, plainIo());
|
|
72
|
+
const claims = new BufferDrainClaims(stateDir, plainIo());
|
|
73
|
+
const storage: BufferStorage = {
|
|
74
|
+
loadBuffer: () => files.load(),
|
|
75
|
+
saveBuffer: (state: BufferState) => files.reconcile(state),
|
|
76
|
+
saveBufferSession: (key: string, entryState: BufferEntryState) => files.save(key, entryState),
|
|
77
|
+
deleteBufferSession: (key: string) => files.remove(key),
|
|
78
|
+
isBufferSessionClaimed: (key: string) => files.isClaimed(key),
|
|
79
|
+
};
|
|
80
|
+
const buffer = new SmartBuffer(makeConfig(), storage);
|
|
81
|
+
const now = new Date().toISOString();
|
|
82
|
+
return {
|
|
83
|
+
stateDir,
|
|
84
|
+
buffer,
|
|
85
|
+
claims,
|
|
86
|
+
storage,
|
|
87
|
+
lease: { ownerId: "worker-1", pid: process.pid, host: hostname(), acquiredAt: now, heartbeatAt: now, leaseMs: 60_000 },
|
|
88
|
+
cleanup: () => rmSync(stateDir, { recursive: true, force: true }),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
test("a claimed session's save hands over and evicts instead of writing a session file", async () => {
|
|
93
|
+
const h = makeHarness();
|
|
94
|
+
try {
|
|
95
|
+
await h.buffer.load();
|
|
96
|
+
await h.buffer.addTurn("sess-1", turn("old-1"));
|
|
97
|
+
await h.buffer.addTurn("sess-1", turn("old-2"));
|
|
98
|
+
assert.ok(readdirSync(path.join(h.stateDir, "buffer")).some((name) => name.endsWith(".json")), "seeded turns persisted as a session file");
|
|
99
|
+
|
|
100
|
+
const outcome = await h.claims.claimFromSessionFile("sess-1", h.lease);
|
|
101
|
+
assert.equal(outcome, "claimed");
|
|
102
|
+
|
|
103
|
+
// The daemon owner buffers one more turn for the claimed session and its
|
|
104
|
+
// debounced-off save fires: this is the resurrection path, and it must
|
|
105
|
+
// hand over instead.
|
|
106
|
+
await h.buffer.addTurn("sess-1", turn("new-3"));
|
|
107
|
+
await h.buffer.save();
|
|
108
|
+
|
|
109
|
+
const claim = await h.claims.load("sess-1");
|
|
110
|
+
assert.ok(claim, "claim must exist after handover");
|
|
111
|
+
assert.deepEqual(
|
|
112
|
+
claim.entry.turns.map((t) => t.content),
|
|
113
|
+
["old-1", "old-2", "new-3"],
|
|
114
|
+
"the owner's newest turn merges into the claim; nothing duplicates",
|
|
115
|
+
);
|
|
116
|
+
const topLevel = readdirSync(path.join(h.stateDir, "buffer")).filter((name) => name.endsWith(".json"));
|
|
117
|
+
assert.deepEqual(topLevel, [], "a claimed session must never be written back as a session file");
|
|
118
|
+
|
|
119
|
+
// The in-memory entry was evicted with the handover: the next observe
|
|
120
|
+
// starts fresh (its turns flow into the claim at the next handover).
|
|
121
|
+
await h.buffer.addTurn("sess-1", turn("post-eviction", "sess-1", "2026-11-01T00:05:00.000Z"));
|
|
122
|
+
await h.buffer.save();
|
|
123
|
+
const claimAfter = await h.claims.load("sess-1");
|
|
124
|
+
assert.ok(claimAfter);
|
|
125
|
+
assert.deepEqual(
|
|
126
|
+
claimAfter.entry.turns.map((t) => t.content),
|
|
127
|
+
["old-1", "old-2", "new-3", "post-eviction"],
|
|
128
|
+
"post-handover observes keep flowing into the claim",
|
|
129
|
+
);
|
|
130
|
+
assert.deepEqual(
|
|
131
|
+
readdirSync(path.join(h.stateDir, "buffer")).filter((name) => name.endsWith(".json")),
|
|
132
|
+
[],
|
|
133
|
+
);
|
|
134
|
+
} finally {
|
|
135
|
+
h.cleanup();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("handover of the default key clears the legacy top-level mirror", async () => {
|
|
140
|
+
const h = makeHarness();
|
|
141
|
+
try {
|
|
142
|
+
await h.buffer.load();
|
|
143
|
+
await h.buffer.addTurn("default", turn("legacy", "legacy-session"));
|
|
144
|
+
const outcome = await h.claims.claimFromSessionFile("default", h.lease);
|
|
145
|
+
assert.equal(outcome, "claimed");
|
|
146
|
+
await h.buffer.addTurn("default", turn("legacy-2", "legacy-session"));
|
|
147
|
+
await h.buffer.save();
|
|
148
|
+
const claim = await h.claims.load("default");
|
|
149
|
+
assert.ok(claim);
|
|
150
|
+
assert.deepEqual(claim.entry.turns.map((t) => t.content), ["legacy", "legacy-2"]);
|
|
151
|
+
// No duplicate writes: the snapshot must settle empty after handover.
|
|
152
|
+
assert.deepEqual(
|
|
153
|
+
readdirSync(path.join(h.stateDir, "buffer")).filter((name) => name.endsWith(".json")),
|
|
154
|
+
[],
|
|
155
|
+
);
|
|
156
|
+
} finally {
|
|
157
|
+
h.cleanup();
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Narrow a test double to the dependency contract. The fixture provides the
|
|
163
|
+
* members the drain path reads; the rest are unused by that path.
|
|
164
|
+
*/
|
|
165
|
+
/** The dependency members the drain path reads. */
|
|
166
|
+
interface DrainDepsFixture {
|
|
167
|
+
buffer: unknown;
|
|
168
|
+
config: unknown;
|
|
169
|
+
extractionQueueCoordinator: unknown;
|
|
170
|
+
extractionWindowBudgetChars: unknown;
|
|
171
|
+
getStorage: unknown;
|
|
172
|
+
shouldQueueExtraction: unknown;
|
|
173
|
+
runExtraction: unknown;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function asDeps<T extends object>(value: DrainDepsFixture): T {
|
|
177
|
+
// SAFETY: the fixture provides the members the drain path reads; the
|
|
178
|
+
// remaining contract members are unused by that path.
|
|
179
|
+
return value as T;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function gateFixture(isClaimed: boolean) {
|
|
183
|
+
const runs: { turns: BufferTurn[] }[] = [];
|
|
184
|
+
const config = {
|
|
185
|
+
extractionChunkDropAfterFailures: 3,
|
|
186
|
+
extractionMinUserTurns: 1,
|
|
187
|
+
extractionMinChars: 0,
|
|
188
|
+
extractionMaxTurnChars: 100_000,
|
|
189
|
+
};
|
|
190
|
+
const deps = {
|
|
191
|
+
buffer: {
|
|
192
|
+
isSessionClaimed: () => isClaimed,
|
|
193
|
+
getTurns: () => [],
|
|
194
|
+
dropExtractionChunk: async () => 0,
|
|
195
|
+
},
|
|
196
|
+
config,
|
|
197
|
+
extractionQueueCoordinator: {
|
|
198
|
+
enqueue: (fn: () => Promise<void>) => {
|
|
199
|
+
void fn().catch(() => {});
|
|
200
|
+
return true;
|
|
201
|
+
},
|
|
202
|
+
runPreemptable: (_preemptable: boolean, parent: AbortSignal | undefined, fn: (signal: AbortSignal) => Promise<void>) => fn(parent ?? new AbortController().signal),
|
|
203
|
+
beginPreemptable: () => {},
|
|
204
|
+
},
|
|
205
|
+
extractionWindowBudgetChars: () => 500,
|
|
206
|
+
getStorage: async () => ({ saveDroppedExtractionChunk: async () => "/dropped" }),
|
|
207
|
+
shouldQueueExtraction: () => true,
|
|
208
|
+
runExtraction: (async (turns: BufferTurn[]) => {
|
|
209
|
+
runs.push({ turns });
|
|
210
|
+
return { status: "completed", persistedCount: 0, durableOutputCount: 0 };
|
|
211
|
+
}),
|
|
212
|
+
// SAFETY: the fixture provides exactly the deps the coordinator reads
|
|
213
|
+
// for the drain path (buffer, queue coordinator, budget, storage, dedupe
|
|
214
|
+
// gate and runExtraction); the cast only fills the unused members.
|
|
215
|
+
};
|
|
216
|
+
const coordinator = new TurnIngestionCoordinator(asDeps<TurnIngestionDeps>(deps));
|
|
217
|
+
return { coordinator, runs };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
test("a claimed session's queued extraction settles skipped without an LLM call", async () => {
|
|
221
|
+
const claimed = gateFixture(true);
|
|
222
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
223
|
+
void claimed.coordinator.queueBufferedExtraction([turn("blocked")], "trigger_mode", {
|
|
224
|
+
bufferKey: "sess-1",
|
|
225
|
+
clearBufferAfterExtraction: true,
|
|
226
|
+
onTaskSettled: (error, result) => {
|
|
227
|
+
try {
|
|
228
|
+
assert.equal(error, undefined);
|
|
229
|
+
assert.equal(result?.status, "skipped");
|
|
230
|
+
assert.equal(result?.reason, "claimed_by_buffer_drain");
|
|
231
|
+
assert.equal(claimed.runs.length, 0, "the LLM must not run for a claimed session");
|
|
232
|
+
resolve();
|
|
233
|
+
} catch (err) {
|
|
234
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
await promise;
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("an unclaimed session still extracts normally (gate is claim-only)", async () => {
|
|
242
|
+
const unclaimed = gateFixture(false);
|
|
243
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
244
|
+
void unclaimed.coordinator.queueBufferedExtraction([turn("flows")], "trigger_mode", {
|
|
245
|
+
bufferKey: "sess-2",
|
|
246
|
+
clearBufferAfterExtraction: true,
|
|
247
|
+
onTaskSettled: (error, result) => {
|
|
248
|
+
try {
|
|
249
|
+
assert.equal(error, undefined);
|
|
250
|
+
assert.notEqual(result?.reason, "claimed_by_buffer_drain");
|
|
251
|
+
assert.equal(unclaimed.runs.length, 1);
|
|
252
|
+
resolve();
|
|
253
|
+
} catch (err) {
|
|
254
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
await promise;
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("the owner keeps the claim while a drain is still running on it", async () => {
|
|
262
|
+
const h = makeHarness();
|
|
263
|
+
try {
|
|
264
|
+
await h.buffer.load();
|
|
265
|
+
await h.buffer.addTurn("sess-1", turn("old-1"));
|
|
266
|
+
await h.buffer.addTurn("sess-1", turn("old-2"));
|
|
267
|
+
const claimOutcome = await h.claims.claimFromSessionFile("sess-1", h.lease);
|
|
268
|
+
assert.equal(claimOutcome, "claimed");
|
|
269
|
+
|
|
270
|
+
// The drainer extracts old-1 (progress write moves it to `drained`) but
|
|
271
|
+
// old-2 is still pending, and the drainer holds the lease.
|
|
272
|
+
const loaded = await h.claims.load("sess-1");
|
|
273
|
+
assert.ok(loaded);
|
|
274
|
+
await h.claims.writeEntry("sess-1", h.lease.ownerId, entry([turn("old-2")]), loaded.entry);
|
|
275
|
+
|
|
276
|
+
// The owner saves while the drain is mid-flight: this must hand over into
|
|
277
|
+
// the claim (keeping the claim AND the lease), not consume the tombstone.
|
|
278
|
+
await h.buffer.addTurn("sess-1", turn("new-3"));
|
|
279
|
+
await h.buffer.save();
|
|
280
|
+
const saveSession = h.storage.saveBufferSession;
|
|
281
|
+
assert.ok(saveSession, "the harness wires saveBufferSession");
|
|
282
|
+
const saveOutcome = await saveSession("sess-1", {
|
|
283
|
+
turns: [turn("old-1"), turn("old-2"), turn("new-3")],
|
|
284
|
+
lastExtractionAt: null,
|
|
285
|
+
extractionCount: 0,
|
|
286
|
+
});
|
|
287
|
+
assert.equal(saveOutcome?.status, "handed_over", "a running drain keeps its claim");
|
|
288
|
+
assert.equal(await h.claims.load("sess-1") !== null, true, "the claim survives the owner's mid-drain save");
|
|
289
|
+
assert.equal(existsSync(h.claims.leasePathForTest("sess-1")), true, "the lease survives the owner's mid-drain save");
|
|
290
|
+
|
|
291
|
+
// The drain finishes: everything extracted, the drainer acks (drops its
|
|
292
|
+
// lease, leaving the tombstone). Only now does the owner's save consume it.
|
|
293
|
+
const current = await h.claims.load("sess-1");
|
|
294
|
+
assert.ok(current);
|
|
295
|
+
await h.claims.writeEntry("sess-1", h.lease.ownerId, entry([]), current.entry);
|
|
296
|
+
await h.claims.releaseLease("sess-1", h.lease.ownerId);
|
|
297
|
+
const finalOutcome = await saveSession("sess-1", {
|
|
298
|
+
turns: [turn("old-1"), turn("old-2"), turn("new-3")],
|
|
299
|
+
lastExtractionAt: null,
|
|
300
|
+
extractionCount: 0,
|
|
301
|
+
});
|
|
302
|
+
assert.equal(finalOutcome?.status, "saved", "the acked tombstone is consumed");
|
|
303
|
+
assert.equal(await h.claims.load("sess-1"), null, "the tombstone is gone after the consume");
|
|
304
|
+
} finally {
|
|
305
|
+
h.cleanup();
|
|
306
|
+
}
|
|
307
|
+
});
|
|
@@ -163,7 +163,7 @@ test("clearAfterExtraction persists the cleared session state", async () => {
|
|
|
163
163
|
|
|
164
164
|
test("evicted sessions get their files deleted", async () => {
|
|
165
165
|
const storage = new IncrementalStorage(emptyState());
|
|
166
|
-
const buffer = new SmartBuffer(parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100 }), storage);
|
|
166
|
+
const buffer = new SmartBuffer(parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100, bufferMaxSessions: 200 }), storage);
|
|
167
167
|
|
|
168
168
|
// normalizeState always materializes a "default" entry, so 198 seeded
|
|
169
169
|
// sessions + default = 199; new-session reaches exactly 200 (the
|
|
@@ -191,6 +191,24 @@ test("evicted sessions get their files deleted", async () => {
|
|
|
191
191
|
}
|
|
192
192
|
});
|
|
193
193
|
|
|
194
|
+
test("the default entry cap leaves headroom for live sessions during backlog drains (#3303)", async () => {
|
|
195
|
+
// w9C: live session captures were dropped when the extraction worker was busy
|
|
196
|
+
// on a backlog doc and the 200-session default cap refused new sessions. The
|
|
197
|
+
// default must leave enough headroom that live captures survive a backlog.
|
|
198
|
+
const storage = new IncrementalStorage(emptyState());
|
|
199
|
+
const buffer = new SmartBuffer(parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100 }), storage);
|
|
200
|
+
|
|
201
|
+
for (let i = 0; i < 200; i++) {
|
|
202
|
+
await buffer.addTurnBounded(`backlog-${i}`, makeTurn(`backlog-${i}`, `backlog doc ${i}`));
|
|
203
|
+
}
|
|
204
|
+
await buffer.flushPendingSave();
|
|
205
|
+
|
|
206
|
+
// A live session arriving while the backlog occupies 200 entries must be
|
|
207
|
+
// admitted, not dropped.
|
|
208
|
+
const outcome = await buffer.addTurnBounded("live-session", makeTurn("live-session", "live turn"));
|
|
209
|
+
assert.notEqual(outcome, null, "a live session at the default cap must not be dropped");
|
|
210
|
+
});
|
|
211
|
+
|
|
194
212
|
test("resetToEmpty drops every session file on the next persist", async () => {
|
|
195
213
|
const storage = new IncrementalStorage(emptyState());
|
|
196
214
|
const buffer = new SmartBuffer(parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100 }), storage);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { BufferEntryState, BufferState, BufferSurpriseEvent } from "./types.js";
|
|
1
|
+
import type { BufferEntryState, BufferState, BufferSurpriseEvent, BufferTurn } from "./types.js";
|
|
2
|
+
import type { BufferSessionSaveOutcome } from "./storage/buffer-drain-claims.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* The buffer persistence surface SmartBuffer depends on.
|
|
@@ -12,12 +13,23 @@ import type { BufferEntryState, BufferState, BufferSurpriseEvent } from "./types
|
|
|
12
13
|
export interface BufferStorage {
|
|
13
14
|
loadBuffer(): Promise<BufferState>;
|
|
14
15
|
saveBuffer(state: BufferState): Promise<void>;
|
|
15
|
-
|
|
16
|
+
/** Reports "handed_over" when the session is claimed by `remnic buffer drain` and the entry moved into the claim instead of a session file. Legacy doubles may return void (treated as "saved"). */
|
|
17
|
+
saveBufferSession?(key: string, entry: BufferEntryState): Promise<BufferSessionSaveOutcome | void>;
|
|
16
18
|
deleteBufferSession?(key: string): Promise<void>;
|
|
19
|
+
/** Sync claim check for the extraction gate; omitted when the backend has no drain-claim store. */
|
|
20
|
+
isBufferSessionClaimed?(key: string): boolean;
|
|
17
21
|
/** Optional surprise-telemetry sink; older storage and test doubles omit it. */
|
|
18
22
|
appendBufferSurpriseEvents?(events: BufferSurpriseEvent[]): Promise<number>;
|
|
19
23
|
}
|
|
20
24
|
|
|
25
|
+
/** What one persist pass changed beyond plain writes. */
|
|
26
|
+
export interface BufferPersistOutcome {
|
|
27
|
+
/** Sessions whose in-memory entry moved into a drain claim: the caller must evict them (a claimed session no longer lives in this process). */
|
|
28
|
+
handedOver: string[];
|
|
29
|
+
/** Per session, the turns a drain already extracted: the caller subtracts them from its in-memory copy. */
|
|
30
|
+
drainedTurns: Map<string, BufferTurn[]>;
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
/**
|
|
22
34
|
* Tracks which sessions' persisted files are stale and writes only those.
|
|
23
35
|
*
|
|
@@ -61,15 +73,17 @@ export class BufferSessionPersistence {
|
|
|
61
73
|
storage: BufferStorage,
|
|
62
74
|
state: BufferState,
|
|
63
75
|
peekEntry: (key: string) => BufferEntryState | undefined,
|
|
64
|
-
): Promise<
|
|
76
|
+
): Promise<BufferPersistOutcome> {
|
|
65
77
|
const saveSession = storage.saveBufferSession?.bind(storage);
|
|
66
78
|
const deleteSession = storage.deleteBufferSession?.bind(storage);
|
|
67
79
|
if (!saveSession || !deleteSession) {
|
|
68
80
|
await storage.saveBuffer(state);
|
|
69
81
|
this.dirty.clear();
|
|
70
82
|
this.deleted.clear();
|
|
71
|
-
return;
|
|
83
|
+
return { handedOver: [], drainedTurns: new Map() };
|
|
72
84
|
}
|
|
85
|
+
const handedOver: string[] = [];
|
|
86
|
+
const drainedTurns = new Map<string, BufferTurn[]>();
|
|
73
87
|
for (const key of [...this.deleted]) {
|
|
74
88
|
await deleteSession(key);
|
|
75
89
|
this.deleted.delete(key);
|
|
@@ -82,8 +96,16 @@ export class BufferSessionPersistence {
|
|
|
82
96
|
this.markDeleted(key);
|
|
83
97
|
continue;
|
|
84
98
|
}
|
|
85
|
-
await saveSession(key, entry);
|
|
99
|
+
const outcome = await saveSession(key, entry);
|
|
86
100
|
this.dirty.delete(key);
|
|
101
|
+
// "handed_over": the entry moved into a drain claim. Content is durable
|
|
102
|
+
// in the claim, so this key's pending state is settled — the caller
|
|
103
|
+
// evicts the in-memory entry on its side.
|
|
104
|
+
if (outcome?.status === "handed_over") handedOver.push(key);
|
|
105
|
+
// A drain tombstone: the turns it extracted must leave this process's
|
|
106
|
+
// copy too, or the owner would extract them a second time.
|
|
107
|
+
if (outcome?.drainedTurns && outcome.drainedTurns.length > 0) drainedTurns.set(key, outcome.drainedTurns);
|
|
87
108
|
}
|
|
109
|
+
return { handedOver, drainedTurns };
|
|
88
110
|
}
|
|
89
111
|
}
|
|
@@ -428,7 +428,7 @@ test("SmartBuffer prunes stale logical session buffers to a bounded entry set",
|
|
|
428
428
|
...entries,
|
|
429
429
|
},
|
|
430
430
|
});
|
|
431
|
-
const buffer = new SmartBuffer(parseConfig({ bufferSaveDebounceMs: 0 }), storage as unknown as ConstructorParameters<typeof SmartBuffer>[1]);
|
|
431
|
+
const buffer = new SmartBuffer(parseConfig({ bufferSaveDebounceMs: 0, bufferMaxSessions: 200 }), storage as unknown as ConstructorParameters<typeof SmartBuffer>[1]);
|
|
432
432
|
|
|
433
433
|
await buffer.addTurn("active-thread", makeTurn("active-thread", "pending memory"));
|
|
434
434
|
|
|
@@ -672,7 +672,7 @@ test("SmartBuffer prefers pruning empty entries and bounds pending-turn buffers
|
|
|
672
672
|
...entries,
|
|
673
673
|
},
|
|
674
674
|
});
|
|
675
|
-
const buffer = new SmartBuffer(parseConfig({ bufferSaveDebounceMs: 0 }), storage as unknown as ConstructorParameters<typeof SmartBuffer>[1]);
|
|
675
|
+
const buffer = new SmartBuffer(parseConfig({ bufferSaveDebounceMs: 0, bufferMaxSessions: 200 }), storage as unknown as ConstructorParameters<typeof SmartBuffer>[1]);
|
|
676
676
|
|
|
677
677
|
await buffer.addTurn("active-thread", makeTurn("active-thread", "pending memory"));
|
|
678
678
|
|
|
@@ -699,7 +699,7 @@ test("pruneEntries evicts oldest NON-empty session entries past the cap (#1908 r
|
|
|
699
699
|
};
|
|
700
700
|
}
|
|
701
701
|
const storage = new FakeStorage({ turns: [], lastExtractionAt: null, extractionCount: 0, entries });
|
|
702
|
-
const buffer = new SmartBuffer(parseConfig({ bufferSaveDebounceMs: 0 }), storage as unknown as ConstructorParameters<typeof SmartBuffer>[1]);
|
|
702
|
+
const buffer = new SmartBuffer(parseConfig({ bufferSaveDebounceMs: 0, bufferMaxSessions: 200 }), storage as unknown as ConstructorParameters<typeof SmartBuffer>[1]);
|
|
703
703
|
await buffer.load();
|
|
704
704
|
await buffer.addTurn("session-newest", makeTurn("session-newest", "new turn"));
|
|
705
705
|
|
|
@@ -104,6 +104,22 @@ export function copyBufferTurn(turn: BufferTurn): BufferTurn {
|
|
|
104
104
|
return copy;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Remove the turns that a drain already extracted (`drained`) from `turns`.
|
|
109
|
+
* Identity-based (role/content/timestamp/session/...): the same comparison
|
|
110
|
+
* the buffer uses to match live turns, so a stale owner can subtract exactly
|
|
111
|
+
* what the drain took and keep everything else.
|
|
112
|
+
*/
|
|
113
|
+
export function subtractBufferTurns(turns: readonly BufferTurn[], drained: readonly BufferTurn[]): BufferTurn[] {
|
|
114
|
+
if (drained.length === 0) return [...turns];
|
|
115
|
+
const kept = [...turns];
|
|
116
|
+
for (const gone of drained) {
|
|
117
|
+
const at = kept.findIndex((live) => bufferTurnsEqual(live, gone));
|
|
118
|
+
if (at >= 0) kept.splice(at, 1);
|
|
119
|
+
}
|
|
120
|
+
return kept;
|
|
121
|
+
}
|
|
122
|
+
|
|
107
123
|
export function bufferTurnsEqual(left: BufferTurn | undefined, right: BufferTurn): boolean {
|
|
108
124
|
if (!left) return false;
|
|
109
125
|
return (
|
package/src/buffer.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
} from "./types.js";
|
|
14
14
|
import { resolvePresentationCapabilities, resolveNamespaceCapabilities } from "./capabilities.js";
|
|
15
15
|
import { resolvePrincipal } from "./namespaces/principal.js";
|
|
16
|
-
import { scopeProfilePlanIdentity } from "./buffer-turn-helpers.js";
|
|
16
|
+
import { scopeProfilePlanIdentity, subtractBufferTurns } from "./buffer-turn-helpers.js";
|
|
17
17
|
import { ExtractionDeadlineError } from "./orchestration/extraction-run.js";
|
|
18
18
|
import type { ExtractionBufferSnapshot } from "./extraction-liveness.js";
|
|
19
19
|
import {
|
|
@@ -49,7 +49,9 @@ export interface RetainedTurnCleanupOptions {
|
|
|
49
49
|
codingContext?: import("./types.js").CodingContext;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
// Raised from 200 in #3303: live captures were being dropped at busy hours
|
|
53
|
+
// while the single extraction worker drained a backlog.
|
|
54
|
+
const FALLBACK_MAX_BUFFER_SESSIONS = 1000;
|
|
53
55
|
const isAcceptedTurn = (t: { acceptedCapture?: boolean; writeNamespaceOverride?: string }): boolean =>
|
|
54
56
|
t.acceptedCapture === true || t.writeNamespaceOverride !== undefined;
|
|
55
57
|
|
|
@@ -129,6 +131,14 @@ export class SmartBuffer {
|
|
|
129
131
|
this.surpriseProbe = surpriseProbe;
|
|
130
132
|
}
|
|
131
133
|
|
|
134
|
+
/** Distinct sessions admitted before backpressure; configurable (#3303, raised from 200). */
|
|
135
|
+
private get maxSessionEntries(): number {
|
|
136
|
+
// parseConfig defaults, validates, and floors bufferMaxSessions at the I/O
|
|
137
|
+
// boundary; FALLBACK_MAX_BUFFER_SESSIONS covers direct constructions with
|
|
138
|
+
// configs from before the knob existed.
|
|
139
|
+
return this.config.bufferMaxSessions ?? FALLBACK_MAX_BUFFER_SESSIONS;
|
|
140
|
+
}
|
|
141
|
+
|
|
132
142
|
private enqueueMutation<T>(op: () => Promise<T>): Promise<T> {
|
|
133
143
|
const run = this.mutationChain.catch(() => {}).then(op);
|
|
134
144
|
this.mutationChain = run.catch(() => {});
|
|
@@ -206,11 +216,11 @@ export class SmartBuffer {
|
|
|
206
216
|
);
|
|
207
217
|
}
|
|
208
218
|
|
|
209
|
-
/** Evict oldest entries past
|
|
219
|
+
/** Evict oldest entries past this.maxSessionEntries and queue their session files for deletion. */
|
|
210
220
|
private pruneEntries(retainKeys: string[]): void {
|
|
211
221
|
const entries = this.state.entries;
|
|
212
222
|
if (!entries) return;
|
|
213
|
-
const evicted = selectEvictedBufferKeys(entries, retainKeys,
|
|
223
|
+
const evicted = selectEvictedBufferKeys(entries, retainKeys, this.maxSessionEntries, {
|
|
214
224
|
activityAt: (entry) => this.entryActivityAt(entry),
|
|
215
225
|
holdsAcceptedCapture: (entry) => this.entryHoldsAcceptedCapture(entry),
|
|
216
226
|
});
|
|
@@ -222,7 +232,7 @@ export class SmartBuffer {
|
|
|
222
232
|
// be observable, not silent.
|
|
223
233
|
log.warn(
|
|
224
234
|
`buffer: pruned ${evicted.length} oldest session entr${evicted.length === 1 ? "y" : "ies"} ` +
|
|
225
|
-
`past
|
|
235
|
+
`past the session-entry cap (${this.maxSessionEntries}); buffered turn data for those sessions was dropped`,
|
|
226
236
|
);
|
|
227
237
|
}
|
|
228
238
|
}
|
|
@@ -289,7 +299,37 @@ export class SmartBuffer {
|
|
|
289
299
|
}
|
|
290
300
|
|
|
291
301
|
private async saveUnlocked(): Promise<void> {
|
|
292
|
-
await this.persistence.persist(this.storage, this.state, (key) => this.peekEntry(key) ?? undefined);
|
|
302
|
+
const { handedOver, drainedTurns } = await this.persistence.persist(this.storage, this.state, (key) => this.peekEntry(key) ?? undefined);
|
|
303
|
+
// A drain tombstone: the turns the drain extracted leave this copy too,
|
|
304
|
+
// so a stale owner never extracts (or persists) them a second time.
|
|
305
|
+
for (const [key, drained] of drainedTurns) {
|
|
306
|
+
const entry = this.peekEntry(key);
|
|
307
|
+
if (!entry) continue;
|
|
308
|
+
entry.turns = subtractBufferTurns(entry.turns, drained);
|
|
309
|
+
if (entry.retainedTurns && entry.retainedTurns.length > 0) {
|
|
310
|
+
entry.retainedTurns = subtractBufferTurns(entry.retainedTurns, drained);
|
|
311
|
+
if (entry.retainedTurns.length === 0) delete entry.retainedTurns;
|
|
312
|
+
}
|
|
313
|
+
if (key === "default") this.state.turns = entry.turns;
|
|
314
|
+
}
|
|
315
|
+
if (handedOver.length === 0) return;
|
|
316
|
+
// A drain claim now owns these sessions' turns (durable in the claim
|
|
317
|
+
// file): drop the in-memory entries so this process cannot resurrect
|
|
318
|
+
// them. A later observe for the same key starts a fresh entry whose
|
|
319
|
+
// turns flow into the claim at the next handover.
|
|
320
|
+
const entries = this.state.entries;
|
|
321
|
+
if (!entries) return;
|
|
322
|
+
for (const key of handedOver) {
|
|
323
|
+
delete entries[key];
|
|
324
|
+
if (key === "default") {
|
|
325
|
+
this.state.turns = [];
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Sync gate for the extraction queue: is this session claimed by a buffer drain? */
|
|
331
|
+
isSessionClaimed(key: string): boolean {
|
|
332
|
+
return this.storage.isBufferSessionClaimed?.(key) === true;
|
|
293
333
|
}
|
|
294
334
|
|
|
295
335
|
async save(): Promise<void> {
|
|
@@ -457,9 +497,9 @@ export class SmartBuffer {
|
|
|
457
497
|
await this.loadUnlocked();
|
|
458
498
|
if (this.peekEntry(bufferKey) === null) {
|
|
459
499
|
this.makeRoomForEntryUnlocked();
|
|
460
|
-
if (Object.keys(this.state.entries ?? {}).length >=
|
|
500
|
+
if (Object.keys(this.state.entries ?? {}).length >= this.maxSessionEntries) {
|
|
461
501
|
log.warn(
|
|
462
|
-
`buffer[${bufferKey}]: durable capture refused at
|
|
502
|
+
`buffer[${bufferKey}]: durable capture refused at the session-entry cap (${this.maxSessionEntries})`,
|
|
463
503
|
);
|
|
464
504
|
return null;
|
|
465
505
|
}
|
|
@@ -475,7 +515,7 @@ export class SmartBuffer {
|
|
|
475
515
|
const entries = this.state.entries;
|
|
476
516
|
if (!entries) return;
|
|
477
517
|
const keys = Object.keys(entries);
|
|
478
|
-
if (keys.length <
|
|
518
|
+
if (keys.length < this.maxSessionEntries) return;
|
|
479
519
|
const byOldestActivity = (left: string, right: string): number =>
|
|
480
520
|
this.entryActivityAt(entries[left] ?? { turns: [], lastExtractionAt: null, extractionCount: 0 }) -
|
|
481
521
|
this.entryActivityAt(entries[right] ?? { turns: [], lastExtractionAt: null, extractionCount: 0 });
|
|
@@ -488,7 +528,7 @@ export class SmartBuffer {
|
|
|
488
528
|
)
|
|
489
529
|
.sort(byOldestActivity);
|
|
490
530
|
for (const key of emptyRemovable) {
|
|
491
|
-
if (Object.keys(entries).length <
|
|
531
|
+
if (Object.keys(entries).length < this.maxSessionEntries) break;
|
|
492
532
|
delete entries[key];
|
|
493
533
|
this.persistence.markDeleted(key);
|
|
494
534
|
}
|
package/src/config.ts
CHANGED
|
@@ -1591,6 +1591,7 @@ export function parseConfig(
|
|
|
1591
1591
|
...parseSecurityConfig(cfg, rawOperatorConfig),
|
|
1592
1592
|
bufferMaxTurns:
|
|
1593
1593
|
typeof cfg.bufferMaxTurns === "number" ? cfg.bufferMaxTurns : 5,
|
|
1594
|
+
bufferMaxSessions: parsePositiveInteger(cfg.bufferMaxSessions, "bufferMaxSessions") ?? 1000,
|
|
1594
1595
|
bufferMaxMinutes:
|
|
1595
1596
|
typeof cfg.bufferMaxMinutes === "number" ? cfg.bufferMaxMinutes : 15,
|
|
1596
1597
|
// Surprise-gated buffer flush (issue #563, D-MEM). See types.ts for
|
package/src/index.ts
CHANGED
|
@@ -346,7 +346,7 @@ export { MeilisearchBackend } from "./search/meilisearch-backend.js";
|
|
|
346
346
|
// ---------------------------------------------------------------------------
|
|
347
347
|
|
|
348
348
|
export { buildEntityRecallSection } from "./entity-retrieval.js";
|
|
349
|
-
export { isNamespacePolicyCovered, resolvePrincipal } from "./namespaces/principal.js";
|
|
349
|
+
export { defaultNamespaceForPrincipal, isNamespacePolicyCovered, resolvePrincipal } from "./namespaces/principal.js";
|
|
350
350
|
export {
|
|
351
351
|
NamespaceCatalog,
|
|
352
352
|
type NamespaceRecord,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `skipClaimedSession` gate: a buffer without a claim registry never reports a
|
|
3
|
+
* claim (the legacy/test-double shape), and a claimed session settles as
|
|
4
|
+
* skipped without running an extraction.
|
|
5
|
+
*/
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
import { skipClaimedSession, type ClaimAwareBuffer } from "./buffer-drain-gate.js";
|
|
9
|
+
import type { ExtractionRunResult } from "./extraction-run.js";
|
|
10
|
+
|
|
11
|
+
test("a buffer without isSessionClaimed is never treated as claimed", () => {
|
|
12
|
+
const settled: ExtractionRunResult[] = [];
|
|
13
|
+
const buffer: ClaimAwareBuffer = {};
|
|
14
|
+
const result = skipClaimedSession(buffer, "sess-1", (r) => settled.push(r));
|
|
15
|
+
assert.equal(result, false);
|
|
16
|
+
assert.deepEqual(settled, []);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("an unclaimed session with the method proceeds", () => {
|
|
20
|
+
const settled: ExtractionRunResult[] = [];
|
|
21
|
+
const result = skipClaimedSession({ isSessionClaimed: () => false }, "sess-1", (r) => settled.push(r));
|
|
22
|
+
assert.equal(result, false);
|
|
23
|
+
assert.deepEqual(settled, []);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("a claimed session settles as skipped without extracting", () => {
|
|
27
|
+
const settled: ExtractionRunResult[] = [];
|
|
28
|
+
const result = skipClaimedSession({ isSessionClaimed: () => true }, "sess-1", (r) => settled.push(r));
|
|
29
|
+
assert.equal(result, true);
|
|
30
|
+
assert.equal(settled.length, 1);
|
|
31
|
+
assert.equal(settled[0].status, "skipped");
|
|
32
|
+
assert.equal(settled[0].reason, "claimed_by_buffer_drain");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("a buffer-less deps bundle (older host or test double) is never claimed", () => {
|
|
36
|
+
const settled: ExtractionRunResult[] = [];
|
|
37
|
+
const result = skipClaimedSession(undefined, "sess-1", (r) => settled.push(r));
|
|
38
|
+
assert.equal(result, false);
|
|
39
|
+
assert.deepEqual(settled, []);
|
|
40
|
+
});
|