@remnic/core 10.0.85 → 10.0.86
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 +72 -71
- package/dist/access-authorization-probe.js +73 -72
- package/dist/access-boundary.d.ts.map +1 -1
- package/dist/access-boundary.js +72 -71
- package/dist/access-cli.js +157 -155
- package/dist/access-cli.js.map +1 -1
- package/dist/access-errors.d.ts +14 -0
- package/dist/access-errors.d.ts.map +1 -1
- package/dist/access-errors.js +5 -1
- package/dist/access-extraction-force-flush.js +72 -71
- package/dist/access-http-briefing.js +73 -72
- package/dist/access-http-query.js +74 -73
- package/dist/access-http.d.ts +12 -0
- package/dist/access-http.d.ts.map +1 -1
- package/dist/access-http.js +95 -94
- package/dist/access-identity-continuity-surface.js +72 -71
- package/dist/access-lcm-surface.js +72 -71
- package/dist/access-mcp.d.ts.map +1 -1
- package/dist/access-mcp.js +82 -81
- package/dist/access-memory-search-fanout.js +2 -2
- package/dist/access-namespace-preflight.js +72 -71
- package/dist/access-observe-write-category.js +10 -9
- package/dist/access-observe-write-surface.js +72 -71
- package/dist/access-offline-manifest.js +8 -8
- package/dist/access-operations-batch.js +73 -72
- package/dist/access-operations.js +80 -79
- package/dist/access-recall-concurrency.js +72 -71
- package/dist/access-recall-diagnostics.js +2 -2
- package/dist/access-recall-response.js +72 -71
- package/dist/access-recall-surface.js +72 -71
- package/dist/access-schema.d.ts.map +1 -1
- package/dist/access-schema.js +9 -8
- package/dist/access-service.d.ts +3 -3
- package/dist/access-service.d.ts.map +1 -1
- package/dist/access-service.js +72 -71
- package/dist/access-surface-catalog.js +74 -73
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/access-token-capabilities.js +2 -2
- package/dist/active-recall.js +32 -32
- package/dist/adapters/index.js +7 -7
- package/dist/adapters/registry.js +3 -3
- package/dist/briefing-deadline.d.ts +2 -2
- package/dist/briefing-deadline.d.ts.map +1 -1
- package/dist/briefing-deadline.js +1 -1
- package/dist/briefing.js +23 -23
- package/dist/buffer-admission.d.ts +22 -0
- package/dist/buffer-admission.d.ts.map +1 -0
- package/dist/buffer-admission.js +13 -0
- package/dist/buffer-eviction.d.ts +4 -5
- package/dist/buffer-eviction.d.ts.map +1 -1
- package/dist/buffer-eviction.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 +9 -1
- package/dist/buffer-types.d.ts +24 -1
- package/dist/buffer-types.d.ts.map +1 -1
- package/dist/buffer.d.ts +17 -18
- package/dist/buffer.d.ts.map +1 -1
- package/dist/buffer.js +12 -8
- package/dist/calibration.js +2 -2
- package/dist/{capsule-crypto-GWVG7LGC.js → capsule-crypto-7FJQINUR.js} +2 -2
- package/dist/capsule-crypto-7FJQINUR.js.map +1 -0
- package/dist/capsule-import-gate.d.ts +11 -0
- package/dist/capsule-import-gate.d.ts.map +1 -0
- package/dist/capsule-import-gate.js +12 -0
- package/dist/capsule-import-gate.js.map +1 -0
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.js +25 -25
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-73YVMZ7D.js → chunk-2G4TV2ML.js} +11 -11
- package/dist/{chunk-BMIM6ESC.js → chunk-2JG2NI7C.js} +38 -38
- package/dist/{chunk-IR4C3DP2.js → chunk-2K3AUDH7.js} +2 -2
- package/dist/{chunk-6KFFRMPQ.js → chunk-2N4OUSIA.js} +2 -2
- package/dist/{chunk-OWHERGF2.js → chunk-2NLLXCJG.js} +2 -2
- package/dist/{chunk-44FQCC5D.js → chunk-2NUDHVYM.js} +2 -2
- package/dist/{chunk-FK4WC6VK.js → chunk-2U4LMSEX.js} +3 -3
- package/dist/{chunk-LT3CMO65.js → chunk-2XOFVTTT.js} +2 -2
- package/dist/{chunk-I6ORDT54.js → chunk-32CA7B6L.js} +2 -2
- package/dist/{chunk-TQVECGUQ.js → chunk-32YX7EJ3.js} +3 -3
- package/dist/{chunk-H4D5YURJ.js → chunk-3CTKEXXF.js} +120 -118
- package/dist/{chunk-H4D5YURJ.js.map → chunk-3CTKEXXF.js.map} +1 -1
- package/dist/{chunk-HLWXCDUQ.js → chunk-3DAWHP5K.js} +5 -5
- package/dist/{chunk-GBOYOL5L.js → chunk-3PJEDHLD.js} +2 -2
- package/dist/{chunk-ZWTHVUT5.js → chunk-3UZ2VMKD.js} +2 -2
- package/dist/{chunk-2XYDL2FG.js → chunk-3Y5X6YFH.js} +2 -2
- package/dist/{chunk-23B4HLWG.js → chunk-4GMI5HFH.js} +9 -9
- package/dist/{chunk-AP3TSV6S.js → chunk-4LCVA2LW.js} +3 -3
- package/dist/{chunk-MZCGHPIJ.js → chunk-4X7HQKAG.js} +2 -2
- package/dist/{chunk-YNBMBL4C.js → chunk-4XYAWK5H.js} +2 -2
- package/dist/{chunk-2JAT4S7N.js → chunk-5SOSQTVE.js} +2 -2
- package/dist/{chunk-G3T6YMBO.js → chunk-64S6X4IU.js} +7 -7
- package/dist/{chunk-NIV7RPWQ.js → chunk-6BCWFCDG.js} +7 -7
- package/dist/{chunk-V27FK6D6.js → chunk-6XTUZKCZ.js} +2 -2
- package/dist/{chunk-KASJJGRG.js → chunk-6ZELOK6O.js} +3 -8
- package/dist/chunk-6ZELOK6O.js.map +1 -0
- package/dist/{chunk-2ASEDFOA.js → chunk-73UZZFJC.js} +3 -3
- package/dist/{chunk-UHPHH74F.js → chunk-77PDJ4OE.js} +3 -3
- package/dist/{chunk-BATPQNRG.js → chunk-7GSL4LBG.js} +2 -2
- package/dist/{chunk-B6SPPYGJ.js → chunk-7IWRLYFJ.js} +2 -2
- package/dist/{chunk-BUKK5SWA.js → chunk-7LCGFUMB.js} +4 -4
- package/dist/{chunk-ZXF3O7P4.js → chunk-ARRTLPRC.js} +2 -2
- package/dist/{chunk-S5CQI2KF.js → chunk-B3GKQ6DU.js} +2 -2
- package/dist/{chunk-2URKYLMK.js → chunk-B56LORLY.js} +2 -2
- package/dist/{chunk-4IWEPQEV.js → chunk-BIVKPQ6Q.js} +2 -2
- package/dist/{chunk-DM4RRTN7.js → chunk-BLSHFIR6.js} +7 -6
- package/dist/chunk-BLSHFIR6.js.map +1 -0
- package/dist/{chunk-4DPLCYEK.js → chunk-C2HORX76.js} +3 -3
- package/dist/{chunk-ELWI2R6H.js → chunk-CC4V7HNP.js} +22 -22
- package/dist/{chunk-LGEH473S.js → chunk-CJCTKL6Y.js} +9 -1
- package/dist/chunk-CJCTKL6Y.js.map +1 -0
- package/dist/{chunk-WFDWLWDK.js → chunk-CN7Q3T2J.js} +4 -4
- package/dist/{chunk-GJVGFKOU.js → chunk-CYCEROPR.js} +3 -3
- package/dist/{chunk-CCU7URRP.js → chunk-D5EV6Y2L.js} +3 -3
- package/dist/{chunk-N6II66E5.js → chunk-DT7SNJH6.js} +3 -3
- package/dist/{chunk-FRM4UTDR.js → chunk-DZ7DPDVW.js} +8 -6
- package/dist/chunk-DZ7DPDVW.js.map +1 -0
- package/dist/{chunk-MVOJDLXS.js → chunk-EA2LA5RI.js} +2 -2
- package/dist/{chunk-WBN66HPI.js → chunk-ELHJK7LM.js} +6 -6
- package/dist/{chunk-GKYIRTNX.js → chunk-EQ4U73UH.js} +45 -14
- package/dist/chunk-EQ4U73UH.js.map +1 -0
- package/dist/{chunk-ONN2E5QD.js → chunk-EUBDYOLD.js} +6 -6
- package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
- package/dist/{chunk-3LHPTBWG.js → chunk-F74G4KIH.js} +2 -2
- package/dist/{chunk-X7REO4CH.js → chunk-FF3EFLUT.js} +7 -5
- package/dist/chunk-FF3EFLUT.js.map +1 -0
- package/dist/chunk-FKBGK3NC.js +62 -0
- package/dist/chunk-FKBGK3NC.js.map +1 -0
- package/dist/{chunk-X2KO4FK5.js → chunk-FNFURZBZ.js} +5 -5
- package/dist/chunk-FNFURZBZ.js.map +1 -0
- package/dist/{chunk-MYCCO6AA.js → chunk-G54BOKZ6.js} +2 -2
- package/dist/{chunk-JFH6JAGY.js → chunk-G67EUQAB.js} +5 -5
- package/dist/{chunk-TV65NR7L.js → chunk-GULUWOA5.js} +2 -2
- package/dist/{chunk-6DANGCMO.js → chunk-HFUKXOUW.js} +2 -2
- package/dist/{chunk-Y3IYKNBY.js → chunk-HLN2HOXX.js} +2 -2
- package/dist/{chunk-Y43X3E24.js → chunk-IBDWYRDR.js} +3 -3
- package/dist/{chunk-XSFQN7TP.js → chunk-IBGU6AN6.js} +15 -14
- package/dist/chunk-IBGU6AN6.js.map +1 -0
- package/dist/{chunk-AJG2TAMR.js → chunk-IEEV27BC.js} +2 -2
- package/dist/{chunk-FZPDRCOB.js → chunk-IJ3OEECW.js} +4 -4
- package/dist/{chunk-D6THW7PA.js → chunk-IPD5GC7C.js} +2 -2
- package/dist/{chunk-VCVEMQJK.js → chunk-JM2YZKCQ.js} +2 -2
- package/dist/{chunk-H34INI3P.js → chunk-KVSOEVAF.js} +34 -11
- package/dist/chunk-KVSOEVAF.js.map +1 -0
- package/dist/chunk-M3BEJJNV.js +31 -0
- package/dist/chunk-M3BEJJNV.js.map +1 -0
- package/dist/{chunk-OSTFDWZJ.js → chunk-NIVKG3DS.js} +4 -4
- package/dist/{chunk-VZELUMKK.js → chunk-NQP4ZYGG.js} +2 -2
- package/dist/{chunk-3ODZLCHC.js → chunk-NT7F7WJQ.js} +2 -2
- package/dist/{chunk-YO2JEX2Z.js → chunk-OGBQMY46.js} +2 -2
- package/dist/{chunk-O55AGDTI.js → chunk-OL553AWT.js} +10 -10
- package/dist/{chunk-CLVILSBR.js → chunk-OZNBZJ3S.js} +22 -22
- package/dist/{chunk-RNGDKFY2.js → chunk-P452SEKO.js} +2 -2
- package/dist/{chunk-JX3E4XCW.js → chunk-PDOOUXCS.js} +2 -2
- package/dist/{chunk-V45TEVDA.js → chunk-PKZ274PI.js} +3 -3
- package/dist/{chunk-MNYQVYJ4.js → chunk-QD5R6U5J.js} +2 -2
- package/dist/{chunk-DI5ET2IE.js → chunk-QEZ3DW5O.js} +9 -9
- package/dist/{chunk-WYPCJK6K.js → chunk-QKERFEJS.js} +2 -2
- package/dist/{chunk-BLMTOIBK.js → chunk-QKES7GU6.js} +2 -2
- package/dist/{chunk-PCQWKMOL.js → chunk-QNSZYTSG.js} +2 -2
- package/dist/{chunk-433EXTC4.js → chunk-ROGO7FRA.js} +5 -4
- package/dist/{chunk-433EXTC4.js.map → chunk-ROGO7FRA.js.map} +1 -1
- package/dist/{chunk-MHSBOEH7.js → chunk-ROKDHO74.js} +2 -2
- package/dist/{chunk-BLGQUZFB.js → chunk-RWGRSKN5.js} +7 -5
- package/dist/chunk-RWGRSKN5.js.map +1 -0
- package/dist/{chunk-3VU2FDTU.js → chunk-SYKJBS7S.js} +10 -10
- package/dist/{chunk-5BE3FLKL.js → chunk-TAUHAP5J.js} +4 -4
- package/dist/{chunk-QCP75JGO.js → chunk-TFA6HPOA.js} +5 -5
- package/dist/{chunk-SGHV6LZ7.js → chunk-TQ4SCPGQ.js} +67 -51
- package/dist/chunk-TQ4SCPGQ.js.map +1 -0
- package/dist/{chunk-E7V5AK7I.js → chunk-TRMZHGIX.js} +8 -8
- package/dist/{chunk-WBDIZWJQ.js → chunk-TSS3D7ZY.js} +2 -2
- package/dist/{chunk-HVFX2N2G.js → chunk-UAUV3BWS.js} +65 -83
- package/dist/chunk-UAUV3BWS.js.map +1 -0
- package/dist/{chunk-OMYRPMQC.js → chunk-US7KPQA5.js} +2 -2
- package/dist/{chunk-KHZVAISH.js → chunk-UWRHHRDK.js} +2 -2
- package/dist/{chunk-WLNWFHX3.js → chunk-UXXK523X.js} +40 -2
- package/dist/{chunk-WLNWFHX3.js.map → chunk-UXXK523X.js.map} +1 -1
- package/dist/{chunk-SDESKFKT.js → chunk-UYHDMCY2.js} +2 -2
- package/dist/{chunk-YAAKVTZA.js → chunk-VKILERSL.js} +2 -2
- package/dist/{chunk-2TGIKMMZ.js → chunk-WXDTOE6F.js} +2 -2
- package/dist/{chunk-UVYI6VIX.js → chunk-X7Y7WX73.js} +1 -1
- package/dist/{chunk-P225W4RQ.js → chunk-XU37EJTW.js} +22 -22
- package/dist/chunk-XU37EJTW.js.map +1 -0
- package/dist/{chunk-QA3Y2O6H.js → chunk-YAVIBVCU.js} +2 -2
- package/dist/{chunk-CMRHXPLQ.js → chunk-YL4UKMD7.js} +117 -51
- package/dist/chunk-YL4UKMD7.js.map +1 -0
- package/dist/{chunk-GYNRNKMW.js → chunk-YMJRLT7S.js} +28 -8
- package/dist/chunk-YMJRLT7S.js.map +1 -0
- package/dist/{chunk-E5QEF3JO.js → chunk-YRZPBN46.js} +3 -3
- package/dist/{chunk-FMFCQRDM.js → chunk-ZANNIXXE.js} +3 -3
- package/dist/{chunk-3KGCHTTK.js → chunk-ZBFLV657.js} +3 -3
- package/dist/{chunk-VEFGHRKV.js → chunk-ZOJUWVAU.js} +1 -1
- package/dist/cli.js +117 -116
- package/dist/coding/export-okf-codegraph.js +21 -21
- package/dist/coding/pre-action-gate.js +2 -2
- package/dist/compounding/engine.js +23 -23
- package/dist/config.js +32 -32
- package/dist/connectors/codex-materialize-runner.js +22 -22
- package/dist/connectors/index.js +26 -26
- package/dist/convergence-refresh.js +24 -24
- package/dist/conversation-index/backend.js +2 -2
- package/dist/corpus-watermark.js +26 -26
- package/dist/dashboard-runtime.js +2 -2
- package/dist/deep-recall-config.js +2 -2
- package/dist/deep-recall-http-glue.js +11 -11
- package/dist/entity-retrieval.js +23 -23
- package/dist/explicit-capture.js +1 -1
- package/dist/external-wiki-access.js +73 -72
- package/dist/extraction-quote-grounding.d.ts +5 -1
- package/dist/extraction-quote-grounding.d.ts.map +1 -1
- package/dist/extraction-quote-grounding.js +1 -1
- package/dist/extraction-source-grounding-rules.d.ts +3 -1
- package/dist/extraction-source-grounding-rules.d.ts.map +1 -1
- package/dist/extraction-source-grounding-rules.js +2 -2
- package/dist/extraction-source-grounding.d.ts +1 -1
- package/dist/extraction-source-grounding.d.ts.map +1 -1
- package/dist/extraction-source-grounding.js +3 -3
- package/dist/extraction.d.ts.map +1 -1
- package/dist/extraction.js +40 -40
- package/dist/harmonic-retrieval.js +22 -22
- package/dist/index.js +196 -194
- package/dist/lcm/index.js +3 -3
- package/dist/local-llm.js +2 -2
- package/dist/location/index.js +25 -25
- package/dist/maintenance/memory-governance.js +22 -22
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +22 -22
- package/dist/maintenance/rebuild-memory-projection-runner.js +24 -24
- package/dist/maintenance/rebuild-memory-projection.js +23 -23
- package/dist/memory-browse-http-glue.js +2 -2
- package/dist/memory-browse-operations.js +73 -72
- package/dist/memory-browse.js +5 -5
- package/dist/namespaces/migrate.js +23 -23
- package/dist/namespaces/search.d.ts +1 -0
- package/dist/namespaces/search.d.ts.map +1 -1
- package/dist/namespaces/search.js +10 -8
- package/dist/namespaces/storage.js +22 -22
- package/dist/offline-sync-changeset-generations.js +3 -3
- package/dist/offline-sync-embedding-generation.js +2 -2
- package/dist/offline-sync-impression-drain.js +2 -2
- package/dist/offline-sync-snapshot-stream.js +6 -6
- package/dist/offline-sync.js +5 -5
- package/dist/operator-doctor-corpus.js +29 -29
- package/dist/operator-doctor-replica.js +4 -4
- package/dist/operator-toolkit.js +44 -44
- package/dist/orchestration/backlog-scheduler.js +158 -156
- package/dist/orchestration/backlog-scheduler.js.map +1 -1
- package/dist/orchestration/buffer-drain-runner.js +158 -156
- package/dist/orchestration/maintenance.js +36 -36
- package/dist/orchestration/turn-ingestion.d.ts +2 -9
- package/dist/orchestration/turn-ingestion.d.ts.map +1 -1
- package/dist/orchestrator.js +159 -157
- package/dist/qmd.js +4 -4
- package/dist/recall-navigation-http-glue.js +2 -2
- package/dist/recall-navigation-operations.js +73 -72
- package/dist/recall-navigation.js +72 -71
- package/dist/recall-state-view-admission.js +5 -5
- package/dist/recall-why-service.js +5 -5
- package/dist/replica-divergence.js +3 -3
- package/dist/resume-bundles.js +33 -33
- package/dist/search/factory.js +7 -7
- package/dist/search/index.js +11 -11
- package/dist/semantic-consolidation.js +23 -23
- package/dist/semantic-rule-promotion.js +22 -22
- package/dist/semantic-rule-verifier.js +22 -22
- package/dist/source-agent-qualifier.d.ts +2 -0
- package/dist/source-agent-qualifier.d.ts.map +1 -1
- package/dist/source-agent-qualifier.js +33 -33
- package/dist/standup/build.js +23 -23
- package/dist/storage/buffer-drain-claims.js +2 -2
- package/dist/storage/buffer-session-files.js +2 -2
- package/dist/storage.js +23 -23
- package/dist/summarizer.js +3 -3
- package/dist/support-passport/index.js +75 -74
- package/dist/tokens.js +3 -3
- 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 +22 -22
- package/dist/verified-recall.js +22 -22
- package/dist/who-knows-cli.js +24 -24
- package/dist/who-knows.js +23 -23
- package/package.json +2 -2
- package/src/access-boundary.ts +15 -0
- package/src/access-errors.ts +22 -0
- package/src/access-fleet-wide-work-peer.test.ts +236 -0
- package/src/access-http-peer-fleet-wide.test.ts +192 -0
- package/src/access-http.test.ts +249 -4
- package/src/access-http.ts +114 -39
- package/src/access-mcp.test.ts +64 -0
- package/src/access-mcp.ts +17 -7
- package/src/access-observe-replay-durability.test.ts +40 -4
- package/src/access-operations-batch.ts +23 -12
- package/src/access-proto-injection.test.ts +340 -0
- package/src/access-schema.ts +31 -5
- package/src/access-service-capsule-import.test.ts +235 -0
- package/src/access-service.ts +6 -45
- package/src/briefing-deadline.ts +5 -4
- package/src/buffer-admission.ts +45 -0
- package/src/buffer-eviction-backpressure.test.ts +211 -0
- package/src/buffer-eviction.ts +8 -14
- package/src/buffer-incremental-persistence.test.ts +25 -37
- package/src/buffer-session.test.ts +32 -16
- package/src/buffer-turn-helpers.ts +41 -0
- package/src/buffer-types.ts +28 -1
- package/src/buffer.ts +77 -85
- package/src/capsule-import-gate.ts +109 -0
- package/src/extraction-quote-grounding.test.ts +76 -0
- package/src/extraction-quote-grounding.ts +49 -13
- package/src/extraction-source-grounding-rules.ts +6 -1
- package/src/extraction-source-grounding.ts +2 -0
- package/src/extraction.ts +3 -4
- package/src/namespaces/search.test.ts +141 -1
- package/src/namespaces/search.ts +39 -9
- package/src/orchestration/turn-ingestion.ts +38 -39
- package/src/recall-navigation-operations.ts +9 -2
- package/src/source-agent-qualifier.ts +3 -0
- package/dist/chunk-BLGQUZFB.js.map +0 -1
- package/dist/chunk-CMRHXPLQ.js.map +0 -1
- package/dist/chunk-DM4RRTN7.js.map +0 -1
- package/dist/chunk-FRM4UTDR.js.map +0 -1
- package/dist/chunk-GKYIRTNX.js.map +0 -1
- package/dist/chunk-GYNRNKMW.js.map +0 -1
- package/dist/chunk-H34INI3P.js.map +0 -1
- package/dist/chunk-HVFX2N2G.js.map +0 -1
- package/dist/chunk-KASJJGRG.js.map +0 -1
- package/dist/chunk-LGEH473S.js.map +0 -1
- package/dist/chunk-P225W4RQ.js.map +0 -1
- package/dist/chunk-SGHV6LZ7.js.map +0 -1
- package/dist/chunk-X2KO4FK5.js.map +0 -1
- package/dist/chunk-X7REO4CH.js.map +0 -1
- package/dist/chunk-XSFQN7TP.js.map +0 -1
- /package/dist/{capsule-crypto-GWVG7LGC.js.map → buffer-admission.js.map} +0 -0
- /package/dist/{chunk-73YVMZ7D.js.map → chunk-2G4TV2ML.js.map} +0 -0
- /package/dist/{chunk-BMIM6ESC.js.map → chunk-2JG2NI7C.js.map} +0 -0
- /package/dist/{chunk-IR4C3DP2.js.map → chunk-2K3AUDH7.js.map} +0 -0
- /package/dist/{chunk-6KFFRMPQ.js.map → chunk-2N4OUSIA.js.map} +0 -0
- /package/dist/{chunk-OWHERGF2.js.map → chunk-2NLLXCJG.js.map} +0 -0
- /package/dist/{chunk-44FQCC5D.js.map → chunk-2NUDHVYM.js.map} +0 -0
- /package/dist/{chunk-FK4WC6VK.js.map → chunk-2U4LMSEX.js.map} +0 -0
- /package/dist/{chunk-LT3CMO65.js.map → chunk-2XOFVTTT.js.map} +0 -0
- /package/dist/{chunk-I6ORDT54.js.map → chunk-32CA7B6L.js.map} +0 -0
- /package/dist/{chunk-TQVECGUQ.js.map → chunk-32YX7EJ3.js.map} +0 -0
- /package/dist/{chunk-HLWXCDUQ.js.map → chunk-3DAWHP5K.js.map} +0 -0
- /package/dist/{chunk-GBOYOL5L.js.map → chunk-3PJEDHLD.js.map} +0 -0
- /package/dist/{chunk-ZWTHVUT5.js.map → chunk-3UZ2VMKD.js.map} +0 -0
- /package/dist/{chunk-2XYDL2FG.js.map → chunk-3Y5X6YFH.js.map} +0 -0
- /package/dist/{chunk-23B4HLWG.js.map → chunk-4GMI5HFH.js.map} +0 -0
- /package/dist/{chunk-AP3TSV6S.js.map → chunk-4LCVA2LW.js.map} +0 -0
- /package/dist/{chunk-MZCGHPIJ.js.map → chunk-4X7HQKAG.js.map} +0 -0
- /package/dist/{chunk-YNBMBL4C.js.map → chunk-4XYAWK5H.js.map} +0 -0
- /package/dist/{chunk-2JAT4S7N.js.map → chunk-5SOSQTVE.js.map} +0 -0
- /package/dist/{chunk-G3T6YMBO.js.map → chunk-64S6X4IU.js.map} +0 -0
- /package/dist/{chunk-NIV7RPWQ.js.map → chunk-6BCWFCDG.js.map} +0 -0
- /package/dist/{chunk-V27FK6D6.js.map → chunk-6XTUZKCZ.js.map} +0 -0
- /package/dist/{chunk-2ASEDFOA.js.map → chunk-73UZZFJC.js.map} +0 -0
- /package/dist/{chunk-UHPHH74F.js.map → chunk-77PDJ4OE.js.map} +0 -0
- /package/dist/{chunk-BATPQNRG.js.map → chunk-7GSL4LBG.js.map} +0 -0
- /package/dist/{chunk-B6SPPYGJ.js.map → chunk-7IWRLYFJ.js.map} +0 -0
- /package/dist/{chunk-BUKK5SWA.js.map → chunk-7LCGFUMB.js.map} +0 -0
- /package/dist/{chunk-ZXF3O7P4.js.map → chunk-ARRTLPRC.js.map} +0 -0
- /package/dist/{chunk-S5CQI2KF.js.map → chunk-B3GKQ6DU.js.map} +0 -0
- /package/dist/{chunk-2URKYLMK.js.map → chunk-B56LORLY.js.map} +0 -0
- /package/dist/{chunk-4IWEPQEV.js.map → chunk-BIVKPQ6Q.js.map} +0 -0
- /package/dist/{chunk-4DPLCYEK.js.map → chunk-C2HORX76.js.map} +0 -0
- /package/dist/{chunk-ELWI2R6H.js.map → chunk-CC4V7HNP.js.map} +0 -0
- /package/dist/{chunk-WFDWLWDK.js.map → chunk-CN7Q3T2J.js.map} +0 -0
- /package/dist/{chunk-GJVGFKOU.js.map → chunk-CYCEROPR.js.map} +0 -0
- /package/dist/{chunk-CCU7URRP.js.map → chunk-D5EV6Y2L.js.map} +0 -0
- /package/dist/{chunk-N6II66E5.js.map → chunk-DT7SNJH6.js.map} +0 -0
- /package/dist/{chunk-MVOJDLXS.js.map → chunk-EA2LA5RI.js.map} +0 -0
- /package/dist/{chunk-WBN66HPI.js.map → chunk-ELHJK7LM.js.map} +0 -0
- /package/dist/{chunk-ONN2E5QD.js.map → chunk-EUBDYOLD.js.map} +0 -0
- /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
- /package/dist/{chunk-3LHPTBWG.js.map → chunk-F74G4KIH.js.map} +0 -0
- /package/dist/{chunk-MYCCO6AA.js.map → chunk-G54BOKZ6.js.map} +0 -0
- /package/dist/{chunk-JFH6JAGY.js.map → chunk-G67EUQAB.js.map} +0 -0
- /package/dist/{chunk-TV65NR7L.js.map → chunk-GULUWOA5.js.map} +0 -0
- /package/dist/{chunk-6DANGCMO.js.map → chunk-HFUKXOUW.js.map} +0 -0
- /package/dist/{chunk-Y3IYKNBY.js.map → chunk-HLN2HOXX.js.map} +0 -0
- /package/dist/{chunk-Y43X3E24.js.map → chunk-IBDWYRDR.js.map} +0 -0
- /package/dist/{chunk-AJG2TAMR.js.map → chunk-IEEV27BC.js.map} +0 -0
- /package/dist/{chunk-FZPDRCOB.js.map → chunk-IJ3OEECW.js.map} +0 -0
- /package/dist/{chunk-D6THW7PA.js.map → chunk-IPD5GC7C.js.map} +0 -0
- /package/dist/{chunk-VCVEMQJK.js.map → chunk-JM2YZKCQ.js.map} +0 -0
- /package/dist/{chunk-OSTFDWZJ.js.map → chunk-NIVKG3DS.js.map} +0 -0
- /package/dist/{chunk-VZELUMKK.js.map → chunk-NQP4ZYGG.js.map} +0 -0
- /package/dist/{chunk-3ODZLCHC.js.map → chunk-NT7F7WJQ.js.map} +0 -0
- /package/dist/{chunk-YO2JEX2Z.js.map → chunk-OGBQMY46.js.map} +0 -0
- /package/dist/{chunk-O55AGDTI.js.map → chunk-OL553AWT.js.map} +0 -0
- /package/dist/{chunk-CLVILSBR.js.map → chunk-OZNBZJ3S.js.map} +0 -0
- /package/dist/{chunk-RNGDKFY2.js.map → chunk-P452SEKO.js.map} +0 -0
- /package/dist/{chunk-JX3E4XCW.js.map → chunk-PDOOUXCS.js.map} +0 -0
- /package/dist/{chunk-V45TEVDA.js.map → chunk-PKZ274PI.js.map} +0 -0
- /package/dist/{chunk-MNYQVYJ4.js.map → chunk-QD5R6U5J.js.map} +0 -0
- /package/dist/{chunk-DI5ET2IE.js.map → chunk-QEZ3DW5O.js.map} +0 -0
- /package/dist/{chunk-WYPCJK6K.js.map → chunk-QKERFEJS.js.map} +0 -0
- /package/dist/{chunk-BLMTOIBK.js.map → chunk-QKES7GU6.js.map} +0 -0
- /package/dist/{chunk-PCQWKMOL.js.map → chunk-QNSZYTSG.js.map} +0 -0
- /package/dist/{chunk-MHSBOEH7.js.map → chunk-ROKDHO74.js.map} +0 -0
- /package/dist/{chunk-3VU2FDTU.js.map → chunk-SYKJBS7S.js.map} +0 -0
- /package/dist/{chunk-5BE3FLKL.js.map → chunk-TAUHAP5J.js.map} +0 -0
- /package/dist/{chunk-QCP75JGO.js.map → chunk-TFA6HPOA.js.map} +0 -0
- /package/dist/{chunk-E7V5AK7I.js.map → chunk-TRMZHGIX.js.map} +0 -0
- /package/dist/{chunk-WBDIZWJQ.js.map → chunk-TSS3D7ZY.js.map} +0 -0
- /package/dist/{chunk-OMYRPMQC.js.map → chunk-US7KPQA5.js.map} +0 -0
- /package/dist/{chunk-KHZVAISH.js.map → chunk-UWRHHRDK.js.map} +0 -0
- /package/dist/{chunk-SDESKFKT.js.map → chunk-UYHDMCY2.js.map} +0 -0
- /package/dist/{chunk-YAAKVTZA.js.map → chunk-VKILERSL.js.map} +0 -0
- /package/dist/{chunk-2TGIKMMZ.js.map → chunk-WXDTOE6F.js.map} +0 -0
- /package/dist/{chunk-UVYI6VIX.js.map → chunk-X7Y7WX73.js.map} +0 -0
- /package/dist/{chunk-QA3Y2O6H.js.map → chunk-YAVIBVCU.js.map} +0 -0
- /package/dist/{chunk-E5QEF3JO.js.map → chunk-YRZPBN46.js.map} +0 -0
- /package/dist/{chunk-FMFCQRDM.js.map → chunk-ZANNIXXE.js.map} +0 -0
- /package/dist/{chunk-3KGCHTTK.js.map → chunk-ZBFLV657.js.map} +0 -0
- /package/dist/{chunk-VEFGHRKV.js.map → chunk-ZOJUWVAU.js.map} +0 -0
package/src/access-service.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { constants as fsConstants } from "node:fs";
|
|
3
2
|
import { readdirSync, unlinkSync } from "node:fs";
|
|
4
3
|
import { stat } from "node:fs/promises";
|
|
5
4
|
import * as nodeFs from "node:fs/promises";
|
|
6
|
-
import { ZodError } from "zod";
|
|
7
5
|
import { summarizeHourlyStatus } from "./summarizer.js";
|
|
8
6
|
import { decodeCitationNamespace } from "./access-citation-namespace.js";
|
|
9
7
|
import {
|
|
@@ -288,6 +286,7 @@ import {
|
|
|
288
286
|
} from "./capabilities.js";
|
|
289
287
|
import { resolveRecallEnhancementCapabilities } from "./capabilities.js";
|
|
290
288
|
import { type CapsuleListEntry, defaultCapsulesDir } from "./capsule-cli.js";
|
|
289
|
+
import { isCapsuleImportArchiveInputError, validateCapsuleImportArchive } from "./capsule-import-gate.js";
|
|
291
290
|
import type { SemanticDedupLookup } from "./dedup/semantic.js";
|
|
292
291
|
import type { FallbackLlmClient } from "./fallback-llm.js";
|
|
293
292
|
import type { LocalLlmClient } from "./local-llm.js";
|
|
@@ -5366,6 +5365,9 @@ export class EngramAccessService extends SupportPassportAccessServiceBase {
|
|
|
5366
5365
|
* `versioning` defaults to the orchestrator's page-versioning config so
|
|
5367
5366
|
* `mode: "overwrite"` automatically snapshots prior content without the
|
|
5368
5367
|
* caller having to construct the config object.
|
|
5368
|
+
*
|
|
5369
|
+
* Remote callers (HTTP and MCP) may only import from the destination
|
|
5370
|
+
* namespace's capsule store; the CLI keeps unrestricted local paths.
|
|
5369
5371
|
*/
|
|
5370
5372
|
async capsuleImport(
|
|
5371
5373
|
opts: Omit<ImportCapsuleOptions, "root" | "memoryDir"> & {
|
|
@@ -5385,59 +5387,18 @@ export class EngramAccessService extends SupportPassportAccessServiceBase {
|
|
|
5385
5387
|
maxVersionsPerPage: this.orchestrator.config.versioningMaxPerPage,
|
|
5386
5388
|
sidecarDir: this.orchestrator.config.versioningSidecarDir,
|
|
5387
5389
|
};
|
|
5388
|
-
await
|
|
5390
|
+
await validateCapsuleImportArchive(importOptions.archivePath, storage.dir);
|
|
5389
5391
|
try {
|
|
5390
5392
|
return await importCapsuleFn({ ...importOptions, root, memoryDir, versioning });
|
|
5391
5393
|
} catch (err) {
|
|
5392
5394
|
const message = err instanceof Error ? err.message : String(err);
|
|
5393
|
-
if (
|
|
5395
|
+
if (isCapsuleImportArchiveInputError(err, message)) {
|
|
5394
5396
|
throw new EngramAccessInputError(`capsule import failed: ${message}`);
|
|
5395
5397
|
}
|
|
5396
5398
|
throw err;
|
|
5397
5399
|
}
|
|
5398
5400
|
}
|
|
5399
5401
|
|
|
5400
|
-
private async validateCapsuleImportArchivePath(archivePath: string): Promise<void> {
|
|
5401
|
-
let archiveStat;
|
|
5402
|
-
try {
|
|
5403
|
-
archiveStat = await stat(archivePath);
|
|
5404
|
-
} catch (err) {
|
|
5405
|
-
if (!this.isCapsuleImportPathInputFsError(err)) throw err;
|
|
5406
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
5407
|
-
throw new EngramAccessInputError(`capsule import failed: archive is not readable: ${message}`);
|
|
5408
|
-
}
|
|
5409
|
-
if (!archiveStat.isFile()) {
|
|
5410
|
-
throw new EngramAccessInputError("capsule import failed: archivePath must point to a file");
|
|
5411
|
-
}
|
|
5412
|
-
try {
|
|
5413
|
-
await nodeFs.access(archivePath, fsConstants.R_OK);
|
|
5414
|
-
} catch (err) {
|
|
5415
|
-
if (!this.isCapsuleImportPathInputFsError(err)) throw err;
|
|
5416
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
5417
|
-
throw new EngramAccessInputError(`capsule import failed: archive is not readable: ${message}`);
|
|
5418
|
-
}
|
|
5419
|
-
}
|
|
5420
|
-
|
|
5421
|
-
private isCapsuleImportPathInputFsError(err: unknown): boolean {
|
|
5422
|
-
const code =
|
|
5423
|
-
typeof err === "object" && err !== null && "code" in err ? (err as { code?: unknown }).code : undefined;
|
|
5424
|
-
return code === "ENOENT" || code === "ENOTDIR" || code === "EACCES" || code === "EPERM" || code === "ELOOP";
|
|
5425
|
-
}
|
|
5426
|
-
|
|
5427
|
-
private isCapsuleImportArchiveInputError(err: unknown, message: string): boolean {
|
|
5428
|
-
if (err instanceof ZodError) return true;
|
|
5429
|
-
const code =
|
|
5430
|
-
typeof err === "object" && err !== null && "code" in err ? (err as { code?: unknown }).code : undefined;
|
|
5431
|
-
if (typeof code === "string" && code.startsWith("Z_")) return true;
|
|
5432
|
-
return (
|
|
5433
|
-
message.startsWith("importCapsule: archive") ||
|
|
5434
|
-
message.startsWith("importCapsule: bundle") ||
|
|
5435
|
-
message.startsWith("importCapsule: manifest") ||
|
|
5436
|
-
message.startsWith("importCapsule: record") ||
|
|
5437
|
-
/incorrect header check|invalid stored block lengths|not in gzip format|unexpected end of file/i.test(message)
|
|
5438
|
-
);
|
|
5439
|
-
}
|
|
5440
|
-
|
|
5441
5402
|
/**
|
|
5442
5403
|
* Export a capsule archive from the orchestrator's memory directory.
|
|
5443
5404
|
*
|
package/src/briefing-deadline.ts
CHANGED
|
@@ -3,20 +3,21 @@ import { isAbortError, raceAbort } from "./abort-error.js";
|
|
|
3
3
|
export const BRIEFING_DEADLINE_MS = 30_000;
|
|
4
4
|
|
|
5
5
|
export class BriefingTimeoutError extends Error {
|
|
6
|
-
constructor(readonly deadlineMs: number) {
|
|
7
|
-
super(
|
|
6
|
+
constructor(readonly deadlineMs: number, label = "briefing") {
|
|
7
|
+
super(`${label} timed out after ${deadlineMs}ms`);
|
|
8
8
|
this.name = "TimeoutError";
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export async function withBriefingDeadline<T>(
|
|
13
13
|
run: (signal: AbortSignal) => Promise<T>,
|
|
14
|
-
abortSignal?: AbortSignal
|
|
14
|
+
abortSignal?: AbortSignal,
|
|
15
|
+
label = "briefing",
|
|
15
16
|
): Promise<T> {
|
|
16
17
|
const controller = new AbortController();
|
|
17
18
|
const signal = abortSignal ? AbortSignal.any([abortSignal, controller.signal]) : controller.signal;
|
|
18
19
|
const timer = setTimeout(
|
|
19
|
-
() => controller.abort(new BriefingTimeoutError(BRIEFING_DEADLINE_MS)),
|
|
20
|
+
() => controller.abort(new BriefingTimeoutError(BRIEFING_DEADLINE_MS, label)),
|
|
20
21
|
BRIEFING_DEADLINE_MS
|
|
21
22
|
);
|
|
22
23
|
try {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Buffer session-entry admission: refusal errors and the live-admission probe. */
|
|
2
|
+
|
|
3
|
+
import { selectEvictedBufferKeys, type BufferEvictionHooks } from "./buffer-eviction.js";
|
|
4
|
+
import type { BufferEntryState } from "./types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Raised when a NEW live-capture session cannot be admitted because the
|
|
8
|
+
* session-entry cap is full of non-evictable backlog. The turn was never
|
|
9
|
+
* accepted; the request is retryable once extraction drains an entry.
|
|
10
|
+
*/
|
|
11
|
+
export class BufferAdmissionRefusedError extends Error {
|
|
12
|
+
constructor(bufferKey: string, maxSessionEntries: number) {
|
|
13
|
+
super(
|
|
14
|
+
`buffer[${bufferKey}]: live capture refused at the session-entry cap (${maxSessionEntries}); ` +
|
|
15
|
+
"buffered backlog is no longer evicted to admit new sessions. Retry after extraction frees an entry",
|
|
16
|
+
);
|
|
17
|
+
this.name = "BufferAdmissionRefusedError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Durable observe capture refused at the entry cap; the request is retryable. */
|
|
22
|
+
export class ReplayAdmissionRefusedError extends Error {
|
|
23
|
+
constructor(message: string) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "ReplayAdmissionRefusedError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Live-admission bound (#1908 policy, backlog-safe): a NEW session enters
|
|
31
|
+
* only while under the cap or when an EMPTY entry can be reclaimed. Buffered
|
|
32
|
+
* backlog is never dropped for a live capture; the caller refuses instead.
|
|
33
|
+
*/
|
|
34
|
+
export function canAdmitNewEntryUnlocked(
|
|
35
|
+
entries: Readonly<Record<string, BufferEntryState>>,
|
|
36
|
+
bufferKey: string,
|
|
37
|
+
maxSessionEntries: number,
|
|
38
|
+
hooks: BufferEvictionHooks,
|
|
39
|
+
): boolean {
|
|
40
|
+
const count = Object.keys(entries).length;
|
|
41
|
+
if (count < maxSessionEntries) return true;
|
|
42
|
+
// Probe post-admission (as if this key had landed): admission is safe
|
|
43
|
+
// exactly when an EMPTY entry could then be reclaimed.
|
|
44
|
+
return selectEvictedBufferKeys(entries, [bufferKey], Math.max(0, count - 1), hooks).length > 0;
|
|
45
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { SmartBuffer, BufferAdmissionRefusedError } from "./buffer.js";
|
|
4
|
+
import { parseConfig } from "./config.js";
|
|
5
|
+
import { log } from "./logger.js";
|
|
6
|
+
import { TurnIngestionCoordinator } from "./orchestration/turn-ingestion.js";
|
|
7
|
+
import type { BufferEntryState, BufferState, BufferTurn } from "./types.js";
|
|
8
|
+
|
|
9
|
+
class FakeStorage {
|
|
10
|
+
public saved: BufferState | null = null;
|
|
11
|
+
public saveCount = 0;
|
|
12
|
+
|
|
13
|
+
constructor(private readonly initial: BufferState) {}
|
|
14
|
+
|
|
15
|
+
async loadBuffer(): Promise<BufferState> {
|
|
16
|
+
return structuredClone(this.initial);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async saveBuffer(state: BufferState): Promise<void> {
|
|
20
|
+
this.saveCount += 1;
|
|
21
|
+
this.saved = structuredClone(state);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function makeTurn(key: string, content: string, accepted = false): BufferTurn {
|
|
26
|
+
const turn: BufferTurn = {
|
|
27
|
+
role: "user",
|
|
28
|
+
content,
|
|
29
|
+
timestamp: new Date().toISOString(),
|
|
30
|
+
sessionKey: key,
|
|
31
|
+
};
|
|
32
|
+
if (accepted) turn.acceptedCapture = true;
|
|
33
|
+
return turn;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function entry(turns: BufferTurn[]): BufferEntryState {
|
|
37
|
+
return { turns, lastExtractionAt: null, extractionCount: 0 };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function emptyEntry(): BufferEntryState {
|
|
41
|
+
return { turns: [], lastExtractionAt: null, extractionCount: 0 };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** default + the given entries; default stays empty unless given. */
|
|
45
|
+
function seededState(entries: Record<string, BufferEntryState>): BufferState {
|
|
46
|
+
const defaultEntry = entries.default ?? emptyEntry();
|
|
47
|
+
return {
|
|
48
|
+
turns: defaultEntry.turns,
|
|
49
|
+
lastExtractionAt: defaultEntry.lastExtractionAt,
|
|
50
|
+
extractionCount: defaultEntry.extractionCount,
|
|
51
|
+
entries: { default: defaultEntry, ...entries },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function makeBuffer(state: BufferState, maxSessions: number) {
|
|
56
|
+
const config = parseConfig({ bufferSaveDebounceMs: 0, bufferMaxSessions: maxSessions });
|
|
57
|
+
const storage = new FakeStorage(state);
|
|
58
|
+
const buffer = new SmartBuffer(config, storage);
|
|
59
|
+
return { buffer, storage };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const CAP = 4;
|
|
63
|
+
|
|
64
|
+
function savedKeys(storage: FakeStorage): string[] {
|
|
65
|
+
return Object.keys(storage.saved?.entries ?? {});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
test("live capture at a cap full of non-empty backlog is refused, never evicts buffered turns", async () => {
|
|
69
|
+
const { buffer } = makeBuffer(
|
|
70
|
+
seededState({
|
|
71
|
+
a: entry([makeTurn("a", "backlog a")]),
|
|
72
|
+
b: entry([makeTurn("b", "backlog b")]),
|
|
73
|
+
c: entry([makeTurn("c", "backlog c")]),
|
|
74
|
+
}),
|
|
75
|
+
CAP,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
await assert.rejects(
|
|
79
|
+
buffer.addTurn("new-session", makeTurn("new-session", "live turn")),
|
|
80
|
+
BufferAdmissionRefusedError,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
assert.deepEqual(buffer.getTurns("a").map((t) => t.content), ["backlog a"], "oldest backlog lost to live admission");
|
|
84
|
+
assert.deepEqual(buffer.getTurns("b").map((t) => t.content), ["backlog b"]);
|
|
85
|
+
assert.deepEqual(buffer.getTurns("c").map((t) => t.content), ["backlog c"]);
|
|
86
|
+
assert.equal(buffer.getTurns("new-session").length, 0, "refused turn was not buffered");
|
|
87
|
+
|
|
88
|
+
// Accepted captures are subject to the same no-backlog-loss limit: refusal
|
|
89
|
+
// happens before acceptance, preserving accepted-capture non-eviction.
|
|
90
|
+
assert.equal(
|
|
91
|
+
await buffer.addTurnBounded("accepted-1", makeTurn("accepted-1", "accepted", true)),
|
|
92
|
+
null,
|
|
93
|
+
"accepted capture is refused rather than evicting native backlog",
|
|
94
|
+
);
|
|
95
|
+
assert.deepEqual(buffer.getTurns("a").map((t) => t.content), ["backlog a"]);
|
|
96
|
+
assert.deepEqual(buffer.getTurns("b").map((t) => t.content), ["backlog b"]);
|
|
97
|
+
assert.deepEqual(buffer.getTurns("c").map((t) => t.content), ["backlog c"]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("an already-admitted session keeps accepting turns at a full cap", async () => {
|
|
101
|
+
const { buffer, storage } = makeBuffer(
|
|
102
|
+
seededState({
|
|
103
|
+
a: entry([makeTurn("a", "backlog a")]),
|
|
104
|
+
b: entry([makeTurn("b", "backlog b")]),
|
|
105
|
+
c: entry([makeTurn("c", "backlog c")]),
|
|
106
|
+
}),
|
|
107
|
+
CAP,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const decision = await buffer.addTurn("b", makeTurn("b", "another b turn"));
|
|
111
|
+
assert.equal(decision, "keep_buffering");
|
|
112
|
+
assert.deepEqual(buffer.getTurns("b").map((t) => t.content), ["backlog b", "another b turn"]);
|
|
113
|
+
assert.deepEqual(buffer.getTurns("a").map((t) => t.content), ["backlog a"], "existing-session admission must not evict peers");
|
|
114
|
+
assert.deepEqual(buffer.getTurns("c").map((t) => t.content), ["backlog c"]);
|
|
115
|
+
assert.equal(savedKeys(storage).length, CAP, "entry count stays at the cap");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("empty entries are still evicted first to admit a new live session", async () => {
|
|
119
|
+
const { buffer, storage } = makeBuffer(
|
|
120
|
+
seededState({
|
|
121
|
+
"empty-oldest": emptyEntry(),
|
|
122
|
+
n1: entry([makeTurn("n1", "native 1")]),
|
|
123
|
+
n2: entry([makeTurn("n2", "native 2")]),
|
|
124
|
+
}),
|
|
125
|
+
CAP,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const decision = await buffer.addTurn("new-session", makeTurn("new-session", "live turn"));
|
|
129
|
+
assert.equal(decision, "keep_buffering");
|
|
130
|
+
const keys = savedKeys(storage);
|
|
131
|
+
assert.ok(!keys.includes("empty-oldest"), "oldest empty entry reclaimed");
|
|
132
|
+
assert.ok(keys.includes("new-session"), "new session admitted");
|
|
133
|
+
assert.deepEqual(buffer.getTurns("n1").map((t) => t.content), ["native 1"], "empty eviction must not touch backlog");
|
|
134
|
+
assert.deepEqual(buffer.getTurns("n2").map((t) => t.content), ["native 2"]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("a retained-only entry is non-evictable backlog: live admission is refused instead", async () => {
|
|
138
|
+
const retainedOnly: BufferEntryState = {
|
|
139
|
+
turns: [],
|
|
140
|
+
retainedTurns: [makeTurn("r", "deferred retry turn")],
|
|
141
|
+
lastExtractionAt: null,
|
|
142
|
+
extractionCount: 0,
|
|
143
|
+
};
|
|
144
|
+
const { buffer } = makeBuffer(
|
|
145
|
+
seededState({
|
|
146
|
+
r: retainedOnly,
|
|
147
|
+
n1: entry([makeTurn("n1", "native 1")]),
|
|
148
|
+
n2: entry([makeTurn("n2", "native 2")]),
|
|
149
|
+
}),
|
|
150
|
+
CAP,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
await assert.rejects(
|
|
154
|
+
buffer.addTurn("new-session", makeTurn("new-session", "live turn")),
|
|
155
|
+
BufferAdmissionRefusedError,
|
|
156
|
+
);
|
|
157
|
+
assert.equal(
|
|
158
|
+
buffer.getRetainedDeferredTurns("r").length,
|
|
159
|
+
1,
|
|
160
|
+
"retained retry turns must not be evicted as 'empty'",
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
test("concurrent multi-session admissions reserve capacity without partial batches", async () => {
|
|
164
|
+
const { buffer } = makeBuffer(seededState({}), 2);
|
|
165
|
+
const results = await Promise.all([
|
|
166
|
+
buffer.addTurnsBounded([{ bufferKey: "batch-a", turns: [makeTurn("batch-a", "a")] }]),
|
|
167
|
+
buffer.addTurnsBounded([{ bufferKey: "batch-b", turns: [makeTurn("batch-b", "b")] }]),
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
assert.equal(results.filter((result) => result !== null).length, 1);
|
|
171
|
+
assert.equal(results.filter((result) => result === null).length, 1);
|
|
172
|
+
assert.equal(buffer.getTurns("batch-a").length + buffer.getTurns("batch-b").length, 1);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("processTurn propagates retryable admission refusal without queueing extraction", async () => {
|
|
176
|
+
const { buffer } = makeBuffer(
|
|
177
|
+
seededState({
|
|
178
|
+
a: entry([makeTurn("a", "backlog a")]),
|
|
179
|
+
b: entry([makeTurn("b", "backlog b")]),
|
|
180
|
+
c: entry([makeTurn("c", "backlog c")]),
|
|
181
|
+
}),
|
|
182
|
+
CAP,
|
|
183
|
+
);
|
|
184
|
+
const config = parseConfig({
|
|
185
|
+
memoryDir: "/tmp/unused-buffer-eviction-backpressure",
|
|
186
|
+
bufferSaveDebounceMs: 0,
|
|
187
|
+
bufferMaxSessions: CAP,
|
|
188
|
+
});
|
|
189
|
+
const queued: BufferTurn[][] = [];
|
|
190
|
+
type CoordinatorDeps = ConstructorParameters<typeof TurnIngestionCoordinator>[0];
|
|
191
|
+
// SAFETY: this fixture exercises only admission + refusal; the remaining
|
|
192
|
+
// coordinator deps are never reached on this path.
|
|
193
|
+
const deps = Object.assign({} as CoordinatorDeps, {
|
|
194
|
+
buffer,
|
|
195
|
+
config,
|
|
196
|
+
queueBufferedExtraction: async (turns: BufferTurn[]) => {
|
|
197
|
+
queued.push(turns);
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
const coordinator = new TurnIngestionCoordinator(deps);
|
|
201
|
+
|
|
202
|
+
await assert.rejects(
|
|
203
|
+
coordinator.processTurn("user", "live turn", "new-session"),
|
|
204
|
+
BufferAdmissionRefusedError,
|
|
205
|
+
"caller must receive retryable refusal rather than successful acknowledgment",
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
assert.equal(queued.length, 0, "a refused turn must not queue extraction");
|
|
209
|
+
assert.equal(buffer.getTurns("new-session").length, 0, "a refused turn must not be buffered");
|
|
210
|
+
assert.deepEqual(buffer.getTurns("a").map((x) => x.content), ["backlog a"], "backlog dropped during refused admission");
|
|
211
|
+
});
|
package/src/buffer-eviction.ts
CHANGED
|
@@ -10,11 +10,10 @@ export interface BufferEvictionHooks {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Pick
|
|
14
|
-
*
|
|
15
|
-
* empty entries
|
|
16
|
-
*
|
|
17
|
-
* entries holding an accepted capture are never chosen.
|
|
13
|
+
* Pick empty entries to evict so `entries` returns to `maxCount`. Backlog
|
|
14
|
+
* (active or retained turns) is never evicted; admission must refuse when
|
|
15
|
+
* empty entries cannot free a slot. "default", retained keys and entries
|
|
16
|
+
* holding an accepted capture are never chosen.
|
|
18
17
|
*/
|
|
19
18
|
export function selectEvictedBufferKeys(
|
|
20
19
|
entries: Readonly<Record<string, BufferEntryState>>,
|
|
@@ -35,17 +34,12 @@ export function selectEvictedBufferKeys(
|
|
|
35
34
|
const candidates = keys.filter(
|
|
36
35
|
(key) => key !== "default" && !retainKeys.includes(key) && !hooks.holdsAcceptedCapture(entries[key]),
|
|
37
36
|
);
|
|
37
|
+
// Backlog = active turns OR retained retry turns (#1908); only an entry
|
|
38
|
+
// holding neither counts as empty.
|
|
38
39
|
const emptyRemovable = candidates
|
|
39
|
-
.filter((key) => (entries[key]?.turns.length ?? 0) === 0)
|
|
40
|
+
.filter((key) => (entries[key]?.turns.length ?? 0) === 0 && (entries[key]?.retainedTurns?.length ?? 0) === 0)
|
|
40
41
|
.sort(byOldestActivity);
|
|
41
42
|
|
|
42
43
|
const removableCount = Math.max(0, keys.length - maxCount);
|
|
43
|
-
|
|
44
|
-
if (evicted.length < removableCount) {
|
|
45
|
-
const nonEmptyRemovable = candidates
|
|
46
|
-
.filter((key) => (entries[key]?.turns.length ?? 0) > 0)
|
|
47
|
-
.sort(byOldestActivity);
|
|
48
|
-
return evicted.concat(nonEmptyRemovable.slice(0, removableCount - evicted.length));
|
|
49
|
-
}
|
|
50
|
-
return evicted;
|
|
44
|
+
return emptyRemovable.slice(0, removableCount);
|
|
51
45
|
}
|
|
@@ -22,7 +22,7 @@ import { mkdtemp, rm } from "node:fs/promises";
|
|
|
22
22
|
import os from "node:os";
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
|
|
25
|
-
import { SmartBuffer } from "./buffer.js";
|
|
25
|
+
import { SmartBuffer, BufferAdmissionRefusedError } from "./buffer.js";
|
|
26
26
|
import { parseConfig } from "./config.js";
|
|
27
27
|
import { StorageManager } from "./storage.js";
|
|
28
28
|
import type { BufferEntryState, BufferState, BufferTurn } from "./types.js";
|
|
@@ -161,34 +161,28 @@ test("clearAfterExtraction persists the cleared session state", async () => {
|
|
|
161
161
|
assert.equal(record?.entry.extractionCount, 1);
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
-
test("evicted sessions get their files deleted", async () => {
|
|
164
|
+
test("empty 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, bufferMaxSessions:
|
|
167
|
-
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
166
|
+
const buffer = new SmartBuffer(parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100, bufferMaxSessions: 4 }), storage);
|
|
167
|
+
|
|
168
|
+
// Clear one persisted entry to make it genuinely empty. Accepted-capture
|
|
169
|
+
// admission may reclaim this slot, but never a session with buffered turns.
|
|
170
|
+
await buffer.addTurn("empty-old", makeTurn("empty-old", "will be extracted"));
|
|
171
|
+
await buffer.flushPendingSave();
|
|
172
|
+
await buffer.clearAfterExtraction("empty-old");
|
|
173
|
+
await buffer.addTurnBounded("native-1", makeTurn("native-1", "native 1"));
|
|
174
|
+
await buffer.addTurnBounded("native-2", makeTurn("native-2", "native 2"));
|
|
176
175
|
await buffer.flushPendingSave();
|
|
177
176
|
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
177
|
+
const accepted = await buffer.addTurnBounded("accepted", {
|
|
178
|
+
...makeTurn("accepted", "durable accepted"),
|
|
179
|
+
acceptedCapture: true,
|
|
180
|
+
});
|
|
181
|
+
assert.notEqual(accepted, null, "empty entry should be reclaimed to admit accepted capture");
|
|
183
182
|
await buffer.flushPendingSave();
|
|
184
|
-
assert.ok(storage.deleted.includes("
|
|
185
|
-
assert.
|
|
186
|
-
|
|
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
|
-
}
|
|
183
|
+
assert.ok(storage.deleted.includes("empty-old"), "reclaimed empty entry's file was not deleted");
|
|
184
|
+
assert.equal(buffer.getTurns("native-1").length, 1);
|
|
185
|
+
assert.equal(buffer.getTurns("native-2").length, 1);
|
|
192
186
|
});
|
|
193
187
|
|
|
194
188
|
test("the default entry cap leaves headroom for live sessions during backlog drains (#3303)", async () => {
|
|
@@ -273,9 +267,7 @@ test("legacy-only storage keeps the whole-state saveBuffer behavior", async () =
|
|
|
273
267
|
assert.equal(storage.lastSaved?.entries?.["thread-b"]?.turns[0]?.content, "beta");
|
|
274
268
|
});
|
|
275
269
|
|
|
276
|
-
test("durable capture at the entry cap
|
|
277
|
-
// Stalled extraction left the cap full of native (non-accepted)
|
|
278
|
-
// sessions; addTurnBounded refused every new accepted session.
|
|
270
|
+
test("durable capture at the entry cap is refused without evicting native backlog (COS-244c)", async () => {
|
|
279
271
|
const storage = new IncrementalStorage(emptyState());
|
|
280
272
|
const buffer = new SmartBuffer(
|
|
281
273
|
parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100, bufferMaxSessions: 4 }),
|
|
@@ -286,17 +278,13 @@ test("durable capture at the entry cap evicts native entries instead of refusing
|
|
|
286
278
|
await buffer.flushPendingSave();
|
|
287
279
|
|
|
288
280
|
const accepted = (key: string): BufferTurn => ({ ...makeTurn(key, "accepted"), acceptedCapture: true });
|
|
281
|
+
assert.equal(await buffer.addTurnBounded("accepted-0", accepted("accepted-0")), null);
|
|
282
|
+
await buffer.flushPendingSave();
|
|
283
|
+
assert.equal(storage.deleted.length, 0, "accepted refusal must not evict native session files");
|
|
289
284
|
for (let i = 0; i < 3; i++) {
|
|
290
|
-
|
|
291
|
-
assert.notEqual(outcome, null, `accepted-${i} refused while native entries were evictable`);
|
|
285
|
+
assert.ok(buffer.getTurns(`native-${i}`).some((turn) => turn.content === `native ${i}`));
|
|
292
286
|
}
|
|
293
|
-
|
|
294
|
-
assert.ok(storage.deleted.includes("native-0"), "oldest native entry was not evicted");
|
|
295
|
-
|
|
296
|
-
// Accepted work is still never evicted: with only accepted entries left the
|
|
297
|
-
// cap refuses rather than dropping accepted captures.
|
|
298
|
-
assert.equal(await buffer.addTurnBounded("accepted-3", accepted("accepted-3")), null);
|
|
299
|
-
for (let i = 0; i < 3; i++) assert.ok(!storage.deleted.includes(`accepted-${i}`));
|
|
287
|
+
assert.equal(buffer.getTurns("accepted-0").length, 0, "refused capture was never accepted");
|
|
300
288
|
});
|
|
301
289
|
|
|
302
290
|
test("at-cap admission evicts nothing when eviction cannot free a slot (COS-244c)", async () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { SmartBuffer } from "./buffer.js";
|
|
3
|
+
import { SmartBuffer, BufferAdmissionRefusedError } from "./buffer.js";
|
|
4
4
|
import { parseConfig } from "./config.js";
|
|
5
5
|
import type { BufferSurpriseProbe } from "./buffer.js";
|
|
6
6
|
import type { BufferState, BufferTurn } from "./types.js";
|
|
@@ -642,13 +642,14 @@ test("getTurns prepends retained deferred turns before live turns", async () =>
|
|
|
642
642
|
assert.equal(all[1]?.content, "new live turn");
|
|
643
643
|
});
|
|
644
644
|
|
|
645
|
-
test("SmartBuffer
|
|
645
|
+
test("SmartBuffer refuses new sessions instead of dropping backlog at the cap (#1908 policy)", async () => {
|
|
646
646
|
// Pre-#1908 this contract was "never prune non-empty entries". With retry
|
|
647
647
|
// retention (failed extractions keep their turns), unbounded non-empty
|
|
648
648
|
// growth during a provider outage would eventually OOM the daemon — the
|
|
649
|
-
// documented policy (#1908 step 7)
|
|
650
|
-
//
|
|
651
|
-
// Empty entries are still evicted
|
|
649
|
+
// documented policy (#1908 step 7) bounds the ENTRY COUNT, not by dropping
|
|
650
|
+
// buffered turns: a NEW session is refused (retryable backpressure) when
|
|
651
|
+
// only non-empty backlog fills the cap. Empty entries are still evicted
|
|
652
|
+
// first, and already-admitted sessions keep buffering.
|
|
652
653
|
const entries = Object.fromEntries(
|
|
653
654
|
Array.from({ length: 205 }, (_, index) => [
|
|
654
655
|
`thread-${index}`,
|
|
@@ -674,21 +675,32 @@ test("SmartBuffer prefers pruning empty entries and bounds pending-turn buffers
|
|
|
674
675
|
});
|
|
675
676
|
const buffer = new SmartBuffer(parseConfig({ bufferSaveDebounceMs: 0, bufferMaxSessions: 200 }), storage as unknown as ConstructorParameters<typeof SmartBuffer>[1]);
|
|
676
677
|
|
|
677
|
-
await
|
|
678
|
+
await assert.rejects(
|
|
679
|
+
buffer.addTurn("active-thread", makeTurn("active-thread", "pending memory")),
|
|
680
|
+
BufferAdmissionRefusedError,
|
|
681
|
+
"a new session must be refused while only non-empty backlog fills the cap",
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
// Already-admitted sessions keep buffering; the save reflects the
|
|
685
|
+
// post-refusal state: nothing was evicted, nothing refused was buffered.
|
|
686
|
+
await buffer.addTurn("thread-204", makeTurn("thread-204", "another turn"));
|
|
687
|
+
assert.equal(buffer.getTurns("thread-204").length, 2);
|
|
678
688
|
|
|
679
689
|
const persistedKeys = Object.keys(storage.saved?.entries ?? {});
|
|
680
|
-
assert.ok(persistedKeys.length <=
|
|
690
|
+
assert.ok(persistedKeys.length <= 206, `entry count stays at the seeded cap state (got ${persistedKeys.length})`);
|
|
681
691
|
assert.ok(persistedKeys.includes("default"), "default entry never pruned");
|
|
682
|
-
assert.ok(persistedKeys.includes("
|
|
683
|
-
assert.ok(!persistedKeys.includes("thread-0"), "oldest pending entry pruned once over the cap");
|
|
692
|
+
assert.ok(persistedKeys.includes("thread-0"), "oldest backlog keeps its turns for extraction");
|
|
684
693
|
assert.ok(persistedKeys.includes("thread-204"), "newest pending entry survives");
|
|
694
|
+
assert.ok(!persistedKeys.includes("active-thread"), "the refused turn was never buffered");
|
|
685
695
|
});
|
|
686
696
|
|
|
687
|
-
test("pruneEntries evicts
|
|
697
|
+
test("pruneEntries never evicts non-empty backlog; live admission is refused past the cap (#1908 retry-retention bound)", async () => {
|
|
688
698
|
// Retained failed extractions keep their turns in the buffer, so during a
|
|
689
699
|
// provider outage non-empty entries can exceed MAX_BUFFER_ENTRY_COUNT (200).
|
|
690
|
-
// The cap is the documented bound:
|
|
691
|
-
//
|
|
700
|
+
// The cap is still the documented bound: buffered turn data is never the
|
|
701
|
+
// release valve — a NEW session is refused (retryable backpressure) until
|
|
702
|
+
// extraction drains an entry, so the daemon cannot OOM on unbounded growth
|
|
703
|
+
// and no buffered turn is dropped (codex review).
|
|
692
704
|
const entries: NonNullable<BufferState["entries"]> = {};
|
|
693
705
|
for (let i = 0; i < 210; i += 1) {
|
|
694
706
|
const at = new Date(1_700_000_000_000 + i * 60_000).toISOString();
|
|
@@ -701,13 +713,17 @@ test("pruneEntries evicts oldest NON-empty session entries past the cap (#1908 r
|
|
|
701
713
|
const storage = new FakeStorage({ turns: [], lastExtractionAt: null, extractionCount: 0, entries });
|
|
702
714
|
const buffer = new SmartBuffer(parseConfig({ bufferSaveDebounceMs: 0, bufferMaxSessions: 200 }), storage as unknown as ConstructorParameters<typeof SmartBuffer>[1]);
|
|
703
715
|
await buffer.load();
|
|
704
|
-
await
|
|
716
|
+
await assert.rejects(
|
|
717
|
+
buffer.addTurn("session-newest", makeTurn("session-newest", "new turn")),
|
|
718
|
+
BufferAdmissionRefusedError,
|
|
719
|
+
"live admission refuses rather than evicting oldest non-empty backlog",
|
|
720
|
+
);
|
|
705
721
|
|
|
706
722
|
const savedKeys = Object.keys(storage.saved?.entries ?? {});
|
|
707
|
-
assert.ok(savedKeys.length <=
|
|
708
|
-
assert.
|
|
709
|
-
assert.equal(buffer.getTurns("session-000").length, 0, "oldest non-empty entry was evicted");
|
|
723
|
+
assert.ok(savedKeys.length <= 211, `entries bounded at MAX_BUFFER_ENTRY_COUNT plus default (got ${savedKeys.length})`);
|
|
724
|
+
assert.equal(buffer.getTurns("session-000").length, 1, "oldest non-empty entry keeps its turn for extraction");
|
|
710
725
|
assert.ok(buffer.getTurns("session-209").length > 0, "newest pre-existing entry survives");
|
|
726
|
+
assert.ok(!savedKeys.includes("session-newest"), "the refused turn was never buffered");
|
|
711
727
|
});
|
|
712
728
|
|
|
713
729
|
// ---------------------------------------------------------------------------
|
|
@@ -398,3 +398,44 @@ export function probeWithTimeout<T>(inflight: Promise<T>, timeoutMs: number): Pr
|
|
|
398
398
|
if (timer) clearTimeout(timer);
|
|
399
399
|
});
|
|
400
400
|
}
|
|
401
|
+
|
|
402
|
+
export const isAcceptedTurn = (t: { acceptedCapture?: boolean; writeNamespaceOverride?: string }): boolean =>
|
|
403
|
+
t.acceptedCapture === true || t.writeNamespaceOverride !== undefined;
|
|
404
|
+
|
|
405
|
+
export function entryActivityAt(entry: BufferEntryState): number {
|
|
406
|
+
const lastTurnAt = entry.turns.reduce((latest, turn) => {
|
|
407
|
+
const parsed = Date.parse(turn.timestamp);
|
|
408
|
+
return Number.isFinite(parsed) ? Math.max(latest, parsed) : latest;
|
|
409
|
+
}, -1);
|
|
410
|
+
const lastExtractionAt =
|
|
411
|
+
entry.lastExtractionAt === null ? Number.NaN : Date.parse(entry.lastExtractionAt);
|
|
412
|
+
return Math.max(
|
|
413
|
+
lastTurnAt,
|
|
414
|
+
Number.isFinite(lastExtractionAt) ? lastExtractionAt : -1,
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function entryHoldsAcceptedCapture(entry: BufferEntryState | undefined): boolean {
|
|
419
|
+
return (entry?.turns ?? []).some(isAcceptedTurn) || (entry?.retainedTurns ?? []).some(isAcceptedTurn);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export function normalizeBufferState(state: BufferState): BufferState {
|
|
423
|
+
// SAFETY: persisted-state contract — entries, when present, is a record of BufferEntryState.
|
|
424
|
+
const entries = Object.assign(
|
|
425
|
+
Object.create(null),
|
|
426
|
+
state.entries ?? {},
|
|
427
|
+
) as NonNullable<BufferState["entries"]>;
|
|
428
|
+
if (!entries.default) {
|
|
429
|
+
entries.default = {
|
|
430
|
+
turns: Array.isArray(state.turns) ? [...state.turns] : [],
|
|
431
|
+
lastExtractionAt: state.lastExtractionAt ?? null,
|
|
432
|
+
extractionCount: state.extractionCount ?? 0,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
turns: entries.default.turns,
|
|
437
|
+
lastExtractionAt: entries.default.lastExtractionAt,
|
|
438
|
+
extractionCount: entries.default.extractionCount,
|
|
439
|
+
entries,
|
|
440
|
+
};
|
|
441
|
+
}
|