github-issue-tower-defence-management 1.82.0 → 1.83.0

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.
@@ -61,6 +61,29 @@ export class StartPreparationUseCase {
61
61
  return general !== undefined && general.rejected;
62
62
  };
63
63
 
64
+ private selectModelForToken = (
65
+ usage: ClaudeTokenUsage,
66
+ defaultModelName: string | null,
67
+ fallbackModelName: string | null,
68
+ ): string | null => {
69
+ const generalWeeklyLimit = usage.modelWeeklyLimits['seven_day'];
70
+ if (generalWeeklyLimit !== undefined && generalWeeklyLimit.rejected) {
71
+ return null;
72
+ }
73
+ const candidateModelNames = [defaultModelName, fallbackModelName].filter(
74
+ (modelName): modelName is string =>
75
+ modelName !== null && modelName !== '',
76
+ );
77
+ for (const candidateModelName of candidateModelNames) {
78
+ const weeklyLimitType = this.weeklyLimitTypeForModel(candidateModelName);
79
+ const specificWeeklyLimit = usage.modelWeeklyLimits[weeklyLimitType];
80
+ if (specificWeeklyLimit === undefined || !specificWeeklyLimit.rejected) {
81
+ return candidateModelName;
82
+ }
83
+ }
84
+ return null;
85
+ };
86
+
64
87
  private secondsUntilSevenDayReset = (
65
88
  usage: ClaudeTokenUsage,
66
89
  weeklyLimitType: string,
@@ -79,18 +102,19 @@ export class StartPreparationUseCase {
79
102
 
80
103
  private compareBySevenDayDeadlineThenUtilization = (
81
104
  a: ClaudeTokenUsage,
105
+ aWeeklyLimitType: string,
82
106
  b: ClaudeTokenUsage,
83
- weeklyLimitType: string,
107
+ bWeeklyLimitType: string,
84
108
  nowEpochSeconds: number,
85
109
  ): number => {
86
110
  const aSecondsUntilReset = this.secondsUntilSevenDayReset(
87
111
  a,
88
- weeklyLimitType,
112
+ aWeeklyLimitType,
89
113
  nowEpochSeconds,
90
114
  );
91
115
  const bSecondsUntilReset = this.secondsUntilSevenDayReset(
92
116
  b,
93
- weeklyLimitType,
117
+ bWeeklyLimitType,
94
118
  nowEpochSeconds,
95
119
  );
96
120
  if (aSecondsUntilReset !== bSecondsUntilReset) {
@@ -128,31 +152,43 @@ export class StartPreparationUseCase {
128
152
  private selectRotationTokens = (
129
153
  tokenUsages: ClaudeTokenUsage[],
130
154
  utilizationPercentageThreshold: number,
131
- modelName: string | null,
155
+ defaultModelName: string | null,
156
+ fallbackModelName: string | null,
132
157
  maxConcurrent: number,
133
158
  ): {
134
159
  tokens: string[];
135
160
  effectiveCap: number;
136
- tokensWithLimits: Array<{ token: string; limit: number }>;
161
+ tokensWithLimits: Array<{
162
+ token: string;
163
+ model: string;
164
+ limit: number;
165
+ secondsUntilSevenDayReset: number;
166
+ }>;
137
167
  } => {
138
- const weeklyLimitType = this.weeklyLimitTypeForModel(modelName);
139
168
  const nowEpochSeconds = Date.now() / 1000;
140
169
  const eligibleTokens = tokenUsages
141
170
  .filter((usage) => !usage.blocked)
142
171
  .filter((usage) => !usage.rejected)
143
172
  .filter((usage) => !this.isWithinCooldown(usage, nowEpochSeconds))
144
- .filter(
145
- (usage) => !this.isModelWeeklyLimitRejected(usage, weeklyLimitType),
146
- )
147
173
  .filter(
148
174
  (usage) =>
149
175
  usage.fiveHourUtilization * 100 < utilizationPercentageThreshold,
150
176
  )
177
+ .flatMap((usage) => {
178
+ const model = this.selectModelForToken(
179
+ usage,
180
+ defaultModelName,
181
+ fallbackModelName,
182
+ );
183
+ if (model === null) return [];
184
+ return [{ usage, model }];
185
+ })
151
186
  .sort((a, b) =>
152
187
  this.compareBySevenDayDeadlineThenUtilization(
153
- a,
154
- b,
155
- weeklyLimitType,
188
+ a.usage,
189
+ this.weeklyLimitTypeForModel(a.model),
190
+ b.usage,
191
+ this.weeklyLimitTypeForModel(b.model),
156
192
  nowEpochSeconds,
157
193
  ),
158
194
  );
@@ -161,12 +197,18 @@ export class StartPreparationUseCase {
161
197
  return { tokens: [], effectiveCap: 0, tokensWithLimits: [] };
162
198
  }
163
199
 
164
- const tokensWithLimits = eligibleTokens.map((usage) => ({
200
+ const tokensWithLimits = eligibleTokens.map(({ usage, model }) => ({
165
201
  token: usage.token,
202
+ model,
166
203
  limit: this.getTokenConcurrentLimit(
167
204
  usage.fiveHourUtilization,
168
205
  usage.sevenDayUtilization,
169
206
  ),
207
+ secondsUntilSevenDayReset: this.secondsUntilSevenDayReset(
208
+ usage,
209
+ this.weeklyLimitTypeForModel(model),
210
+ nowEpochSeconds,
211
+ ),
170
212
  }));
171
213
 
172
214
  const totalCapacity = tokensWithLimits.reduce((sum, t) => sum + t.limit, 0);
@@ -206,6 +248,7 @@ export class StartPreparationUseCase {
206
248
  .sort((a, b) =>
207
249
  this.compareBySevenDayDeadlineThenUtilization(
208
250
  a,
251
+ weeklyLimitType,
209
252
  b,
210
253
  weeklyLimitType,
211
254
  nowEpochSeconds,
@@ -261,7 +304,12 @@ export class StartPreparationUseCase {
261
304
  await this.claudeTokenUsageRepository.getAvailableTokenUsages();
262
305
  let rotationTokens: string[] | null = null;
263
306
  let proxyBaseUrl: string | null = null;
264
- let selectedTokensWithLimits: Array<{ token: string; limit: number }> = [];
307
+ let selectedTokensWithLimits: Array<{
308
+ token: string;
309
+ model: string;
310
+ limit: number;
311
+ secondsUntilSevenDayReset: number;
312
+ }> = [];
265
313
  let tokenInFlightCounts: Record<string, number> = {};
266
314
  const rotationOrder: RotationOrderEntry[] | null =
267
315
  tokenUsages.length > 0
@@ -274,51 +322,23 @@ export class StartPreparationUseCase {
274
322
  const maximumPreparingIssuesCount =
275
323
  params.maximumPreparingIssuesCount ?? NORMAL_CONCURRENT_LIMIT;
276
324
  let effectiveMaxPreparingIssuesCount = maximumPreparingIssuesCount;
277
- let effectiveDefaultLlmModelName = params.defaultLlmModelName;
325
+ const fallbackLlmModelName =
326
+ params.fallbackLlmModelName ?? DEFAULT_FALLBACK_LLM_MODEL_NAME;
278
327
  if (tokenUsages.length > 0) {
279
328
  const {
280
- tokens: ranked,
281
- effectiveCap,
282
- tokensWithLimits: rankedTokensWithLimits,
329
+ tokens: selectedTokens,
330
+ effectiveCap: selectedCap,
331
+ tokensWithLimits: selectedTokensWithLimitsLocal,
283
332
  } = this.selectRotationTokens(
284
333
  tokenUsages,
285
334
  params.utilizationPercentageThreshold,
286
335
  params.defaultLlmModelName,
336
+ fallbackLlmModelName,
287
337
  maximumPreparingIssuesCount,
288
338
  );
289
- let selectedTokens = ranked;
290
- let selectedCap = effectiveCap;
291
- let selectedTokensWithLimitsLocal = rankedTokensWithLimits;
292
- if (
293
- selectedTokens.length === 0 &&
294
- this.weeklyLimitTypeForModel(params.defaultLlmModelName) ===
295
- 'seven_day_sonnet'
296
- ) {
297
- const fallbackModelName =
298
- params.fallbackLlmModelName ?? DEFAULT_FALLBACK_LLM_MODEL_NAME;
299
- const {
300
- tokens: fallbackRanked,
301
- effectiveCap: fallbackCap,
302
- tokensWithLimits: fallbackTokensWithLimits,
303
- } = this.selectRotationTokens(
304
- tokenUsages,
305
- params.utilizationPercentageThreshold,
306
- fallbackModelName,
307
- maximumPreparingIssuesCount,
308
- );
309
- if (fallbackRanked.length > 0) {
310
- console.warn(
311
- `Sonnet 7-day weekly limit (${this.weeklyLimitTypeForModel(params.defaultLlmModelName)}) is exhausted across all configured Claude OAuth token(s). Falling back to ${fallbackModelName}.`,
312
- );
313
- selectedTokens = fallbackRanked;
314
- selectedCap = fallbackCap;
315
- selectedTokensWithLimitsLocal = fallbackTokensWithLimits;
316
- effectiveDefaultLlmModelName = fallbackModelName;
317
- }
318
- }
319
339
  if (selectedTokens.length === 0) {
320
340
  console.warn(
321
- `All ${tokenUsages.length} configured Claude OAuth token(s) are unavailable (blocked, rejected, weekly limit for ${this.weeklyLimitTypeForModel(params.defaultLlmModelName)} exhausted, or 5h utilization >= ${params.utilizationPercentageThreshold}%). Skipping starting preparation.`,
341
+ `All ${tokenUsages.length} configured Claude OAuth token(s) are unavailable (blocked, rejected, weekly limits for the configured model(s) exhausted, or 5h utilization >= ${params.utilizationPercentageThreshold}%). Skipping starting preparation.`,
322
342
  );
323
343
  return { rotationOrder };
324
344
  }
@@ -421,12 +441,15 @@ export class StartPreparationUseCase {
421
441
  .trim() ||
422
442
  params.defaultLlmAgentName ||
423
443
  params.defaultAgentName;
424
- const model =
425
- issue.labels
426
- .find((label: string) => label.startsWith('llm-model:'))
427
- ?.replace('llm-model:', '')
428
- .trim() || effectiveDefaultLlmModelName;
429
- if (!model) {
444
+ const labelModelName = issue.labels
445
+ .find((label: string) => label.startsWith('llm-model:'))
446
+ ?.replace('llm-model:', '')
447
+ .trim();
448
+ if (
449
+ !labelModelName &&
450
+ !params.defaultLlmModelName &&
451
+ rotationTokens === null
452
+ ) {
430
453
  console.error(
431
454
  `No LLM model configured for issue ${issue.url}. Provide --defaultLlmModelName or add an llm-model: label.`,
432
455
  );
@@ -509,41 +532,31 @@ export class StartPreparationUseCase {
509
532
  );
510
533
  issue.status = PREPARATION_STATUS_NAME;
511
534
 
512
- const awArgs: string[] = [
513
- issue.url,
514
- agent,
515
- model,
516
- '--configFilePath',
517
- params.configFilePath,
518
- '--branch',
519
- branchName,
520
- ];
521
- if (
522
- params.codexHomeCandidates !== null &&
523
- params.codexHomeCandidates.length > 0
524
- ) {
525
- const codexHome =
526
- params.codexHomeCandidates[
527
- startedInThisRunCount % params.codexHomeCandidates.length
528
- ];
529
- awArgs.push('--codexHome', codexHome);
530
- }
531
535
  let spawnEnv: Record<string, string> | undefined;
536
+ let routedModelName: string | null = null;
532
537
  if (rotationTokens !== null && proxyBaseUrl !== null) {
533
- const tokenWithMostRemainingCapacity = selectedTokensWithLimits
538
+ const tokenWithSoonestResetAmongAvailable = selectedTokensWithLimits
534
539
  .map((t) => ({
535
540
  token: t.token,
541
+ model: t.model,
536
542
  remaining:
537
543
  t.limit -
538
544
  (tokenInFlightCounts[t.token] ?? 0) -
539
545
  (spawnedInThisRunByToken[t.token] ?? 0),
546
+ secondsUntilSevenDayReset: t.secondsUntilSevenDayReset,
540
547
  }))
541
548
  .filter((t) => t.remaining > 0)
542
- .sort((a, b) => b.remaining - a.remaining)[0];
543
- if (tokenWithMostRemainingCapacity === undefined) {
549
+ .sort((a, b) => {
550
+ if (a.secondsUntilSevenDayReset !== b.secondsUntilSevenDayReset) {
551
+ return a.secondsUntilSevenDayReset - b.secondsUntilSevenDayReset;
552
+ }
553
+ return b.remaining - a.remaining;
554
+ })[0];
555
+ if (tokenWithSoonestResetAmongAvailable === undefined) {
544
556
  break;
545
557
  }
546
- const selected = tokenWithMostRemainingCapacity.token;
558
+ const selected = tokenWithSoonestResetAmongAvailable.token;
559
+ routedModelName = tokenWithSoonestResetAmongAvailable.model;
547
560
  spawnedInThisRunByToken[selected] =
548
561
  (spawnedInThisRunByToken[selected] ?? 0) + 1;
549
562
  spawnEnv = {
@@ -551,6 +564,33 @@ export class StartPreparationUseCase {
551
564
  ANTHROPIC_BASE_URL: proxyBaseUrl,
552
565
  };
553
566
  }
567
+ const model =
568
+ labelModelName || routedModelName || params.defaultLlmModelName;
569
+ if (!model) {
570
+ console.error(
571
+ `No LLM model configured for issue ${issue.url}. Provide --defaultLlmModelName or add an llm-model: label.`,
572
+ );
573
+ continue;
574
+ }
575
+ const awArgs: string[] = [
576
+ issue.url,
577
+ agent,
578
+ model,
579
+ '--configFilePath',
580
+ params.configFilePath,
581
+ '--branch',
582
+ branchName,
583
+ ];
584
+ if (
585
+ params.codexHomeCandidates !== null &&
586
+ params.codexHomeCandidates.length > 0
587
+ ) {
588
+ const codexHome =
589
+ params.codexHomeCandidates[
590
+ startedInThisRunCount % params.codexHomeCandidates.length
591
+ ];
592
+ awArgs.push('--codexHome', codexHome);
593
+ }
554
594
  await this.localCommandRunner.runCommand(
555
595
  'aw',
556
596
  awArgs,
@@ -21,6 +21,7 @@ export declare class StartPreparationUseCase {
21
21
  private weeklyLimitTypeForModel;
22
22
  private isWithinCooldown;
23
23
  private isModelWeeklyLimitRejected;
24
+ private selectModelForToken;
24
25
  private secondsUntilSevenDayReset;
25
26
  private compareBySevenDayDeadlineThenUtilization;
26
27
  private taperedConcurrentLimit;
@@ -1 +1 @@
1
- {"version":3,"file":"StartPreparationUseCase.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/StartPreparationUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,EAAE,0BAA0B,EAAE,MAAM,iDAAiD,CAAC;AAC7F,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAShE,eAAO,MAAM,+BAA+B,oBAAoB,CAAC;AAEjE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,mBAAmB,EAAE,MAAM,CAAC;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,gBAAgB,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,qBAAa,uBAAuB;IAEhC,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,eAAe;IAUhC,OAAO,CAAC,QAAQ,CAAC,kBAAkB;IACnC,OAAO,CAAC,QAAQ,CAAC,0BAA0B;gBAZ1B,iBAAiB,EAAE,IAAI,CAAC,iBAAiB,EAAE,UAAU,CAAC,EACtD,eAAe,EAAE,IAAI,CACpC,eAAe,EACb,mBAAmB,GACnB,cAAc,GACd,oBAAoB,GACpB,oBAAoB,GACpB,kBAAkB,GAClB,yBAAyB,GACzB,oBAAoB,CACvB,EACgB,kBAAkB,EAAE,kBAAkB,EACtC,0BAA0B,EAAE,0BAA0B;IAGzE,OAAO,CAAC,uBAAuB,CAK7B;IAEF,OAAO,CAAC,gBAAgB,CAGgC;IAExD,OAAO,CAAC,0BAA0B,CAQhC;IAEF,OAAO,CAAC,yBAAyB,CAc/B;IAEF,OAAO,CAAC,wCAAwC,CAoB9C;IAEF,OAAO,CAAC,sBAAsB,CAS5B;IAEF,uBAAuB,GACrB,qBAAqB,MAAM,EAC3B,qBAAqB,MAAM,KAC1B,MAAM,CAUP;IAEF,OAAO,CAAC,oBAAoB,CA0D1B;IAEF,kBAAkB,GAChB,aAAa,gBAAgB,EAAE,EAC/B,gCAAgC,MAAM,EACtC,WAAW,MAAM,GAAG,IAAI,KACvB,kBAAkB,EAAE,CAoDrB;IAEF,GAAG,GAAU,QAAQ;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,gBAAgB,EAAE,MAAM,CAAC;QACzB,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;QACnC,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;QACpC,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;QACnC,cAAc,EAAE,MAAM,CAAC;QACvB,2BAA2B,EAAE,MAAM,GAAG,IAAI,CAAC;QAC3C,8BAA8B,EAAE,MAAM,CAAC;QACvC,mBAAmB,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QACrC,mBAAmB,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QACrC,sBAAsB,EAAE,MAAM,CAAC;QAC/B,oBAAoB,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;KACvC,KAAG,OAAO,CAAC;QAAE,aAAa,EAAE,kBAAkB,EAAE,GAAG,IAAI,CAAA;KAAE,CAAC,CAgTzD;CACH"}
1
+ {"version":3,"file":"StartPreparationUseCase.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/StartPreparationUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,EAAE,0BAA0B,EAAE,MAAM,iDAAiD,CAAC;AAC7F,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAShE,eAAO,MAAM,+BAA+B,oBAAoB,CAAC;AAEjE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,mBAAmB,EAAE,MAAM,CAAC;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,gBAAgB,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,qBAAa,uBAAuB;IAEhC,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,eAAe;IAUhC,OAAO,CAAC,QAAQ,CAAC,kBAAkB;IACnC,OAAO,CAAC,QAAQ,CAAC,0BAA0B;gBAZ1B,iBAAiB,EAAE,IAAI,CAAC,iBAAiB,EAAE,UAAU,CAAC,EACtD,eAAe,EAAE,IAAI,CACpC,eAAe,EACb,mBAAmB,GACnB,cAAc,GACd,oBAAoB,GACpB,oBAAoB,GACpB,kBAAkB,GAClB,yBAAyB,GACzB,oBAAoB,CACvB,EACgB,kBAAkB,EAAE,kBAAkB,EACtC,0BAA0B,EAAE,0BAA0B;IAGzE,OAAO,CAAC,uBAAuB,CAK7B;IAEF,OAAO,CAAC,gBAAgB,CAGgC;IAExD,OAAO,CAAC,0BAA0B,CAQhC;IAEF,OAAO,CAAC,mBAAmB,CAqBzB;IAEF,OAAO,CAAC,yBAAyB,CAc/B;IAEF,OAAO,CAAC,wCAAwC,CAqB9C;IAEF,OAAO,CAAC,sBAAsB,CAS5B;IAEF,uBAAuB,GACrB,qBAAqB,MAAM,EAC3B,qBAAqB,MAAM,KAC1B,MAAM,CAUP;IAEF,OAAO,CAAC,oBAAoB,CA4E1B;IAEF,kBAAkB,GAChB,aAAa,gBAAgB,EAAE,EAC/B,gCAAgC,MAAM,EACtC,WAAW,MAAM,GAAG,IAAI,KACvB,kBAAkB,EAAE,CAqDrB;IAEF,GAAG,GAAU,QAAQ;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,gBAAgB,EAAE,MAAM,CAAC;QACzB,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;QACnC,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;QACpC,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;QACnC,cAAc,EAAE,MAAM,CAAC;QACvB,2BAA2B,EAAE,MAAM,GAAG,IAAI,CAAC;QAC3C,8BAA8B,EAAE,MAAM,CAAC;QACvC,mBAAmB,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QACrC,mBAAmB,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QACrC,sBAAsB,EAAE,MAAM,CAAC;QAC/B,oBAAoB,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;KACvC,KAAG,OAAO,CAAC;QAAE,aAAa,EAAE,kBAAkB,EAAE,GAAG,IAAI,CAAA;KAAE,CAAC,CA6SzD;CACH"}