github-issue-tower-defence-management 1.69.7 → 1.69.8

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/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [1.69.8](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.69.7...v1.69.8) (2026-06-04)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **rotation:** bridge snapshot sevenDayReset into modelWeeklyLimits so 7d deadline sort works on proxy path ([b56e46d](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/commit/b56e46ddd6b1ec61036ca42ea517a0bec55841b3)), closes [#733](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/733)
7
+
1
8
  ## [1.69.7](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.69.6...v1.69.7) (2026-06-04)
2
9
 
3
10
 
package/README.md CHANGED
@@ -218,7 +218,7 @@ When `claudeCodeOauthTokenListJsonPath` is set, `startDaemon` distributes prepar
218
218
 
219
219
  2. Local reverse proxy (`127.0.0.1:8787`): on each `startDaemon` run, the daemon TCP-probes the port. If nothing responds, it spawns a detached child running `bin/adapter/proxy/proxyEntry.js`. The proxy forwards every request to `api.anthropic.com`, observes the `anthropic-ratelimit-unified-*` response headers, and writes them to a per-token cache file at `${XDG_CACHE_HOME:-~/.cache}/tdpm/ratelimit/<sha256-of-token>.json`. The cache file stores the latest 5-hour and 7-day utilization, reset epochs, and the unified, 5-hour, and 7-day statuses (used to detect `blocked` and `rejected` state per window). In addition to the unified headers, the proxy inspects the streamed response body for `rate_limit` events (objects carrying `rateLimitType`, `status`, and `resetsAt`) and records the model-specific weekly limits — at minimum `seven_day_sonnet` and `seven_day` — per token. These model-specific weekly limits are exposed only in the response body, never in the unified headers, so a token can appear healthy on `anthropic-ratelimit-unified-7d-*` while its Sonnet weekly limit is exhausted.
220
220
 
221
- 3. Selection: before spawning each `aw` job, the daemon reads every token's cache file. A cached observation is treated as expired once its reset epoch has passed: when the current time is past the 5-hour reset, that token's 5-hour utilization is treated as `0` and any 5-hour-window rejection is cleared; likewise, when the 7-day reset has passed, the 7-day utilization is treated as `0` and any 7-day-window rejection is cleared; a model-specific weekly limit rejection is cleared once that limit's `resetsAt` has passed. This stale-reset expiry prevents a token that has actually recovered from being locked out of rotation forever (an excluded token receives no new requests, so the proxy never re-observes it). After expiry normalization, tokens whose status is `blocked`, whose remaining (non-expired) rejection is still active, whose 5-hour utilization meets or exceeds the configured threshold, or whose weekly limit for the model that will be used is rejected are excluded. The model name maps to a limit type: a model name containing `sonnet` maps to `seven_day_sonnet`, one containing `opus` maps to `seven_day_opus`, otherwise `seven_day`; the generic `seven_day` rejection also excludes regardless of model. The remaining eligible tokens are sorted by time until the 7-day reset deadline ascending (soonest 7-day reset first, so quota that would otherwise expire unused is consumed first), with 5-hour utilization ascending as the tiebreaker. The 7-day reset deadline for a given spawn is read from the per-model weekly limit entry in `modelWeeklyLimits` matching the model that will be used (`seven_day_sonnet`, `seven_day_opus`, or the generic `seven_day`); when no such entry is present on a token, that token is treated as having an infinite deadline and sorts last. Each token's per-run concurrent preparation slot count is determined by its 7-day utilization: at or below 80%, the token receives the full six concurrent slots; above 80%, the slot count is reduced proportionally — specifically `ceil(6 × (1 − 7d_util) / 0.2)` — with a minimum of 1 slot at any utilization level, so near-exhausted tokens still participate rather than being cut off entirely. Tokens are dispatched round-robin across their available slots, with the token whose 7-day reset is soonest selected preferentially. The effective preparation limit for a run is the lesser of the sum of all eligible tokens' slot counts and the explicit `maximumPreparingIssuesCount` (which defaults to 6). When a token list is configured but no eligible token remains after filtering, preparation is skipped for that run. The selected token and the proxy URL (`http://127.0.0.1:8787`) are passed to the child `aw` process as `CLAUDE_CODE_OAUTH_TOKEN` and `ANTHROPIC_BASE_URL` environment variables. The child inherits the parent process's environment, so no further wiring is required inside `aw`.
221
+ 3. Selection: before spawning each `aw` job, the daemon reads every token's cache file. A cached observation is treated as expired once its reset epoch has passed: when the current time is past the 5-hour reset, that token's 5-hour utilization is treated as `0` and any 5-hour-window rejection is cleared; likewise, when the 7-day reset has passed, the 7-day utilization is treated as `0` and any 7-day-window rejection is cleared; a model-specific weekly limit rejection is cleared once that limit's `resetsAt` has passed. This stale-reset expiry prevents a token that has actually recovered from being locked out of rotation forever (an excluded token receives no new requests, so the proxy never re-observes it). After expiry normalization, tokens whose status is `blocked`, whose remaining (non-expired) rejection is still active, whose 5-hour utilization meets or exceeds the configured threshold, or whose weekly limit for the model that will be used is rejected are excluded. The model name maps to a limit type: a model name containing `sonnet` maps to `seven_day_sonnet`, one containing `opus` maps to `seven_day_opus`, otherwise `seven_day`; the generic `seven_day` rejection also excludes regardless of model. The remaining eligible tokens are sorted by time until the 7-day reset deadline ascending (soonest 7-day reset first, so quota that would otherwise expire unused is consumed first), with 5-hour utilization ascending as the tiebreaker. The 7-day reset deadline for a given spawn is read from the per-model weekly limit entry in `modelWeeklyLimits` matching the model that will be used (`seven_day_sonnet`, `seven_day_opus`, or the generic `seven_day`); when no model-specific or generic `seven_day` entry exists but the cache snapshot recorded a non-expired top-level `anthropic-ratelimit-unified-7d-reset` deadline, that header value is bridged into a synthesized generic `seven_day` entry whose `rejected` flag matches the (non-expired) `anthropic-ratelimit-unified-7d-status` rejection. When neither source supplies a deadline, the token is treated as having an infinite deadline and sorts last. Each token's per-run concurrent preparation slot count is determined by its 7-day utilization: at or below 80%, the token receives the full six concurrent slots; above 80%, the slot count is reduced proportionally — specifically `ceil(6 × (1 − 7d_util) / 0.2)` — with a minimum of 1 slot at any utilization level, so near-exhausted tokens still participate rather than being cut off entirely. Tokens are dispatched round-robin across their available slots, with the token whose 7-day reset is soonest selected preferentially. The effective preparation limit for a run is the lesser of the sum of all eligible tokens' slot counts and the explicit `maximumPreparingIssuesCount` (which defaults to 6). When a token list is configured but no eligible token remains after filtering, preparation is skipped for that run. The selected token and the proxy URL (`http://127.0.0.1:8787`) are passed to the child `aw` process as `CLAUDE_CODE_OAUTH_TOKEN` and `ANTHROPIC_BASE_URL` environment variables. The child inherits the parent process's environment, so no further wiring is required inside `aw`.
222
222
 
223
223
  When `claudeCodeOauthTokenListJsonPath` is unset, no proxy is started and `aw` runs with whatever `CLAUDE_CODE_OAUTH_TOKEN` and `ANTHROPIC_BASE_URL` are already in the environment.
224
224
 
@@ -55,6 +55,17 @@ class ProxyClaudeTokenUsageRepository {
55
55
  resetsAt: limit.resetsAt,
56
56
  };
57
57
  }
