@remnic/core 10.0.84 → 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 +90 -88
- package/dist/access-authorization-probe.js +91 -89
- package/dist/access-boundary.d.ts.map +1 -1
- package/dist/access-boundary.js +90 -88
- package/dist/access-cli.js +198 -194
- 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 +90 -88
- package/dist/access-http-briefing.js +91 -89
- package/dist/access-http-query.js +92 -90
- package/dist/access-http.d.ts +12 -0
- package/dist/access-http.d.ts.map +1 -1
- package/dist/access-http.js +119 -116
- package/dist/access-identity-continuity-surface.js +90 -88
- package/dist/access-lcm-surface.js +90 -88
- package/dist/access-mcp-frames.d.ts +29 -0
- package/dist/access-mcp-frames.d.ts.map +1 -0
- package/dist/access-mcp-frames.js +10 -0
- package/dist/access-mcp.d.ts +1 -1
- package/dist/access-mcp.d.ts.map +1 -1
- package/dist/access-mcp.js +105 -102
- package/dist/access-memory-search-fanout.js +2 -2
- package/dist/access-namespace-preflight.js +90 -88
- package/dist/access-observe-write-category.js +15 -14
- package/dist/access-observe-write-surface.d.ts.map +1 -1
- package/dist/access-observe-write-surface.js +90 -88
- package/dist/access-offline-manifest.js +10 -10
- package/dist/access-operations-batch.js +94 -92
- package/dist/access-operations.js +101 -99
- package/dist/access-recall-concurrency.js +90 -88
- package/dist/access-recall-diagnostics.js +2 -2
- package/dist/access-recall-response.js +90 -88
- package/dist/access-recall-surface.js +90 -88
- package/dist/access-schema.d.ts.map +1 -1
- package/dist/access-schema.js +10 -9
- package/dist/access-service.d.ts +3 -3
- package/dist/access-service.d.ts.map +1 -1
- package/dist/access-service.js +90 -88
- package/dist/access-surface-catalog.js +92 -90
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/access-token-capabilities.js +2 -2
- package/dist/active-recall.js +38 -38
- package/dist/adapters/index.js +7 -7
- package/dist/adapters/registry.js +3 -3
- package/dist/{auto-sync-C7E6G7Y2.js → auto-sync-3AZW7MXI.js} +6 -6
- 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 +25 -25
- 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-persistence.d.ts +8 -4
- package/dist/buffer-persistence.d.ts.map +1 -1
- package/dist/buffer-persistence.js +1 -1
- package/dist/buffer-turn-helpers.d.ts +12 -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 +21 -19
- package/dist/buffer.d.ts.map +1 -1
- package/dist/buffer.js +17 -13
- package/dist/calibration.js +3 -3
- package/dist/{capsule-crypto-YO5QJ6L3.js → capsule-crypto-7FJQINUR.js} +2 -2
- package/dist/capsule-gunzip-KGBDGF7P.js +11 -0
- package/dist/capsule-gunzip-KGBDGF7P.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/{capsule-merge-XVTQJJ6R.js → capsule-merge-2IIXFXN5.js} +5 -11
- package/dist/capsule-merge-2IIXFXN5.js.map +1 -0
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.js +27 -27
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory-graph.js +2 -2
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-MSIRVXQ3.js → chunk-2G4TV2ML.js} +12 -12
- package/dist/{chunk-B74NPUHT.js → chunk-2JG2NI7C.js} +54 -54
- package/dist/chunk-2JG2NI7C.js.map +1 -0
- package/dist/{chunk-IR4C3DP2.js → chunk-2K3AUDH7.js} +2 -2
- package/dist/{chunk-2IW5PT7U.js → chunk-2N4OUSIA.js} +2 -2
- package/dist/{chunk-BXLOS5AJ.js → chunk-2NLLXCJG.js} +2 -2
- package/dist/{chunk-QZSIDSB2.js → chunk-2NUDHVYM.js} +2 -2
- package/dist/{chunk-HPFVZPRH.js → chunk-2U4LMSEX.js} +3 -3
- package/dist/{chunk-WHQXWFH7.js → chunk-2XOFVTTT.js} +2 -2
- package/dist/{chunk-I6ORDT54.js → chunk-32CA7B6L.js} +2 -2
- package/dist/{chunk-T77AX6DG.js → chunk-32YX7EJ3.js} +4 -4
- package/dist/{chunk-HLKUZW42.js → chunk-3CTKEXXF.js} +144 -156
- package/dist/chunk-3CTKEXXF.js.map +1 -0
- package/dist/{chunk-HLWXCDUQ.js → chunk-3DAWHP5K.js} +5 -5
- package/dist/{chunk-ZJJFUEPO.js → chunk-3PJEDHLD.js} +2 -2
- package/dist/{chunk-PJYK7HH2.js → chunk-3UZ2VMKD.js} +2 -2
- package/dist/{chunk-IN4OFAHX.js → chunk-3Y5X6YFH.js} +2 -2
- package/dist/{chunk-YUTT4ZTI.js → chunk-45VUHDV2.js} +2 -2
- package/dist/{chunk-T6T2RMWW.js → chunk-4GMI5HFH.js} +12 -12
- package/dist/{chunk-YOEYL6CH.js → chunk-4LCVA2LW.js} +3 -3
- package/dist/{chunk-66JOLMWS.js → chunk-4X7HQKAG.js} +2 -2
- package/dist/{chunk-65SKM4BF.js → chunk-4XYAWK5H.js} +2 -2
- package/dist/{chunk-H7BZBSP6.js → chunk-52II7PB6.js} +4 -4
- package/dist/{chunk-TPX7XXY4.js → chunk-5SOSQTVE.js} +2 -2
- package/dist/{chunk-G3T6YMBO.js → chunk-64S6X4IU.js} +7 -7
- package/dist/{chunk-BK7AJUWB.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-LECSS7EE.js → chunk-73UZZFJC.js} +3 -3
- package/dist/{chunk-4EACCIPO.js → chunk-77PDJ4OE.js} +3 -3
- package/dist/{chunk-GQZSGXR6.js → chunk-7GSL4LBG.js} +2 -2
- package/dist/{chunk-B6SPPYGJ.js → chunk-7IWRLYFJ.js} +2 -2
- package/dist/{chunk-R45L4TV6.js → chunk-7LCGFUMB.js} +6 -6
- package/dist/{chunk-A7BM2JTH.js → chunk-AJHNXIQU.js} +2 -2
- package/dist/{chunk-2D7BTSDW.js → chunk-ANZZLRUX.js} +2 -2
- package/dist/{chunk-ZXF3O7P4.js → chunk-ARRTLPRC.js} +2 -2
- package/dist/{chunk-S5CQI2KF.js → chunk-B3GKQ6DU.js} +2 -2
- package/dist/{chunk-XRBHIR7Q.js → chunk-B56LORLY.js} +2 -2
- package/dist/{chunk-KO3YBHOV.js → chunk-BIVKPQ6Q.js} +3 -3
- package/dist/{chunk-QZBTIYKD.js → chunk-BLSHFIR6.js} +7 -6
- package/dist/chunk-BLSHFIR6.js.map +1 -0
- package/dist/{chunk-54FDG7K7.js → chunk-C2HORX76.js} +3 -3
- package/dist/{chunk-WAD32CZG.js → chunk-CC4V7HNP.js} +43 -30
- package/dist/chunk-CC4V7HNP.js.map +1 -0
- package/dist/{chunk-LGEH473S.js → chunk-CJCTKL6Y.js} +9 -1
- package/dist/chunk-CJCTKL6Y.js.map +1 -0
- package/dist/{chunk-E33UJHY3.js → chunk-CN7Q3T2J.js} +4 -4
- package/dist/{chunk-YUTG35M4.js → chunk-CYCEROPR.js} +3 -3
- package/dist/{chunk-TFLPDNQV.js → chunk-D5EV6Y2L.js} +3 -3
- package/dist/{chunk-BFBS7OFQ.js → chunk-DC55RIEZ.js} +4 -4
- package/dist/{chunk-B4ZU34JQ.js → chunk-DT7SNJH6.js} +3 -3
- package/dist/{chunk-VLMBSGQF.js → chunk-DZ7DPDVW.js} +10 -8
- package/dist/chunk-DZ7DPDVW.js.map +1 -0
- package/dist/{chunk-FVRTCYZ3.js → chunk-EA2LA5RI.js} +2 -2
- package/dist/{chunk-WBN66HPI.js → chunk-ELHJK7LM.js} +6 -6
- package/dist/{chunk-77R7BQZ2.js → chunk-EQ4U73UH.js} +77 -24
- package/dist/chunk-EQ4U73UH.js.map +1 -0
- package/dist/{chunk-3JPTUCD7.js → chunk-EUBDYOLD.js} +32 -23
- package/dist/chunk-EUBDYOLD.js.map +1 -0
- package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
- package/dist/{chunk-YAUHSNPB.js → chunk-F74G4KIH.js} +2 -2
- package/dist/{chunk-MPGLM2VU.js → chunk-FF3EFLUT.js} +7 -5
- package/dist/chunk-FF3EFLUT.js.map +1 -0
- package/dist/{chunk-BV2BRHD4.js → chunk-FHGWKODN.js} +2 -2
- 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-KESJ5XX3.js → chunk-G54BOKZ6.js} +2 -2
- package/dist/{chunk-YK7GOPIE.js → chunk-G67EUQAB.js} +9 -7
- package/dist/chunk-G67EUQAB.js.map +1 -0
- package/dist/{chunk-K36RMINQ.js → chunk-GOZXQNIM.js} +3 -3
- package/dist/{chunk-OQSSQOKQ.js → chunk-GULUWOA5.js} +2 -2
- package/dist/{chunk-K6RTQZA4.js → chunk-HFUKXOUW.js} +2 -2
- package/dist/{chunk-WV254W4V.js → chunk-HLN2HOXX.js} +2 -2
- package/dist/{chunk-FUOAIJKQ.js → chunk-IBDWYRDR.js} +3 -3
- package/dist/{chunk-Z3DDQXM4.js → chunk-IBGU6AN6.js} +45 -45
- package/dist/chunk-IBGU6AN6.js.map +1 -0
- package/dist/{chunk-AJG2TAMR.js → chunk-IEEV27BC.js} +2 -2
- package/dist/chunk-IGLJNHOX.js +62 -0
- package/dist/chunk-IGLJNHOX.js.map +1 -0
- package/dist/{chunk-LTDLJCMQ.js → chunk-IJ3OEECW.js} +4 -4
- package/dist/{chunk-BK4FBHZ3.js → chunk-IPD5GC7C.js} +3 -3
- package/dist/{chunk-KFLD7ZZG.js → chunk-IUKYFTGW.js} +3 -3
- package/dist/{chunk-GCAQ3VUJ.js → chunk-JM2YZKCQ.js} +2 -2
- package/dist/{chunk-BNQJKWWZ.js → chunk-K3FH3LAQ.js} +4 -4
- package/dist/{chunk-LU66I56Q.js → chunk-KVSOEVAF.js} +61 -12
- 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-RZH5RVJC.js → chunk-MTTYIXSU.js} +2 -2
- package/dist/{chunk-XHGYZRVW.js → chunk-NIVKG3DS.js} +5 -5
- package/dist/{chunk-VZELUMKK.js → chunk-NQP4ZYGG.js} +2 -2
- package/dist/{chunk-7PMOCQOL.js → chunk-NT7F7WJQ.js} +192 -63
- package/dist/chunk-NT7F7WJQ.js.map +1 -0
- package/dist/{chunk-YO2JEX2Z.js → chunk-OGBQMY46.js} +2 -2
- package/dist/{chunk-TLHF75OR.js → chunk-OL553AWT.js} +12 -12
- package/dist/{chunk-CLVILSBR.js → chunk-OZNBZJ3S.js} +22 -22
- package/dist/{chunk-VUROWXFL.js → chunk-P452SEKO.js} +2 -2
- package/dist/{chunk-JX3E4XCW.js → chunk-PDOOUXCS.js} +2 -2
- package/dist/{chunk-6QYPL6NI.js → chunk-PKZ274PI.js} +3 -3
- package/dist/{chunk-KJCL2FZO.js → chunk-QD5R6U5J.js} +2 -2
- package/dist/{chunk-JRVMCZE3.js → chunk-QEYTRFR3.js} +2 -2
- package/dist/{chunk-VKDAJJQP.js → chunk-QEZ3DW5O.js} +11 -11
- package/dist/{chunk-WYPCJK6K.js → chunk-QKERFEJS.js} +2 -2
- package/dist/{chunk-BDN3TDGV.js → chunk-QKES7GU6.js} +2 -2
- package/dist/{chunk-PCQWKMOL.js → chunk-QNSZYTSG.js} +2 -2
- package/dist/chunk-QV2VZKD5.js +31 -0
- package/dist/chunk-QV2VZKD5.js.map +1 -0
- package/dist/{chunk-IZJQNDGI.js → chunk-R6HMAIKM.js} +2 -2
- package/dist/{chunk-CIIHIAJ2.js → chunk-ROGO7FRA.js} +6 -5
- package/dist/{chunk-CIIHIAJ2.js.map → chunk-ROGO7FRA.js.map} +1 -1
- package/dist/{chunk-NUSAAN75.js → chunk-ROKDHO74.js} +2 -2
- package/dist/{chunk-D5SSBCYY.js → chunk-RWGRSKN5.js} +7 -5
- package/dist/chunk-RWGRSKN5.js.map +1 -0
- package/dist/{chunk-FF6N7BXC.js → chunk-S45INSV3.js} +4 -4
- package/dist/{chunk-2RUTN7PR.js → chunk-S7GJNA3K.js} +2 -2
- package/dist/{chunk-XT45WCKX.js → chunk-SYKJBS7S.js} +10 -10
- package/dist/{chunk-5BE3FLKL.js → chunk-TAUHAP5J.js} +4 -4
- package/dist/{chunk-RJWL3RHB.js → chunk-TFEHSPGQ.js} +2 -2
- package/dist/{chunk-XYP5WBDO.js → chunk-TQ4SCPGQ.js} +76 -60
- package/dist/chunk-TQ4SCPGQ.js.map +1 -0
- package/dist/{chunk-K3B72DKJ.js → chunk-TRMZHGIX.js} +8 -8
- package/dist/{chunk-WBDIZWJQ.js → chunk-TSS3D7ZY.js} +2 -2
- package/dist/{chunk-6LCWKS62.js → chunk-UAUV3BWS.js} +104 -121
- package/dist/chunk-UAUV3BWS.js.map +1 -0
- package/dist/{chunk-KQUSP6LB.js → chunk-UDKWFDV2.js} +1 -1
- package/dist/chunk-UDKWFDV2.js.map +1 -0
- package/dist/{chunk-HQPFACXO.js → chunk-UH3FPX3L.js} +2 -2
- package/dist/{chunk-GGJFRVRD.js → chunk-US7KPQA5.js} +2 -2
- package/dist/{chunk-FJ2VJTGA.js → chunk-UWRHHRDK.js} +3 -3
- package/dist/{chunk-4KLQI3L2.js → chunk-UXXK523X.js} +40 -2
- package/dist/chunk-UXXK523X.js.map +1 -0
- package/dist/{chunk-SDESKFKT.js → chunk-UYHDMCY2.js} +2 -2
- package/dist/{chunk-3T3VHXUH.js → chunk-VKILERSL.js} +2 -2
- package/dist/{chunk-2TGIKMMZ.js → chunk-WXDTOE6F.js} +2 -2
- package/dist/{chunk-KQLBXUUQ.js → chunk-WYG5UNDY.js} +2 -2
- package/dist/{chunk-DQEMWVMT.js → chunk-X7Y7WX73.js} +1 -1
- package/dist/{chunk-MPDLLLOG.js → chunk-XO23OKMK.js} +9 -5
- package/dist/chunk-XO23OKMK.js.map +1 -0
- package/dist/{chunk-WC475JOG.js → chunk-XU37EJTW.js} +24 -24
- package/dist/chunk-XU37EJTW.js.map +1 -0
- package/dist/{chunk-FZVROZKC.js → chunk-XVZA6VXC.js} +4 -4
- package/dist/{chunk-QA3Y2O6H.js → chunk-YAVIBVCU.js} +2 -2
- package/dist/{chunk-2XJZLSXE.js → chunk-YL4UKMD7.js} +124 -58
- package/dist/chunk-YL4UKMD7.js.map +1 -0
- package/dist/{chunk-VPEIWMUT.js → chunk-YLTU2DC4.js} +2 -2
- package/dist/{chunk-HWHNQVRV.js → chunk-YMJRLT7S.js} +28 -8
- package/dist/chunk-YMJRLT7S.js.map +1 -0
- package/dist/{chunk-EBOHFZI4.js → chunk-YRZPBN46.js} +3 -3
- package/dist/{chunk-FMFCQRDM.js → chunk-ZANNIXXE.js} +3 -3
- package/dist/{chunk-HQGATKG2.js → chunk-ZBFLV657.js} +3 -3
- package/dist/{chunk-6L63IN3M.js → chunk-ZOJUWVAU.js} +1 -1
- package/dist/cli.js +144 -141
- package/dist/coding/export-okf-codegraph.js +24 -24
- package/dist/coding/pre-action-gate.js +2 -2
- package/dist/compounding/engine.js +26 -26
- package/dist/config.js +35 -35
- package/dist/connectors/codex-materialize-runner.js +24 -24
- package/dist/connectors/index.js +28 -28
- package/dist/contradiction/index.js +4 -4
- package/dist/convergence-refresh.js +26 -26
- package/dist/corpus-watermark.js +28 -28
- package/dist/dashboard-runtime.js +2 -2
- package/dist/deep-recall-config.js +2 -2
- package/dist/deep-recall-http-glue.js +12 -12
- package/dist/entity-retrieval.js +25 -25
- package/dist/event-order-recall.js +2 -2
- package/dist/explicit-capture.js +6 -6
- package/dist/external-wiki-access.js +91 -89
- package/dist/extraction-evidence-class.js +2 -2
- 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 +5 -5
- package/dist/extraction-span-materialize.js +2 -2
- package/dist/extraction.d.ts.map +1 -1
- package/dist/extraction.js +43 -43
- package/dist/{first-start-migration-QVD22WWS.js → first-start-migration-A6PHIBUQ.js} +4 -4
- package/dist/{graph-edge-decay-CXTNJ7IQ.js → graph-edge-decay-EKWXRBWF.js} +5 -5
- package/dist/graph-edge-decay-EKWXRBWF.js.map +1 -0
- package/dist/graph-jsonl.js +3 -3
- package/dist/graph-snapshot.js +3 -3
- package/dist/graph.js +2 -2
- package/dist/harmonic-retrieval.js +24 -24
- package/dist/index.js +255 -251
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +5 -5
- package/dist/local-llm.js +3 -3
- package/dist/location/index.js +28 -28
- package/dist/maintenance/memory-governance.js +24 -24
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +24 -24
- package/dist/maintenance/rebuild-memory-projection-runner.js +26 -26
- package/dist/maintenance/rebuild-memory-projection.js +25 -25
- package/dist/memory-browse-http-glue.js +2 -2
- package/dist/memory-browse-operations.js +91 -89
- package/dist/memory-browse.js +5 -5
- package/dist/memory-subject.js +2 -2
- package/dist/namespaces/migrate.js +25 -25
- package/dist/namespaces/search.d.ts +9 -0
- package/dist/namespaces/search.d.ts.map +1 -1
- package/dist/namespaces/search.js +13 -7
- package/dist/namespaces/storage.js +24 -24
- 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 +31 -31
- package/dist/operator-doctor-replica.js +4 -4
- package/dist/operator-toolkit.js +49 -49
- package/dist/orchestration/backlog-scheduler.d.ts +3 -2
- package/dist/orchestration/backlog-scheduler.d.ts.map +1 -1
- package/dist/orchestration/backlog-scheduler.js +209 -205
- package/dist/orchestration/backlog-scheduler.js.map +1 -1
- package/dist/orchestration/buffer-drain-runner.d.ts +6 -1
- package/dist/orchestration/buffer-drain-runner.d.ts.map +1 -1
- package/dist/orchestration/buffer-drain-runner.js +199 -195
- package/dist/orchestration/extraction-persist.d.ts.map +1 -1
- package/dist/orchestration/fact-dedup-window.d.ts +1 -4
- package/dist/orchestration/fact-dedup-window.d.ts.map +1 -1
- package/dist/orchestration/maintenance.js +41 -41
- package/dist/orchestration/turn-ingestion.d.ts +2 -9
- package/dist/orchestration/turn-ingestion.d.ts.map +1 -1
- package/dist/orchestrator.js +198 -194
- package/dist/provenance.js +2 -2
- package/dist/qmd.js +4 -4
- package/dist/recall-navigation-http-glue.js +2 -2
- package/dist/recall-navigation-operations.js +91 -89
- package/dist/recall-navigation.js +90 -88
- package/dist/recall-state-view-admission.js +5 -5
- package/dist/recall-why-service.js +5 -5
- package/dist/reconcile/manifest.js +2 -2
- package/dist/replica-divergence.js +3 -3
- package/dist/{resolution-BNWE5Y25.js → resolution-23ZBDM6A.js} +5 -5
- package/dist/resolution-23ZBDM6A.js.map +1 -0
- package/dist/resume-bundles.js +36 -36
- package/dist/salvage-envelope.js +4 -4
- package/dist/search/factory.js +6 -6
- package/dist/search/index.js +12 -12
- package/dist/semantic-consolidation.js +25 -25
- package/dist/semantic-rule-promotion.js +24 -24
- package/dist/semantic-rule-verifier.js +24 -24
- 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 +36 -36
- package/dist/standup/build.js +25 -25
- package/dist/storage/buffer-drain-claims.d.ts +18 -6
- package/dist/storage/buffer-drain-claims.d.ts.map +1 -1
- package/dist/storage/buffer-drain-claims.js +2 -2
- package/dist/storage/buffer-session-files.js +2 -2
- package/dist/storage/content-hash-index.d.ts +8 -0
- package/dist/storage/content-hash-index.d.ts.map +1 -1
- package/dist/storage.d.ts +10 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +23 -23
- package/dist/subject-config.js +3 -3
- package/dist/summarizer.js +4 -4
- package/dist/support-passport/index.js +93 -91
- package/dist/tokens.js +3 -3
- package/dist/transfer/autodetect.js +1 -1
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +3 -3
- package/dist/transfer/capsule-gunzip.d.ts +14 -0
- package/dist/transfer/capsule-gunzip.d.ts.map +1 -0
- package/dist/transfer/capsule-import.d.ts.map +1 -1
- package/dist/transfer/capsule-import.js +3 -2
- package/dist/transfer/capsule-merge.d.ts.map +1 -1
- package/dist/transfer/export-okf.js +24 -24
- package/dist/verified-recall.js +24 -24
- package/dist/who-knows-cli.js +26 -26
- package/dist/who-knows.js +25 -25
- package/dist/write-envelope.js +3 -3
- 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-frames.test.ts +47 -0
- package/src/access-mcp-frames.ts +74 -0
- package/src/access-mcp.test.ts +210 -0
- package/src/access-mcp.ts +47 -37
- package/src/access-observe-replay-durability.test.ts +40 -4
- package/src/access-observe-write-surface.ts +4 -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/backlog-scheduler.test.ts +443 -3
- 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-persistence.ts +15 -6
- package/src/buffer-session.test.ts +32 -16
- package/src/buffer-turn-helpers.ts +47 -0
- package/src/buffer-types.ts +28 -1
- package/src/buffer.ts +83 -91
- package/src/capsule-import-gate.ts +109 -0
- package/src/cli.ts +4 -4
- package/src/extraction-quote-grounding.test.ts +125 -0
- package/src/extraction-quote-grounding.ts +99 -25
- 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/fact-dedup-window.test.ts +103 -20
- package/src/namespaces/search.test.ts +480 -1
- package/src/namespaces/search.ts +93 -10
- package/src/orchestration/backlog-scheduler.ts +13 -12
- package/src/orchestration/buffer-drain-runner.ts +40 -16
- package/src/orchestration/extraction-persist.ts +2 -10
- package/src/orchestration/fact-dedup-window.ts +5 -12
- 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/src/storage/buffer-drain-claims.test.ts +330 -6
- package/src/storage/buffer-drain-claims.ts +226 -80
- package/src/storage/buffer-session-files.ts +2 -2
- package/src/storage/content-hash-index.ts +9 -0
- package/src/storage.ts +15 -0
- package/src/transfer/capsule-gunzip.ts +39 -0
- package/src/transfer/capsule-import.test.ts +73 -0
- package/src/transfer/capsule-import.ts +2 -2
- package/src/transfer/capsule-merge.test.ts +34 -0
- package/src/transfer/capsule-merge.ts +2 -10
- package/dist/capsule-merge-XVTQJJ6R.js.map +0 -1
- package/dist/chunk-2XJZLSXE.js.map +0 -1
- package/dist/chunk-3JPTUCD7.js.map +0 -1
- package/dist/chunk-4KLQI3L2.js.map +0 -1
- package/dist/chunk-6LCWKS62.js.map +0 -1
- package/dist/chunk-77R7BQZ2.js.map +0 -1
- package/dist/chunk-7PMOCQOL.js.map +0 -1
- package/dist/chunk-B74NPUHT.js.map +0 -1
- package/dist/chunk-D5SSBCYY.js.map +0 -1
- package/dist/chunk-HLKUZW42.js.map +0 -1
- package/dist/chunk-HWHNQVRV.js.map +0 -1
- package/dist/chunk-KASJJGRG.js.map +0 -1
- package/dist/chunk-KQUSP6LB.js.map +0 -1
- package/dist/chunk-LGEH473S.js.map +0 -1
- package/dist/chunk-LU66I56Q.js.map +0 -1
- package/dist/chunk-MPDLLLOG.js.map +0 -1
- package/dist/chunk-MPGLM2VU.js.map +0 -1
- package/dist/chunk-QZBTIYKD.js.map +0 -1
- package/dist/chunk-VLMBSGQF.js.map +0 -1
- package/dist/chunk-WAD32CZG.js.map +0 -1
- package/dist/chunk-WC475JOG.js.map +0 -1
- package/dist/chunk-X2KO4FK5.js.map +0 -1
- package/dist/chunk-XYP5WBDO.js.map +0 -1
- package/dist/chunk-YK7GOPIE.js.map +0 -1
- package/dist/chunk-Z3DDQXM4.js.map +0 -1
- /package/dist/{capsule-crypto-YO5QJ6L3.js.map → access-mcp-frames.js.map} +0 -0
- /package/dist/{auto-sync-C7E6G7Y2.js.map → auto-sync-3AZW7MXI.js.map} +0 -0
- /package/dist/{graph-edge-decay-CXTNJ7IQ.js.map → buffer-admission.js.map} +0 -0
- /package/dist/{resolution-BNWE5Y25.js.map → capsule-crypto-7FJQINUR.js.map} +0 -0
- /package/dist/{chunk-MSIRVXQ3.js.map → chunk-2G4TV2ML.js.map} +0 -0
- /package/dist/{chunk-IR4C3DP2.js.map → chunk-2K3AUDH7.js.map} +0 -0
- /package/dist/{chunk-2IW5PT7U.js.map → chunk-2N4OUSIA.js.map} +0 -0
- /package/dist/{chunk-BXLOS5AJ.js.map → chunk-2NLLXCJG.js.map} +0 -0
- /package/dist/{chunk-QZSIDSB2.js.map → chunk-2NUDHVYM.js.map} +0 -0
- /package/dist/{chunk-HPFVZPRH.js.map → chunk-2U4LMSEX.js.map} +0 -0
- /package/dist/{chunk-WHQXWFH7.js.map → chunk-2XOFVTTT.js.map} +0 -0
- /package/dist/{chunk-I6ORDT54.js.map → chunk-32CA7B6L.js.map} +0 -0
- /package/dist/{chunk-T77AX6DG.js.map → chunk-32YX7EJ3.js.map} +0 -0
- /package/dist/{chunk-HLWXCDUQ.js.map → chunk-3DAWHP5K.js.map} +0 -0
- /package/dist/{chunk-ZJJFUEPO.js.map → chunk-3PJEDHLD.js.map} +0 -0
- /package/dist/{chunk-PJYK7HH2.js.map → chunk-3UZ2VMKD.js.map} +0 -0
- /package/dist/{chunk-IN4OFAHX.js.map → chunk-3Y5X6YFH.js.map} +0 -0
- /package/dist/{chunk-YUTT4ZTI.js.map → chunk-45VUHDV2.js.map} +0 -0
- /package/dist/{chunk-T6T2RMWW.js.map → chunk-4GMI5HFH.js.map} +0 -0
- /package/dist/{chunk-YOEYL6CH.js.map → chunk-4LCVA2LW.js.map} +0 -0
- /package/dist/{chunk-66JOLMWS.js.map → chunk-4X7HQKAG.js.map} +0 -0
- /package/dist/{chunk-65SKM4BF.js.map → chunk-4XYAWK5H.js.map} +0 -0
- /package/dist/{chunk-H7BZBSP6.js.map → chunk-52II7PB6.js.map} +0 -0
- /package/dist/{chunk-TPX7XXY4.js.map → chunk-5SOSQTVE.js.map} +0 -0
- /package/dist/{chunk-G3T6YMBO.js.map → chunk-64S6X4IU.js.map} +0 -0
- /package/dist/{chunk-BK7AJUWB.js.map → chunk-6BCWFCDG.js.map} +0 -0
- /package/dist/{chunk-V27FK6D6.js.map → chunk-6XTUZKCZ.js.map} +0 -0
- /package/dist/{chunk-LECSS7EE.js.map → chunk-73UZZFJC.js.map} +0 -0
- /package/dist/{chunk-4EACCIPO.js.map → chunk-77PDJ4OE.js.map} +0 -0
- /package/dist/{chunk-GQZSGXR6.js.map → chunk-7GSL4LBG.js.map} +0 -0
- /package/dist/{chunk-B6SPPYGJ.js.map → chunk-7IWRLYFJ.js.map} +0 -0
- /package/dist/{chunk-R45L4TV6.js.map → chunk-7LCGFUMB.js.map} +0 -0
- /package/dist/{chunk-A7BM2JTH.js.map → chunk-AJHNXIQU.js.map} +0 -0
- /package/dist/{chunk-2D7BTSDW.js.map → chunk-ANZZLRUX.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-XRBHIR7Q.js.map → chunk-B56LORLY.js.map} +0 -0
- /package/dist/{chunk-KO3YBHOV.js.map → chunk-BIVKPQ6Q.js.map} +0 -0
- /package/dist/{chunk-54FDG7K7.js.map → chunk-C2HORX76.js.map} +0 -0
- /package/dist/{chunk-E33UJHY3.js.map → chunk-CN7Q3T2J.js.map} +0 -0
- /package/dist/{chunk-YUTG35M4.js.map → chunk-CYCEROPR.js.map} +0 -0
- /package/dist/{chunk-TFLPDNQV.js.map → chunk-D5EV6Y2L.js.map} +0 -0
- /package/dist/{chunk-BFBS7OFQ.js.map → chunk-DC55RIEZ.js.map} +0 -0
- /package/dist/{chunk-B4ZU34JQ.js.map → chunk-DT7SNJH6.js.map} +0 -0
- /package/dist/{chunk-FVRTCYZ3.js.map → chunk-EA2LA5RI.js.map} +0 -0
- /package/dist/{chunk-WBN66HPI.js.map → chunk-ELHJK7LM.js.map} +0 -0
- /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
- /package/dist/{chunk-YAUHSNPB.js.map → chunk-F74G4KIH.js.map} +0 -0
- /package/dist/{chunk-BV2BRHD4.js.map → chunk-FHGWKODN.js.map} +0 -0
- /package/dist/{chunk-KESJ5XX3.js.map → chunk-G54BOKZ6.js.map} +0 -0
- /package/dist/{chunk-K36RMINQ.js.map → chunk-GOZXQNIM.js.map} +0 -0
- /package/dist/{chunk-OQSSQOKQ.js.map → chunk-GULUWOA5.js.map} +0 -0
- /package/dist/{chunk-K6RTQZA4.js.map → chunk-HFUKXOUW.js.map} +0 -0
- /package/dist/{chunk-WV254W4V.js.map → chunk-HLN2HOXX.js.map} +0 -0
- /package/dist/{chunk-FUOAIJKQ.js.map → chunk-IBDWYRDR.js.map} +0 -0
- /package/dist/{chunk-AJG2TAMR.js.map → chunk-IEEV27BC.js.map} +0 -0
- /package/dist/{chunk-LTDLJCMQ.js.map → chunk-IJ3OEECW.js.map} +0 -0
- /package/dist/{chunk-BK4FBHZ3.js.map → chunk-IPD5GC7C.js.map} +0 -0
- /package/dist/{chunk-KFLD7ZZG.js.map → chunk-IUKYFTGW.js.map} +0 -0
- /package/dist/{chunk-GCAQ3VUJ.js.map → chunk-JM2YZKCQ.js.map} +0 -0
- /package/dist/{chunk-BNQJKWWZ.js.map → chunk-K3FH3LAQ.js.map} +0 -0
- /package/dist/{chunk-RZH5RVJC.js.map → chunk-MTTYIXSU.js.map} +0 -0
- /package/dist/{chunk-XHGYZRVW.js.map → chunk-NIVKG3DS.js.map} +0 -0
- /package/dist/{chunk-VZELUMKK.js.map → chunk-NQP4ZYGG.js.map} +0 -0
- /package/dist/{chunk-YO2JEX2Z.js.map → chunk-OGBQMY46.js.map} +0 -0
- /package/dist/{chunk-TLHF75OR.js.map → chunk-OL553AWT.js.map} +0 -0
- /package/dist/{chunk-CLVILSBR.js.map → chunk-OZNBZJ3S.js.map} +0 -0
- /package/dist/{chunk-VUROWXFL.js.map → chunk-P452SEKO.js.map} +0 -0
- /package/dist/{chunk-JX3E4XCW.js.map → chunk-PDOOUXCS.js.map} +0 -0
- /package/dist/{chunk-6QYPL6NI.js.map → chunk-PKZ274PI.js.map} +0 -0
- /package/dist/{chunk-KJCL2FZO.js.map → chunk-QD5R6U5J.js.map} +0 -0
- /package/dist/{chunk-JRVMCZE3.js.map → chunk-QEYTRFR3.js.map} +0 -0
- /package/dist/{chunk-VKDAJJQP.js.map → chunk-QEZ3DW5O.js.map} +0 -0
- /package/dist/{chunk-WYPCJK6K.js.map → chunk-QKERFEJS.js.map} +0 -0
- /package/dist/{chunk-BDN3TDGV.js.map → chunk-QKES7GU6.js.map} +0 -0
- /package/dist/{chunk-PCQWKMOL.js.map → chunk-QNSZYTSG.js.map} +0 -0
- /package/dist/{chunk-IZJQNDGI.js.map → chunk-R6HMAIKM.js.map} +0 -0
- /package/dist/{chunk-NUSAAN75.js.map → chunk-ROKDHO74.js.map} +0 -0
- /package/dist/{chunk-FF6N7BXC.js.map → chunk-S45INSV3.js.map} +0 -0
- /package/dist/{chunk-2RUTN7PR.js.map → chunk-S7GJNA3K.js.map} +0 -0
- /package/dist/{chunk-XT45WCKX.js.map → chunk-SYKJBS7S.js.map} +0 -0
- /package/dist/{chunk-5BE3FLKL.js.map → chunk-TAUHAP5J.js.map} +0 -0
- /package/dist/{chunk-RJWL3RHB.js.map → chunk-TFEHSPGQ.js.map} +0 -0
- /package/dist/{chunk-K3B72DKJ.js.map → chunk-TRMZHGIX.js.map} +0 -0
- /package/dist/{chunk-WBDIZWJQ.js.map → chunk-TSS3D7ZY.js.map} +0 -0
- /package/dist/{chunk-HQPFACXO.js.map → chunk-UH3FPX3L.js.map} +0 -0
- /package/dist/{chunk-GGJFRVRD.js.map → chunk-US7KPQA5.js.map} +0 -0
- /package/dist/{chunk-FJ2VJTGA.js.map → chunk-UWRHHRDK.js.map} +0 -0
- /package/dist/{chunk-SDESKFKT.js.map → chunk-UYHDMCY2.js.map} +0 -0
- /package/dist/{chunk-3T3VHXUH.js.map → chunk-VKILERSL.js.map} +0 -0
- /package/dist/{chunk-2TGIKMMZ.js.map → chunk-WXDTOE6F.js.map} +0 -0
- /package/dist/{chunk-KQLBXUUQ.js.map → chunk-WYG5UNDY.js.map} +0 -0
- /package/dist/{chunk-DQEMWVMT.js.map → chunk-X7Y7WX73.js.map} +0 -0
- /package/dist/{chunk-FZVROZKC.js.map → chunk-XVZA6VXC.js.map} +0 -0
- /package/dist/{chunk-QA3Y2O6H.js.map → chunk-YAVIBVCU.js.map} +0 -0
- /package/dist/{chunk-VPEIWMUT.js.map → chunk-YLTU2DC4.js.map} +0 -0
- /package/dist/{chunk-EBOHFZI4.js.map → chunk-YRZPBN46.js.map} +0 -0
- /package/dist/{chunk-FMFCQRDM.js.map → chunk-ZANNIXXE.js.map} +0 -0
- /package/dist/{chunk-HQGATKG2.js.map → chunk-ZBFLV657.js.map} +0 -0
- /package/dist/{chunk-6L63IN3M.js.map → chunk-ZOJUWVAU.js.map} +0 -0
- /package/dist/{first-start-migration-QVD22WWS.js.map → first-start-migration-A6PHIBUQ.js.map} +0 -0
package/src/access-http.test.ts
CHANGED
|
@@ -13,8 +13,8 @@ import {
|
|
|
13
13
|
import { peerManifestRevision } from "./reconcile/manifest.js";
|
|
14
14
|
import {
|
|
15
15
|
EngramAccessInputError,
|
|
16
|
+
EngramAccessService,
|
|
16
17
|
type EngramAccessRecallRequest,
|
|
17
|
-
type EngramAccessService,
|
|
18
18
|
type EngramAccessBriefingRequest,
|
|
19
19
|
} from "./access-service.js";
|
|
20
20
|
import {
|
|
@@ -815,6 +815,48 @@ test("HTTP coding-context endpoint accepts projectTag shorthand", async () => {
|
|
|
815
815
|
}
|
|
816
816
|
});
|
|
817
817
|
|
|
818
|
+
test("HTTP observe answers 503 buffer_admission_refused when durable capture hits the entry cap", async () => {
|
|
819
|
+
const refusal = new Error("buffer at session-entry cap");
|
|
820
|
+
refusal.name = "ReplayAdmissionRefusedError";
|
|
821
|
+
// SAFETY: this fixture drives only the observe route; the remaining service members are unreachable.
|
|
822
|
+
const service = Object.assign({} as EngramAccessService, {
|
|
823
|
+
observe: async () => {
|
|
824
|
+
throw refusal;
|
|
825
|
+
},
|
|
826
|
+
});
|
|
827
|
+
const server = new EngramAccessHttpServer({
|
|
828
|
+
service,
|
|
829
|
+
port: 0,
|
|
830
|
+
authToken: "test-token",
|
|
831
|
+
adminConsoleEnabled: false,
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
const status = await server.start();
|
|
835
|
+
try {
|
|
836
|
+
const response = await fetch(
|
|
837
|
+
`http://127.0.0.1:${status.port}/engram/v1/observe`,
|
|
838
|
+
{
|
|
839
|
+
method: "POST",
|
|
840
|
+
headers: {
|
|
841
|
+
authorization: "Bearer test-token",
|
|
842
|
+
"content-type": "application/json",
|
|
843
|
+
},
|
|
844
|
+
body: JSON.stringify({
|
|
845
|
+
sessionKey: "capacity-test-session",
|
|
846
|
+
messages: [{ role: "user", content: "retry me" }],
|
|
847
|
+
}),
|
|
848
|
+
},
|
|
849
|
+
);
|
|
850
|
+
assert.equal(response.status, 503, "admission backpressure is retryable, not a server fault");
|
|
851
|
+
assert.deepEqual(await response.json(), {
|
|
852
|
+
error: "buffer at session-entry cap",
|
|
853
|
+
code: "buffer_admission_refused",
|
|
854
|
+
});
|
|
855
|
+
} finally {
|
|
856
|
+
await server.stop();
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
|
|
818
860
|
test("HTTP briefing endpoint dispatches through the briefing operation", async () => {
|
|
819
861
|
const calls: unknown[] = [];
|
|
820
862
|
const service = {
|
|
@@ -2996,11 +3038,11 @@ test("HTTP scoped-token namespace allow-list is enforced on every namespace rout
|
|
|
2996
3038
|
"scoped memory_get: the allowed namespace reaches the service",
|
|
2997
3039
|
);
|
|
2998
3040
|
|
|
2999
|
-
// ── a
|
|
3041
|
+
// ── a global-store route is fleet-wide: a namespace-scoped token is denied (issue #3225 B3) ──
|
|
3000
3042
|
assert.equal(
|
|
3001
3043
|
(await request("scoped-ns-a", "/engram/v1/peers")).status,
|
|
3002
|
-
|
|
3003
|
-
"a
|
|
3044
|
+
403,
|
|
3045
|
+
"peer routes touch global stores, so a namespace-scoped token is denied",
|
|
3004
3046
|
);
|
|
3005
3047
|
|
|
3006
3048
|
// ── unrestricted token: omitting namespace still reaches the default tenant ──
|
|
@@ -6515,3 +6557,206 @@ test("a budget above the backend cap still gets the rows it asked for", async ()
|
|
|
6515
6557
|
"the excluded hit was replaced, not subtracted",
|
|
6516
6558
|
);
|
|
6517
6559
|
});
|
|
6560
|
+
|
|
6561
|
+
/**
|
|
6562
|
+
* Prototype-backed service stub for the write-quota tests: the constructor's
|
|
6563
|
+
* surface getters read only `orchestrator.config`, and each test overrides the
|
|
6564
|
+
* single member its route dispatches.
|
|
6565
|
+
*/
|
|
6566
|
+
function quotaStubService(): EngramAccessService {
|
|
6567
|
+
// SAFETY: the prototype supplies the untouched service surface; each quota test overrides only the member its route dispatches.
|
|
6568
|
+
const service = Object.create(EngramAccessService.prototype) as EngramAccessService;
|
|
6569
|
+
// SAFETY: startup reads only orchestrator.config from the service; an empty config keeps every surface flag false.
|
|
6570
|
+
Reflect.set(service, "orchestrator", { config: {} });
|
|
6571
|
+
return service;
|
|
6572
|
+
}
|
|
6573
|
+
|
|
6574
|
+
test("concurrent distinct-payload writes cannot overshoot the write quota (issue #3225 C4)", async () => {
|
|
6575
|
+
// Checking hasCapacity before the operation and calling record after it
|
|
6576
|
+
// left a window where N in-flight writes all pass against the same
|
|
6577
|
+
// committed count. Admission must reserve a slot, so only LIMIT of TOTAL
|
|
6578
|
+
// overlapping writes proceed and the rest get 429.
|
|
6579
|
+
const LIMIT = 2;
|
|
6580
|
+
const TOTAL = 6;
|
|
6581
|
+
let dispatches = 0;
|
|
6582
|
+
const firstDispatched = Promise.withResolvers<void>();
|
|
6583
|
+
const releaseWrites = Promise.withResolvers<void>();
|
|
6584
|
+
const service = quotaStubService();
|
|
6585
|
+
service.capsuleExport = async (request) => {
|
|
6586
|
+
dispatches += 1;
|
|
6587
|
+
if (dispatches === 1) firstDispatched.resolve();
|
|
6588
|
+
await releaseWrites.promise;
|
|
6589
|
+
// SAFETY: the route serializes this stub result into the HTTP response; the test asserts statuses, not this shape.
|
|
6590
|
+
return { ok: true, name: request.name } as never;
|
|
6591
|
+
};
|
|
6592
|
+
const server = new EngramAccessHttpServer({
|
|
6593
|
+
service,
|
|
6594
|
+
port: 0,
|
|
6595
|
+
authToken: "test-token",
|
|
6596
|
+
adminConsoleEnabled: false,
|
|
6597
|
+
writeRateLimitMaxRequests: LIMIT,
|
|
6598
|
+
});
|
|
6599
|
+
const status = await server.start();
|
|
6600
|
+
const post = (index: number) =>
|
|
6601
|
+
fetch(`http://127.0.0.1:${status.port}/engram/v1/capsules/export`, {
|
|
6602
|
+
method: "POST",
|
|
6603
|
+
headers: {
|
|
6604
|
+
authorization: "Bearer test-token",
|
|
6605
|
+
"content-type": "application/json",
|
|
6606
|
+
},
|
|
6607
|
+
body: JSON.stringify({ name: `quota-race-${index}` }),
|
|
6608
|
+
});
|
|
6609
|
+
try {
|
|
6610
|
+
const first = post(0);
|
|
6611
|
+
await firstDispatched.promise;
|
|
6612
|
+
const rest = Array.from({ length: TOTAL - 1 }, (_, index) => post(index + 1));
|
|
6613
|
+
// The first write is admitted and held open inside the service, so every
|
|
6614
|
+
// other request must run its admission check against an unrecorded
|
|
6615
|
+
// window. Loopback arrival needs a bounded grace; the overlap cannot be
|
|
6616
|
+
// driven deterministically because admission is server-internal.
|
|
6617
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
6618
|
+
releaseWrites.resolve();
|
|
6619
|
+
const responses = await Promise.all([first, ...rest]);
|
|
6620
|
+
const statuses = responses.map((response) => response.status).sort((a, b) => a - b);
|
|
6621
|
+
assert.deepEqual(
|
|
6622
|
+
statuses,
|
|
6623
|
+
[...Array<number>(LIMIT).fill(200), ...Array<number>(TOTAL - LIMIT).fill(429)],
|
|
6624
|
+
`expected exactly ${LIMIT} admitted writes and ${TOTAL - LIMIT} rejections, got ${JSON.stringify(statuses)}`,
|
|
6625
|
+
);
|
|
6626
|
+
} finally {
|
|
6627
|
+
releaseWrites.resolve();
|
|
6628
|
+
await server.stop();
|
|
6629
|
+
}
|
|
6630
|
+
});
|
|
6631
|
+
|
|
6632
|
+
test("a failed write releases its reserved quota slot (issue #3225 C4)", async () => {
|
|
6633
|
+
let calls = 0;
|
|
6634
|
+
const service = quotaStubService();
|
|
6635
|
+
service.capsuleExport = async (request) => {
|
|
6636
|
+
calls += 1;
|
|
6637
|
+
if (calls === 1) throw new Error("export backend failed");
|
|
6638
|
+
// SAFETY: the route serializes this stub result into the HTTP response; the test asserts statuses, not this shape.
|
|
6639
|
+
return { ok: true, name: request.name } as never;
|
|
6640
|
+
};
|
|
6641
|
+
const server = new EngramAccessHttpServer({
|
|
6642
|
+
service,
|
|
6643
|
+
port: 0,
|
|
6644
|
+
authToken: "test-token",
|
|
6645
|
+
adminConsoleEnabled: false,
|
|
6646
|
+
writeRateLimitMaxRequests: 1,
|
|
6647
|
+
});
|
|
6648
|
+
const status = await server.start();
|
|
6649
|
+
const post = (name: string) =>
|
|
6650
|
+
fetch(`http://127.0.0.1:${status.port}/engram/v1/capsules/export`, {
|
|
6651
|
+
method: "POST",
|
|
6652
|
+
headers: {
|
|
6653
|
+
authorization: "Bearer test-token",
|
|
6654
|
+
"content-type": "application/json",
|
|
6655
|
+
},
|
|
6656
|
+
body: JSON.stringify({ name }),
|
|
6657
|
+
});
|
|
6658
|
+
try {
|
|
6659
|
+
// The first export fails in the backend; the slot it reserved at
|
|
6660
|
+
// admission must be released, so the retry is admitted and only then
|
|
6661
|
+
// does the window fill.
|
|
6662
|
+
assert.equal((await post("quota-fail-1")).status, 500);
|
|
6663
|
+
assert.equal((await post("quota-fail-2")).status, 200);
|
|
6664
|
+
assert.equal((await post("quota-fail-3")).status, 429);
|
|
6665
|
+
} finally {
|
|
6666
|
+
await server.stop();
|
|
6667
|
+
}
|
|
6668
|
+
});
|
|
6669
|
+
|
|
6670
|
+
test("a replayed observe never 429s and never consumes a second quota slot (issue #1434, #3225 C4)", async () => {
|
|
6671
|
+
const stored = new Map<string, { sessionKey: string }>();
|
|
6672
|
+
let hookCalls = 0;
|
|
6673
|
+
const service = quotaStubService();
|
|
6674
|
+
service.observe = async (request, hooks) => {
|
|
6675
|
+
const key = request.idempotencyKey?.trim();
|
|
6676
|
+
const cached = key ? stored.get(key) : undefined;
|
|
6677
|
+
if (cached) {
|
|
6678
|
+
// SAFETY: the route only forwards this result into the HTTP response and reads its dryRun/idempotencyReplay flags.
|
|
6679
|
+
return { ...cached, idempotencyReplay: true } as never;
|
|
6680
|
+
}
|
|
6681
|
+
// Mirror the real service contract: the quota hook fires exactly once,
|
|
6682
|
+
// immediately before an actual write commits, never on a replay.
|
|
6683
|
+
await hooks?.enforceWriteQuota?.();
|
|
6684
|
+
hookCalls += 1;
|
|
6685
|
+
const response = { sessionKey: request.sessionKey };
|
|
6686
|
+
stored.set(key ?? "", response);
|
|
6687
|
+
// SAFETY: the route only forwards this result into the HTTP response and reads its dryRun/idempotencyReplay flags.
|
|
6688
|
+
return response as never;
|
|
6689
|
+
};
|
|
6690
|
+
const server = new EngramAccessHttpServer({
|
|
6691
|
+
service,
|
|
6692
|
+
port: 0,
|
|
6693
|
+
authToken: "test-token",
|
|
6694
|
+
adminConsoleEnabled: false,
|
|
6695
|
+
writeRateLimitMaxRequests: 1,
|
|
6696
|
+
});
|
|
6697
|
+
const status = await server.start();
|
|
6698
|
+
const post = (idempotencyKey: string) =>
|
|
6699
|
+
fetch(`http://127.0.0.1:${status.port}/engram/v1/observe`, {
|
|
6700
|
+
method: "POST",
|
|
6701
|
+
headers: {
|
|
6702
|
+
authorization: "Bearer test-token",
|
|
6703
|
+
"content-type": "application/json",
|
|
6704
|
+
},
|
|
6705
|
+
body: JSON.stringify({
|
|
6706
|
+
sessionKey: "quota-replay",
|
|
6707
|
+
messages: [{ role: "user", content: "hello" }],
|
|
6708
|
+
idempotencyKey,
|
|
6709
|
+
}),
|
|
6710
|
+
});
|
|
6711
|
+
try {
|
|
6712
|
+
assert.equal((await post("quota-replay-1")).status, 202);
|
|
6713
|
+
const replay = await post("quota-replay-1");
|
|
6714
|
+
assert.equal(replay.status, 202);
|
|
6715
|
+
// SAFETY: our own server serialized this body; the test reads only the idempotencyReplay flag.
|
|
6716
|
+
const replayBody = (await replay.json()) as { idempotencyReplay?: boolean };
|
|
6717
|
+
assert.equal(replayBody.idempotencyReplay, true);
|
|
6718
|
+
assert.equal(hookCalls, 1, "the replay must not re-run the quota hook");
|
|
6719
|
+
// The window still holds exactly the first write's slot: a different
|
|
6720
|
+
// payload is rejected, proving the replay consumed nothing.
|
|
6721
|
+
assert.equal((await post("quota-replay-2")).status, 429);
|
|
6722
|
+
} finally {
|
|
6723
|
+
await server.stop();
|
|
6724
|
+
}
|
|
6725
|
+
});
|
|
6726
|
+
|
|
6727
|
+
test("a dreams dry run neither 429s nor consumes quota at a full window (#3225 C4)", async () => {
|
|
6728
|
+
const service = quotaStubService();
|
|
6729
|
+
service.dreamsRun = async (options) => {
|
|
6730
|
+
// SAFETY: the route serializes this stub result into the HTTP response and reads its dryRun flag.
|
|
6731
|
+
return { phase: options.phase, dryRun: options.dryRun === true } as never;
|
|
6732
|
+
};
|
|
6733
|
+
const server = new EngramAccessHttpServer({
|
|
6734
|
+
service,
|
|
6735
|
+
port: 0,
|
|
6736
|
+
authToken: "test-token",
|
|
6737
|
+
adminConsoleEnabled: false,
|
|
6738
|
+
writeRateLimitMaxRequests: 1,
|
|
6739
|
+
});
|
|
6740
|
+
const status = await server.start();
|
|
6741
|
+
const post = (body: Record<string, boolean>) =>
|
|
6742
|
+
fetch(`http://127.0.0.1:${status.port}/engram/v1/dreams/run`, {
|
|
6743
|
+
method: "POST",
|
|
6744
|
+
headers: {
|
|
6745
|
+
authorization: "Bearer test-token",
|
|
6746
|
+
"content-type": "application/json",
|
|
6747
|
+
},
|
|
6748
|
+
body: JSON.stringify({ phase: "lightSleep", ...body }),
|
|
6749
|
+
});
|
|
6750
|
+
try {
|
|
6751
|
+
assert.equal((await post({})).status, 200);
|
|
6752
|
+
// The window is full; the dry run must pass and leave the count alone.
|
|
6753
|
+
const dry = await post({ dryRun: true });
|
|
6754
|
+
assert.equal(dry.status, 200);
|
|
6755
|
+
// SAFETY: our own server serialized this body; the test reads only the dryRun flag.
|
|
6756
|
+
const dryBody = (await dry.json()) as { dryRun?: boolean };
|
|
6757
|
+
assert.equal(dryBody.dryRun, true);
|
|
6758
|
+
assert.equal((await post({})).status, 429);
|
|
6759
|
+
} finally {
|
|
6760
|
+
await server.stop();
|
|
6761
|
+
}
|
|
6762
|
+
});
|
package/src/access-http.ts
CHANGED
|
@@ -417,6 +417,11 @@ export class EngramAccessHttpServer extends ReviewDeckAccessHttpBase {
|
|
|
417
417
|
private readonly resourceMetadataUrl?: string;
|
|
418
418
|
private readonly externalRequestHandler?: SupportPassportExternalRequestHandler;
|
|
419
419
|
private readonly writeLimiter: WriteRateLimiter;
|
|
420
|
+
// Reservations taken by ensureWriteRateLimitAvailable and not yet committed
|
|
421
|
+
// by recordWriteRateLimitHit, keyed by request. The per-request release in
|
|
422
|
+
// the connection callback covers every early return and error path, so a
|
|
423
|
+
// failed or exempt write never leaks its slot (issue #3225 C4).
|
|
424
|
+
private readonly pendingWriteQuotas = new WeakMap<IncomingMessage, WriteRateLimitReservation>();
|
|
420
425
|
private readonly mcpServer: EngramMcpServer;
|
|
421
426
|
private server: Server | null = null;
|
|
422
427
|
private boundPort = 0;
|
|
@@ -544,6 +549,13 @@ export class EngramAccessHttpServer extends ReviewDeckAccessHttpBase {
|
|
|
544
549
|
});
|
|
545
550
|
return;
|
|
546
551
|
}
|
|
552
|
+
if (err.name === "ReplayAdmissionRefusedError") {
|
|
553
|
+
// Durable-capture backpressure (#3251/#1908): the request was
|
|
554
|
+
// never accepted — the buffer is at entry capacity. Retryable,
|
|
555
|
+
// not a server fault (503, not 500).
|
|
556
|
+
this.respondJson(res, 503, { error: err.message, code: "buffer_admission_refused" });
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
547
559
|
if (res.headersSent) {
|
|
548
560
|
res.destroy(err as Error);
|
|
549
561
|
return;
|
|
@@ -556,6 +568,9 @@ export class EngramAccessHttpServer extends ReviewDeckAccessHttpBase {
|
|
|
556
568
|
}).finally(() => {
|
|
557
569
|
req.off("aborted", abortDisconnectedRequest);
|
|
558
570
|
res.off("close", abortDisconnectedRequest);
|
|
571
|
+
// A counted write that failed or was exempt (dry run, replay) must
|
|
572
|
+
// not hold its reserved slot past the request (issue #3225 C4).
|
|
573
|
+
this.releasePendingWriteQuota(req);
|
|
559
574
|
});
|
|
560
575
|
});
|
|
561
576
|
});
|
|
@@ -2739,26 +2754,38 @@ export class EngramAccessHttpServer extends ReviewDeckAccessHttpBase {
|
|
|
2739
2754
|
// DELETE /engram/v1/peers/:id?forget=true — destructive full purge (issue #679 completion)
|
|
2740
2755
|
// GET /engram/v1/peers/:id/profile — get peer profile
|
|
2741
2756
|
if (req.method === "GET" && pathname === "/engram/v1/peers") {
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2757
|
+
const op = getOperation("peer_list"); // boundary dispatch (issue #1525)
|
|
2758
|
+
if (!op) throw new EngramAccessInputError("access-boundary: operation not registered: peer_list");
|
|
2759
|
+
// SAFETY: the registered peer_list handler returns { result } from the service.
|
|
2760
|
+
const output = (await op.run({}, {
|
|
2761
|
+
service: this.service,
|
|
2762
|
+
authenticatedPrincipal: this.resolveRequestPrincipal(req),
|
|
2763
|
+
sourceConnector: this.resolveConnector(req),
|
|
2764
|
+
})) as { result: unknown };
|
|
2765
|
+
this.respondJson(res, 200, output.result);
|
|
2745
2766
|
return;
|
|
2746
2767
|
}
|
|
2747
2768
|
|
|
2748
2769
|
const peerProfileMatch = /^\/engram\/v1\/peers\/([^/]+)\/profile$/.exec(pathname);
|
|
2749
2770
|
if (peerProfileMatch) {
|
|
2750
|
-
this.enforceTokenOp("peer_profile_get"); // boundary dispatch (issue #1525)
|
|
2751
2771
|
if (req.method !== "GET") {
|
|
2752
2772
|
this.respondJson(res, 405, { error: "method_not_allowed", code: "method_not_allowed" });
|
|
2753
2773
|
return;
|
|
2754
2774
|
}
|
|
2775
|
+
const op = getOperation("peer_profile_get"); // boundary dispatch (issue #1525)
|
|
2776
|
+
if (!op) throw new EngramAccessInputError("access-boundary: operation not registered: peer_profile_get");
|
|
2755
2777
|
const peerId = decodePeerIdSegment(peerProfileMatch[1] ?? "");
|
|
2756
|
-
|
|
2757
|
-
|
|
2778
|
+
// SAFETY: the registered peer_profile_get handler returns { result } from the service.
|
|
2779
|
+
const output = (await op.run({ id: peerId }, {
|
|
2780
|
+
service: this.service,
|
|
2781
|
+
authenticatedPrincipal: this.resolveRequestPrincipal(req),
|
|
2782
|
+
sourceConnector: this.resolveConnector(req),
|
|
2783
|
+
})) as { result: { found: boolean } };
|
|
2784
|
+
if (!output.result.found) {
|
|
2758
2785
|
this.respondJson(res, 404, { error: "peer_profile_not_found", code: "peer_profile_not_found" });
|
|
2759
2786
|
return;
|
|
2760
2787
|
}
|
|
2761
|
-
this.respondJson(res, 200, result);
|
|
2788
|
+
this.respondJson(res, 200, output.result);
|
|
2762
2789
|
return;
|
|
2763
2790
|
}
|
|
2764
2791
|
|
|
@@ -2767,47 +2794,49 @@ export class EngramAccessHttpServer extends ReviewDeckAccessHttpBase {
|
|
|
2767
2794
|
const peerId = decodePeerIdSegment(peerIdMatch[1] ?? "");
|
|
2768
2795
|
|
|
2769
2796
|
if (req.method === "GET") {
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2797
|
+
const op = getOperation("peer_get"); // boundary dispatch (issue #1525)
|
|
2798
|
+
if (!op) throw new EngramAccessInputError("access-boundary: operation not registered: peer_get");
|
|
2799
|
+
// SAFETY: the registered peer_get handler returns { result } with a found flag.
|
|
2800
|
+
const output = (await op.run({ id: peerId }, {
|
|
2801
|
+
service: this.service,
|
|
2802
|
+
authenticatedPrincipal: this.resolveRequestPrincipal(req),
|
|
2803
|
+
sourceConnector: this.resolveConnector(req),
|
|
2804
|
+
})) as { result: { found: boolean } };
|
|
2805
|
+
if (!output.result.found) {
|
|
2773
2806
|
this.respondJson(res, 404, { error: "peer_not_found", code: "peer_not_found" });
|
|
2774
2807
|
return;
|
|
2775
2808
|
}
|
|
2776
|
-
this.respondJson(res, 200, result);
|
|
2809
|
+
this.respondJson(res, 200, output.result);
|
|
2777
2810
|
return;
|
|
2778
2811
|
}
|
|
2779
2812
|
|
|
2780
2813
|
if (req.method === "PUT") {
|
|
2781
|
-
this.enforceTokenOp("peer_set"); // boundary dispatch (issue #1525)
|
|
2782
2814
|
const body = await this.readJsonBody(req) as Record<string, unknown>;
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
//
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
if (
|
|
2790
|
-
"displayName" in body &&
|
|
2791
|
-
body.displayName !== undefined &&
|
|
2792
|
-
typeof body.displayName !== "string"
|
|
2793
|
-
) {
|
|
2794
|
-
throw new EngramAccessInputError("displayName must be a string when provided");
|
|
2795
|
-
}
|
|
2796
|
-
if ("notes" in body && body.notes !== undefined && typeof body.notes !== "string") {
|
|
2797
|
-
throw new EngramAccessInputError("notes must be a string when provided");
|
|
2815
|
+
const op = getOperation("peer_set"); // boundary dispatch (issue #1525)
|
|
2816
|
+
if (!op) throw new EngramAccessInputError("access-boundary: operation not registered: peer_set");
|
|
2817
|
+
// The operation schema strips nulls as "absent", so an explicit null
|
|
2818
|
+
// must fail here or the peer would register with default fields.
|
|
2819
|
+
for (const field of ["kind", "displayName", "notes"]) {
|
|
2820
|
+
if (body[field] === null) throw new EngramAccessInputError(`${field} must be a string when provided`);
|
|
2798
2821
|
}
|
|
2799
|
-
|
|
2822
|
+
// Raw body fields: the operation schema rejects malformed types with an
|
|
2823
|
+
// input error instead of silently defaulting them (rule 51).
|
|
2824
|
+
// SAFETY: the registered peer_set handler returns { result } with the upsert outcome.
|
|
2825
|
+
const output = (await op.run({
|
|
2800
2826
|
id: peerId,
|
|
2801
|
-
kind:
|
|
2802
|
-
displayName:
|
|
2803
|
-
notes:
|
|
2804
|
-
}
|
|
2805
|
-
|
|
2827
|
+
kind: body.kind,
|
|
2828
|
+
displayName: body.displayName,
|
|
2829
|
+
notes: body.notes,
|
|
2830
|
+
}, {
|
|
2831
|
+
service: this.service,
|
|
2832
|
+
authenticatedPrincipal: this.resolveRequestPrincipal(req),
|
|
2833
|
+
sourceConnector: this.resolveConnector(req),
|
|
2834
|
+
})) as { result: { created: boolean } };
|
|
2835
|
+
this.respondJson(res, output.result.created ? 201 : 200, output.result);
|
|
2806
2836
|
return;
|
|
2807
2837
|
}
|
|
2808
2838
|
|
|
2809
2839
|
if (req.method === "DELETE") {
|
|
2810
|
-
this.enforceTokenOp("peer_delete"); // boundary dispatch (issue #1525)
|
|
2811
2840
|
// `?forget=true` triggers the destructive full-purge path (issue #679
|
|
2812
2841
|
// completion). The caller must also pass `confirm=yes` in the request
|
|
2813
2842
|
// body; absent confirmation yields 400. Plain DELETE (no ?forget) keeps
|
|
@@ -2824,12 +2853,26 @@ export class EngramAccessHttpServer extends ReviewDeckAccessHttpBase {
|
|
|
2824
2853
|
});
|
|
2825
2854
|
return;
|
|
2826
2855
|
}
|
|
2827
|
-
const
|
|
2828
|
-
|
|
2856
|
+
const op = getOperation("peer_forget"); // boundary dispatch (issue #1525)
|
|
2857
|
+
if (!op) throw new EngramAccessInputError("access-boundary: operation not registered: peer_forget");
|
|
2858
|
+
// SAFETY: the registered peer_forget handler returns { result } with the purge outcome.
|
|
2859
|
+
const output = (await op.run({ id: peerId, confirm: "yes" }, {
|
|
2860
|
+
service: this.service,
|
|
2861
|
+
authenticatedPrincipal: this.resolveRequestPrincipal(req),
|
|
2862
|
+
sourceConnector: this.resolveConnector(req),
|
|
2863
|
+
})) as { result: unknown };
|
|
2864
|
+
this.respondJson(res, 200, output.result);
|
|
2829
2865
|
return;
|
|
2830
2866
|
}
|
|
2831
|
-
const
|
|
2832
|
-
|
|
2867
|
+
const op = getOperation("peer_delete"); // boundary dispatch (issue #1525)
|
|
2868
|
+
if (!op) throw new EngramAccessInputError("access-boundary: operation not registered: peer_delete");
|
|
2869
|
+
// SAFETY: the registered peer_delete handler returns { result } with the delete outcome.
|
|
2870
|
+
const output = (await op.run({ id: peerId }, {
|
|
2871
|
+
service: this.service,
|
|
2872
|
+
authenticatedPrincipal: this.resolveRequestPrincipal(req),
|
|
2873
|
+
sourceConnector: this.resolveConnector(req),
|
|
2874
|
+
})) as { result: unknown };
|
|
2875
|
+
this.respondJson(res, 200, output.result);
|
|
2833
2876
|
return;
|
|
2834
2877
|
}
|
|
2835
2878
|
|
|
@@ -3931,16 +3974,48 @@ export class EngramAccessHttpServer extends ReviewDeckAccessHttpBase {
|
|
|
3931
3974
|
return 200;
|
|
3932
3975
|
}
|
|
3933
3976
|
|
|
3977
|
+
/**
|
|
3978
|
+
* Admit a quota-counted write by RESERVING a limiter slot for the request
|
|
3979
|
+
* (issue #3225 C4). A bare hasCapacity check admits N concurrent requests
|
|
3980
|
+
* against the same committed count; the reservation holds in-flight
|
|
3981
|
+
* capacity until the write commits at recordWriteRateLimitHit or the
|
|
3982
|
+
* request ends. Idempotent per request, so a quota hook that fires after
|
|
3983
|
+
* an earlier reservation is a no-op.
|
|
3984
|
+
*/
|
|
3934
3985
|
protected ensureWriteRateLimitAvailable(req?: IncomingMessage): void {
|
|
3935
|
-
if (!
|
|
3986
|
+
if (!req) {
|
|
3987
|
+
if (!this.writeLimiter.hasCapacity()) {
|
|
3988
|
+
throw new HttpError(429, "write_rate_limited", "write_rate_limited");
|
|
3989
|
+
}
|
|
3990
|
+
return;
|
|
3991
|
+
}
|
|
3992
|
+
if (this.pendingWriteQuotas.has(req)) return;
|
|
3993
|
+
const reservation = this.writeLimiter.reserve(this.principalForRateLimit(req));
|
|
3994
|
+
if (!reservation) {
|
|
3936
3995
|
throw new HttpError(429, "write_rate_limited", "write_rate_limited");
|
|
3937
3996
|
}
|
|
3997
|
+
this.pendingWriteQuotas.set(req, reservation);
|
|
3938
3998
|
}
|
|
3939
3999
|
|
|
4000
|
+
/** Commit a counted write at the durable boundary; consumes the request's reservation. */
|
|
3940
4001
|
protected recordWriteRateLimitHit(req?: IncomingMessage): void {
|
|
4002
|
+
const pending = req ? this.pendingWriteQuotas.get(req) : undefined;
|
|
4003
|
+
if (pending && req) {
|
|
4004
|
+
this.pendingWriteQuotas.delete(req);
|
|
4005
|
+
pending.commit();
|
|
4006
|
+
return;
|
|
4007
|
+
}
|
|
3941
4008
|
this.writeLimiter.record(this.principalForRateLimit(req));
|
|
3942
4009
|
}
|
|
3943
4010
|
|
|
4011
|
+
/** Release an uncommitted reservation after a rejection, failure, or exempt outcome. */
|
|
4012
|
+
private releasePendingWriteQuota(req: IncomingMessage): void {
|
|
4013
|
+
const pending = this.pendingWriteQuotas.get(req);
|
|
4014
|
+
if (!pending) return;
|
|
4015
|
+
this.pendingWriteQuotas.delete(req);
|
|
4016
|
+
pending.release();
|
|
4017
|
+
}
|
|
4018
|
+
|
|
3944
4019
|
protected reserveWriteRateLimitSlot(req?: IncomingMessage): WriteRateLimitReservation {
|
|
3945
4020
|
const reservation = this.writeLimiter.reserve(this.principalForRateLimit(req));
|
|
3946
4021
|
if (!reservation) {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { MCP_STDIO_MAX_FRAME_BYTES, McpFrameReader } from "./access-mcp-frames.js";
|
|
5
|
+
|
|
6
|
+
function frame(body: string): Buffer {
|
|
7
|
+
return Buffer.from(`Content-Length: ${Buffer.byteLength(body, "utf-8")}\r\n\r\n${body}`, "utf-8");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
test("a frame that arrives in two reads is returned once, without re-reading the partial buffer", () => {
|
|
11
|
+
const reader = new McpFrameReader();
|
|
12
|
+
const whole = frame('{"id":1}');
|
|
13
|
+
reader.push(whole.subarray(0, 10));
|
|
14
|
+
assert.equal(reader.next(), null);
|
|
15
|
+
assert.equal(reader.hasUnread(), false);
|
|
16
|
+
|
|
17
|
+
reader.push(whole.subarray(10));
|
|
18
|
+
assert.equal(reader.hasUnread(), true);
|
|
19
|
+
assert.deepEqual(reader.next(), { kind: "body", text: '{"id":1}' });
|
|
20
|
+
assert.equal(reader.next(), null);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("bytes that arrive after the reader stopped on an empty buffer are unread", () => {
|
|
24
|
+
const reader = new McpFrameReader();
|
|
25
|
+
reader.push(frame("first"));
|
|
26
|
+
assert.deepEqual(reader.next(), { kind: "body", text: "first" });
|
|
27
|
+
assert.equal(reader.next(), null);
|
|
28
|
+
assert.equal(reader.hasUnread(), false);
|
|
29
|
+
|
|
30
|
+
reader.push(frame("second"));
|
|
31
|
+
assert.equal(reader.hasUnread(), true);
|
|
32
|
+
assert.deepEqual(reader.next(), { kind: "body", text: "second" });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("the body of an oversized frame is dropped across reads and the next frame is read", () => {
|
|
36
|
+
const reader = new McpFrameReader();
|
|
37
|
+
const declared = MCP_STDIO_MAX_FRAME_BYTES + 5;
|
|
38
|
+
reader.push(Buffer.from(`Content-Length: ${declared}\r\n\r\n${"A".repeat(100)}`, "utf-8"));
|
|
39
|
+
assert.deepEqual(reader.next(), { kind: "too-large" });
|
|
40
|
+
assert.equal(reader.next(), null);
|
|
41
|
+
|
|
42
|
+
reader.push(Buffer.from("B".repeat(declared - 100), "utf-8"));
|
|
43
|
+
assert.equal(reader.hasUnread(), false);
|
|
44
|
+
|
|
45
|
+
reader.push(frame("after"));
|
|
46
|
+
assert.deepEqual(reader.next(), { kind: "body", text: "after" });
|
|
47
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-Length framing for the MCP stdio transport.
|
|
3
|
+
*
|
|
4
|
+
* `push` appends raw bytes. `next` returns one frame at a time and `null` when
|
|
5
|
+
* the buffered bytes hold no complete frame yet. The header and the body are
|
|
6
|
+
* each bounded to MCP_STDIO_MAX_FRAME_BYTES, which matches the HTTP access
|
|
7
|
+
* default `maxBodyBytes`, so a peer cannot grow the buffer without limit.
|
|
8
|
+
*/
|
|
9
|
+
export const MCP_STDIO_MAX_FRAME_BYTES = 131072;
|
|
10
|
+
|
|
11
|
+
export type McpFrame = { kind: "body"; text: string } | { kind: "too-large" };
|
|
12
|
+
|
|
13
|
+
const TOO_LARGE: McpFrame = { kind: "too-large" };
|
|
14
|
+
|
|
15
|
+
export class McpFrameReader {
|
|
16
|
+
private buffer = Buffer.alloc(0);
|
|
17
|
+
/** Body bytes of a refused oversized frame that have not arrived yet. */
|
|
18
|
+
private skipBytes = 0;
|
|
19
|
+
/** Buffer length when `next` last stopped on an incomplete frame. */
|
|
20
|
+
private waitingBytes = 0;
|
|
21
|
+
|
|
22
|
+
push(chunk: Buffer): void {
|
|
23
|
+
const dropped = Math.min(this.skipBytes, chunk.length);
|
|
24
|
+
this.skipBytes -= dropped;
|
|
25
|
+
const kept = chunk.subarray(dropped);
|
|
26
|
+
if (kept.length > 0) this.buffer = Buffer.concat([this.buffer, kept]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** True when bytes arrived after `next` last examined the buffer. */
|
|
30
|
+
hasUnread(): boolean {
|
|
31
|
+
return this.buffer.length > this.waitingBytes;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
next(): McpFrame | null {
|
|
35
|
+
this.waitingBytes = 0;
|
|
36
|
+
const headerEnd = this.buffer.indexOf("\r\n\r\n");
|
|
37
|
+
if (headerEnd < 0) {
|
|
38
|
+
if (this.buffer.length <= MCP_STDIO_MAX_FRAME_BYTES) return this.wait();
|
|
39
|
+
return this.discardBuffer();
|
|
40
|
+
}
|
|
41
|
+
if (headerEnd > MCP_STDIO_MAX_FRAME_BYTES) return this.discardBuffer();
|
|
42
|
+
|
|
43
|
+
const headers = this.buffer.subarray(0, headerEnd).toString("utf-8").split("\r\n");
|
|
44
|
+
const lengthHeader = headers.find((line) => line.toLowerCase().startsWith("content-length:"));
|
|
45
|
+
const contentLength = lengthHeader ? Number.parseInt(lengthHeader.split(":")[1]?.trim() ?? "0", 10) : NaN;
|
|
46
|
+
if (!Number.isFinite(contentLength) || contentLength < 0) {
|
|
47
|
+
this.buffer = Buffer.alloc(0);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const bodyStart = headerEnd + 4;
|
|
52
|
+
if (contentLength > MCP_STDIO_MAX_FRAME_BYTES) {
|
|
53
|
+
const rest = this.buffer.subarray(bodyStart);
|
|
54
|
+
this.skipBytes = Math.max(0, contentLength - rest.length);
|
|
55
|
+
this.buffer = rest.subarray(Math.min(contentLength, rest.length));
|
|
56
|
+
return TOO_LARGE;
|
|
57
|
+
}
|
|
58
|
+
const bodyEnd = bodyStart + contentLength;
|
|
59
|
+
if (this.buffer.length < bodyEnd) return this.wait();
|
|
60
|
+
const text = this.buffer.subarray(bodyStart, bodyEnd).toString("utf-8");
|
|
61
|
+
this.buffer = this.buffer.subarray(bodyEnd);
|
|
62
|
+
return { kind: "body", text };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private wait(): null {
|
|
66
|
+
this.waitingBytes = this.buffer.length;
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private discardBuffer(): McpFrame {
|
|
71
|
+
this.buffer = Buffer.alloc(0);
|
|
72
|
+
return TOO_LARGE;
|
|
73
|
+
}
|
|
74
|
+
}
|