nexus-agents 3.8.0 → 3.8.2

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.
Files changed (120) hide show
  1. package/dist/{child-mcp-config-R4GQMDAV.js → child-mcp-config-6NPPPUVY.js} +2 -2
  2. package/dist/{chunk-SQ35C5SD.js → chunk-222AX67B.js} +4 -4
  3. package/dist/{chunk-GJDNMK3G.js → chunk-24KV7AHU.js} +7 -7
  4. package/dist/{chunk-MB2T54VO.js → chunk-2CTS47RO.js} +2 -2
  5. package/dist/{chunk-SJGNFNFT.js → chunk-2IFBPQX2.js} +5 -5
  6. package/dist/{chunk-A5P7Y4SB.js → chunk-2VYSPAPK.js} +3 -3
  7. package/dist/{chunk-P3XQMXIW.js → chunk-7LEFPG22.js} +35 -35
  8. package/dist/{chunk-D55LWKBW.js → chunk-A4DFWNLB.js} +8 -8
  9. package/dist/{chunk-OIBSPO6Q.js → chunk-A7NXSL62.js} +3 -3
  10. package/dist/{chunk-2XFBHREJ.js → chunk-ATOORYKU.js} +2 -2
  11. package/dist/{chunk-C62JS6A7.js → chunk-BIG3SAUY.js} +2 -2
  12. package/dist/{chunk-JIZ7B6FX.js → chunk-CYGJGP52.js} +5 -5
  13. package/dist/{chunk-6Y4QIIQD.js → chunk-DVVYK7AU.js} +2 -2
  14. package/dist/{chunk-H5G4N5UL.js → chunk-E25DVSOH.js} +2 -2
  15. package/dist/{chunk-IJXJGQUY.js → chunk-FG3BJ3EG.js} +4 -4
  16. package/dist/{chunk-GX3QJX7X.js → chunk-FH5GVIUB.js} +2 -2
  17. package/dist/{chunk-PPZTSOSH.js → chunk-FJNCXY4H.js} +8 -8
  18. package/dist/{chunk-NTBGFI5Q.js → chunk-FN4IJNEL.js} +3 -3
  19. package/dist/{chunk-5WGOPKHZ.js → chunk-K4PIBH4M.js} +2 -2
  20. package/dist/{chunk-QWI4VF5L.js → chunk-LIYYP74L.js} +2 -2
  21. package/dist/{chunk-P4QFJ7OX.js → chunk-LLO24WNM.js} +2 -2
  22. package/dist/{chunk-C5O7Z4P7.js → chunk-NDIEJ2IW.js} +2 -2
  23. package/dist/{chunk-RYJ2VU7E.js → chunk-NEEDICVD.js} +2 -2
  24. package/dist/{chunk-DHWEXVCJ.js → chunk-OVDMUJJJ.js} +3 -3
  25. package/dist/{chunk-RYOQ3WJR.js → chunk-PRMJRGTB.js} +2 -2
  26. package/dist/{chunk-6G4FMCXS.js → chunk-Q4N2AMUH.js} +262 -230
  27. package/dist/chunk-Q4N2AMUH.js.map +1 -0
  28. package/dist/{chunk-7ILVQDUT.js → chunk-RH2XESKI.js} +2 -2
  29. package/dist/{chunk-Q4L2DXA4.js → chunk-SDKH4DZS.js} +5 -5
  30. package/dist/{chunk-5LFB77LA.js → chunk-SIAT5KIP.js} +2 -2
  31. package/dist/{chunk-YSE3VQ4A.js → chunk-TSCF7SCI.js} +3 -3
  32. package/dist/{chunk-YUPUHMWP.js → chunk-WN4GPQEW.js} +4 -4
  33. package/dist/{chunk-PQCKQKKP.js → chunk-XF5NBPHS.js} +5 -5
  34. package/dist/{chunk-5CXAYPSU.js → chunk-YCJVIYXU.js} +2 -2
  35. package/dist/{chunk-R3ZNOB3V.js → chunk-YIMAWNZF.js} +2 -2
  36. package/dist/{cli-circuit-breaker-YKY7NCQ5.js → cli-circuit-breaker-GGDXSODU.js} +3 -3
  37. package/dist/cli.js +41 -41
  38. package/dist/{composite-router-63QDM6CQ.js → composite-router-3JTQWELQ.js} +2 -2
  39. package/dist/{consensus-vote-C7QTJJH7.js → consensus-vote-VIVMYSH7.js} +16 -16
  40. package/dist/{context-retriever-DIKQ7QZA.js → context-retriever-CD4M2XZI.js} +8 -8
  41. package/dist/{doctor-deep-JVUEAUAG.js → doctor-deep-DGIEA3BS.js} +3 -3
  42. package/dist/{doctor-live-CT2SKNLI.js → doctor-live-UAXCKP2P.js} +9 -9
  43. package/dist/{expert-bridge-G5WXRNUH.js → expert-bridge-NJ7ROLKV.js} +4 -4
  44. package/dist/{factory-LYRONJ3P.js → factory-HXTWV5M2.js} +9 -9
  45. package/dist/{factory-72QUNWLV.js → factory-YQTOTSAG.js} +5 -5
  46. package/dist/{improvement-review-CJMYGUDM.js → improvement-review-BXMMAIF7.js} +5 -5
  47. package/dist/index.js +30 -30
  48. package/dist/{init-opencode-UATS6Q6W.js → init-opencode-5JABJDNL.js} +6 -6
  49. package/dist/{issue-triage-CZWWJSOT.js → issue-triage-EV64NQRS.js} +6 -6
  50. package/dist/{pr-reviewer-helpers-OLEK46AR.js → pr-reviewer-helpers-MDLLZ2XR.js} +4 -4
  51. package/dist/{registry-command-FKRZFXYQ.js → registry-command-LP7HOKWD.js} +2 -2
  52. package/dist/{repo-security-plan-BULZVX3A.js → repo-security-plan-CJNJQNIM.js} +3 -3
  53. package/dist/{research-helpers-synthesize-LTMSKVUB.js → research-helpers-synthesize-J5ZVG6PP.js} +4 -4
  54. package/dist/{routing-memory-H7DT3HVY.js → routing-memory-UPCTNDFG.js} +2 -2
  55. package/dist/{session-memory-UTPN4MCS.js → session-memory-K67JAZTU.js} +3 -3
  56. package/dist/{setup-command-TGGSIV6F.js → setup-command-UR4CR5UA.js} +14 -14
  57. package/dist/{setup-config-WIXJT5YV.js → setup-config-UEQMYXQ5.js} +3 -3
  58. package/dist/{setup-custom-api-36BFRFWO.js → setup-custom-api-SHMT2T3M.js} +4 -4
  59. package/dist/{tool-memory-YQTO4WUG.js → tool-memory-J2RP3FTT.js} +5 -5
  60. package/dist/{unified-registry-N2BQJSLK.js → unified-registry-2JCOLE4F.js} +10 -10
  61. package/dist/{weather-report-AOX6RULO.js → weather-report-XHGOK6E7.js} +2 -2
  62. package/package.json +1 -1
  63. package/dist/chunk-6G4FMCXS.js.map +0 -1
  64. /package/dist/{child-mcp-config-R4GQMDAV.js.map → child-mcp-config-6NPPPUVY.js.map} +0 -0
  65. /package/dist/{chunk-SQ35C5SD.js.map → chunk-222AX67B.js.map} +0 -0
  66. /package/dist/{chunk-GJDNMK3G.js.map → chunk-24KV7AHU.js.map} +0 -0
  67. /package/dist/{chunk-MB2T54VO.js.map → chunk-2CTS47RO.js.map} +0 -0
  68. /package/dist/{chunk-SJGNFNFT.js.map → chunk-2IFBPQX2.js.map} +0 -0
  69. /package/dist/{chunk-A5P7Y4SB.js.map → chunk-2VYSPAPK.js.map} +0 -0
  70. /package/dist/{chunk-P3XQMXIW.js.map → chunk-7LEFPG22.js.map} +0 -0
  71. /package/dist/{chunk-D55LWKBW.js.map → chunk-A4DFWNLB.js.map} +0 -0
  72. /package/dist/{chunk-OIBSPO6Q.js.map → chunk-A7NXSL62.js.map} +0 -0
  73. /package/dist/{chunk-2XFBHREJ.js.map → chunk-ATOORYKU.js.map} +0 -0
  74. /package/dist/{chunk-C62JS6A7.js.map → chunk-BIG3SAUY.js.map} +0 -0
  75. /package/dist/{chunk-JIZ7B6FX.js.map → chunk-CYGJGP52.js.map} +0 -0
  76. /package/dist/{chunk-6Y4QIIQD.js.map → chunk-DVVYK7AU.js.map} +0 -0
  77. /package/dist/{chunk-H5G4N5UL.js.map → chunk-E25DVSOH.js.map} +0 -0
  78. /package/dist/{chunk-IJXJGQUY.js.map → chunk-FG3BJ3EG.js.map} +0 -0
  79. /package/dist/{chunk-GX3QJX7X.js.map → chunk-FH5GVIUB.js.map} +0 -0
  80. /package/dist/{chunk-PPZTSOSH.js.map → chunk-FJNCXY4H.js.map} +0 -0
  81. /package/dist/{chunk-NTBGFI5Q.js.map → chunk-FN4IJNEL.js.map} +0 -0
  82. /package/dist/{chunk-5WGOPKHZ.js.map → chunk-K4PIBH4M.js.map} +0 -0
  83. /package/dist/{chunk-QWI4VF5L.js.map → chunk-LIYYP74L.js.map} +0 -0
  84. /package/dist/{chunk-P4QFJ7OX.js.map → chunk-LLO24WNM.js.map} +0 -0
  85. /package/dist/{chunk-C5O7Z4P7.js.map → chunk-NDIEJ2IW.js.map} +0 -0
  86. /package/dist/{chunk-RYJ2VU7E.js.map → chunk-NEEDICVD.js.map} +0 -0
  87. /package/dist/{chunk-DHWEXVCJ.js.map → chunk-OVDMUJJJ.js.map} +0 -0
  88. /package/dist/{chunk-RYOQ3WJR.js.map → chunk-PRMJRGTB.js.map} +0 -0
  89. /package/dist/{chunk-7ILVQDUT.js.map → chunk-RH2XESKI.js.map} +0 -0
  90. /package/dist/{chunk-Q4L2DXA4.js.map → chunk-SDKH4DZS.js.map} +0 -0
  91. /package/dist/{chunk-5LFB77LA.js.map → chunk-SIAT5KIP.js.map} +0 -0
  92. /package/dist/{chunk-YSE3VQ4A.js.map → chunk-TSCF7SCI.js.map} +0 -0
  93. /package/dist/{chunk-YUPUHMWP.js.map → chunk-WN4GPQEW.js.map} +0 -0
  94. /package/dist/{chunk-PQCKQKKP.js.map → chunk-XF5NBPHS.js.map} +0 -0
  95. /package/dist/{chunk-5CXAYPSU.js.map → chunk-YCJVIYXU.js.map} +0 -0
  96. /package/dist/{chunk-R3ZNOB3V.js.map → chunk-YIMAWNZF.js.map} +0 -0
  97. /package/dist/{cli-circuit-breaker-YKY7NCQ5.js.map → cli-circuit-breaker-GGDXSODU.js.map} +0 -0
  98. /package/dist/{composite-router-63QDM6CQ.js.map → composite-router-3JTQWELQ.js.map} +0 -0
  99. /package/dist/{consensus-vote-C7QTJJH7.js.map → consensus-vote-VIVMYSH7.js.map} +0 -0
  100. /package/dist/{context-retriever-DIKQ7QZA.js.map → context-retriever-CD4M2XZI.js.map} +0 -0
  101. /package/dist/{doctor-deep-JVUEAUAG.js.map → doctor-deep-DGIEA3BS.js.map} +0 -0
  102. /package/dist/{doctor-live-CT2SKNLI.js.map → doctor-live-UAXCKP2P.js.map} +0 -0
  103. /package/dist/{expert-bridge-G5WXRNUH.js.map → expert-bridge-NJ7ROLKV.js.map} +0 -0
  104. /package/dist/{factory-72QUNWLV.js.map → factory-HXTWV5M2.js.map} +0 -0
  105. /package/dist/{factory-LYRONJ3P.js.map → factory-YQTOTSAG.js.map} +0 -0
  106. /package/dist/{improvement-review-CJMYGUDM.js.map → improvement-review-BXMMAIF7.js.map} +0 -0
  107. /package/dist/{init-opencode-UATS6Q6W.js.map → init-opencode-5JABJDNL.js.map} +0 -0
  108. /package/dist/{issue-triage-CZWWJSOT.js.map → issue-triage-EV64NQRS.js.map} +0 -0
  109. /package/dist/{pr-reviewer-helpers-OLEK46AR.js.map → pr-reviewer-helpers-MDLLZ2XR.js.map} +0 -0
  110. /package/dist/{registry-command-FKRZFXYQ.js.map → registry-command-LP7HOKWD.js.map} +0 -0
  111. /package/dist/{repo-security-plan-BULZVX3A.js.map → repo-security-plan-CJNJQNIM.js.map} +0 -0
  112. /package/dist/{research-helpers-synthesize-LTMSKVUB.js.map → research-helpers-synthesize-J5ZVG6PP.js.map} +0 -0
  113. /package/dist/{routing-memory-H7DT3HVY.js.map → routing-memory-UPCTNDFG.js.map} +0 -0
  114. /package/dist/{session-memory-UTPN4MCS.js.map → session-memory-K67JAZTU.js.map} +0 -0
  115. /package/dist/{setup-command-TGGSIV6F.js.map → setup-command-UR4CR5UA.js.map} +0 -0
  116. /package/dist/{setup-config-WIXJT5YV.js.map → setup-config-UEQMYXQ5.js.map} +0 -0
  117. /package/dist/{setup-custom-api-36BFRFWO.js.map → setup-custom-api-SHMT2T3M.js.map} +0 -0
  118. /package/dist/{tool-memory-YQTO4WUG.js.map → tool-memory-J2RP3FTT.js.map} +0 -0
  119. /package/dist/{unified-registry-N2BQJSLK.js.map → unified-registry-2JCOLE4F.js.map} +0 -0
  120. /package/dist/{weather-report-AOX6RULO.js.map → weather-report-XHGOK6E7.js.map} +0 -0
