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 +7 -0
- package/README.md +1 -1
- package/bin/domain/usecases/OauthTokenSelectUseCase.js +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.test.ts +1 -1
- package/src/domain/usecases/OauthTokenSelectUseCase.test.ts +21 -10
- package/src/domain/usecases/OauthTokenSelectUseCase.ts +1 -1
- package/types/domain/usecases/OauthTokenSelectUseCase.d.ts +1 -1
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
|
|
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.
|
|
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
|
@@ -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 >=
|
|
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('
|
|
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.
|
|
96
|
-
candidate('ok', snapshot({ sevenDayUtilization: 0.
|
|
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
|
|
118
|
+
it('treats exactly 99% used 7d utilization as eligible (boundary)', () => {
|
|
108
119
|
const result = useCase.run(
|
|
109
|
-
[candidate('boundary', snapshot({ sevenDayUtilization: 0.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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;
|