58
+ const hasAnySevenDayWeeklyLimit = modelWeeklyLimits['seven_day'] !== undefined ||
59
+ modelWeeklyLimits['seven_day_opus'] !== undefined ||
60
+ modelWeeklyLimits['seven_day_sonnet'] !== undefined;
61
+ if (snapshot.sevenDayReset > 0 &&
62
+ !sevenDayExpired &&
63
+ !hasAnySevenDayWeeklyLimit) {
64
+ modelWeeklyLimits['seven_day'] = {
65
+ rejected: sevenDayRejectionActive,
66
+ resetsAt: snapshot.sevenDayReset,
67
+ };
68
+ }
58
69
  return {
59
70
  name,
60
71
  token,
@@ -1 +1 @@
1
- {"version":3,"file":"ProxyClaudeTokenUsageRepository.js","sourceRoot":"","sources":["../../../src/adapter/repositories/ProxyClaudeTokenUsageRepository.ts"],"names":[],"mappings":";;;AAEA,oEAAiE;AACjE,4DAAoE;AACpE,8DAA4D;AAE5D,MAAa,+BAA+B;IAC1C,YACmB,iBAAgC,EAChC,OAAe,2BAAU;QADzB,sBAAiB,GAAjB,iBAAiB,CAAe;QAChC,SAAI,GAAJ,IAAI,CAAqB;QAG5C,qBAAgB,GAAG,KAAK,IAAmB,EAAE;YAC3C,MAAM,IAAA,uCAAkB,EAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtC,CAAC,CAAC;QAEF,4BAAuB,GAAG,KAAK,IAAiC,EAAE;YAChE,IAAI,IAAI,CAAC,iBAAiB,KAAK,IAAI,EAAE,CAAC;gBACpC,OAAO,EAAE,CAAC;YACZ,CAAC;YACD,MAAM,OAAO,GAAG,IAAA,kCAAgB,EAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;YACzD,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;gBACrB,OAAO,EAAE,CAAC;YACZ,CAAC;YACD,MAAM,eAAe,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;YAC1C,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE;gBACrC,MAAM,QAAQ,GAAG,IAAA,8BAAa,EAAC,KAAK,CAAC,CAAC;gBACtC,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;oBACtB,OAAO;wBACL,IAAI;wBACJ,KAAK;wBACL,mBAAmB,EAAE,CAAC;wBACtB,mBAAmB,EAAE,CAAC;wBACtB,OAAO,EAAE,KAAK;wBACd,QAAQ,EAAE,KAAK;wBACf,iBAAiB,EAAE,EAAE;qBACtB,CAAC;gBACJ,CAAC;gBACD,MAAM,eAAe,GAAG,eAAe,GAAG,QAAQ,CAAC,aAAa,CAAC;gBACjE,MAAM,eAAe,GAAG,eAAe,GAAG,QAAQ,CAAC,aAAa,CAAC;gBACjE,MAAM,mBAAmB,GAAG,eAAe;oBACzC,CAAC,CAAC,CAAC;oBACH,CAAC,CAAC,QAAQ,CAAC,mBAAmB,CAAC;gBACjC,MAAM,mBAAmB,GAAG,eAAe;oBACzC,CAAC,CAAC,CAAC;oBACH,CAAC,CAAC,QAAQ,CAAC,mBAAmB,CAAC;gBACjC,MAAM,uBAAuB,GAC3B,QAAQ,CAAC,gBAAgB,IAAI,CAAC,eAAe,CAAC;gBAChD,MAAM,uBAAuB,GAC3B,QAAQ,CAAC,gBAAgB,IAAI,CAAC,eAAe,CAAC;gBAChD,MAAM,sBAAsB,GAC1B,QAAQ,CAAC,eAAe,IAAI,CAAC,eAAe,CAAC;gBAC/C,MAAM,QAAQ,GACZ,sBAAsB;oBACtB,uBAAuB;oBACvB,uBAAuB,CAAC;gBAC1B,MAAM,iBAAiB,GAGnB,EAAE,CAAC;gBACP,KAAK,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAC7C,QAAQ,CAAC,iBAAiB,CAC3B,EAAE,CAAC;oBACF,MAAM,OAAO,GAAG,eAAe,GAAG,KAAK,CAAC,QAAQ,CAAC;oBACjD,iBAAiB,CAAC,SAAS,CAAC,GAAG;wBAC7B,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,CAAC,OAAO;wBACpC,QAAQ,EAAE,KAAK,CAAC,QAAQ;qBACzB,CAAC;gBACJ,CAAC;gBACD,OAAO;oBACL,IAAI;oBACJ,KAAK;oBACL,mBAAmB;oBACnB,mBAAmB;oBACnB,OAAO,EAAE,QAAQ,CAAC,OAAO;oBACzB,QAAQ;oBACR,iBAAiB;iBAClB,CAAC;YACJ,CAAC,CAAC,CAAC;QACL,CAAC,CAAC;QAEF,iBAAY,GAAG,GAAW,EAAE,CAAC,oBAAoB,IAAI,CAAC,IAAI,EAAE,CAAC;IAvE1D,CAAC;CAwEL;AA5ED,0EA4EC"}
1
+ {"version":3,"file":"ProxyClaudeTokenUsageRepository.js","sourceRoot":"","sources":["../../../src/adapter/repositories/ProxyClaudeTokenUsageRepository.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-issue-tower-defence-management",
3
- "version": "1.69.7",
3
+ "version": "1.69.8",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -102,7 +102,9 @@ describe('ProxyClaudeTokenUsageRepository', () => {
102
102
  sevenDayUtilization: 0,
103
103
  blocked: false,
104
104
  rejected: false,
105
- modelWeeklyLimits: {},
105
+ modelWeeklyLimits: {
106
+ seven_day: { rejected: false, resetsAt: futureReset },
107
+ },
106
108
  },
107
109
  {
108
110
  name: 'bob',
@@ -144,7 +146,9 @@ describe('ProxyClaudeTokenUsageRepository', () => {
144
146
  sevenDayUtilization: 0,
145
147
  blocked: true,
146
148
  rejected: false,
147
- modelWeeklyLimits: {},
149
+ modelWeeklyLimits: {
150
+ seven_day: { rejected: false, resetsAt: futureReset },
151
+ },
148
152
  },
149
153
  ]);
150
154
  });
@@ -177,7 +181,9 @@ describe('ProxyClaudeTokenUsageRepository', () => {
177
181
  sevenDayUtilization: 0,
178
182
  blocked: false,
179
183
  rejected: true,
180
- modelWeeklyLimits: {},
184
+ modelWeeklyLimits: {
185
+ seven_day: { rejected: false, resetsAt: futureReset },
186
+ },
181
187
  },
182
188
  ]);
183
189
  });
