github-issue-tower-defence-management 2.19.0 → 2.19.1

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
+ ## [2.19.1](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v2.19.0...v2.19.1) (2026-08-30)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **token-select:** lower 7d min free ratio to prevent wasting expiring budget ([#1885](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/1885)) ([2f8783c](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/commit/2f8783cd799dc52b694dd17a9643a1cd38014693)), closes [HiromiShikata/umino-corporait-operation#31116](https://github.com/HiromiShikata/umino-corporait-operation/issues/31116)
7
+
1
8
  # [2.19.0](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v2.18.0...v2.19.0) (2026-08-30)
2
9
 
3
10
 
package/README.md CHANGED
@@ -162,7 +162,7 @@ The `checkIssueReviewReadiness` sub-command lets an agent self-check whether an
162
162
 
163
163
  The `selectOauthToken` sub-command reads the same per-token rate-limit cache that the `startDaemon` proxy writes (see "Claude OAuth Token Rotation" below) and prints exactly one token string to stdout so a caller can choose an appropriate token before launching Claude Code. It is read-only: it never starts the proxy, never mutates any cache file, and never writes the token anywhere. Selection runs in two stages. First, a candidate filter keeps tokens whose 5-hour window is at least 60% free (5-hour utilization at most 0.40) AND whose 7-day window is at least 14% free (7-day utilization at most 0.86), where "% free" is `1 - utilization`. A token with no cache file, or whose window reset epoch has already passed, is treated as fully free (utilization 0) for these checks. The filter additionally excludes any token carrying a non-expired reactive `seven_day_fable` rejection marker — set when a `fable`-model request on that token was rejected with HTTP 429 (see "Claude OAuth Token Rotation" below) — because these interactive sessions run on the `fable` model; a token without the marker is treated as `fable`-usable, and the marker is ignored once its stored reset epoch has passed. Second, among the surviving candidates it selects the single token whose 7-day window reset epoch is nearest in the future (soonest reset), so weekly quota that would otherwise reset unused is consumed first; a candidate with no active 7-day window is treated as having the farthest reset (now + 7 days) and therefore sorts last. Each token entry may carry an optional `selectionWeight` (a positive number, default `1`); when the eligible candidates carry differing weights the selection among them becomes weighted-random by that weight, so a token with a smaller weight is chosen proportionally less often, while a token that is the only eligible candidate is always chosen regardless of its weight. When every eligible candidate shares the same weight (the default), the deterministic soonest-reset selection above is used unchanged. The selected token string is written to stdout (pipeable) and the per-candidate decision trace is written to stderr. When no token passes the filter, nothing is written to stdout and the command exits non-zero with an explanatory message on stderr. The token-list path comes from `--tokenListJsonPath` or the `CLAUDE_CODE_OAUTH_TOKEN_LIST_JSON_PATH` environment variable; the cache directory comes from `--cacheDir` or the `TDPM_RATELIMIT_CACHE_DIR` environment variable, defaulting to `${XDG_CACHE_HOME:-~/.cache}/tdpm/ratelimit`.
164
164
 
