github-issue-tower-defence-management 1.82.1 → 1.84.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.
- package/.github/workflows/commit-lint.yml +7 -2
- package/.github/workflows/create-pr.yml +23 -5
- package/CHANGELOG.md +14 -0
- package/README.md +67 -4
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +19 -2
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/consoleListsWriter.js +43 -0
- package/bin/adapter/entry-points/handlers/consoleListsWriter.js.map +1 -0
- package/bin/domain/usecases/StartPreparationUseCase.js +60 -46
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js +101 -0
- package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +18 -0
- package/src/adapter/entry-points/handlers/consoleListsWriter.test.ts +167 -0
- package/src/adapter/entry-points/handlers/consoleListsWriter.ts +60 -0
- package/src/domain/usecases/StartPreparationUseCase.test.ts +265 -68
- package/src/domain/usecases/StartPreparationUseCase.ts +94 -73
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +372 -0
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.ts +206 -0
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/consoleListsWriter.d.ts +13 -0
- package/types/adapter/entry-points/handlers/consoleListsWriter.d.ts.map +1 -0
- package/types/domain/usecases/StartPreparationUseCase.d.ts +1 -0
- package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/console/GenerateConsoleListsUseCase.d.ts +63 -0
- package/types/domain/usecases/console/GenerateConsoleListsUseCase.d.ts.map +1 -0
|
@@ -3776,74 +3776,6 @@ describe('StartPreparationUseCase', () => {
|
|
|
3776
3776
|
expect(mockLocalCommandRunner.runCommand.mock.calls[0][2]).toBeUndefined();
|
|
3777
3777
|
});
|
|
3778
3778
|
|
|
3779
|
-
it('should exclude a token whose seven_day_sonnet weekly limit is rejected when the model is sonnet', async () => {
|
|
3780
|
-
const awaitingIssue = createMockIssue({
|
|
3781
|
-
url: 'url1',
|
|
3782
|
-
title: 'Issue 1',
|
|
3783
|
-
labels: ['category:impl'],
|
|
3784
|
-
status: 'Awaiting Workspace',
|
|
3785
|
-
number: 1,
|
|
3786
|
-
itemId: 'item-1',
|
|
3787
|
-
});
|
|
3788
|
-
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
3789
|
-
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
3790
|
-
createMockStoryObjectMap([awaitingIssue]),
|
|
3791
|
-
);
|
|
3792
|
-
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
3793
|
-
stdout: '',
|
|
3794
|
-
stderr: '',
|
|
3795
|
-
exitCode: 0,
|
|
3796
|
-
});
|
|
3797
|
-
const futureReset = Math.floor(Date.now() / 1000) + 3600;
|
|
3798
|
-
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
3799
|
-
{
|
|
3800
|
-
name: 'token-sonnet-exhausted',
|
|
3801
|
-
token: 'token-sonnet-exhausted',
|
|
3802
|
-
fiveHourUtilization: 0.1,
|
|
3803
|
-
sevenDayUtilization: 0,
|
|
3804
|
-
blocked: false,
|
|
3805
|
-
rejected: false,
|
|
3806
|
-
blockedUntilEpoch: 0,
|
|
3807
|
-
modelWeeklyLimits: {
|
|
3808
|
-
seven_day_sonnet: { rejected: true, resetsAt: futureReset },
|
|
3809
|
-
},
|
|
3810
|
-
},
|
|
3811
|
-
{
|
|
3812
|
-
name: 'token-ok',
|
|
3813
|
-
token: 'token-ok',
|
|
3814
|
-
fiveHourUtilization: 0.5,
|
|
3815
|
-
sevenDayUtilization: 0,
|
|
3816
|
-
blocked: false,
|
|
3817
|
-
rejected: false,
|
|
3818
|
-
blockedUntilEpoch: 0,
|
|
3819
|
-
modelWeeklyLimits: {},
|
|
3820
|
-
},
|
|
3821
|
-
]);
|
|
3822
|
-
|
|
3823
|
-
await useCase.run({
|
|
3824
|
-
projectUrl: 'https://github.com/user/repo',
|
|
3825
|
-
defaultAgentName: 'agent1',
|
|
3826
|
-
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
3827
|
-
fallbackLlmModelName: null,
|
|
3828
|
-
defaultLlmAgentName: null,
|
|
3829
|
-
configFilePath: '/path/to/config.yml',
|
|
3830
|
-
maximumPreparingIssuesCount: null,
|
|
3831
|
-
utilizationPercentageThreshold: 90,
|
|
3832
|
-
allowedIssueAuthors: null,
|
|
3833
|
-
codexHomeCandidates: null,
|
|
3834
|
-
allowIssueCacheMinutes: 0,
|
|
3835
|
-
labelsAsLlmAgentName: null,
|
|
3836
|
-
});
|
|
3837
|
-
|
|
3838
|
-
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
3839
|
-
expect(mockLocalCommandRunner.runCommand.mock.calls[0][2]).toEqual({
|
|
3840
|
-
env: {
|
|
3841
|
-
CLAUDE_CODE_OAUTH_TOKEN: 'token-ok',
|
|
3842
|
-
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8787',
|
|
3843
|
-
},
|
|
3844
|
-
});
|
|
3845
|
-
});
|
|
3846
|
-
|
|
3847
3779
|
it('should re-admit a token whose seven_day_sonnet rejection has been cleared by stale-reset expiry', async () => {
|
|
3848
3780
|
const awaitingIssue = createMockIssue({
|
|
3849
3781
|
url: 'url1',
|
|
@@ -5015,6 +4947,271 @@ describe('StartPreparationUseCase', () => {
|
|
|
5015
4947
|
});
|
|
5016
4948
|
});
|
|
5017
4949
|
|
|
4950
|
+
describe('per-token model weekly-limit routing', () => {
|
|
4951
|
+
const futureReset = Math.floor(Date.now() / 1000) + 3600;
|
|
4952
|
+
|
|
4953
|
+
it('routes a token whose seven_day_sonnet weekly limit is rejected to Opus while a sibling token with Sonnet headroom uses Sonnet in the same pass', async () => {
|
|
4954
|
+
const issues = [
|
|
4955
|
+
createMockIssue({
|
|
4956
|
+
url: 'url1',
|
|
4957
|
+
title: 'Issue 1',
|
|
4958
|
+
labels: ['category:impl'],
|
|
4959
|
+
status: 'Awaiting Workspace',
|
|
4960
|
+
number: 1,
|
|
4961
|
+
itemId: 'item-1',
|
|
4962
|
+
}),
|
|
4963
|
+
createMockIssue({
|
|
4964
|
+
url: 'url2',
|
|
4965
|
+
title: 'Issue 2',
|
|
4966
|
+
labels: ['category:impl'],
|
|
4967
|
+
status: 'Awaiting Workspace',
|
|
4968
|
+
number: 2,
|
|
4969
|
+
itemId: 'item-2',
|
|
4970
|
+
}),
|
|
4971
|
+
];
|
|
4972
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
4973
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
4974
|
+
createMockStoryObjectMap(issues),
|
|
4975
|
+
);
|
|
4976
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
4977
|
+
stdout: '',
|
|
4978
|
+
stderr: '',
|
|
4979
|
+
exitCode: 0,
|
|
4980
|
+
});
|
|
4981
|
+
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
4982
|
+
{
|
|
4983
|
+
name: 'token-sonnet-exhausted',
|
|
4984
|
+
token: 'token-sonnet-exhausted',
|
|
4985
|
+
fiveHourUtilization: 0.1,
|
|
4986
|
+
sevenDayUtilization: 0,
|
|
4987
|
+
blocked: false,
|
|
4988
|
+
rejected: false,
|
|
4989
|
+
blockedUntilEpoch: 0,
|
|
4990
|
+
modelWeeklyLimits: {
|
|
4991
|
+
seven_day_sonnet: { rejected: true, resetsAt: futureReset },
|
|
4992
|
+
},
|
|
4993
|
+
},
|
|
4994
|
+
{
|
|
4995
|
+
name: 'token-sonnet-ok',
|
|
4996
|
+
token: 'token-sonnet-ok',
|
|
4997
|
+
fiveHourUtilization: 0.5,
|
|
4998
|
+
sevenDayUtilization: 0,
|
|
4999
|
+
blocked: false,
|
|
5000
|
+
rejected: false,
|
|
5001
|
+
blockedUntilEpoch: 0,
|
|
5002
|
+
modelWeeklyLimits: {},
|
|
5003
|
+
},
|
|
5004
|
+
]);
|
|
5005
|
+
|
|
5006
|
+
await useCase.run({
|
|
5007
|
+
projectUrl: 'https://github.com/user/repo',
|
|
5008
|
+
defaultAgentName: 'agent1',
|
|
5009
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
5010
|
+
fallbackLlmModelName: 'claude-opus-4-8',
|
|
5011
|
+
defaultLlmAgentName: null,
|
|
5012
|
+
configFilePath: '/path/to/config.yml',
|
|
5013
|
+
maximumPreparingIssuesCount: null,
|
|
5014
|
+
utilizationPercentageThreshold: 90,
|
|
5015
|
+
allowedIssueAuthors: null,
|
|
5016
|
+
codexHomeCandidates: null,
|
|
5017
|
+
allowIssueCacheMinutes: 0,
|
|
5018
|
+
labelsAsLlmAgentName: null,
|
|
5019
|
+
});
|
|
5020
|
+
|
|
5021
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(2);
|
|
5022
|
+
const callForExhausted =
|
|
5023
|
+
mockLocalCommandRunner.runCommand.mock.calls.find(
|
|
5024
|
+
(call) =>
|
|
5025
|
+
call[2]?.env?.CLAUDE_CODE_OAUTH_TOKEN === 'token-sonnet-exhausted',
|
|
5026
|
+
);
|
|
5027
|
+
const callForOk = mockLocalCommandRunner.runCommand.mock.calls.find(
|
|
5028
|
+
(call) => call[2]?.env?.CLAUDE_CODE_OAUTH_TOKEN === 'token-sonnet-ok',
|
|
5029
|
+
);
|
|
5030
|
+
expect(callForExhausted).toBeDefined();
|
|
5031
|
+
expect(callForOk).toBeDefined();
|
|
5032
|
+
expect(callForExhausted?.[1][2]).toBe('claude-opus-4-8');
|
|
5033
|
+
expect(callForOk?.[1][2]).toBe('claude-sonnet-4-6');
|
|
5034
|
+
});
|
|
5035
|
+
|
|
5036
|
+
it('excludes a token whose seven_day_sonnet and seven_day_opus weekly limits are both rejected', async () => {
|
|
5037
|
+
const awaitingIssue = createMockIssue({
|
|
5038
|
+
url: 'url1',
|
|
5039
|
+
title: 'Issue 1',
|
|
5040
|
+
labels: ['category:impl'],
|
|
5041
|
+
status: 'Awaiting Workspace',
|
|
5042
|
+
number: 1,
|
|
5043
|
+
itemId: 'item-1',
|
|
5044
|
+
});
|
|
5045
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
5046
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
5047
|
+
createMockStoryObjectMap([awaitingIssue]),
|
|
5048
|
+
);
|
|
5049
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
5050
|
+
stdout: '',
|
|
5051
|
+
stderr: '',
|
|
5052
|
+
exitCode: 0,
|
|
5053
|
+
});
|
|
5054
|
+
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
5055
|
+
{
|
|
5056
|
+
name: 'token-both-exhausted',
|
|
5057
|
+
token: 'token-both-exhausted',
|
|
5058
|
+
fiveHourUtilization: 0.1,
|
|
5059
|
+
sevenDayUtilization: 0,
|
|
5060
|
+
blocked: false,
|
|
5061
|
+
rejected: false,
|
|
5062
|
+
blockedUntilEpoch: 0,
|
|
5063
|
+
modelWeeklyLimits: {
|
|
5064
|
+
seven_day_sonnet: { rejected: true, resetsAt: futureReset },
|
|
5065
|
+
seven_day_opus: { rejected: true, resetsAt: futureReset },
|
|
5066
|
+
},
|
|
5067
|
+
},
|
|
5068
|
+
]);
|
|
5069
|
+
const consoleWarnSpy = jest
|
|
5070
|
+
.spyOn(console, 'warn')
|
|
5071
|
+
.mockImplementation(() => {});
|
|
5072
|
+
|
|
5073
|
+
await useCase.run({
|
|
5074
|
+
projectUrl: 'https://github.com/user/repo',
|
|
5075
|
+
defaultAgentName: 'agent1',
|
|
5076
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
5077
|
+
fallbackLlmModelName: 'claude-opus-4-8',
|
|
5078
|
+
defaultLlmAgentName: null,
|
|
5079
|
+
configFilePath: '/path/to/config.yml',
|
|
5080
|
+
maximumPreparingIssuesCount: null,
|
|
5081
|
+
utilizationPercentageThreshold: 90,
|
|
5082
|
+
allowedIssueAuthors: null,
|
|
5083
|
+
codexHomeCandidates: null,
|
|
5084
|
+
allowIssueCacheMinutes: 0,
|
|
5085
|
+
labelsAsLlmAgentName: null,
|
|
5086
|
+
});
|
|
5087
|
+
|
|
5088
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(0);
|
|
5089
|
+
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(0);
|
|
5090
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
5091
|
+
expect.stringContaining('Skipping starting preparation'),
|
|
5092
|
+
);
|
|
5093
|
+
consoleWarnSpy.mockRestore();
|
|
5094
|
+
});
|
|
5095
|
+
|
|
5096
|
+
it('excludes a token whose generic seven_day weekly limit is rejected even when the per-model windows are open', async () => {
|
|
5097
|
+
const awaitingIssue = createMockIssue({
|
|
5098
|
+
url: 'url1',
|
|
5099
|
+
title: 'Issue 1',
|
|
5100
|
+
labels: ['category:impl'],
|
|
5101
|
+
status: 'Awaiting Workspace',
|
|
5102
|
+
number: 1,
|
|
5103
|
+
itemId: 'item-1',
|
|
5104
|
+
});
|
|
5105
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
5106
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
5107
|
+
createMockStoryObjectMap([awaitingIssue]),
|
|
5108
|
+
);
|
|
5109
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
5110
|
+
stdout: '',
|
|
5111
|
+
stderr: '',
|
|
5112
|
+
exitCode: 0,
|
|
5113
|
+
});
|
|
5114
|
+
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
5115
|
+
{
|
|
5116
|
+
name: 'token-general-exhausted',
|
|
5117
|
+
token: 'token-general-exhausted',
|
|
5118
|
+
fiveHourUtilization: 0.1,
|
|
5119
|
+
sevenDayUtilization: 0,
|
|
5120
|
+
blocked: false,
|
|
5121
|
+
rejected: false,
|
|
5122
|
+
blockedUntilEpoch: 0,
|
|
5123
|
+
modelWeeklyLimits: {
|
|
5124
|
+
seven_day: { rejected: true, resetsAt: futureReset },
|
|
5125
|
+
},
|
|
5126
|
+
},
|
|
5127
|
+
]);
|
|
5128
|
+
const consoleWarnSpy = jest
|
|
5129
|
+
.spyOn(console, 'warn')
|
|
5130
|
+
.mockImplementation(() => {});
|
|
5131
|
+
|
|
5132
|
+
await useCase.run({
|
|
5133
|
+
projectUrl: 'https://github.com/user/repo',
|
|
5134
|
+
defaultAgentName: 'agent1',
|
|
5135
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
5136
|
+
fallbackLlmModelName: 'claude-opus-4-8',
|
|
5137
|
+
defaultLlmAgentName: null,
|
|
5138
|
+
configFilePath: '/path/to/config.yml',
|
|
5139
|
+
maximumPreparingIssuesCount: null,
|
|
5140
|
+
utilizationPercentageThreshold: 90,
|
|
5141
|
+
allowedIssueAuthors: null,
|
|
5142
|
+
codexHomeCandidates: null,
|
|
5143
|
+
allowIssueCacheMinutes: 0,
|
|
5144
|
+
labelsAsLlmAgentName: null,
|
|
5145
|
+
});
|
|
5146
|
+
|
|
5147
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(0);
|
|
5148
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
5149
|
+
expect.stringContaining('Skipping starting preparation'),
|
|
5150
|
+
);
|
|
5151
|
+
consoleWarnSpy.mockRestore();
|
|
5152
|
+
});
|
|
5153
|
+
|
|
5154
|
+
it('lets a per-issue llm-model label override the per-token routed model', async () => {
|
|
5155
|
+
const awaitingIssue = createMockIssue({
|
|
5156
|
+
url: 'url1',
|
|
5157
|
+
title: 'Issue 1',
|
|
5158
|
+
labels: ['category:impl', 'llm-model:claude-3-5-haiku-20241022'],
|
|
5159
|
+
status: 'Awaiting Workspace',
|
|
5160
|
+
number: 1,
|
|
5161
|
+
itemId: 'item-1',
|
|
5162
|
+
});
|
|
5163
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
5164
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
5165
|
+
createMockStoryObjectMap([awaitingIssue]),
|
|
5166
|
+
);
|
|
5167
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
5168
|
+
stdout: '',
|
|
5169
|
+
stderr: '',
|
|
5170
|
+
exitCode: 0,
|
|
5171
|
+
});
|
|
5172
|
+
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
5173
|
+
{
|
|
5174
|
+
name: 'token-sonnet-exhausted',
|
|
5175
|
+
token: 'token-sonnet-exhausted',
|
|
5176
|
+
fiveHourUtilization: 0.1,
|
|
5177
|
+
sevenDayUtilization: 0,
|
|
5178
|
+
blocked: false,
|
|
5179
|
+
rejected: false,
|
|
5180
|
+
blockedUntilEpoch: 0,
|
|
5181
|
+
modelWeeklyLimits: {
|
|
5182
|
+
seven_day_sonnet: { rejected: true, resetsAt: futureReset },
|
|
5183
|
+
},
|
|
5184
|
+
},
|
|
5185
|
+
]);
|
|
5186
|
+
|
|
5187
|
+
await useCase.run({
|
|
5188
|
+
projectUrl: 'https://github.com/user/repo',
|
|
5189
|
+
defaultAgentName: 'agent1',
|
|
5190
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
5191
|
+
fallbackLlmModelName: 'claude-opus-4-8',
|
|
5192
|
+
defaultLlmAgentName: null,
|
|
5193
|
+
configFilePath: '/path/to/config.yml',
|
|
5194
|
+
maximumPreparingIssuesCount: null,
|
|
5195
|
+
utilizationPercentageThreshold: 90,
|
|
5196
|
+
allowedIssueAuthors: null,
|
|
5197
|
+
codexHomeCandidates: null,
|
|
5198
|
+
allowIssueCacheMinutes: 0,
|
|
5199
|
+
labelsAsLlmAgentName: null,
|
|
5200
|
+
});
|
|
5201
|
+
|
|
5202
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
5203
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0][1][2]).toBe(
|
|
5204
|
+
'claude-3-5-haiku-20241022',
|
|
5205
|
+
);
|
|
5206
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0][2]).toEqual({
|
|
5207
|
+
env: {
|
|
5208
|
+
CLAUDE_CODE_OAUTH_TOKEN: 'token-sonnet-exhausted',
|
|
5209
|
+
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8787',
|
|
5210
|
+
},
|
|
5211
|
+
});
|
|
5212
|
+
});
|
|
5213
|
+
});
|
|
5214
|
+
|
|
5018
5215
|
describe('per-token in-flight global concurrency enforcement', () => {
|
|
5019
5216
|
it('should not spawn when the selected token already has its full in-flight limit occupied by processes from other projects', async () => {
|
|
5020
5217
|
const awaitingIssues: Issue[] = [
|
|
@@ -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
|
-
|
|
107
|
+
bWeeklyLimitType: string,
|
|
84
108
|
nowEpochSeconds: number,
|
|
85
109
|
): number => {
|
|
86
110
|
const aSecondsUntilReset = this.secondsUntilSevenDayReset(
|
|
87
111
|
a,
|
|
88
|
-
|
|
112
|
+
aWeeklyLimitType,
|
|
89
113
|
nowEpochSeconds,
|
|
90
114
|
);
|
|
91
115
|
const bSecondsUntilReset = this.secondsUntilSevenDayReset(
|
|
92
116
|
b,
|
|
93
|
-
|
|
117
|
+
bWeeklyLimitType,
|
|
94
118
|
nowEpochSeconds,
|
|
95
119
|
);
|
|
96
120
|
if (aSecondsUntilReset !== bSecondsUntilReset) {
|
|
@@ -128,35 +152,43 @@ export class StartPreparationUseCase {
|
|
|
128
152
|
private selectRotationTokens = (
|
|
129
153
|
tokenUsages: ClaudeTokenUsage[],
|
|
130
154
|
utilizationPercentageThreshold: number,
|
|
131
|
-
|
|
155
|
+
defaultModelName: string | null,
|
|
156
|
+
fallbackModelName: string | null,
|
|
132
157
|
maxConcurrent: number,
|
|
133
158
|
): {
|
|
134
159
|
tokens: string[];
|
|
135
160
|
effectiveCap: number;
|
|
136
161
|
tokensWithLimits: Array<{
|
|
137
162
|
token: string;
|
|
163
|
+
model: string;
|
|
138
164
|
limit: number;
|
|
139
165
|
secondsUntilSevenDayReset: number;
|
|
140
166
|
}>;
|
|
141
167
|
} => {
|
|
142
|
-
const weeklyLimitType = this.weeklyLimitTypeForModel(modelName);
|
|
143
168
|
const nowEpochSeconds = Date.now() / 1000;
|
|
144
169
|
const eligibleTokens = tokenUsages
|
|
145
170
|
.filter((usage) => !usage.blocked)
|
|
146
171
|
.filter((usage) => !usage.rejected)
|
|
147
172
|
.filter((usage) => !this.isWithinCooldown(usage, nowEpochSeconds))
|
|
148
|
-
.filter(
|
|
149
|
-
(usage) => !this.isModelWeeklyLimitRejected(usage, weeklyLimitType),
|
|
150
|
-
)
|
|
151
173
|
.filter(
|
|
152
174
|
(usage) =>
|
|
153
175
|
usage.fiveHourUtilization * 100 < utilizationPercentageThreshold,
|
|
154
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
|
+
})
|
|
155
186
|
.sort((a, b) =>
|
|
156
187
|
this.compareBySevenDayDeadlineThenUtilization(
|
|
157
|
-
a,
|
|
158
|
-
|
|
159
|
-
|
|
188
|
+
a.usage,
|
|
189
|
+
this.weeklyLimitTypeForModel(a.model),
|
|
190
|
+
b.usage,
|
|
191
|
+
this.weeklyLimitTypeForModel(b.model),
|
|
160
192
|
nowEpochSeconds,
|
|
161
193
|
),
|
|
162
194
|
);
|
|
@@ -165,15 +197,16 @@ export class StartPreparationUseCase {
|
|
|
165
197
|
return { tokens: [], effectiveCap: 0, tokensWithLimits: [] };
|
|
166
198
|
}
|
|
167
199
|
|
|
168
|
-
const tokensWithLimits = eligibleTokens.map((usage) => ({
|
|
200
|
+
const tokensWithLimits = eligibleTokens.map(({ usage, model }) => ({
|
|
169
201
|
token: usage.token,
|
|
202
|
+
model,
|
|
170
203
|
limit: this.getTokenConcurrentLimit(
|
|
171
204
|
usage.fiveHourUtilization,
|
|
172
205
|
usage.sevenDayUtilization,
|
|
173
206
|
),
|
|
174
207
|
secondsUntilSevenDayReset: this.secondsUntilSevenDayReset(
|
|
175
208
|
usage,
|
|
176
|
-
|
|
209
|
+
this.weeklyLimitTypeForModel(model),
|
|
177
210
|
nowEpochSeconds,
|
|
178
211
|
),
|
|
179
212
|
}));
|
|
@@ -215,6 +248,7 @@ export class StartPreparationUseCase {
|
|
|
215
248
|
.sort((a, b) =>
|
|
216
249
|
this.compareBySevenDayDeadlineThenUtilization(
|
|
217
250
|
a,
|
|
251
|
+
weeklyLimitType,
|
|
218
252
|
b,
|
|
219
253
|
weeklyLimitType,
|
|
220
254
|
nowEpochSeconds,
|
|
@@ -272,6 +306,7 @@ export class StartPreparationUseCase {
|
|
|
272
306
|
let proxyBaseUrl: string | null = null;
|
|
273
307
|
let selectedTokensWithLimits: Array<{
|
|
274
308
|
token: string;
|
|
309
|
+
model: string;
|
|
275
310
|
limit: number;
|
|
276
311
|
secondsUntilSevenDayReset: number;
|
|
277
312
|
}> = [];
|
|
@@ -287,51 +322,23 @@ export class StartPreparationUseCase {
|
|
|
287
322
|
const maximumPreparingIssuesCount =
|
|
288
323
|
params.maximumPreparingIssuesCount ?? NORMAL_CONCURRENT_LIMIT;
|
|
289
324
|
let effectiveMaxPreparingIssuesCount = maximumPreparingIssuesCount;
|
|
290
|
-
|
|
325
|
+
const fallbackLlmModelName =
|
|
326
|
+
params.fallbackLlmModelName ?? DEFAULT_FALLBACK_LLM_MODEL_NAME;
|
|
291
327
|
if (tokenUsages.length > 0) {
|
|
292
328
|
const {
|
|
293
|
-
tokens:
|
|
294
|
-
effectiveCap,
|
|
295
|
-
tokensWithLimits:
|
|
329
|
+
tokens: selectedTokens,
|
|
330
|
+
effectiveCap: selectedCap,
|
|
331
|
+
tokensWithLimits: selectedTokensWithLimitsLocal,
|
|
296
332
|
} = this.selectRotationTokens(
|
|
297
333
|
tokenUsages,
|
|
298
334
|
params.utilizationPercentageThreshold,
|
|
299
335
|
params.defaultLlmModelName,
|
|
336
|
+
fallbackLlmModelName,
|
|
300
337
|
maximumPreparingIssuesCount,
|
|
301
338
|
);
|
|
302
|
-
let selectedTokens = ranked;
|
|
303
|
-
let selectedCap = effectiveCap;
|
|
304
|
-
let selectedTokensWithLimitsLocal = rankedTokensWithLimits;
|
|
305
|
-
if (
|
|
306
|
-
selectedTokens.length === 0 &&
|
|
307
|
-
this.weeklyLimitTypeForModel(params.defaultLlmModelName) ===
|
|
308
|
-
'seven_day_sonnet'
|
|
309
|
-
) {
|
|
310
|
-
const fallbackModelName =
|
|
311
|
-
params.fallbackLlmModelName ?? DEFAULT_FALLBACK_LLM_MODEL_NAME;
|
|
312
|
-
const {
|
|
313
|
-
tokens: fallbackRanked,
|
|
314
|
-
effectiveCap: fallbackCap,
|
|
315
|
-
tokensWithLimits: fallbackTokensWithLimits,
|
|
316
|
-
} = this.selectRotationTokens(
|
|
317
|
-
tokenUsages,
|
|
318
|
-
params.utilizationPercentageThreshold,
|
|
319
|
-
fallbackModelName,
|
|
320
|
-
maximumPreparingIssuesCount,
|
|
321
|
-
);
|
|
322
|
-
if (fallbackRanked.length > 0) {
|
|
323
|
-
console.warn(
|
|
324
|
-
`Sonnet 7-day weekly limit (${this.weeklyLimitTypeForModel(params.defaultLlmModelName)}) is exhausted across all configured Claude OAuth token(s). Falling back to ${fallbackModelName}.`,
|
|
325
|
-
);
|
|
326
|
-
selectedTokens = fallbackRanked;
|
|
327
|
-
selectedCap = fallbackCap;
|
|
328
|
-
selectedTokensWithLimitsLocal = fallbackTokensWithLimits;
|
|
329
|
-
effectiveDefaultLlmModelName = fallbackModelName;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
339
|
if (selectedTokens.length === 0) {
|
|
333
340
|
console.warn(
|
|
334
|
-
`All ${tokenUsages.length} configured Claude OAuth token(s) are unavailable (blocked, rejected, weekly
|
|
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.`,
|
|
335
342
|
);
|
|
336
343
|
return { rotationOrder };
|
|
337
344
|
}
|
|
@@ -434,12 +441,15 @@ export class StartPreparationUseCase {
|
|
|
434
441
|
.trim() ||
|
|
435
442
|
params.defaultLlmAgentName ||
|
|
436
443
|
params.defaultAgentName;
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
+
) {
|
|
443
453
|
console.error(
|
|
444
454
|
`No LLM model configured for issue ${issue.url}. Provide --defaultLlmModelName or add an llm-model: label.`,
|
|
445
455
|
);
|
|
@@ -522,30 +532,13 @@ export class StartPreparationUseCase {
|
|
|
522
532
|
);
|
|
523
533
|
issue.status = PREPARATION_STATUS_NAME;
|
|
524
534
|
|
|
525
|
-
const awArgs: string[] = [
|
|
526
|
-
issue.url,
|
|
527
|
-
agent,
|
|
528
|
-
model,
|
|
529
|
-
'--configFilePath',
|
|
530
|
-
params.configFilePath,
|
|
531
|
-
'--branch',
|
|
532
|
-
branchName,
|
|
533
|
-
];
|
|
534
|
-
if (
|
|
535
|
-
params.codexHomeCandidates !== null &&
|
|
536
|
-
params.codexHomeCandidates.length > 0
|
|
537
|
-
) {
|
|
538
|
-
const codexHome =
|
|
539
|
-
params.codexHomeCandidates[
|
|
540
|
-
startedInThisRunCount % params.codexHomeCandidates.length
|
|
541
|
-
];
|
|
542
|
-
awArgs.push('--codexHome', codexHome);
|
|
543
|
-
}
|
|
544
535
|
let spawnEnv: Record<string, string> | undefined;
|
|
536
|
+
let routedModelName: string | null = null;
|
|
545
537
|
if (rotationTokens !== null && proxyBaseUrl !== null) {
|
|
546
538
|
const tokenWithSoonestResetAmongAvailable = selectedTokensWithLimits
|
|
547
539
|
.map((t) => ({
|
|
548
540
|
token: t.token,
|
|
541
|
+
model: t.model,
|
|
549
542
|
remaining:
|
|
550
543
|
t.limit -
|
|
551
544
|
(tokenInFlightCounts[t.token] ?? 0) -
|
|
@@ -563,6 +556,7 @@ export class StartPreparationUseCase {
|
|
|
563
556
|
break;
|
|
564
557
|
}
|
|
565
558
|
const selected = tokenWithSoonestResetAmongAvailable.token;
|
|
559
|
+
routedModelName = tokenWithSoonestResetAmongAvailable.model;
|
|
566
560
|
spawnedInThisRunByToken[selected] =
|
|
567
561
|
(spawnedInThisRunByToken[selected] ?? 0) + 1;
|
|
568
562
|
spawnEnv = {
|
|
@@ -570,6 +564,33 @@ export class StartPreparationUseCase {
|
|
|
570
564
|
ANTHROPIC_BASE_URL: proxyBaseUrl,
|
|
571
565
|
};
|
|
572
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
|
+
}
|
|
573
594
|
await this.localCommandRunner.runCommand(
|
|
574
595
|
'aw',
|
|
575
596
|
awArgs,
|