@@ -210,7 +216,9 @@ describe('ProxyClaudeTokenUsageRepository', () => {
210
216
  sevenDayUtilization: 30,
211
217
  blocked: false,
212
218
  rejected: false,
213
- modelWeeklyLimits: {},
219
+ modelWeeklyLimits: {
220
+ seven_day: { rejected: false, resetsAt: futureReset },
221
+ },
214
222
  },
215
223
  ]);
216
224
  });
@@ -243,7 +251,9 @@ describe('ProxyClaudeTokenUsageRepository', () => {
243
251
  sevenDayUtilization: 0,
244
252
  blocked: false,
245
253
  rejected: false,
246
- modelWeeklyLimits: {},
254
+ modelWeeklyLimits: {
255
+ seven_day: { rejected: false, resetsAt: futureReset },
256
+ },
247
257
  },
248
258
  ]);
249
259
  });
@@ -276,7 +286,9 @@ describe('ProxyClaudeTokenUsageRepository', () => {
276
286
  sevenDayUtilization: 0,
277
287
  blocked: false,
278
288
  rejected: false,
279
- modelWeeklyLimits: {},
289
+ modelWeeklyLimits: {
290
+ seven_day: { rejected: false, resetsAt: futureReset },
291
+ },
280
292
  },
281
293
  ]);