165
- The `selectLiveSessionOauthToken` sub-command picks a token for a new live interactive Claude Code session a human is about to start, so that concurrent interactive sessions spread across distinct tokens instead of stacking onto the same one. It loads the token list and reads the same per-token rate-limit cache as `selectOauthToken`, and it is read-only in exactly the same way: it never starts the proxy, never mutates any cache file, and never writes the token anywhere. It applies a base rate-limit eligibility filter (5-hour window at least 25% free AND 7-day window at least 3% free, with a missing cache file or an expired window treated as fully free, and any token carrying a non-expired reactive `seven_day_fable` rejection marker excluded), and additionally excludes any token whose 5-hour window is less than `minFiveHourFreeRatio` (default 60%) free or whose 7-day window is less than `minSevenDayFreeRatio` (default 14%) free; these stricter thresholds prevent the `cl` script from starting a new interactive session on a token that is nearly exhausted. It additionally measures current live occupancy per token by scanning running Claude Code processes on the local Linux host: for each process under `/proc` it reads the NUL-separated `/proc/<pid>/environ` and, when the process is a Claude Code process, takes its `CLAUDE_CODE_OAUTH_TOKEN` and `CLAUDE_CODE_SESSION_ID`. A token's occupancy is the number of distinct `CLAUDE_CODE_SESSION_ID` values bound to it, so child processes that inherit one session id count once. Processes without `CLAUDE_CODE_OAUTH_TOKEN` (for example API-key sessions) are ignored, and a process whose environ cannot be read is skipped. Among the eligible tokens the selection is deterministic and reset-first. Each eligible token is given a concurrent session limit of `maxConcurrentSessionCount` (default `10`), scaled by its optional `selectionWeight` (a positive number, default `1`). That limit is held at its full value while the free share of the token's 5-hour window is at or above `fullSpeedFiveHourFreeRatio` (default `0.5`), and is tapered linearly in proportion to the free share below that point, never dropping under `1`. The 5-hour window is the only window that lowers the limit: it is the one that runs out within a single working stretch, and a session that hits it mid-flight has to be restarted, which costs more than it saves. The 7-day window never lowers the limit, so a weekly allowance that is about to expire is drained at full speed rather than discarded unused; the 7-day window is instead what orders the candidates. The token whose 7-day window resets soonest among those still under their limit is selected, so allowance that is about to expire is consumed before the reset discards it; ties are broken by the fewer live sessions. All four tuning numbers (`maxConcurrentSessionCount`, `fullSpeedFiveHourFreeRatio`, `minFiveHourFreeRatio`, `minSevenDayFreeRatio`) are read from the fleet-wide config file named by `--fleetConfigFilePath` or the `TDPM_FLEET_CONFIG` environment variable, under a `liveSessionOauthTokenSelection` mapping; a key the file omits keeps its built-in value, while an unreadable file or an out-of-range value is reported as an error rather than silently ignored. A token at its limit is skipped in favour of the next soonest-resetting token that still has room, and when every eligible token is at its limit the soonest-resetting one is returned rather than none. A token that is the only eligible candidate is always chosen regardless of its weight or occupancy. The selected token string is written to stdout (pipeable) and the per-candidate decision trace is written to stderr; when no token passes the filter, nothing is written to stdout and the command exits non-zero with an explanatory message on stderr. The token-list path and cache directory are resolved exactly as for `selectOauthToken`. Because occupancy is read from `/proc/<pid>/environ`, this sub-command is Linux-specific.
165
+ The `selectLiveSessionOauthToken` sub-command picks a token for a new live interactive Claude Code session a human is about to start, so that concurrent interactive sessions spread across distinct tokens instead of stacking onto the same one. It loads the token list and reads the same per-token rate-limit cache as `selectOauthToken`, and it is read-only in exactly the same way: it never starts the proxy, never mutates any cache file, and never writes the token anywhere. It applies a base rate-limit eligibility filter (5-hour window at least 25% free AND 7-day window at least 1% free, with a missing cache file or an expired window treated as fully free, and any token carrying a non-expired reactive `seven_day_fable` rejection marker excluded), and additionally excludes any token whose 5-hour window is less than `minFiveHourFreeRatio` (default 60%) free or whose 7-day window is less than `minSevenDayFreeRatio` (default 14%) free; these stricter thresholds prevent the `cl` script from starting a new interactive session on a token that is nearly exhausted. It additionally measures current live occupancy per token by scanning running Claude Code processes on the local Linux host: for each process under `/proc` it reads the NUL-separated `/proc/<pid>/environ` and, when the process is a Claude Code process, takes its `CLAUDE_CODE_OAUTH_TOKEN` and `CLAUDE_CODE_SESSION_ID`. A token's occupancy is the number of distinct `CLAUDE_CODE_SESSION_ID` values bound to it, so child processes that inherit one session id count once. Processes without `CLAUDE_CODE_OAUTH_TOKEN` (for example API-key sessions) are ignored, and a process whose environ cannot be read is skipped. Among the eligible tokens the selection is deterministic and reset-first. Each eligible token is given a concurrent session limit of `maxConcurrentSessionCount` (default `10`), scaled by its optional `selectionWeight` (a positive number, default `1`). That limit is held at its full value while the free share of the token's 5-hour window is at or above `fullSpeedFiveHourFreeRatio` (default `0.5`), and is tapered linearly in proportion to the free share below that point, never dropping under `1`. The 5-hour window is the only window that lowers the limit: it is the one that runs out within a single working stretch, and a session that hits it mid-flight has to be restarted, which costs more than it saves. The 7-day window never lowers the limit, so a weekly allowance that is about to expire is drained at full speed rather than discarded unused; the 7-day window is instead what orders the candidates. The token whose 7-day window resets soonest among those still under their limit is selected, so allowance that is about to expire is consumed before the reset discards it; ties are broken by the fewer live sessions. All four tuning numbers (`maxConcurrentSessionCount`, `fullSpeedFiveHourFreeRatio`, `minFiveHourFreeRatio`, `minSevenDayFreeRatio`) are read from the fleet-wide config file named by `--fleetConfigFilePath` or the `TDPM_FLEET_CONFIG` environment variable, under a `liveSessionOauthTokenSelection` mapping; a key the file omits keeps its built-in value, while an unreadable file or an out-of-range value is reported as an error rather than silently ignored. A token at its limit is skipped in favour of the next soonest-resetting token that still has room, and when every eligible token is at its limit the soonest-resetting one is returned rather than none. A token that is the only eligible candidate is always chosen regardless of its weight or occupancy. The selected token string is written to stdout (pipeable) and the per-candidate decision trace is written to stderr; when no token passes the filter, nothing is written to stdout and the command exits non-zero with an explanatory message on stderr. The token-list path and cache directory are resolved exactly as for `selectOauthToken`. Because occupancy is read from `/proc/<pid>/environ`, this sub-command is Linux-specific.
166
166
 