@@ -5,34 +5,34 @@ import {
5
5
  planOptionalParams,
6
6
  requireApiKey,
7
7
  validateApiKeyPresence
8
- } from "./chunk-RYOQ3WJR.js";
8
+ } from "./chunk-PRMJRGTB.js";
9
9
  import {
10
10
  CUSTOM_API_DEFAULT_MODEL
11
- } from "./chunk-C62JS6A7.js";
11
+ } from "./chunk-BIG3SAUY.js";
12
12
  import {
13
13
  CUSTOM_API_BASE_URL_ENV,
14
14
  PROVIDER_ENV_KEYS,
15
15
  assertCustomApiHostResolvesPublic,
16
16
  validateCustomApiBaseUrl
17
- } from "./chunk-2XFBHREJ.js";
17
+ } from "./chunk-ATOORYKU.js";
18
18
  import {
19
19
  nexusMkdtempSync
20
20
  } from "./chunk-SNJNRDKZ.js";
21
21
  import {
22
22
  getAvailabilityCache
23
- } from "./chunk-5CXAYPSU.js";
23
+ } from "./chunk-YCJVIYXU.js";
24
24
  import {
25
25
  extractSystemPrompt
26
26
  } from "./chunk-QZUG667L.js";
27
27
  import {
28
28
  generateHyphenId
29
- } from "./chunk-GX3QJX7X.js";
29
+ } from "./chunk-FH5GVIUB.js";
30
30
  import {
31
31
  CliCircuitBreaker,
32
32
  DEFAULT_CIRCUIT_BREAKER_CONFIG,
33
33
  getCliCircuitBreakerSnapshot,
34
34
  getDefaultCliCircuitBreakerRegistry
35
- } from "./chunk-7ILVQDUT.js";
35
+ } from "./chunk-RH2XESKI.js";
36
36
  import {
37
37
  CLI_SUBPROCESS_TIMEOUTS,
38
38
  CLI_TIMEOUTS,
@@ -69,7 +69,7 @@ import {
69
69
  ok,
70
70
  parseRetryAfterMs,
71
71
  resolveCliAlias
72
- } from "./chunk-YSE3VQ4A.js";
72
+ } from "./chunk-TSCF7SCI.js";
73
73
 
