@remnic/core 10.0.24 → 10.0.26
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 +27 -26
- package/dist/access-authorization-probe.js +28 -27
- package/dist/access-boundary.js +27 -26
- package/dist/access-cli.js +84 -83
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.js +27 -26
- package/dist/access-http-briefing.js +28 -27
- package/dist/access-http-query.js +29 -28
- package/dist/access-http.js +46 -45
- package/dist/access-identity-continuity-surface.js +27 -26
- package/dist/access-lcm-surface.js +27 -26
- package/dist/access-mcp.js +38 -37
- package/dist/access-namespace-preflight.js +27 -26
- package/dist/access-observe-write-category.js +4 -4
- package/dist/access-observe-write-surface.js +27 -26
- package/dist/access-operations-batch.js +28 -27
- package/dist/access-operations.js +35 -34
- package/dist/access-recall-concurrency.js +27 -26
- package/dist/access-recall-diagnostics.js +2 -2
- package/dist/access-recall-response.js +27 -26
- package/dist/access-recall-surface.js +27 -26
- package/dist/access-schema.js +3 -3
- package/dist/access-service.js +27 -26
- package/dist/access-surface-catalog.js +29 -28
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/active-recall.js +4 -4
- package/dist/briefing.js +4 -4
- package/dist/calibration.js +2 -1
- package/dist/calibration.js.map +1 -1
- package/dist/causal-consolidation.js +5 -5
- package/dist/{chunk-ZUCNOTNF.js → chunk-2NQD4N42.js} +12 -12
- package/dist/chunk-3VRMUNWL.js +289 -0
- package/dist/chunk-3VRMUNWL.js.map +1 -0
- package/dist/{chunk-466EGMFQ.js → chunk-42KSEX6C.js} +1 -1
- package/dist/chunk-42KSEX6C.js.map +1 -0
- package/dist/{chunk-O6ADSFGY.js → chunk-4B7JO3LG.js} +2 -2
- package/dist/{chunk-D42L5SQF.js → chunk-5A4QMATS.js} +5 -5
- package/dist/{chunk-SSUTJLCF.js → chunk-5ALVWXQG.js} +2 -2
- package/dist/{chunk-N36PBG4A.js → chunk-5BNDPLGI.js} +2 -2
- package/dist/{chunk-RTV3DB4I.js → chunk-7BFE2UD4.js} +2 -2
- package/dist/{chunk-IVUCF3QO.js → chunk-7RTNJ6M2.js} +2 -2
- package/dist/{chunk-VPXIKBPX.js → chunk-A3IURDU2.js} +22 -1
- package/dist/chunk-A3IURDU2.js.map +1 -0
- package/dist/{chunk-VOEBHBWG.js → chunk-B2BAZPVQ.js} +2 -2
- package/dist/{chunk-7UHCV5B3.js → chunk-B45WUQUU.js} +2 -2
- package/dist/{chunk-2I3OREMR.js → chunk-BGG5BP3M.js} +2 -2
- package/dist/{chunk-EML6HJE6.js → chunk-BO3F4X5U.js} +2 -2
- package/dist/{chunk-5JXNXKKV.js → chunk-BWOLINYY.js} +2 -2
- package/dist/{chunk-V4MB5ONM.js → chunk-BWXKJDDH.js} +4 -4
- package/dist/{chunk-OPNSIFFU.js → chunk-CA35LZAE.js} +8 -8
- package/dist/{chunk-PDFAX4PK.js → chunk-CT5YXLVB.js} +2 -2
- package/dist/{chunk-VUBVNY3W.js → chunk-CZURNXGO.js} +2 -2
- package/dist/{chunk-TXE55Z3X.js → chunk-D3I4P2SX.js} +2 -2
- package/dist/{chunk-BI3CIDIH.js → chunk-DF2G4IXN.js} +3 -3
- package/dist/{chunk-YRKKW4KT.js → chunk-DFL3MBL5.js} +3 -3
- package/dist/{chunk-2C5YAVHA.js → chunk-DIHJ2QWP.js} +2 -2
- package/dist/{chunk-IBGRIAE5.js → chunk-DVE7B2IR.js} +2 -2
- package/dist/{chunk-33G67YZF.js → chunk-DWL6YIZH.js} +2 -2
- package/dist/{chunk-YW4ZYASS.js → chunk-E2I5FVEU.js} +2 -2
- package/dist/{chunk-ZDUMF4H6.js → chunk-ECBAENW7.js} +14 -14
- package/dist/{chunk-QZ7WOCME.js → chunk-EQAIXC4Q.js} +2 -2
- package/dist/{chunk-RYYVMZBR.js → chunk-ETPIYK4J.js} +2 -2
- package/dist/{chunk-RQMSD4CN.js → chunk-FI6CUYMJ.js} +48 -48
- package/dist/{chunk-GXVLEXIX.js → chunk-G7QUQILS.js} +2 -2
- package/dist/{chunk-VYVQZRXF.js → chunk-H3AW6QZF.js} +2 -2
- package/dist/{chunk-3K6J5GO5.js → chunk-H53F22SJ.js} +3 -3
- package/dist/{chunk-NIS3FOMD.js → chunk-HGSERGFX.js} +2 -2
- package/dist/{chunk-ZDDDWTC7.js → chunk-HJQXHLQQ.js} +2 -2
- package/dist/{chunk-6VWCL2IZ.js → chunk-HNZTKT4J.js} +3 -3
- package/dist/{chunk-W3GFSWKE.js → chunk-HWMK3QHK.js} +8 -8
- package/dist/{chunk-VVSPHTSU.js → chunk-I75TTE7D.js} +2 -2
- package/dist/{chunk-FBNL4BYD.js → chunk-IXUEFTK4.js} +2 -2
- package/dist/{chunk-W7NVMERN.js → chunk-JT7GSF3T.js} +2 -2
- package/dist/{chunk-LDZD3OQG.js → chunk-KD73Q7EN.js} +2 -2
- package/dist/{chunk-HVIXRLXJ.js → chunk-KDMZZSL4.js} +2 -2
- package/dist/{chunk-7LCGFUMB.js → chunk-KQAFEZQX.js} +4 -4
- package/dist/{chunk-A2IJEV5L.js → chunk-M47LM554.js} +2 -2
- package/dist/{chunk-5NO42IYF.js → chunk-MKP4W52S.js} +4 -4
- package/dist/{chunk-LT4SMGE4.js → chunk-MRRLXXWA.js} +2 -2
- package/dist/{chunk-2JGBX5XY.js → chunk-NJAFC244.js} +2 -2
- package/dist/{chunk-OFQOOLQQ.js → chunk-NKGI7JWP.js} +2 -2
- package/dist/{chunk-UHBZK3CK.js → chunk-OL7LAGBG.js} +2 -2
- package/dist/{chunk-AAQ77A44.js → chunk-QNKEMFZY.js} +2 -2
- package/dist/{chunk-XKPLERHV.js → chunk-QP5GYLMS.js} +36 -36
- package/dist/{chunk-62CRBI5B.js → chunk-ROF3JORU.js} +2 -2
- package/dist/{chunk-OMERTLB7.js → chunk-SA7NPDWJ.js} +2 -2
- package/dist/{chunk-QC5TJUSP.js → chunk-TRG3A2AW.js} +2 -2
- package/dist/{chunk-CA2R2AD4.js → chunk-UFUWKKCV.js} +31 -31
- package/dist/{chunk-DJMGVSST.js → chunk-US4WUDQR.js} +2 -2
- package/dist/{chunk-4OEBL5T3.js → chunk-V7KGDLBC.js} +2 -2
- package/dist/{chunk-ML7ZJAKJ.js → chunk-VBINQDWP.js} +2 -2
- package/dist/{chunk-5FNZ3G6X.js → chunk-VKQI47DJ.js} +4 -4
- package/dist/{chunk-UXXLP2PD.js → chunk-X3ZLVGGX.js} +3 -3
- package/dist/{chunk-32FVH5OQ.js → chunk-XE5QLP3P.js} +6 -6
- package/dist/{chunk-DYJHGJZ4.js → chunk-XSEVBFMD.js} +35 -2
- package/dist/chunk-XSEVBFMD.js.map +1 -0
- package/dist/{chunk-PVHQSLNN.js → chunk-Y6IF6H2U.js} +2 -2
- package/dist/{chunk-CFZ4SXXH.js → chunk-YEF5AAWN.js} +2 -2
- package/dist/{chunk-NFB5QTOH.js → chunk-Z2XUE3VT.js} +2 -2
- package/dist/{chunk-INBB7D3V.js → chunk-ZXVLOKN4.js} +2 -2
- package/dist/cli.js +63 -62
- package/dist/coding/export-okf-codegraph.js +3 -3
- package/dist/compounding/engine.js +5 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -4
- package/dist/connectors/codex-materialize-runner.js +4 -4
- package/dist/connectors/index.js +4 -4
- package/dist/convergence-refresh.js +6 -6
- package/dist/corpus-watermark.js +6 -6
- package/dist/deep-recall-http-glue.js +4 -4
- package/dist/entity-retrieval.js +4 -4
- package/dist/external-wiki-access.js +28 -27
- package/dist/extraction.js +7 -6
- package/dist/{first-start-migration-QVD22WWS.js → first-start-migration-A6PHIBUQ.js} +4 -4
- package/dist/harmonic-retrieval.js +4 -4
- package/dist/index.js +97 -96
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +5 -5
- package/dist/local-llm-rate-limit.d.ts +169 -0
- package/dist/local-llm-rate-limit.d.ts.map +1 -0
- package/dist/local-llm-rate-limit.js +48 -0
- package/dist/local-llm-rate-limit.js.map +1 -0
- package/dist/local-llm.d.ts.map +1 -1
- package/dist/local-llm.js +2 -1
- package/dist/location/index.js +6 -6
- package/dist/maintenance/memory-governance.js +4 -4
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -4
- package/dist/maintenance/rebuild-memory-projection-runner.js +6 -6
- package/dist/maintenance/rebuild-memory-projection.js +5 -5
- package/dist/memory-browse-operations.js +28 -27
- package/dist/namespaces/migrate.js +5 -5
- package/dist/namespaces/storage.js +4 -4
- package/dist/operator-doctor-corpus.js +7 -7
- package/dist/operator-toolkit.js +13 -13
- package/dist/orchestration/maintenance.js +16 -15
- package/dist/orchestrator.js +86 -85
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-navigate-access.js +2 -2
- package/dist/recall-navigate-disclosure.js +2 -2
- package/dist/recall-navigate.js +2 -2
- package/dist/recall-navigation-operations.js +28 -27
- package/dist/recall-navigation.js +27 -26
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.js +2 -2
- package/dist/resume-bundles.js +5 -5
- package/dist/semantic-consolidation.js +5 -5
- package/dist/semantic-rule-promotion.js +4 -4
- package/dist/semantic-rule-verifier.js +4 -4
- package/dist/source-agent-qualifier.js +4 -4
- package/dist/standup/build.js +4 -4
- package/dist/storage.js +3 -3
- package/dist/summarizer.js +3 -2
- package/dist/support-passport/index.js +30 -29
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +2 -2
- package/dist/transfer/export-okf.js +4 -4
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/verified-recall.js +4 -4
- package/dist/who-knows-cli.js +5 -5
- package/dist/who-knows.js +4 -4
- package/package.json +2 -2
- package/src/config.ts +9 -0
- package/src/local-llm-rate-limit.test.ts +406 -0
- package/src/local-llm-rate-limit.ts +418 -0
- package/src/local-llm.ts +35 -1
- package/src/types.ts +8 -0
- package/src/utils/yaml-scalar.test.ts +37 -0
- package/dist/chunk-466EGMFQ.js.map +0 -1
- package/dist/chunk-DYJHGJZ4.js.map +0 -1
- package/dist/chunk-VPXIKBPX.js.map +0 -1
- /package/dist/{chunk-ZUCNOTNF.js.map → chunk-2NQD4N42.js.map} +0 -0
- /package/dist/{chunk-O6ADSFGY.js.map → chunk-4B7JO3LG.js.map} +0 -0
- /package/dist/{chunk-D42L5SQF.js.map → chunk-5A4QMATS.js.map} +0 -0
- /package/dist/{chunk-SSUTJLCF.js.map → chunk-5ALVWXQG.js.map} +0 -0
- /package/dist/{chunk-N36PBG4A.js.map → chunk-5BNDPLGI.js.map} +0 -0
- /package/dist/{chunk-RTV3DB4I.js.map → chunk-7BFE2UD4.js.map} +0 -0
- /package/dist/{chunk-IVUCF3QO.js.map → chunk-7RTNJ6M2.js.map} +0 -0
- /package/dist/{chunk-VOEBHBWG.js.map → chunk-B2BAZPVQ.js.map} +0 -0
- /package/dist/{chunk-7UHCV5B3.js.map → chunk-B45WUQUU.js.map} +0 -0
- /package/dist/{chunk-2I3OREMR.js.map → chunk-BGG5BP3M.js.map} +0 -0
- /package/dist/{chunk-EML6HJE6.js.map → chunk-BO3F4X5U.js.map} +0 -0
- /package/dist/{chunk-5JXNXKKV.js.map → chunk-BWOLINYY.js.map} +0 -0
- /package/dist/{chunk-V4MB5ONM.js.map → chunk-BWXKJDDH.js.map} +0 -0
- /package/dist/{chunk-OPNSIFFU.js.map → chunk-CA35LZAE.js.map} +0 -0
- /package/dist/{chunk-PDFAX4PK.js.map → chunk-CT5YXLVB.js.map} +0 -0
- /package/dist/{chunk-VUBVNY3W.js.map → chunk-CZURNXGO.js.map} +0 -0
- /package/dist/{chunk-TXE55Z3X.js.map → chunk-D3I4P2SX.js.map} +0 -0
- /package/dist/{chunk-BI3CIDIH.js.map → chunk-DF2G4IXN.js.map} +0 -0
- /package/dist/{chunk-YRKKW4KT.js.map → chunk-DFL3MBL5.js.map} +0 -0
- /package/dist/{chunk-2C5YAVHA.js.map → chunk-DIHJ2QWP.js.map} +0 -0
- /package/dist/{chunk-IBGRIAE5.js.map → chunk-DVE7B2IR.js.map} +0 -0
- /package/dist/{chunk-33G67YZF.js.map → chunk-DWL6YIZH.js.map} +0 -0
- /package/dist/{chunk-YW4ZYASS.js.map → chunk-E2I5FVEU.js.map} +0 -0
- /package/dist/{chunk-ZDUMF4H6.js.map → chunk-ECBAENW7.js.map} +0 -0
- /package/dist/{chunk-QZ7WOCME.js.map → chunk-EQAIXC4Q.js.map} +0 -0
- /package/dist/{chunk-RYYVMZBR.js.map → chunk-ETPIYK4J.js.map} +0 -0
- /package/dist/{chunk-RQMSD4CN.js.map → chunk-FI6CUYMJ.js.map} +0 -0
- /package/dist/{chunk-GXVLEXIX.js.map → chunk-G7QUQILS.js.map} +0 -0
- /package/dist/{chunk-VYVQZRXF.js.map → chunk-H3AW6QZF.js.map} +0 -0
- /package/dist/{chunk-3K6J5GO5.js.map → chunk-H53F22SJ.js.map} +0 -0
- /package/dist/{chunk-NIS3FOMD.js.map → chunk-HGSERGFX.js.map} +0 -0
- /package/dist/{chunk-ZDDDWTC7.js.map → chunk-HJQXHLQQ.js.map} +0 -0
- /package/dist/{chunk-6VWCL2IZ.js.map → chunk-HNZTKT4J.js.map} +0 -0
- /package/dist/{chunk-W3GFSWKE.js.map → chunk-HWMK3QHK.js.map} +0 -0
- /package/dist/{chunk-VVSPHTSU.js.map → chunk-I75TTE7D.js.map} +0 -0
- /package/dist/{chunk-FBNL4BYD.js.map → chunk-IXUEFTK4.js.map} +0 -0
- /package/dist/{chunk-W7NVMERN.js.map → chunk-JT7GSF3T.js.map} +0 -0
- /package/dist/{chunk-LDZD3OQG.js.map → chunk-KD73Q7EN.js.map} +0 -0
- /package/dist/{chunk-HVIXRLXJ.js.map → chunk-KDMZZSL4.js.map} +0 -0
- /package/dist/{chunk-7LCGFUMB.js.map → chunk-KQAFEZQX.js.map} +0 -0
- /package/dist/{chunk-A2IJEV5L.js.map → chunk-M47LM554.js.map} +0 -0
- /package/dist/{chunk-5NO42IYF.js.map → chunk-MKP4W52S.js.map} +0 -0
- /package/dist/{chunk-LT4SMGE4.js.map → chunk-MRRLXXWA.js.map} +0 -0
- /package/dist/{chunk-2JGBX5XY.js.map → chunk-NJAFC244.js.map} +0 -0
- /package/dist/{chunk-OFQOOLQQ.js.map → chunk-NKGI7JWP.js.map} +0 -0
- /package/dist/{chunk-UHBZK3CK.js.map → chunk-OL7LAGBG.js.map} +0 -0
- /package/dist/{chunk-AAQ77A44.js.map → chunk-QNKEMFZY.js.map} +0 -0
- /package/dist/{chunk-XKPLERHV.js.map → chunk-QP5GYLMS.js.map} +0 -0
- /package/dist/{chunk-62CRBI5B.js.map → chunk-ROF3JORU.js.map} +0 -0
- /package/dist/{chunk-OMERTLB7.js.map → chunk-SA7NPDWJ.js.map} +0 -0
- /package/dist/{chunk-QC5TJUSP.js.map → chunk-TRG3A2AW.js.map} +0 -0
- /package/dist/{chunk-CA2R2AD4.js.map → chunk-UFUWKKCV.js.map} +0 -0
- /package/dist/{chunk-DJMGVSST.js.map → chunk-US4WUDQR.js.map} +0 -0
- /package/dist/{chunk-4OEBL5T3.js.map → chunk-V7KGDLBC.js.map} +0 -0
- /package/dist/{chunk-ML7ZJAKJ.js.map → chunk-VBINQDWP.js.map} +0 -0
- /package/dist/{chunk-5FNZ3G6X.js.map → chunk-VKQI47DJ.js.map} +0 -0
- /package/dist/{chunk-UXXLP2PD.js.map → chunk-X3ZLVGGX.js.map} +0 -0
- /package/dist/{chunk-32FVH5OQ.js.map → chunk-XE5QLP3P.js.map} +0 -0
- /package/dist/{chunk-PVHQSLNN.js.map → chunk-Y6IF6H2U.js.map} +0 -0
- /package/dist/{chunk-CFZ4SXXH.js.map → chunk-YEF5AAWN.js.map} +0 -0
- /package/dist/{chunk-NFB5QTOH.js.map → chunk-Z2XUE3VT.js.map} +0 -0
- /package/dist/{chunk-INBB7D3V.js.map → chunk-ZXVLOKN4.js.map} +0 -0
- /package/dist/{first-start-migration-QVD22WWS.js.map → first-start-migration-A6PHIBUQ.js.map} +0 -0
package/dist/who-knows.js
CHANGED
|
@@ -9,8 +9,8 @@ import {
|
|
|
9
9
|
loadWhoKnowsEntities,
|
|
10
10
|
tokenizeTopic,
|
|
11
11
|
validateWhoKnowsInput
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import "./chunk-
|
|
12
|
+
} from "./chunk-4B7JO3LG.js";
|
|
13
|
+
import "./chunk-5A4QMATS.js";
|
|
14
14
|
import "./chunk-UHVXE24Y.js";
|
|
15
15
|
import "./chunk-TRMJ6BYC.js";
|
|
16
16
|
import "./chunk-V4JZCTMK.js";
|
|
@@ -31,8 +31,8 @@ import "./chunk-A7Q43EUZ.js";
|
|
|
31
31
|
import "./chunk-S2V7RUCL.js";
|
|
32
32
|
import "./chunk-PPUNC7UX.js";
|
|
33
33
|
import "./chunk-X3724WL4.js";
|
|
34
|
-
import "./chunk-MRPOZBJC.js";
|
|
35
34
|
import "./chunk-S2HLHAZ4.js";
|
|
35
|
+
import "./chunk-MRPOZBJC.js";
|
|
36
36
|
import "./chunk-DM2T26WE.js";
|
|
37
37
|
import "./chunk-LDXUBPMO.js";
|
|
38
38
|
import "./chunk-2DT7PIDZ.js";
|
|
@@ -76,7 +76,7 @@ import "./chunk-YCVB7X56.js";
|
|
|
76
76
|
import "./chunk-PQVCH2AE.js";
|
|
77
77
|
import "./chunk-PJKTFXE2.js";
|
|
78
78
|
import "./chunk-W5BIQOAJ.js";
|
|
79
|
-
import "./chunk-
|
|
79
|
+
import "./chunk-42KSEX6C.js";
|
|
80
80
|
import "./chunk-YBA52OKG.js";
|
|
81
81
|
import "./chunk-5LRV557D.js";
|
|
82
82
|
import "./chunk-BT7NVCML.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/core",
|
|
3
|
-
"version": "10.0.
|
|
3
|
+
"version": "10.0.26",
|
|
4
4
|
"description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -3347,7 +3347,7 @@
|
|
|
3347
3347
|
"core"
|
|
3348
3348
|
],
|
|
3349
3349
|
"peerDependencies": {
|
|
3350
|
-
"@remnic/coding-graph": "^10.0.
|
|
3350
|
+
"@remnic/coding-graph": "^10.0.26"
|
|
3351
3351
|
},
|
|
3352
3352
|
"peerDependenciesMeta": {
|
|
3353
3353
|
"@remnic/coding-graph": {
|
package/src/config.ts
CHANGED
|
@@ -2801,6 +2801,15 @@ export function parseConfig(
|
|
|
2801
2801
|
typeof cfg.localLlm400TripThreshold === "number" ? cfg.localLlm400TripThreshold : 5,
|
|
2802
2802
|
localLlm400CooldownMs:
|
|
2803
2803
|
typeof cfg.localLlm400CooldownMs === "number" ? cfg.localLlm400CooldownMs : 120_000,
|
|
2804
|
+
// COS-447: rate-limit backoff + process-wide concurrency cap.
|
|
2805
|
+
localLlmRateLimitRetryCount: resolveNonNegativeIntegerConfig(
|
|
2806
|
+
cfg.localLlmRateLimitRetryCount, 3, "localLlmRateLimitRetryCount"),
|
|
2807
|
+
localLlmRateLimitBackoffBaseMs: resolvePositiveIntegerConfig(
|
|
2808
|
+
cfg.localLlmRateLimitBackoffBaseMs, 1_000, "localLlmRateLimitBackoffBaseMs"),
|
|
2809
|
+
localLlmRateLimitBackoffMaxMs: resolvePositiveIntegerConfig(
|
|
2810
|
+
cfg.localLlmRateLimitBackoffMaxMs, 60_000, "localLlmRateLimitBackoffMaxMs"),
|
|
2811
|
+
localLlmMaxConcurrency: resolveNonNegativeIntegerConfig(
|
|
2812
|
+
cfg.localLlmMaxConcurrency, 4, "localLlmMaxConcurrency"),
|
|
2804
2813
|
// Ollama thinking suppression dialect (issue #1996): reasoning_effort value
|
|
2805
2814
|
// for THINKING_SUPPRESSED_OPERATIONS on the ollama backend. Default "none"
|
|
2806
2815
|
// (think off). "" disables injection. Invalid values fall back to "none"
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { parseConfig } from "./config.js";
|
|
5
|
+
import type { LocalLlmChatCompletionOptions } from "./local-llm-helpers.js";
|
|
6
|
+
import {
|
|
7
|
+
LocalLlmConcurrencyLimiter,
|
|
8
|
+
LocalLlmRateLimitSession,
|
|
9
|
+
computeFullJitterBackoffMs,
|
|
10
|
+
computeRateLimitDelayMs,
|
|
11
|
+
endpointBackoffRemainingMs,
|
|
12
|
+
extendEndpointBackoff,
|
|
13
|
+
isRateLimitedResponse,
|
|
14
|
+
parseRetryAfterMs,
|
|
15
|
+
parseTryAgainHintMs,
|
|
16
|
+
resetLocalLlmRateLimitStateForTests,
|
|
17
|
+
resolveRateLimitSettings,
|
|
18
|
+
} from "./local-llm-rate-limit.js";
|
|
19
|
+
import { LocalLlmClient } from "./local-llm.js";
|
|
20
|
+
import type { PluginConfig } from "./types.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* COS-447: a saturated LiteLLM proxy answered `429 No deployments available`
|
|
24
|
+
* and the client returned at once, so caller loops re-fired immediately
|
|
25
|
+
* (2,400+ calls/h). These tests pin backoff, Retry-After, the retry cap, the
|
|
26
|
+
* process-wide endpoint deadline, and the concurrency cap.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const LITELLM_429 = JSON.stringify({
|
|
30
|
+
error: {
|
|
31
|
+
message:
|
|
32
|
+
"No deployments available for selected model, Try again in 60 seconds. Passed model=m. pre-call-checks=True, cooldown_list=['a', 'b']",
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
type RateLimitTestOverrides = Partial<Record<"localLlmUrl", string>> &
|
|
37
|
+
Partial<
|
|
38
|
+
Record<
|
|
39
|
+
| "localLlmRetry5xxCount"
|
|
40
|
+
| "localLlmRateLimitRetryCount"
|
|
41
|
+
| "localLlmRateLimitBackoffMaxMs"
|
|
42
|
+
| "localLlmMaxConcurrency",
|
|
43
|
+
number
|
|
44
|
+
>
|
|
45
|
+
>;
|
|
46
|
+
|
|
47
|
+
function createConfig(overrides: RateLimitTestOverrides = {}): PluginConfig {
|
|
48
|
+
return parseConfig({
|
|
49
|
+
memoryDir: "/tmp/remnic-cos-447-rate-limit-test",
|
|
50
|
+
localLlmEnabled: true,
|
|
51
|
+
localLlmModel: "test-local-model",
|
|
52
|
+
localLlmUrl: "http://127.0.0.1:1234",
|
|
53
|
+
localLlmTimeoutMs: 5_000,
|
|
54
|
+
localLlmRetry5xxCount: 0,
|
|
55
|
+
localLlmRetryBackoffMs: 1,
|
|
56
|
+
localLlmAuthHeader: false,
|
|
57
|
+
localLlmRateLimitRetryCount: 3,
|
|
58
|
+
localLlmRateLimitBackoffBaseMs: 1,
|
|
59
|
+
localLlmRateLimitBackoffMaxMs: 20,
|
|
60
|
+
localLlmMaxConcurrency: 4,
|
|
61
|
+
...overrides,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function ok(content = "ok"): Response {
|
|
66
|
+
return new Response(JSON.stringify({ choices: [{ message: { content } }] }), {
|
|
67
|
+
status: 200,
|
|
68
|
+
headers: { "content-type": "application/json" },
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const DEFAULT_429_BODY = JSON.stringify({ error: { message: "No deployments available for selected model." } });
|
|
73
|
+
|
|
74
|
+
function rateLimited(status = 429, headers: Record<string, string> = {}, body = DEFAULT_429_BODY): Response {
|
|
75
|
+
return new Response(body, { status, headers: { "content-type": "application/json", ...headers } });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function modelsResponse(): Response {
|
|
79
|
+
return new Response(JSON.stringify({ object: "list", data: [{ id: "test-local-model", object: "model" }] }), {
|
|
80
|
+
status: 200,
|
|
81
|
+
headers: { "content-type": "application/json" },
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Route chat completions to `chat`; answer availability probes with a model list. */
|
|
86
|
+
async function withFetch(chat: () => Promise<Response>, fn: () => Promise<void>): Promise<void> {
|
|
87
|
+
const original = globalThis.fetch;
|
|
88
|
+
globalThis.fetch = async (input: string | URL | Request) => {
|
|
89
|
+
const url = input instanceof Request ? input.url : String(input);
|
|
90
|
+
return url.includes("/chat/completions") ? chat() : modelsResponse();
|
|
91
|
+
};
|
|
92
|
+
resetLocalLlmRateLimitStateForTests();
|
|
93
|
+
try {
|
|
94
|
+
await fn();
|
|
95
|
+
} finally {
|
|
96
|
+
globalThis.fetch = original;
|
|
97
|
+
resetLocalLlmRateLimitStateForTests();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function failureStatus(diag: LocalLlmChatCompletionOptions["failureDiag"]): number | undefined {
|
|
102
|
+
const err = diag?.lastError;
|
|
103
|
+
return err instanceof Error && "status" in err && Number.isInteger(err.status) ? Number(err.status) : undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---- pure helpers ----------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
test("classifies 429/503/408 and LiteLLM cooldown bodies as rate limits, not 400s", () => {
|
|
109
|
+
assert.equal(isRateLimitedResponse(429, ""), true);
|
|
110
|
+
assert.equal(isRateLimitedResponse(503, ""), true);
|
|
111
|
+
assert.equal(isRateLimitedResponse(408, ""), true);
|
|
112
|
+
assert.equal(isRateLimitedResponse(500, "No deployments available for selected model"), true);
|
|
113
|
+
assert.equal(isRateLimitedResponse(500, "internal error"), false);
|
|
114
|
+
assert.equal(isRateLimitedResponse(400, "Context Window exceeded ... fallbacks cooldown_list"), false);
|
|
115
|
+
assert.equal(isRateLimitedResponse(502, ""), false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("parses Retry-After as delta-seconds or HTTP-date", () => {
|
|
119
|
+
assert.equal(parseRetryAfterMs("5"), 5_000);
|
|
120
|
+
assert.equal(parseRetryAfterMs(" 1.5 "), 1_500);
|
|
121
|
+
const now = Date.parse("2026-10-26T00:00:00Z");
|
|
122
|
+
assert.equal(parseRetryAfterMs("Mon, 26 Oct 2026 00:00:30 GMT", now), 30_000);
|
|
123
|
+
assert.equal(parseRetryAfterMs("Sun, 25 Oct 2026 23:00:00 GMT", now), 0);
|
|
124
|
+
assert.equal(parseRetryAfterMs("soon"), null);
|
|
125
|
+
assert.equal(parseRetryAfterMs(""), null);
|
|
126
|
+
assert.equal(parseRetryAfterMs(null), null);
|
|
127
|
+
assert.equal(parseRetryAfterMs("999999"), 10 * 60_000, "clamped to the sanity bound");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("parses LiteLLM 'Try again in N seconds' body hint", () => {
|
|
131
|
+
assert.equal(parseTryAgainHintMs(LITELLM_429), 60_000);
|
|
132
|
+
assert.equal(parseTryAgainHintMs("try again in 250ms"), 250);
|
|
133
|
+
assert.equal(parseTryAgainHintMs("Try again in 2 minutes"), 120_000);
|
|
134
|
+
assert.equal(parseTryAgainHintMs("busy"), null);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("full-jitter backoff grows exponentially and is capped", () => {
|
|
138
|
+
const max = () => 0.999999;
|
|
139
|
+
assert.equal(computeFullJitterBackoffMs(1, 100, 10_000, max), 99);
|
|
140
|
+
assert.equal(computeFullJitterBackoffMs(2, 100, 10_000, max), 199);
|
|
141
|
+
assert.equal(computeFullJitterBackoffMs(4, 100, 10_000, max), 799);
|
|
142
|
+
assert.equal(computeFullJitterBackoffMs(20, 100, 10_000, max), 9_999);
|
|
143
|
+
assert.equal(
|
|
144
|
+
computeFullJitterBackoffMs(3, 100, 10_000, () => 0),
|
|
145
|
+
0
|
|
146
|
+
);
|
|
147
|
+
for (let i = 0; i < 200; i += 1) {
|
|
148
|
+
const v = computeFullJitterBackoffMs(3, 100, 10_000);
|
|
149
|
+
assert.ok(v >= 0 && v < 400);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("server hint is a floor on the backoff delay", () => {
|
|
154
|
+
const settings = { backoffBaseMs: 100, backoffMaxMs: 1_000 };
|
|
155
|
+
assert.equal(
|
|
156
|
+
computeRateLimitDelayMs(1, settings, 5_000, () => 0.5),
|
|
157
|
+
5_000
|
|
158
|
+
);
|
|
159
|
+
assert.equal(
|
|
160
|
+
computeRateLimitDelayMs(4, settings, 10, () => 0.5),
|
|
161
|
+
400
|
|
162
|
+
);
|
|
163
|
+
assert.equal(
|
|
164
|
+
computeRateLimitDelayMs(4, settings, null, () => 0.5),
|
|
165
|
+
400
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("settings default sanely and reject invalid values", () => {
|
|
170
|
+
assert.deepEqual(resolveRateLimitSettings({}), {
|
|
171
|
+
retryCount: 3,
|
|
172
|
+
backoffBaseMs: 1_000,
|
|
173
|
+
backoffMaxMs: 60_000,
|
|
174
|
+
maxConcurrency: 4,
|
|
175
|
+
});
|
|
176
|
+
const s = resolveRateLimitSettings({
|
|
177
|
+
localLlmRateLimitRetryCount: -1,
|
|
178
|
+
localLlmRateLimitBackoffBaseMs: 5_000,
|
|
179
|
+
localLlmRateLimitBackoffMaxMs: 10,
|
|
180
|
+
localLlmMaxConcurrency: 0,
|
|
181
|
+
});
|
|
182
|
+
assert.equal(s.retryCount, 3);
|
|
183
|
+
assert.equal(s.backoffMaxMs, 5_000, "max never below base");
|
|
184
|
+
assert.equal(s.maxConcurrency, 0);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("limiter caps concurrency, serves high priority first, and drops aborted waiters", async () => {
|
|
188
|
+
const limiter = new LocalLlmConcurrencyLimiter(1);
|
|
189
|
+
const first = await limiter.acquire();
|
|
190
|
+
assert.ok(first);
|
|
191
|
+
const order: string[] = [];
|
|
192
|
+
const low = limiter.acquire().then((r) => {
|
|
193
|
+
order.push("low");
|
|
194
|
+
return r;
|
|
195
|
+
});
|
|
196
|
+
const aborter = new AbortController();
|
|
197
|
+
const aborted = limiter.acquire({ signal: aborter.signal });
|
|
198
|
+
const high = limiter.acquire({ highPriority: true }).then((r) => {
|
|
199
|
+
order.push("high");
|
|
200
|
+
return r;
|
|
201
|
+
});
|
|
202
|
+
assert.equal(limiter.queued, 3);
|
|
203
|
+
aborter.abort();
|
|
204
|
+
assert.equal(await aborted, null);
|
|
205
|
+
assert.equal(limiter.queued, 2);
|
|
206
|
+
first();
|
|
207
|
+
const highRelease = await high;
|
|
208
|
+
assert.deepEqual(order, ["high"]);
|
|
209
|
+
assert.equal(limiter.inFlight, 1);
|
|
210
|
+
highRelease?.();
|
|
211
|
+
(await low)?.();
|
|
212
|
+
assert.deepEqual(order, ["high", "low"]);
|
|
213
|
+
assert.equal(limiter.inFlight, 0);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ---- client integration ----------------------------------------------------
|
|
217
|
+
|
|
218
|
+
test("429 is retried with backoff and then succeeds", async () => {
|
|
219
|
+
let calls = 0;
|
|
220
|
+
await withFetch(
|
|
221
|
+
async () => {
|
|
222
|
+
calls += 1;
|
|
223
|
+
return calls === 1 ? rateLimited(429) : ok("after backoff");
|
|
224
|
+
},
|
|
225
|
+
async () => {
|
|
226
|
+
const client = new LocalLlmClient(createConfig({ localLlmUrl: "http://127.0.0.1:1001" }));
|
|
227
|
+
const result = await client.chatCompletion([{ role: "user", content: "x" }], { operation: "lcm-summarize" });
|
|
228
|
+
assert.equal(result?.content, "after backoff");
|
|
229
|
+
assert.equal(calls, 2);
|
|
230
|
+
}
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("Retry-After header delays the retry", async () => {
|
|
235
|
+
const stamps: number[] = [];
|
|
236
|
+
await withFetch(
|
|
237
|
+
async () => {
|
|
238
|
+
stamps.push(Date.now());
|
|
239
|
+
return stamps.length === 1 ? rateLimited(503, { "retry-after": "1" }) : ok();
|
|
240
|
+
},
|
|
241
|
+
async () => {
|
|
242
|
+
const client = new LocalLlmClient(
|
|
243
|
+
createConfig({ localLlmUrl: "http://127.0.0.1:1002", localLlmRateLimitBackoffMaxMs: 5_000 })
|
|
244
|
+
);
|
|
245
|
+
const result = await client.chatCompletion([{ role: "user", content: "x" }]);
|
|
246
|
+
assert.equal(result?.content, "ok");
|
|
247
|
+
assert.equal(stamps.length, 2);
|
|
248
|
+
assert.ok(stamps[1] - stamps[0] >= 950, `waited ${stamps[1] - stamps[0]}ms, expected >= 1000ms`);
|
|
249
|
+
}
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("rate-limit retries are capped and report status 429", async () => {
|
|
254
|
+
let calls = 0;
|
|
255
|
+
await withFetch(
|
|
256
|
+
async () => {
|
|
257
|
+
calls += 1;
|
|
258
|
+
return rateLimited(429);
|
|
259
|
+
},
|
|
260
|
+
async () => {
|
|
261
|
+
const client = new LocalLlmClient(
|
|
262
|
+
createConfig({ localLlmUrl: "http://127.0.0.1:1003", localLlmRateLimitRetryCount: 2 })
|
|
263
|
+
);
|
|
264
|
+
const options: LocalLlmChatCompletionOptions = { failureDiag: {} };
|
|
265
|
+
const result = await client.chatCompletion([{ role: "user", content: "x" }], options);
|
|
266
|
+
assert.equal(result, null);
|
|
267
|
+
assert.equal(calls, 3, "one initial attempt + 2 rate-limit retries");
|
|
268
|
+
assert.equal(failureStatus(options.failureDiag), 429);
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("a server hint beyond the max skips the retry and gates every client on that endpoint", async () => {
|
|
274
|
+
let calls = 0;
|
|
275
|
+
await withFetch(
|
|
276
|
+
async () => {
|
|
277
|
+
calls += 1;
|
|
278
|
+
return rateLimited(429, {}, LITELLM_429);
|
|
279
|
+
},
|
|
280
|
+
async () => {
|
|
281
|
+
const url = "http://127.0.0.1:1004";
|
|
282
|
+
const a = new LocalLlmClient(createConfig({ localLlmUrl: url }));
|
|
283
|
+
const b = new LocalLlmClient(createConfig({ localLlmUrl: url }));
|
|
284
|
+
assert.equal(await a.chatCompletion([{ role: "user", content: "x" }]), null);
|
|
285
|
+
assert.equal(calls, 1, "60s hint > 20ms max: no immediate retry");
|
|
286
|
+
assert.ok(endpointBackoffRemainingMs(`${url}/v1/chat/completions`) > 50_000);
|
|
287
|
+
const options: LocalLlmChatCompletionOptions = { failureDiag: {} };
|
|
288
|
+
assert.equal(await b.chatCompletion([{ role: "user", content: "y" }], options), null);
|
|
289
|
+
assert.equal(calls, 1, "second client must not hit the proxy during the shared backoff");
|
|
290
|
+
assert.equal(failureStatus(options.failureDiag), 429);
|
|
291
|
+
}
|
|
292
|
+
);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("503 rate-limit retries do not consume the 5xx retry budget", async () => {
|
|
296
|
+
let calls = 0;
|
|
297
|
+
await withFetch(
|
|
298
|
+
async () => {
|
|
299
|
+
calls += 1;
|
|
300
|
+
return calls < 3 ? rateLimited(503) : ok();
|
|
301
|
+
},
|
|
302
|
+
async () => {
|
|
303
|
+
const client = new LocalLlmClient(
|
|
304
|
+
createConfig({ localLlmUrl: "http://127.0.0.1:1005", localLlmRetry5xxCount: 0 })
|
|
305
|
+
);
|
|
306
|
+
assert.equal((await client.chatCompletion([{ role: "user", content: "x" }]))?.content, "ok");
|
|
307
|
+
assert.equal(calls, 3);
|
|
308
|
+
}
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test("400 responses are not treated as rate limits", async () => {
|
|
313
|
+
let calls = 0;
|
|
314
|
+
await withFetch(
|
|
315
|
+
async () => {
|
|
316
|
+
calls += 1;
|
|
317
|
+
return new Response(JSON.stringify({ error: { message: "Context Window exceeded" } }), { status: 400 });
|
|
318
|
+
},
|
|
319
|
+
async () => {
|
|
320
|
+
const client = new LocalLlmClient(createConfig({ localLlmUrl: "http://127.0.0.1:1006" }));
|
|
321
|
+
assert.equal(await client.chatCompletion([{ role: "user", content: "x" }]), null);
|
|
322
|
+
assert.equal(calls, 1);
|
|
323
|
+
}
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("process-wide concurrency cap bounds in-flight requests across clients", async () => {
|
|
328
|
+
let inFlight = 0;
|
|
329
|
+
let peak = 0;
|
|
330
|
+
await withFetch(
|
|
331
|
+
async () => {
|
|
332
|
+
inFlight += 1;
|
|
333
|
+
peak = Math.max(peak, inFlight);
|
|
334
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
335
|
+
inFlight -= 1;
|
|
336
|
+
return ok();
|
|
337
|
+
},
|
|
338
|
+
async () => {
|
|
339
|
+
const config = createConfig({ localLlmUrl: "http://127.0.0.1:1007", localLlmMaxConcurrency: 2 });
|
|
340
|
+
const clients = [new LocalLlmClient(config), new LocalLlmClient(config), new LocalLlmClient(config)];
|
|
341
|
+
const results = await Promise.all(
|
|
342
|
+
Array.from({ length: 7 }, (_, i) => clients[i % 3].chatCompletion([{ role: "user", content: `q${i}` }]))
|
|
343
|
+
);
|
|
344
|
+
assert.equal(results.filter((r) => r?.content === "ok").length, 7);
|
|
345
|
+
assert.equal(peak, 2);
|
|
346
|
+
}
|
|
347
|
+
);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("rate-limit waits never exceed the caller's request timeout", async () => {
|
|
351
|
+
let calls = 0;
|
|
352
|
+
await withFetch(
|
|
353
|
+
async () => {
|
|
354
|
+
calls += 1;
|
|
355
|
+
return rateLimited(429, { "retry-after": "2" });
|
|
356
|
+
},
|
|
357
|
+
async () => {
|
|
358
|
+
const client = new LocalLlmClient(
|
|
359
|
+
createConfig({ localLlmUrl: "http://127.0.0.1:1008", localLlmRateLimitBackoffMaxMs: 5_000 })
|
|
360
|
+
);
|
|
361
|
+
const started = Date.now();
|
|
362
|
+
const options: LocalLlmChatCompletionOptions = { timeoutMs: 300, failureDiag: {} };
|
|
363
|
+
assert.equal(await client.chatCompletion([{ role: "user", content: "x" }], options), null);
|
|
364
|
+
assert.equal(await client.chatCompletion([{ role: "user", content: "y" }], { timeoutMs: 300 }), null);
|
|
365
|
+
assert.ok(Date.now() - started < 1_000, `took ${Date.now() - started}ms despite a 300ms budget`);
|
|
366
|
+
assert.equal(calls, 1, "a 2s hint beyond the 300ms budget is neither retried nor re-sent");
|
|
367
|
+
assert.equal(failureStatus(options.failureDiag), 429);
|
|
368
|
+
}
|
|
369
|
+
);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("a queued attempt rechecks a cooldown recorded while it waited for a slot", async () => {
|
|
373
|
+
await withFetch(
|
|
374
|
+
async () => ok(),
|
|
375
|
+
async () => {
|
|
376
|
+
const endpoint = "http://127.0.0.1:1009/v1/chat/completions";
|
|
377
|
+
const settings = resolveRateLimitSettings(
|
|
378
|
+
createConfig({ localLlmMaxConcurrency: 1, localLlmRateLimitBackoffMaxMs: 1_000 })
|
|
379
|
+
);
|
|
380
|
+
const holder = await new LocalLlmRateLimitSession(endpoint, settings, false).beforeAttempt();
|
|
381
|
+
assert.equal(holder.kind, "go");
|
|
382
|
+
const queued = new LocalLlmRateLimitSession(endpoint, settings, false, 5_000).beforeAttempt();
|
|
383
|
+
extendEndpointBackoff(endpoint, 60_000);
|
|
384
|
+
if (holder.kind === "go") holder.release();
|
|
385
|
+
const gate = await queued;
|
|
386
|
+
assert.equal(gate.kind, "skip");
|
|
387
|
+
assert.equal(gate.kind === "skip" ? gate.reason : null, "backoff");
|
|
388
|
+
}
|
|
389
|
+
);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("waiting for a concurrency slot is bounded by the wait budget", async () => {
|
|
393
|
+
await withFetch(
|
|
394
|
+
async () => ok(),
|
|
395
|
+
async () => {
|
|
396
|
+
const endpoint = "http://127.0.0.1:1010/v1/chat/completions";
|
|
397
|
+
const settings = resolveRateLimitSettings(createConfig({ localLlmMaxConcurrency: 1 }));
|
|
398
|
+
const holder = await new LocalLlmRateLimitSession(endpoint, settings, false).beforeAttempt();
|
|
399
|
+
const started = Date.now();
|
|
400
|
+
const gate = await new LocalLlmRateLimitSession(endpoint, settings, false, 50).beforeAttempt();
|
|
401
|
+
assert.equal(gate.kind === "skip" ? gate.reason : gate.kind, "queue");
|
|
402
|
+
assert.ok(Date.now() - started < 1_000);
|
|
403
|
+
if (holder.kind === "go") holder.release();
|
|
404
|
+
}
|
|
405
|
+
);
|
|
406
|
+
});
|