167
167
  The `startDaemon` command supports a `preparationWorker` mapping in the fleet-wide config file named by `--fleetConfigFilePath` or the `TDPM_FLEET_CONFIG` environment variable. This section controls per-token concurrency for the preparation worker pool. Supported keys:
168
168
 
@@ -10,7 +10,7 @@ exports.selectionWeightOf = selectionWeightOf;
10
10
  const SECONDS_PER_DAY = 86400;
11
11
  const SEVEN_DAYS_IN_SECONDS = 7 * SECONDS_PER_DAY;
12
12
  exports.FIVE_HOUR_MIN_FREE_RATIO = 0.25;
13
- exports.SEVEN_DAY_MIN_FREE_RATIO = 0.03;
13
+ exports.SEVEN_DAY_MIN_FREE_RATIO = 0.01;
14
14
  exports.DEFAULT_OAUTH_TOKEN_SELECTION_THRESHOLDS = {
15
15
  fiveHourMinFreeRatio: exports.FIVE_HOUR_MIN_FREE_RATIO,
16
16
  sevenDayMinFreeRatio: exports.SEVEN_DAY_MIN_FREE_RATIO,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-issue-tower-defence-management",
3
- "version": "2.19.0",
3
+ "version": "2.19.1",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -321,7 +321,7 @@ describe('LiveSessionOauthTokenSelectHandler', () => {
321
321
  const diagnostics = output.diagnostics.join('\n');
322
322
  expect(diagnostics).toContain('No eligible token');
323
323
  expect(diagnostics).toContain('5h >= 25% free');
324
- expect(diagnostics).toContain('7d >= 3% free');
324
+ expect(diagnostics).toContain('7d >= 1% free');
325
325
  expect(diagnostics).not.toContain('fake-busy');
326
326
  });
327
327
 
@@ -89,11 +89,22 @@ describe('OauthTokenSelectUseCase', () => {
89
89
  expect(boundary?.fiveHourFreeRatio).toBe(0.25);
90
90
  });
91
91
 
92
- it('excludes a token whose 7d window is less than 3% free', () => {
92
+ it('does not exclude a token with 2% of its seven day budget remaining', () => {
93
+ const result = useCase.run(
94
+ [candidate('nearFull', snapshot({ sevenDayUtilization: 0.98 }))],
95
+ NOW,
96
+ );
97
+
98
+ expect(result.selected?.name).toBe('nearFull');
99
+ const nearFull = result.metrics.find((m) => m.name === 'nearFull');
100
+ expect(nearFull?.eligible).toBe(true);
101
+ });
102
+
103
+ it('excludes a token whose 7d window is less than 1% free', () => {
93
104
  const result = useCase.run(
94
105
  [
95
- candidate('busy7d', snapshot({ sevenDayUtilization: 0.98 })),
96
- candidate('ok', snapshot({ sevenDayUtilization: 0.97 })),
106
+ candidate('busy7d', snapshot({ sevenDayUtilization: 0.995 })),
107
+ candidate('ok', snapshot({ sevenDayUtilization: 0.99 })),
97
108
  ],
98
109
  NOW,
99
110
  );
@@ -104,23 +115,23 @@ describe('OauthTokenSelectUseCase', () => {
104
115
  expect(busy?.exclusionReason).toContain('7d window');
105
116
  });
106
117
 
107
- it('treats exactly 97% used 7d utilization as eligible (boundary)', () => {
118
+ it('treats exactly 99% used 7d utilization as eligible (boundary)', () => {
108
119
  const result = useCase.run(
109
- [candidate('boundary', snapshot({ sevenDayUtilization: 0.97 }))],
120
+ [candidate('boundary', snapshot({ sevenDayUtilization: 0.99 }))],
110
121
  NOW,
111
122
  );
112
123
 
113
124
  expect(result.selected?.name).toBe('boundary');
114
125
  const boundary = result.metrics.find((m) => m.name === 'boundary');
115
- expect(boundary?.sevenDayFreeRatio).toBeCloseTo(0.03, 9);
126
+ expect(boundary?.sevenDayFreeRatio).toBeCloseTo(0.01, 9);
116
127
  });
117
128
 
118
- it('proceeds when 5h is exactly 25% free and 7d is exactly 3% free (combined boundary)', () => {
129
+ it('proceeds when 5h is exactly 25% free and 7d is exactly 1% free (combined boundary)', () => {
119
130
  const result = useCase.run(
120
131
  [
121
132
  candidate(
122
133
  'boundary',
123
- snapshot({ fiveHourUtilization: 0.75, sevenDayUtilization: 0.97 }),
134
+ snapshot({ fiveHourUtilization: 0.75, sevenDayUtilization: 0.99 }),
124
135
  ),
125
136
  ],
126
137
  NOW,
@@ -130,7 +141,7 @@ describe('OauthTokenSelectUseCase', () => {
130
141
  const boundary = result.metrics.find((m) => m.name === 'boundary');
131
142
  expect(boundary?.eligible).toBe(true);
132
143
  expect(boundary?.fiveHourFreeRatio).toBe(0.25);
133
- expect(boundary?.sevenDayFreeRatio).toBeCloseTo(0.03, 9);
144
+ expect(boundary?.sevenDayFreeRatio).toBeCloseTo(0.01, 9);
134
145
  });
135
146
 
136
147
  it('treats a token with no snapshot as fully free', () => {
@@ -192,7 +203,7 @@ describe('OauthTokenSelectUseCase', () => {
192
203
  const result = useCase.run(
193
204
  [
194
205
  candidate('busy', snapshot({ fiveHourUtilization: 0.9 })),
195
- candidate('alsoBusy', snapshot({ sevenDayUtilization: 0.98 })),
206
+ candidate('alsoBusy', snapshot({ sevenDayUtilization: 0.995 })),
196
207
  ],
197
208
  NOW,
198
209
  );
@@ -48,7 +48,7 @@ const SECONDS_PER_DAY = 86400;
48
48
  const SEVEN_DAYS_IN_SECONDS = 7 * SECONDS_PER_DAY;
49
49
 
50
50
  export const FIVE_HOUR_MIN_FREE_RATIO = 0.25;
51
- export const SEVEN_DAY_MIN_FREE_RATIO = 0.03;
51
+ export const SEVEN_DAY_MIN_FREE_RATIO = 0.01;
52
52
 
53
53
  export const DEFAULT_OAUTH_TOKEN_SELECTION_THRESHOLDS: OauthTokenSelectionThresholds =
54
54
  {
@@ -34,7 +34,7 @@ export type OauthTokenSelectionThresholds = {
34
34
  sevenDayMinFreeRatio: number;
35
35
  };
36
36
  export declare const FIVE_HOUR_MIN_FREE_RATIO = 0.25;
37
- export declare const SEVEN_DAY_MIN_FREE_RATIO = 0.03;
37
+ export declare const SEVEN_DAY_MIN_FREE_RATIO = 0.01;
38
38
  export declare const DEFAULT_OAUTH_TOKEN_SELECTION_THRESHOLDS: OauthTokenSelectionThresholds;
39
39
  export declare const CL_SCRIPT_OAUTH_TOKEN_SELECTION_THRESHOLDS: OauthTokenSelectionThresholds;
40
40
  export declare const SEVEN_DAY_WINDOW_HOURS = 168;