282
294
  });
@@ -342,7 +354,9 @@ describe('ProxyClaudeTokenUsageRepository', () => {
342
354
  sevenDayUtilization: 0,
343
355
  blocked: false,
344
356
  rejected: true,
345
- modelWeeklyLimits: {},
357
+ modelWeeklyLimits: {
358
+ seven_day: { rejected: false, resetsAt: futureReset },
359
+ },
346
360
  },
347
361
  ]);
348
362
  });
@@ -375,7 +389,9 @@ describe('ProxyClaudeTokenUsageRepository', () => {
375
389
  sevenDayUtilization: 100,
376
390
  blocked: false,
377
391
  rejected: true,
378
- modelWeeklyLimits: {},
392
+ modelWeeklyLimits: {
393
+ seven_day: { rejected: true, resetsAt: futureReset },
394
+ },
379
395
  },
380
396
  ]);
381
397
  });
@@ -408,7 +424,9 @@ describe('ProxyClaudeTokenUsageRepository', () => {
408
424
  sevenDayUtilization: 0,
409
425
  blocked: false,
410
426
  rejected: false,
411
- modelWeeklyLimits: {},
427
+ modelWeeklyLimits: {
428
+ seven_day: { rejected: false, resetsAt: futureReset },
429
+ },
412
430
  },
413
431
  ]);
414
432
  });
@@ -508,6 +526,186 @@ describe('ProxyClaudeTokenUsageRepository', () => {
508
526
  },
509
527
  ]);
510
528
  });