74
74
  // src/cli-adapters/cli-to-model-adapter.ts
75
75
  var CliToModelAdapter = class {
@@ -221,6 +221,210 @@ function createCliToModelAdapter(cliAdapter, config) {
221
221
  return new CliToModelAdapter(cliAdapter, config);
222
222
  }
223
223
 
224
+ // src/cli-adapters/capacity-tracker.ts
225
+ var DEFAULT_TOKEN_LIMITS = {
226
+ claude: 1e5,
227
+ // Claude API Build tier
228
+ gemini: 1e6,
229
+ // Gemini Pro generous limits
230
+ codex: 5e5,
231
+ // OpenAI tier 1
232
+ opencode: 5e5
233
+ // Multi-provider proxy
234
+ };
235
+ var DEFAULT_REQUEST_LIMITS = {
236
+ claude: 50,
237
+ // Claude API Build tier
238
+ gemini: 60,
239
+ // Gemini Pro
240
+ codex: 500,
241
+ // OpenAI tier 1
242
+ opencode: 300
243
+ // Multi-provider proxy
244
+ };
245
+ var RATE_LIMIT_WINDOW_MS = 6e4;
246
+ function getDefaultConfig(cli) {
247
+ const envPrefix = `NEXUS_${cli.toUpperCase()}`;
248
+ const tokenEnv = process.env[`${envPrefix}_TOKEN_LIMIT`];
249
+ const requestEnv = process.env[`${envPrefix}_REQUEST_LIMIT`];
250
+ return {
251
+ tokenLimit: tokenEnv !== void 0 && tokenEnv !== "" ? parseInt(tokenEnv, 10) : DEFAULT_TOKEN_LIMITS[cli],
252
+ requestLimit: requestEnv !== void 0 && requestEnv !== "" ? parseInt(requestEnv, 10) : DEFAULT_REQUEST_LIMITS[cli],
253
+ windowMs: RATE_LIMIT_WINDOW_MS
254
+ };
255
+ }
256
+ var CapacityTracker = class {
257
+ config;
258
+ usageHistory;
259
+ requestCount;
260
+ windowStart;
261
+ /**
262
+ * Timestamps of every recorded request (#3026 finding 4).
263
+ *
264
+ * Pre-fix, `requestCount` was a plain counter reset only by the
265
+ * tumbling-window branch of `pruneOldEntries`. Under continuous
266
+ * traffic across a window boundary, that branch drops requests that
267
+ * are still inside the *sliding* window — e.g. with windowMs=60s, a
268
+ * request at t=59s followed by one at t=61s would tumbling-reset
269
+ * the counter to 1 even though the t=59s request is still inside
270
+ * the [1s, 61s] window. The downstream `remainingRequests === 0`
271
+ * exhaustion check fired prematurely (or too late) depending on
272
+ * burst patterns.
273
+ *
274
+ * Counting via a per-request timestamp array that's pruned the same
275
+ * way as `usageHistory` keeps the two views consistent. Every
276
+ * `recordUsage` pushes here; `requestCount` is derived from
277
+ * `.length` after pruning.
278
+ */
279
+ requestTimestamps;
280
+ /**
281
+ * Whether this process has recorded even one request against this adapter
282
+ * (#4374). Sticky: pruning the usage window back to empty does NOT clear it,
283
+ * because the process has still seen the adapter work — it simply has no
284
+ * recent samples. Without this flag a never-used tracker is indistinguishable
285
+ * from an idle healthy one, and both report full remaining capacity.
286
+ */
287
+ hasObserved = false;
288
+ /**
289
+ * Provider-asserted quota exhaustion (#4456), with the horizon the provider
290
+ * gave. Null until a provider says so — never inferred from local counting.
291
+ */
292
+ quotaExhaustedUntil = null;
293
+ constructor(config) {
294
+ this.config = config;
295
+ this.usageHistory = [];
296
+ this.requestCount = 0;
297
+ this.requestTimestamps = [];
298
+ this.windowStart = getTimeProvider().now();
299
+ }
300
+ /**
301
+ * Records token usage from a completed request.
302
+ */
303
+ recordUsage(usage) {
304
+ const now = getTimeProvider().now();
305
+ this.hasObserved = true;
306
+ this.pruneOldEntries(now);
307
+ this.quotaExhaustedUntil = null;
308
+ this.requestTimestamps.push(now);
309
+ this.requestCount = this.requestTimestamps.length;
310
+ if (usage !== void 0) {
311
+ const tokens = usage.totalTokens ?? usage.inputTokens + usage.outputTokens;
312
+ this.usageHistory.push({
313
+ timestamp: now,
314
+ tokens
315
+ });
316
+ }
317
+ }
318
+ /**
319
+ * Gets current capacity status based on tracked usage.
320
+ */
321
+ getCapacity() {
322
+ const now = getTimeProvider().now();
323
+ this.pruneOldEntries(now);
324
+ const usedTokens = this.usageHistory.reduce((sum, entry) => sum + entry.tokens, 0);
325
+ const remainingTokens = Math.max(0, this.config.tokenLimit - usedTokens);
326
+ const remainingRequests = Math.max(0, this.config.requestLimit - this.requestCount);
327
+ const tokenUtilization = usedTokens / this.config.tokenLimit * 100;
328
+ const requestUtilization = this.requestCount / this.config.requestLimit * 100;
329
+ const utilizationPercent = clampPercent(Math.max(tokenUtilization, requestUtilization));
330
+ const rateLimited = remainingTokens === 0 || remainingRequests === 0;
331
+ const resetTime = new Date(this.windowStart + this.config.windowMs);
332
+ if (this.quotaExhaustedUntil !== null && now >= this.quotaExhaustedUntil) {
333
+ this.quotaExhaustedUntil = null;
334
+ }
335
+ const quotaExhausted = this.quotaExhaustedUntil !== null;
336
+ return {
337
+ remainingTokens,
338
+ remainingRequests,
339
+ resetTime,
340
+ utilizationPercent: Math.round(utilizationPercent * 100) / 100,
341
+ rateLimited,
342
+ // #4456: identical value, deprecated name. Kept so the rename is not a
343
+ // breaking change to an exported type; removal is scheduled for the next
344
+ // major, and `no-restricted-syntax` blocks new reads in the meantime.
345
+ exhausted: rateLimited,
346
+ quotaExhausted,
347
+ ...this.quotaExhaustedUntil !== null ? { quotaResetAt: new Date(this.quotaExhaustedUntil) } : {},
348
+ observed: this.hasObserved
349
+ };
350
+ }
351
+ /**
352
+ * Record a PROVIDER's assertion that durable quota is exhausted (#4456).
353
+ *
354
+ * Only a `retryAfterMs` longer than the local window counts. A shorter one
355
+ * is an ordinary per-minute throttle, which {@link CapacityStatus.rateLimited}
356
+ * already covers — treating it as quota exhaustion would empty the candidate
357
+ * pool for a condition that clears in under a minute, the failure mode that
358
+ * kept #4373's enforcement stage switched off.
359
+ *
360
+ * Without a `retryAfterMs` the provider gave no horizon, so nothing is
361
+ * asserted: an exhaustion with no end is not distinguishable here from a
362
+ * transient error, and inventing a horizon would manufacture a measurement.
363
+ *
364
+ * @param retryAfterMs - The provider's stated wait, from `retry-after`.
365
+ * @returns true when the assertion was durable enough to record.
366
+ */
367
+ recordProviderQuotaExhaustion(retryAfterMs) {
368
+ if (retryAfterMs === void 0 || retryAfterMs <= this.config.windowMs) return false;
369
+ this.hasObserved = true;
370
+ this.quotaExhaustedUntil = getTimeProvider().now() + retryAfterMs;
371
+ return true;
372
+ }
373
+ /**
374
+ * Gets time until the rate limit window resets.
375
+ */
376
+ getTimeUntilReset() {
377
+ const now = getTimeProvider().now();
378
+ const resetTime = this.windowStart + this.config.windowMs;
379
+ return Math.max(0, resetTime - now);
380
+ }
381
+ /**
382
+ * Resets all tracked usage (for testing or manual reset).
383
+ */
384
+ reset() {
385
+ this.usageHistory.length = 0;
386
+ this.requestTimestamps.length = 0;
387
+ this.requestCount = 0;
388
+ this.windowStart = getTimeProvider().now();
389
+ this.quotaExhaustedUntil = null;
390
+ }
391
+ /**
392
+ * Updates configuration (e.g., after receiving actual rate limit info).
393
+ */
394
+ updateConfig(partial) {
395
+ Object.assign(this.config, partial);
396
+ }
397
+ /**
398
+ * Gets current configuration.
399
+ */
400
+ getConfig() {
401
+ return { ...this.config };
402
+ }
403
+ /**
404
+ * Removes entries older than the window duration.
405
+ */
406
+ pruneOldEntries(now) {
407
+ const cutoff = now - this.config.windowMs;
408
+ while (this.usageHistory[0] !== void 0 && this.usageHistory[0].timestamp < cutoff) {
409
+ this.usageHistory.shift();
410
+ }
411
+ while (this.requestTimestamps[0] !== void 0 && this.requestTimestamps[0] < cutoff) {
412
+ this.requestTimestamps.shift();
413
+ }
414
+ this.requestCount = this.requestTimestamps.length;
415
+ const earliestRequest = this.requestTimestamps[0];
416
+ const earliestUsage = this.usageHistory[0]?.timestamp;
417
+ if (earliestRequest === void 0 && earliestUsage === void 0) {
418
+ this.windowStart = now;
419
+ } else {
420
+ this.windowStart = Math.min(earliestRequest ?? Infinity, earliestUsage ?? Infinity);
421
+ }
422
+ }
423
+ };
424
+ function createCapacityTracker(cli) {
425
+ return new CapacityTracker(getDefaultConfig(cli));
426
+ }
427
+
224
428
  // src/cli-adapters/model-to-cli-adapter.ts
225
429
  var DEFAULT_CONTEXT_WINDOW = FALLBACK_CONTEXT_WINDOW;
226
430
  var NEUTRAL_CAPABILITIES = {
@@ -236,10 +440,18 @@ var ModelToCliAdapter = class {
236
440
  transport = API_TRANSPORT;
237
441
  capabilities;
238
442
  modelAdapter;
443
+ /**
444
+ * Per-arm capacity state (#4602). The same canonical `CapacityTracker` the
445
+ * subprocess adapters use — this bridge previously answered `getCapacity()`
446
+ * from literals, so no API/SDK arm could report a quota signal at all.
447
+ * Per-instance, so one arm's exhaustion never speaks for another's.
448
+ */
449
+ capacityTracker;
239
450
  constructor(modelAdapter, config) {
240
451
  this.modelAdapter = modelAdapter;
241
452
  this.name = config.name;
242
453
  this.capabilities = config.capabilities ?? NEUTRAL_CAPABILITIES;
454
+ this.capacityTracker = createCapacityTracker(config.name);
243
455
  }
244
456
  /** Build a single-turn CompletionRequest from a CliTask. */
245
457
  toCompletionRequest(task, options) {
@@ -284,24 +496,50 @@ var ModelToCliAdapter = class {
284
496
  * Convert a ModelError to a CliError. Rate-limit text → RATE_LIMITED
285
497
  * (retryable); everything else → EXECUTION_ERROR (non-retryable) so the
286
498
  * routing/circuit layers see a real, typed failure.
499
+ *
500
+ * #4602: parses `retryAfterMs` off the message, mirroring
501
+ * `base-adapter.ts`'s `createError`. Pre-fix this bridge classified
502
+ * RATE_LIMITED and then threw the provider's stated horizon away — and that
503
+ * horizon is the sole input `CapacityTracker.recordProviderQuotaExhaustion`
504
+ * accepts, so the API/SDK arms could never assert quota exhaustion.
505
+ *
506
+ * Only parsed on a retryable error, as on the subprocess path: a wait hint
507
+ * inside a 500 body is not a rate-limit assertion.
287
508
  */
288
509
  toCliError(error) {
289
510
  const rateLimited = isRateLimitText(error.message);
290
511
  const code = rateLimited ? "RATE_LIMITED" : "EXECUTION_ERROR";
512
+ const retryAfterMs = rateLimited ? parseRetryAfterMs(error.message) : void 0;
291
513
  const base = {
292
514
  code,
293
515
  message: error.message,
294
516
  cli: this.name,
295
- retryable: rateLimited
517
+ retryable: rateLimited,
518
+ ...retryAfterMs !== void 0 && { retryAfterMs }
296
519
  };
297
520
  return error.cause instanceof Error ? { ...base, cause: error.cause } : base;
298
521
  }
522
+ /**
523
+ * Feed a provider's own rate-limit assertion into capacity tracking (#4602).
524
+ *
525
+ * The API-side counterpart of `BaseCliAdapter.recordQuotaSignal`. The
526
+ * tracker — not this adapter — decides whether the stated wait is long
527
+ * enough to mean durable quota rather than a per-minute throttle.
528
+ */
529
+ recordQuotaSignal(error) {
530
+ if (error.code !== "RATE_LIMITED") return;
531
+ this.capacityTracker.recordProviderQuotaExhaustion(error.retryAfterMs);
532
+ }
299
533
  async execute(task, options) {
300
534
  const result = await this.modelAdapter.complete(this.toCompletionRequest(task, options));
301
535
  if (!result.ok) {
302
- return err(this.toCliError(result.error));
536
+ const cliError = this.toCliError(result.error);
537
+ this.recordQuotaSignal(cliError);
538
+ return err(cliError);
303
539
  }
304
- return ok(this.toCliResponse(result.value));
540
+ const response = this.toCliResponse(result.value);
541
+ this.capacityTracker.recordUsage(response.usage);
542
+ return ok(response);
305
543
  }
306
544
  /** Health is derived from the model adapter's config validation. */
307
545
  async healthCheck() {
@@ -315,25 +553,22 @@ var ModelToCliAdapter = class {
315
553
  });
316
554
  }
317
555
  /**
318
- * API adapters don't expose subprocess-style rate windows; report
319
- * non-exhausted capacity. Real rate-limit signals surface via execute()'s
320
- * RATE_LIMITED error, which the resilience layer acts on.
556
+ * Report this arm's capacity from its `CapacityTracker` (#4602).
321
557
  *
322
- * #4374: `observed: false` — the infinities below are a stand-in for "this
323
- * adapter has no rate window to report", not a measurement. Reporting them as
324
- * observed would tell consumers we had checked and found unlimited capacity.
558
+ * Previously this returned literals: `POSITIVE_INFINITY` remaining,
559
+ * `quotaExhausted: false`, `observed: false`. The `observed: false` half was
560
+ * honest (#4374 — nothing had been observed), but `quotaExhausted: false`
561
+ * asserted a measurement that never happened, and since `assessCapacity`
562
+ * reaches `'exhausted'` only through `quotaExhausted`, it made every
563
+ * API/SDK arm unexcludable for quota by construction — a check that cannot
564
+ * fail, whatever the provider said.
565
+ *
566
+ * The tracker keeps the honest empty case: until this arm has served a call
567
+ * or a provider has asserted a horizon, it reports `observed: false` and the
568
+ * stage grades it `unmeasured`, never `healthy`.
325
569
  */
326
570
  getCapacity() {
327
- return Promise.resolve({
328
- remainingTokens: Number.POSITIVE_INFINITY,
329
- remainingRequests: Number.POSITIVE_INFINITY,
330
- resetTime: /* @__PURE__ */ new Date(0),
331
- utilizationPercent: 0,
332
- rateLimited: false,
333
- exhausted: false,
334
- quotaExhausted: false,
335
- observed: false
336
- });
571
+ return Promise.resolve(this.capacityTracker.getCapacity());
337
572
  }
338
573
  getVersion() {
339
574
  return Promise.resolve("api");
@@ -1620,209 +1855,6 @@ function getTimeoutForTaskAuto(cli, taskDescription) {
1620
1855
  return getAdaptiveTimeout(cli, taskDescription);
1621
1856
  }
1622
1857
 
1623
- // src/cli-adapters/capacity-tracker.ts
1624
- var DEFAULT_TOKEN_LIMITS = {
1625
- claude: 1e5,
1626
- // Claude API Build tier
1627
- gemini: 1e6,
1628
- // Gemini Pro generous limits
1629
- codex: 5e5,
1630
- // OpenAI tier 1
1631
- opencode: 5e5
1632
- // Multi-provider proxy
1633
- };
1634
- var DEFAULT_REQUEST_LIMITS = {
1635
- claude: 50,
1636
- // Claude API Build tier
1637
- gemini: 60,
1638
- // Gemini Pro
1639
- codex: 500,
1640
- // OpenAI tier 1
1641
- opencode: 300
1642
- // Multi-provider proxy
1643
- };
1644
- var RATE_LIMIT_WINDOW_MS = 6e4;
1645
- function getDefaultConfig(cli) {
1646
- const envPrefix = `NEXUS_${cli.toUpperCase()}`;
1647
- const tokenEnv = process.env[`${envPrefix}_TOKEN_LIMIT`];
1648
- const requestEnv = process.env[`${envPrefix}_REQUEST_LIMIT`];
1649
- return {
1650
- tokenLimit: tokenEnv !== void 0 && tokenEnv !== "" ? parseInt(tokenEnv, 10) : DEFAULT_TOKEN_LIMITS[cli],
1651
- requestLimit: requestEnv !== void 0 && requestEnv !== "" ? parseInt(requestEnv, 10) : DEFAULT_REQUEST_LIMITS[cli],
1652
- windowMs: RATE_LIMIT_WINDOW_MS
1653
- };
1654
- }
1655
- var CapacityTracker = class {
1656
- config;
1657
- usageHistory;
1658
- requestCount;
1659
- windowStart;
1660
- /**
1661
- * Timestamps of every recorded request (#3026 finding 4).
1662
- *
1663
- * Pre-fix, `requestCount` was a plain counter reset only by the
1664
- * tumbling-window branch of `pruneOldEntries`. Under continuous
1665
- * traffic across a window boundary, that branch drops requests that
1666
- * are still inside the *sliding* window — e.g. with windowMs=60s, a
1667
- * request at t=59s followed by one at t=61s would tumbling-reset
1668
- * the counter to 1 even though the t=59s request is still inside
1669
- * the [1s, 61s] window. The downstream `remainingRequests === 0`
1670
- * exhaustion check fired prematurely (or too late) depending on
1671
- * burst patterns.
1672
- *
1673
- * Counting via a per-request timestamp array that's pruned the same
1674
- * way as `usageHistory` keeps the two views consistent. Every
1675
- * `recordUsage` pushes here; `requestCount` is derived from
1676
- * `.length` after pruning.
1677
- */
1678
- requestTimestamps;
1679
- /**
1680
- * Whether this process has recorded even one request against this adapter
1681
- * (#4374). Sticky: pruning the usage window back to empty does NOT clear it,
1682
- * because the process has still seen the adapter work — it simply has no
1683
- * recent samples. Without this flag a never-used tracker is indistinguishable
1684
- * from an idle healthy one, and both report full remaining capacity.
1685
- */
1686
- hasObserved = false;
1687
- /**
1688
- * Provider-asserted quota exhaustion (#4456), with the horizon the provider
1689
- * gave. Null until a provider says so — never inferred from local counting.
1690
- */
1691
- quotaExhaustedUntil = null;
1692
- constructor(config) {
1693
- this.config = config;
1694
- this.usageHistory = [];
1695
- this.requestCount = 0;
1696
- this.requestTimestamps = [];
1697
- this.windowStart = getTimeProvider().now();
1698
- }
1699
- /**
1700
- * Records token usage from a completed request.
1701
- */
1702
- recordUsage(usage) {
1703
- const now = getTimeProvider().now();
1704
- this.hasObserved = true;
1705
- this.pruneOldEntries(now);
1706
- this.quotaExhaustedUntil = null;
1707
- this.requestTimestamps.push(now);
1708
- this.requestCount = this.requestTimestamps.length;
1709
- if (usage !== void 0) {
1710
- const tokens = usage.totalTokens ?? usage.inputTokens + usage.outputTokens;
1711
- this.usageHistory.push({
1712
- timestamp: now,
1713
- tokens
1714
- });
1715
- }
1716
- }
1717
- /**
1718
- * Gets current capacity status based on tracked usage.
1719
- */
1720
- getCapacity() {
1721
- const now = getTimeProvider().now();
1722
- this.pruneOldEntries(now);
1723
- const usedTokens = this.usageHistory.reduce((sum, entry) => sum + entry.tokens, 0);
1724
- const remainingTokens = Math.max(0, this.config.tokenLimit - usedTokens);
1725
- const remainingRequests = Math.max(0, this.config.requestLimit - this.requestCount);
1726
- const tokenUtilization = usedTokens / this.config.tokenLimit * 100;
1727
- const requestUtilization = this.requestCount / this.config.requestLimit * 100;
1728
- const utilizationPercent = clampPercent(Math.max(tokenUtilization, requestUtilization));
1729
- const rateLimited = remainingTokens === 0 || remainingRequests === 0;
1730
- const resetTime = new Date(this.windowStart + this.config.windowMs);
1731
- if (this.quotaExhaustedUntil !== null && now >= this.quotaExhaustedUntil) {
1732
- this.quotaExhaustedUntil = null;
1733
- }
1734
- const quotaExhausted = this.quotaExhaustedUntil !== null;
1735
- return {
1736
- remainingTokens,
1737
- remainingRequests,
1738
- resetTime,
1739
- utilizationPercent: Math.round(utilizationPercent * 100) / 100,
1740
- rateLimited,
1741
- // #4456: identical value, deprecated name. Kept so the rename is not a
1742
- // breaking change to an exported type; removal is scheduled for the next
1743
- // major, and `no-restricted-syntax` blocks new reads in the meantime.
1744
- exhausted: rateLimited,
1745
- quotaExhausted,
1746
- ...this.quotaExhaustedUntil !== null ? { quotaResetAt: new Date(this.quotaExhaustedUntil) } : {},
1747
- observed: this.hasObserved
1748
- };
1749
- }
1750
- /**
1751
- * Record a PROVIDER's assertion that durable quota is exhausted (#4456).
1752
- *
1753
- * Only a `retryAfterMs` longer than the local window counts. A shorter one
1754
- * is an ordinary per-minute throttle, which {@link CapacityStatus.rateLimited}
1755
- * already covers — treating it as quota exhaustion would empty the candidate
1756
- * pool for a condition that clears in under a minute, the failure mode that
1757
- * kept #4373's enforcement stage switched off.
1758
- *
1759
- * Without a `retryAfterMs` the provider gave no horizon, so nothing is
1760
- * asserted: an exhaustion with no end is not distinguishable here from a
1761
- * transient error, and inventing a horizon would manufacture a measurement.
1762
- *
1763
- * @param retryAfterMs - The provider's stated wait, from `retry-after`.
1764
- * @returns true when the assertion was durable enough to record.
1765
- */
1766
- recordProviderQuotaExhaustion(retryAfterMs) {
1767
- if (retryAfterMs === void 0 || retryAfterMs <= this.config.windowMs) return false;
1768
- this.quotaExhaustedUntil = getTimeProvider().now() + retryAfterMs;
1769
- return true;
1770
- }
1771
- /**
1772
- * Gets time until the rate limit window resets.
1773
- */
1774
- getTimeUntilReset() {
1775
- const now = getTimeProvider().now();
1776
- const resetTime = this.windowStart + this.config.windowMs;
1777
- return Math.max(0, resetTime - now);
1778
- }
1779
- /**
1780
- * Resets all tracked usage (for testing or manual reset).
1781
- */
1782
- reset() {
1783
- this.usageHistory.length = 0;
1784
- this.requestTimestamps.length = 0;
1785
- this.requestCount = 0;
1786
- this.windowStart = getTimeProvider().now();
1787
- this.quotaExhaustedUntil = null;
1788
- }
1789
- /**
1790
- * Updates configuration (e.g., after receiving actual rate limit info).
1791
- */
1792
- updateConfig(partial) {
1793
- Object.assign(this.config, partial);
1794
- }
1795
- /**
1796
- * Gets current configuration.
1797
- */
1798
- getConfig() {
1799
- return { ...this.config };
1800
- }
1801
- /**
1802
- * Removes entries older than the window duration.
1803
- */
1804
- pruneOldEntries(now) {
1805
- const cutoff = now - this.config.windowMs;
1806
- while (this.usageHistory[0] !== void 0 && this.usageHistory[0].timestamp < cutoff) {
1807
- this.usageHistory.shift();
1808
- }
1809
- while (this.requestTimestamps[0] !== void 0 && this.requestTimestamps[0] < cutoff) {
1810
- this.requestTimestamps.shift();
1811
- }
1812
- this.requestCount = this.requestTimestamps.length;
1813
- const earliestRequest = this.requestTimestamps[0];
1814
- const earliestUsage = this.usageHistory[0]?.timestamp;
1815
- if (earliestRequest === void 0 && earliestUsage === void 0) {
1816
- this.windowStart = now;
1817
- } else {
1818
- this.windowStart = Math.min(earliestRequest ?? Infinity, earliestUsage ?? Infinity);
1819
- }
1820
- }
1821
- };
1822
- function createCapacityTracker(cli) {
1823
- return new CapacityTracker(getDefaultConfig(cli));
1824
- }
1825
-
1826
1858
  // src/utils/async-utils.ts
1827
1859
  function sleep(ms) {
1828
1860
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -4824,4 +4856,4 @@ export {
4824
4856
  SdkAdapter,
4825
4857
  createAutoAdapter
4826
4858
  };
4827
- //# sourceMappingURL=chunk-6G4FMCXS.js.map
4859
+ //# sourceMappingURL=chunk-Q4N2AMUH.js.map