529
+
530
+ it('should bridge sevenDayReset into a synthesized generic seven_day weekly limit when no model-specific weekly limit is present', async () => {
531
+ mockLoadTokenEntries.mockReturnValue([
532
+ { name: 'alice', token: 'token-a' },
533
+ ]);
534
+ mockReadRateLimit.mockReturnValue({
535
+ fiveHourUtilization: 12,
536
+ fiveHourReset: futureReset,
537
+ sevenDayUtilization: 33,
538
+ sevenDayReset: futureReset,
539
+ blocked: false,
540
+ rejected: false,
541
+ unifiedRejected: false,
542
+ fiveHourRejected: false,
543
+ sevenDayRejected: false,
544
+ modelWeeklyLimits: {},
545
+ });
546
+ const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
547
+
548
+ const result = await repository.getAvailableTokenUsages();
549
+
550
+ expect(result[0].modelWeeklyLimits).toEqual({
551
+ seven_day: { rejected: false, resetsAt: futureReset },
552
+ });
553
+ });
554
+
555
+ it('should bridge sevenDayReset with rejected=true when sevenDayRejected is active and 7d reset is in the future', async () => {
556
+ mockLoadTokenEntries.mockReturnValue([
557
+ { name: 'alice', token: 'token-a' },
558
+ ]);
559
+ mockReadRateLimit.mockReturnValue({
560
+ fiveHourUtilization: 10,
561
+ fiveHourReset: futureReset,
562
+ sevenDayUtilization: 100,
563
+ sevenDayReset: futureReset,
564
+ blocked: false,
565
+ rejected: true,
566
+ unifiedRejected: false,
567
+ fiveHourRejected: false,
568
+ sevenDayRejected: true,
569
+ modelWeeklyLimits: {},
570
+ });
571
+ const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
572
+
573
+ const result = await repository.getAvailableTokenUsages();
574
+
575
+ expect(result[0].modelWeeklyLimits).toEqual({
576
+ seven_day: { rejected: true, resetsAt: futureReset },
577
+ });
578
+ });
579
+
580
+ it('should not bridge sevenDayReset when a generic seven_day weekly limit is already present', async () => {
581
+ const existingResetsAt = futureReset + 1000;
582
+ mockLoadTokenEntries.mockReturnValue([
583
+ { name: 'alice', token: 'token-a' },
584
+ ]);
585
+ mockReadRateLimit.mockReturnValue({
586
+ fiveHourUtilization: 10,
587
+ fiveHourReset: futureReset,
588
+ sevenDayUtilization: 50,
589
+ sevenDayReset: futureReset,
590
+ blocked: false,
591
+ rejected: false,
592
+ unifiedRejected: false,
593
+ fiveHourRejected: false,
594
+ sevenDayRejected: false,
595
+ modelWeeklyLimits: {
596
+ seven_day: { rejected: false, resetsAt: existingResetsAt },
597
+ },
598
+ });
599
+ const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
600
+
601
+ const result = await repository.getAvailableTokenUsages();
602
+
603
+ expect(result[0].modelWeeklyLimits).toEqual({
604
+ seven_day: { rejected: false, resetsAt: existingResetsAt },
605
+ });
606
+ });
607
+
608
+ it('should not bridge sevenDayReset when a seven_day_opus weekly limit is already present', async () => {
609
+ const opusResetsAt = futureReset + 2000;
610
+ mockLoadTokenEntries.mockReturnValue([
611
+ { name: 'alice', token: 'token-a' },
612
+ ]);
613
+ mockReadRateLimit.mockReturnValue({
614
+ fiveHourUtilization: 10,
615
+ fiveHourReset: futureReset,
616
+ sevenDayUtilization: 50,
617
+ sevenDayReset: futureReset,
618
+ blocked: false,
619
+ rejected: false,
620
+ unifiedRejected: false,
621
+ fiveHourRejected: false,
622
+ sevenDayRejected: false,
623
+ modelWeeklyLimits: {
624
+ seven_day_opus: { rejected: false, resetsAt: opusResetsAt },
625
+ },
626
+ });
627
+ const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
628
+
629
+ const result = await repository.getAvailableTokenUsages();
630
+
631
+ expect(result[0].modelWeeklyLimits).toEqual({
632
+ seven_day_opus: { rejected: false, resetsAt: opusResetsAt },
633
+ });
634
+ });
635
+
636
+ it('should not bridge sevenDayReset when a seven_day_sonnet weekly limit is already present', async () => {
637
+ const sonnetResetsAt = futureReset + 3000;
638
+ mockLoadTokenEntries.mockReturnValue([
639
+ { name: 'alice', token: 'token-a' },
640
+ ]);
641
+ mockReadRateLimit.mockReturnValue({
642
+ fiveHourUtilization: 10,
643
+ fiveHourReset: futureReset,
644
+ sevenDayUtilization: 50,
645
+ sevenDayReset: futureReset,
646
+ blocked: false,
647
+ rejected: false,
648
+ unifiedRejected: false,
649
+ fiveHourRejected: false,
650
+ sevenDayRejected: false,
651
+ modelWeeklyLimits: {
652
+ seven_day_sonnet: { rejected: false, resetsAt: sonnetResetsAt },
653
+ },
654
+ });
655
+ const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
656
+
657
+ const result = await repository.getAvailableTokenUsages();
658
+
659
+ expect(result[0].modelWeeklyLimits).toEqual({
660
+ seven_day_sonnet: { rejected: false, resetsAt: sonnetResetsAt },
661
+ });
662
+ });
663
+
664
+ it('should not bridge sevenDayReset when the 7d reset has already passed', async () => {
665
+ mockLoadTokenEntries.mockReturnValue([
666
+ { name: 'alice', token: 'token-a' },
667
+ ]);
668
+ mockReadRateLimit.mockReturnValue({
669
+ fiveHourUtilization: 10,
670
+ fiveHourReset: futureReset,
671
+ sevenDayUtilization: 0,
672
+ sevenDayReset: pastReset,
673
+ blocked: false,
674
+ rejected: false,
675
+ unifiedRejected: false,
676
+ fiveHourRejected: false,
677
+ sevenDayRejected: false,
678
+ modelWeeklyLimits: {},
679
+ });
680
+ const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
681
+
682
+ const result = await repository.getAvailableTokenUsages();
683
+
684
+ expect(result[0].modelWeeklyLimits).toEqual({});
685
+ });
686
+
687
+ it('should not bridge sevenDayReset when sevenDayReset is 0', async () => {
688
+ mockLoadTokenEntries.mockReturnValue([
689
+ { name: 'alice', token: 'token-a' },
690
+ ]);
691
+ mockReadRateLimit.mockReturnValue({
692
+ fiveHourUtilization: 10,
693
+ fiveHourReset: futureReset,
694
+ sevenDayUtilization: 0,
695
+ sevenDayReset: 0,
696
+ blocked: false,
697
+ rejected: false,
698
+ unifiedRejected: false,
699
+ fiveHourRejected: false,
700
+ sevenDayRejected: false,
701
+ modelWeeklyLimits: {},
702
+ });
703
+ const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
704
+
705
+ const result = await repository.getAvailableTokenUsages();
706
+
707
+ expect(result[0].modelWeeklyLimits).toEqual({});
708
+ });
511
709
  });
512
710
 
513
711
  describe('proxyBaseUrl', () => {
@@ -67,6 +67,20 @@ export class ProxyClaudeTokenUsageRepository implements ClaudeTokenUsageReposito
67
67
  resetsAt: limit.resetsAt,
68
68
  };
69
69
  }
70
+ const hasAnySevenDayWeeklyLimit =
71
+ modelWeeklyLimits['seven_day'] !== undefined ||
72
+ modelWeeklyLimits['seven_day_opus'] !== undefined ||
73
+ modelWeeklyLimits['seven_day_sonnet'] !== undefined;
74
+ if (
75
+ snapshot.sevenDayReset > 0 &&
76
+ !sevenDayExpired &&
77
+ !hasAnySevenDayWeeklyLimit
78
+ ) {
79
+ modelWeeklyLimits['seven_day'] = {
80
+ rejected: sevenDayRejectionActive,
81
+ resetsAt: snapshot.sevenDayReset,
82
+ };
83
+ }
70
84
  return {
71
85
  name,
72
86
  token,
@@ -3713,6 +3713,182 @@ describe('StartPreparationUseCase', () => {
3713
3713
  });
3714
3714
  });
3715
3715
 
3716
+ it('should sort tokens by 7-day reset deadline ascending when only the generic seven_day weekly limit (as bridged by the proxy from the snapshot top-level sevenDayReset) is present', async () => {
3717
+ const awaitingIssues: Issue[] = [
3718
+ createMockIssue({
3719
+ url: 'url1',
3720
+ title: 'Issue 1',
3721
+ labels: ['category:impl'],
3722
+ status: 'Awaiting Workspace',
3723
+ number: 1,
3724
+ itemId: 'item-1',
3725
+ }),
3726
+ createMockIssue({
3727
+ url: 'url2',
3728
+ title: 'Issue 2',
3729
+ labels: ['category:impl'],
3730
+ status: 'Awaiting Workspace',
3731
+ number: 2,
3732
+ itemId: 'item-2',
3733
+ }),
3734
+ createMockIssue({
3735
+ url: 'url3',
3736
+ title: 'Issue 3',
3737
+ labels: ['category:impl'],
3738
+ status: 'Awaiting Workspace',
3739
+ number: 3,
3740
+ itemId: 'item-3',
3741
+ }),
3742
+ ];
3743
+ mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
3744
+ mockIssueRepository.getStoryObjectMap.mockResolvedValue(
3745
+ createMockStoryObjectMap(awaitingIssues),
3746
+ );
3747
+ mockLocalCommandRunner.runCommand.mockResolvedValue({
3748
+ stdout: '',
3749
+ stderr: '',
3750
+ exitCode: 0,
3751
+ });
3752
+ const nowEpochSeconds = Math.floor(Date.now() / 1000);
3753
+ mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
3754
+ {
3755
+ name: 'token-7d-far',
3756
+ token: 'token-7d-far',
3757
+ fiveHourUtilization: 0.05,
3758
+ sevenDayUtilization: 0.1,
3759
+ blocked: false,
3760
+ rejected: false,
3761
+ modelWeeklyLimits: {
3762
+ seven_day: {
3763
+ rejected: false,
3764
+ resetsAt: nowEpochSeconds + 150 * 3600,
3765
+ },
3766
+ },
3767
+ },
3768
+ {
3769
+ name: 'token-7d-soon',
3770
+ token: 'token-7d-soon',
3771
+ fiveHourUtilization: 0.5,
3772
+ sevenDayUtilization: 0.1,
3773
+ blocked: false,
3774
+ rejected: false,
3775
+ modelWeeklyLimits: {
3776
+ seven_day: {
3777
+ rejected: false,
3778
+ resetsAt: nowEpochSeconds + 10 * 3600,
3779
+ },
3780
+ },
3781
+ },
3782
+ {
3783
+ name: 'token-7d-mid',
3784
+ token: 'token-7d-mid',
3785
+ fiveHourUtilization: 0.3,
3786
+ sevenDayUtilization: 0.1,
3787
+ blocked: false,
3788
+ rejected: false,
3789
+ modelWeeklyLimits: {
3790
+ seven_day: {
3791
+ rejected: false,
3792
+ resetsAt: nowEpochSeconds + 50 * 3600,
3793
+ },
3794
+ },
3795
+ },
3796
+ ]);
3797
+
3798
+ await useCase.run({
3799
+ projectUrl: 'https://github.com/user/repo',
3800
+ defaultAgentName: 'agent1',
3801
+ defaultLlmModelName: 'claude-opus',
3802
+ defaultLlmAgentName: null,
3803
+ configFilePath: '/path/to/config.yml',
3804
+ maximumPreparingIssuesCount: null,
3805
+ utilizationPercentageThreshold: 90,
3806
+ allowedIssueAuthors: null,
3807
+ codexHomeCandidates: null,
3808
+ allowIssueCacheMinutes: 0,
3809
+ labelsAsLlmAgentName: null,
3810
+ });
3811
+
3812
+ expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(3);
3813
+ expect(mockLocalCommandRunner.runCommand.mock.calls[0][2]).toMatchObject({
3814
+ env: { CLAUDE_CODE_OAUTH_TOKEN: 'token-7d-soon' },
3815
+ });
3816
+ expect(mockLocalCommandRunner.runCommand.mock.calls[1][2]).toMatchObject({
3817
+ env: { CLAUDE_CODE_OAUTH_TOKEN: 'token-7d-mid' },
3818
+ });
3819
+ expect(mockLocalCommandRunner.runCommand.mock.calls[2][2]).toMatchObject({
3820
+ env: { CLAUDE_CODE_OAUTH_TOKEN: 'token-7d-far' },
3821
+ });
3822
+ });
3823
+
3824
+ it('should fall back to 5-hour utilization ascending as tiebreaker when only the generic seven_day weekly limit (as bridged by the proxy) is present and deadlines tie', async () => {
3825
+ const awaitingIssue = createMockIssue({
3826
+ url: 'url1',
3827
+ title: 'Issue 1',
3828
+ labels: ['category:impl'],
3829
+ status: 'Awaiting Workspace',
3830
+ number: 1,
3831
+ itemId: 'item-1',
3832
+ });
3833
+ mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
3834
+ mockIssueRepository.getStoryObjectMap.mockResolvedValue(
3835
+ createMockStoryObjectMap([awaitingIssue]),
3836
+ );
3837
+ mockLocalCommandRunner.runCommand.mockResolvedValue({
3838
+ stdout: '',
3839
+ stderr: '',
3840
+ exitCode: 0,
3841
+ });
3842
+ const nowEpochSeconds = Math.floor(Date.now() / 1000);
3843
+ const sharedResetsAt = nowEpochSeconds + 50 * 3600;
3844
+ mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
3845
+ {
3846
+ name: 'token-busy-5h',
3847
+ token: 'token-busy-5h',
3848
+ fiveHourUtilization: 0.6,
3849
+ sevenDayUtilization: 0.1,
3850
+ blocked: false,
3851
+ rejected: false,
3852
+ modelWeeklyLimits: {
3853
+ seven_day: { rejected: false, resetsAt: sharedResetsAt },
3854
+ },
3855
+ },
3856
+ {
3857
+ name: 'token-idle-5h',
3858
+ token: 'token-idle-5h',
3859
+ fiveHourUtilization: 0.1,
3860
+ sevenDayUtilization: 0.1,
3861
+ blocked: false,
3862
+ rejected: false,
3863
+ modelWeeklyLimits: {
3864
+ seven_day: { rejected: false, resetsAt: sharedResetsAt },
3865
+ },
3866
+ },
3867
+ ]);
3868
+
3869
+ await useCase.run({
3870
+ projectUrl: 'https://github.com/user/repo',
3871
+ defaultAgentName: 'agent1',
3872
+ defaultLlmModelName: 'claude-opus',
3873
+ defaultLlmAgentName: null,
3874
+ configFilePath: '/path/to/config.yml',
3875
+ maximumPreparingIssuesCount: null,
3876
+ utilizationPercentageThreshold: 90,
3877
+ allowedIssueAuthors: null,
3878
+ codexHomeCandidates: null,
3879
+ allowIssueCacheMinutes: 0,
3880
+ labelsAsLlmAgentName: null,
3881
+ });
3882
+
3883
+ expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
3884
+ expect(mockLocalCommandRunner.runCommand.mock.calls[0][2]).toEqual({
3885
+ env: {
3886
+ CLAUDE_CODE_OAUTH_TOKEN: 'token-idle-5h',
3887
+ ANTHROPIC_BASE_URL: 'http://127.0.0.1:8787',
3888
+ },
3889
+ });
3890
+ });
3891
+
3716
3892
  it('should fall back to 5-hour utilization ascending as tiebreaker when 7-day reset deadlines are identical', async () => {
3717
3893
  const awaitingIssue = createMockIssue({
3718
3894
  url: 'url1',
@@ -1 +1 @@
1
- {"version":3,"file":"ProxyClaudeTokenUsageRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/ProxyClaudeTokenUsageRepository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wCAAwC,CAAC;AAC1E,OAAO,EAAE,0BAA0B,EAAE,MAAM,qEAAqE,CAAC;AAKjH,qBAAa,+BAAgC,YAAW,0BAA0B;IAE9E,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,IAAI;gBADJ,iBAAiB,EAAE,MAAM,GAAG,IAAI,EAChC,IAAI,GAAE,MAAmB;IAG5C,gBAAgB,QAAa,OAAO,CAAC,IAAI,CAAC,CAExC;IAEF,uBAAuB,QAAa,OAAO,CAAC,gBAAgB,EAAE,CAAC,CA+D7D;IAEF,YAAY,QAAO,MAAM,CAAoC;CAC9D"}
1
+ {"version":3,"file":"ProxyClaudeTokenUsageRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/ProxyClaudeTokenUsageRepository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wCAAwC,CAAC;AAC1E,OAAO,EAAE,0BAA0B,EAAE,MAAM,qEAAqE,CAAC;AAKjH,qBAAa,+BAAgC,YAAW,0BAA0B;IAE9E,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,IAAI;gBADJ,iBAAiB,EAAE,MAAM,GAAG,IAAI,EAChC,IAAI,GAAE,MAAmB;IAG5C,gBAAgB,QAAa,OAAO,CAAC,IAAI,CAAC,CAExC;IAEF,uBAAuB,QAAa,OAAO,CAAC,gBAAgB,EAAE,CAAC,CA6E7D;IAEF,YAAY,QAAO,MAAM,CAAoC;CAC9D"}