github-issue-tower-defence-management 1.109.2 → 1.110.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 +19 -0
- package/README.md +26 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/{index-CO-f_j4f.css → index-BPH6W1yn.css} +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-CT3t4Yje.js +101 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +125 -2
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.js +2 -0
- package/bin/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.js +2 -0
- package/bin/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/OauthTokenSelectHandler.js +2 -0
- package/bin/adapter/entry-points/handlers/OauthTokenSelectHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +62 -0
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -0
- package/bin/adapter/entry-points/handlers/tokenStatusWriter.js +88 -3
- package/bin/adapter/entry-points/handlers/tokenStatusWriter.js.map +1 -1
- package/bin/adapter/proxy/RateLimitCache.js +33 -1
- package/bin/adapter/proxy/RateLimitCache.js.map +1 -1
- package/bin/adapter/proxy/proxyEntry.js +25 -0
- package/bin/adapter/proxy/proxyEntry.js.map +1 -1
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js +37 -0
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js.map +1 -0
- package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js +76 -0
- package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js.map +1 -0
- package/bin/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.js +67 -0
- package/bin/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.js.map +1 -0
- package/bin/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.js +50 -0
- package/bin/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.js.map +1 -0
- package/bin/adapter/repositories/NoUnansweredOwnerCallStatusProvider.js +12 -0
- package/bin/adapter/repositories/NoUnansweredOwnerCallStatusProvider.js.map +1 -0
- package/bin/adapter/repositories/NodeSubAgentProcessLister.js +37 -0
- package/bin/adapter/repositories/NodeSubAgentProcessLister.js.map +1 -0
- package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js +48 -0
- package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js.map +1 -0
- package/bin/adapter/repositories/RealSleeper.js +17 -0
- package/bin/adapter/repositories/RealSleeper.js.map +1 -0
- package/bin/adapter/repositories/TmuxSilentSessionNotificationRepository.js +56 -0
- package/bin/adapter/repositories/TmuxSilentSessionNotificationRepository.js.map +1 -0
- package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js +150 -0
- package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js.map +1 -0
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js +152 -0
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js.map +1 -0
- package/bin/domain/entities/LiveSessionActivitySnapshot.js +3 -0
- package/bin/domain/entities/LiveSessionActivitySnapshot.js.map +1 -0
- package/bin/domain/entities/LiveSessionOutputActivity.js +3 -0
- package/bin/domain/entities/LiveSessionOutputActivity.js.map +1 -0
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js +35 -0
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js.map +1 -0
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +113 -0
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -0
- package/bin/domain/usecases/OauthTokenSelectUseCase.js +8 -2
- package/bin/domain/usecases/OauthTokenSelectUseCase.js.map +1 -1
- package/bin/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/Sleeper.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/Sleeper.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/SubAgentProcessLister.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/SubAgentProcessLister.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCopyUrlButton.stories.tsx +24 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCopyUrlButton.test.tsx +63 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCopyUrlButton.tsx +51 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.test.tsx +7 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +2 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.test.tsx +18 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.tsx +2 -0
- package/src/adapter/entry-points/console/ui/src/index.css +14 -0
- package/src/adapter/entry-points/console/ui-dist/assets/{index-CO-f_j4f.css → index-BPH6W1yn.css} +1 -1
- package/src/adapter/entry-points/console/ui-dist/assets/index-CT3t4Yje.js +101 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +122 -0
- package/src/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.ts +2 -0
- package/src/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.test.ts +112 -0
- package/src/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.ts +2 -0
- package/src/adapter/entry-points/handlers/OauthTokenSelectHandler.test.ts +131 -0
- package/src/adapter/entry-points/handlers/OauthTokenSelectHandler.ts +2 -0
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +338 -0
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +174 -0
- package/src/adapter/entry-points/handlers/tokenStatusWriter.test.ts +79 -0
- package/src/adapter/entry-points/handlers/tokenStatusWriter.ts +117 -3
- package/src/adapter/proxy/RateLimitCache.test.ts +152 -0
- package/src/adapter/proxy/RateLimitCache.ts +42 -0
- package/src/adapter/proxy/proxyEntry.test.ts +92 -0
- package/src/adapter/proxy/proxyEntry.ts +32 -0
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.test.ts +77 -0
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts +51 -0
- package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.test.ts +92 -0
- package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.ts +48 -0
- package/src/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.test.ts +56 -0
- package/src/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.ts +32 -0
- package/src/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.test.ts +32 -0
- package/src/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.ts +17 -0
- package/src/adapter/repositories/NoUnansweredOwnerCallStatusProvider.test.ts +12 -0
- package/src/adapter/repositories/NoUnansweredOwnerCallStatusProvider.ts +9 -0
- package/src/adapter/repositories/NodeSubAgentProcessLister.test.ts +43 -0
- package/src/adapter/repositories/NodeSubAgentProcessLister.ts +40 -0
- package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.test.ts +109 -0
- package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.ts +56 -0
- package/src/adapter/repositories/RealSleeper.test.ts +19 -0
- package/src/adapter/repositories/RealSleeper.ts +12 -0
- package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.test.ts +153 -0
- package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.ts +83 -0
- package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.test.ts +220 -0
- package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.ts +139 -0
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.test.ts +217 -0
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts +145 -0
- package/src/domain/entities/LiveSessionActivitySnapshot.ts +12 -0
- package/src/domain/entities/LiveSessionOutputActivity.ts +4 -0
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.test.ts +52 -0
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.ts +39 -0
- package/src/domain/usecases/InTmuxByHumanSessionTokenCountUseCase.test.ts +2 -0
- package/src/domain/usecases/LiveSessionOauthTokenSelectUseCase.test.ts +40 -0
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +612 -0
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +223 -0
- package/src/domain/usecases/OauthTokenSelectUseCase.test.ts +36 -0
- package/src/domain/usecases/OauthTokenSelectUseCase.ts +12 -0
- package/src/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.ts +5 -0
- package/src/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.ts +7 -0
- package/src/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.ts +7 -0
- package/src/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.ts +6 -0
- package/src/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.ts +11 -0
- package/src/domain/usecases/adapter-interfaces/Sleeper.ts +3 -0
- package/src/domain/usecases/adapter-interfaces/SubAgentProcessLister.ts +8 -0
- package/src/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.ts +3 -0
- package/src/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.ts +3 -0
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/OauthTokenSelectHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts +34 -0
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -0
- package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts +1 -0
- package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts.map +1 -1
- package/types/adapter/proxy/RateLimitCache.d.ts +3 -0
- package/types/adapter/proxy/RateLimitCache.d.ts.map +1 -1
- package/types/adapter/proxy/proxyEntry.d.ts.map +1 -1
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts +15 -0
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts.map +1 -0
- package/types/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts +10 -0
- package/types/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts.map +1 -0
- package/types/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.d.ts +9 -0
- package/types/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.d.ts.map +1 -0
- package/types/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.d.ts +7 -0
- package/types/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.d.ts.map +1 -0
- package/types/adapter/repositories/NoUnansweredOwnerCallStatusProvider.d.ts +5 -0
- package/types/adapter/repositories/NoUnansweredOwnerCallStatusProvider.d.ts.map +1 -0
- package/types/adapter/repositories/NodeSubAgentProcessLister.d.ts +8 -0
- package/types/adapter/repositories/NodeSubAgentProcessLister.d.ts.map +1 -0
- package/types/adapter/repositories/ProcessListSessionSubAgentActivityRepository.d.ts +12 -0
- package/types/adapter/repositories/ProcessListSessionSubAgentActivityRepository.d.ts.map +1 -0
- package/types/adapter/repositories/RealSleeper.d.ts +5 -0
- package/types/adapter/repositories/RealSleeper.d.ts.map +1 -0
- package/types/adapter/repositories/TmuxSilentSessionNotificationRepository.d.ts +13 -0
- package/types/adapter/repositories/TmuxSilentSessionNotificationRepository.d.ts.map +1 -0
- package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts +10 -0
- package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts.map +1 -0
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts +12 -0
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts.map +1 -0
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts +12 -0
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts.map +1 -0
- package/types/domain/entities/LiveSessionOutputActivity.d.ts +5 -0
- package/types/domain/entities/LiveSessionOutputActivity.d.ts.map +1 -0
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts +7 -0
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts.map +1 -0
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +41 -0
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -0
- package/types/domain/usecases/OauthTokenSelectUseCase.d.ts +2 -0
- package/types/domain/usecases/OauthTokenSelectUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.d.ts +4 -0
- package/types/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.d.ts +5 -0
- package/types/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.d.ts +5 -0
- package/types/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts +6 -0
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.d.ts +6 -0
- package/types/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/Sleeper.d.ts +4 -0
- package/types/domain/usecases/adapter-interfaces/Sleeper.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/SubAgentProcessLister.d.ts +8 -0
- package/types/domain/usecases/adapter-interfaces/SubAgentProcessLister.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.d.ts +4 -0
- package/types/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.d.ts +4 -0
- package/types/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.d.ts.map +1 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-BpccigBS.js +0 -101
- package/src/adapter/entry-points/console/ui-dist/assets/index-BpccigBS.js +0 -101
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import type { Issue } from '../../../domain/entities/Issue';
|
|
4
|
+
import { IN_TMUX_STATUS_NAME } from '../../../domain/entities/WorkflowStatus';
|
|
4
5
|
import {
|
|
5
6
|
GenerateTokenStatusUseCase,
|
|
6
7
|
TokenRateLimitSnapshot,
|
|
@@ -19,10 +20,13 @@ import { ProcTakeOwnershipSpawnRepository } from '../../repositories/ProcTakeOwn
|
|
|
19
20
|
const SEVEN_DAY_SONNET_LIMIT_TYPE = 'seven_day_sonnet';
|
|
20
21
|
const SEVEN_DAY_OPUS_LIMIT_TYPE = 'seven_day_opus';
|
|
21
22
|
|
|
23
|
+
const IN_TMUX_PROJECTS_DIR_NAME = 'token-status-in-tmux';
|
|
24
|
+
|
|
22
25
|
export type TokenStatusWriterParams = {
|
|
23
26
|
dashboardDataDir: string | null | undefined;
|
|
24
27
|
tokenListJsonPath: string | null | undefined;
|
|
25
28
|
issues: Issue[];
|
|
29
|
+
pjcode?: string | null | undefined;
|
|
26
30
|
now?: Date;
|
|
27
31
|
readSnapshot?: (token: string) => RateLimitSnapshot | null;
|
|
28
32
|
interactiveSessionRepository?: ClaudeInteractiveSessionRepository;
|
|
@@ -34,6 +38,12 @@ export type TokenStatusFile = {
|
|
|
34
38
|
capturedAt: string;
|
|
35
39
|
};
|
|
36
40
|
|
|
41
|
+
type InTmuxByHumanProjectFile = {
|
|
42
|
+
pjcode: string;
|
|
43
|
+
urls: string[];
|
|
44
|
+
capturedAt: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
37
47
|
const writeJsonAtomic = (filePath: string, data: unknown): void => {
|
|
38
48
|
const dir = path.dirname(filePath);
|
|
39
49
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -42,6 +52,64 @@ const writeJsonAtomic = (filePath: string, data: unknown): void => {
|
|
|
42
52
|
fs.renameSync(tmpPath, filePath);
|
|
43
53
|
};
|
|
44
54
|
|
|
55
|
+
const inTmuxByHumanUrlsFromIssues = (issues: Issue[]): string[] => {
|
|
56
|
+
const urls = new Set<string>();
|
|
57
|
+
for (const issue of issues) {
|
|
58
|
+
if (issue.status === IN_TMUX_STATUS_NAME && issue.isClosed === false) {
|
|
59
|
+
urls.add(issue.url);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return [...urls];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const persistProjectInTmuxByHumanUrls = (
|
|
66
|
+
inTmuxProjectsDir: string,
|
|
67
|
+
pjcode: string,
|
|
68
|
+
urls: string[],
|
|
69
|
+
capturedAt: string,
|
|
70
|
+
): void => {
|
|
71
|
+
const file: InTmuxByHumanProjectFile = { pjcode, urls, capturedAt };
|
|
72
|
+
writeJsonAtomic(path.join(inTmuxProjectsDir, `${pjcode}.json`), file);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const readMachineWideInTmuxByHumanUrls = (
|
|
76
|
+
inTmuxProjectsDir: string,
|
|
77
|
+
): Set<string> => {
|
|
78
|
+
const urls = new Set<string>();
|
|
79
|
+
let fileNames: string[];
|
|
80
|
+
try {
|
|
81
|
+
fileNames = fs.readdirSync(inTmuxProjectsDir);
|
|
82
|
+
} catch {
|
|
83
|
+
return urls;
|
|
84
|
+
}
|
|
85
|
+
for (const fileName of fileNames) {
|
|
86
|
+
if (!fileName.endsWith('.json')) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const parsed: unknown = JSON.parse(
|
|
91
|
+
fs.readFileSync(path.join(inTmuxProjectsDir, fileName), 'utf8'),
|
|
92
|
+
);
|
|
93
|
+
if (
|
|
94
|
+
parsed === null ||
|
|
95
|
+
typeof parsed !== 'object' ||
|
|
96
|
+
!('urls' in parsed) ||
|
|
97
|
+
!Array.isArray(parsed.urls)
|
|
98
|
+
) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
for (const url of parsed.urls) {
|
|
102
|
+
if (typeof url === 'string') {
|
|
103
|
+
urls.add(url);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return urls;
|
|
111
|
+
};
|
|
112
|
+
|
|
45
113
|
export const toTokenRateLimitSnapshot = (
|
|
46
114
|
snapshot: RateLimitSnapshot | null,
|
|
47
115
|
): TokenRateLimitSnapshot | null => {
|
|
@@ -73,7 +141,7 @@ export const toTokenRateLimitSnapshot = (
|
|
|
73
141
|
};
|
|
74
142
|
|
|
75
143
|
export const writeTokenStatus = (params: TokenStatusWriterParams): void => {
|
|
76
|
-
const { dashboardDataDir, tokenListJsonPath, issues } = params;
|
|
144
|
+
const { dashboardDataDir, tokenListJsonPath, issues, pjcode } = params;
|
|
77
145
|
if (!dashboardDataDir || !tokenListJsonPath) {
|
|
78
146
|
return;
|
|
79
147
|
}
|
|
@@ -83,6 +151,23 @@ export const writeTokenStatus = (params: TokenStatusWriterParams): void => {
|
|
|
83
151
|
return;
|
|
84
152
|
}
|
|
85
153
|
|
|
154
|
+
const now = params.now ?? new Date();
|
|
155
|
+
const inTmuxProjectsDir = path.join(
|
|
156
|
+
dashboardDataDir,
|
|
157
|
+
IN_TMUX_PROJECTS_DIR_NAME,
|
|
158
|
+
);
|
|
159
|
+
if (pjcode) {
|
|
160
|
+
persistProjectInTmuxByHumanUrls(
|
|
161
|
+
inTmuxProjectsDir,
|
|
162
|
+
pjcode,
|
|
163
|
+
inTmuxByHumanUrlsFromIssues(issues),
|
|
164
|
+
now.toISOString(),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
const machineWideInTmuxByHumanUrls = pjcode
|
|
168
|
+
? readMachineWideInTmuxByHumanUrls(inTmuxProjectsDir)
|
|
169
|
+
: new Set(inTmuxByHumanUrlsFromIssues(issues));
|
|
170
|
+
|
|
86
171
|
const readSnapshot = params.readSnapshot ?? readRateLimit;
|
|
87
172
|
const interactiveSessionRepository =
|
|
88
173
|
params.interactiveSessionRepository ??
|
|
@@ -112,17 +197,46 @@ export const writeTokenStatus = (params: TokenStatusWriterParams): void => {
|
|
|
112
197
|
name: entry.name,
|
|
113
198
|
token: entry.token,
|
|
114
199
|
snapshot: null,
|
|
200
|
+
subscriptionDisabled: false,
|
|
201
|
+
unifiedRejected: false,
|
|
202
|
+
}));
|
|
203
|
+
const machineWideInTmuxByHumanIssues: Issue[] = [
|
|
204
|
+
...machineWideInTmuxByHumanUrls,
|
|
205
|
+
].map((url) => ({
|
|
206
|
+
nameWithOwner: '',
|
|
207
|
+
number: 0,
|
|
208
|
+
title: '',
|
|
209
|
+
state: 'OPEN',
|
|
210
|
+
status: IN_TMUX_STATUS_NAME,
|
|
211
|
+
story: null,
|
|
212
|
+
nextActionDate: null,
|
|
213
|
+
nextActionHour: null,
|
|
214
|
+
estimationMinutes: null,
|
|
215
|
+
dependedIssueUrls: [],
|
|
216
|
+
completionDate50PercentConfidence: null,
|
|
217
|
+
url,
|
|
218
|
+
assignees: [],
|
|
219
|
+
labels: [],
|
|
220
|
+
org: '',
|
|
221
|
+
repo: '',
|
|
222
|
+
body: '',
|
|
223
|
+
itemId: '',
|
|
224
|
+
isPr: false,
|
|
225
|
+
isInProgress: false,
|
|
226
|
+
isClosed: false,
|
|
227
|
+
createdAt: now,
|
|
228
|
+
author: '',
|
|
229
|
+
closingIssueReferenceUrls: [],
|
|
115
230
|
}));
|
|
116
231
|
const humResult = new InTmuxByHumanSessionTokenCountUseCase().run(
|
|
117
232
|
candidates,
|
|
118
233
|
interactiveSessionRepository.listInteractiveSessions(),
|
|
119
|
-
|
|
234
|
+
machineWideInTmuxByHumanIssues,
|
|
120
235
|
);
|
|
121
236
|
const humCountByToken = new Map<string, number>(
|
|
122
237
|
humResult.counts.map((count) => [count.token, count.count]),
|
|
123
238
|
);
|
|
124
239
|
|
|
125
|
-
const now = params.now ?? new Date();
|
|
126
240
|
const tokens = new GenerateTokenStatusUseCase().run({
|
|
127
241
|
tokens: tokenInputs,
|
|
128
242
|
prepCountByToken,
|
|
@@ -9,10 +9,28 @@ import {
|
|
|
9
9
|
HEADERLESS_429_MAX_COOLDOWN_SECONDS,
|
|
10
10
|
parseModelRateLimitsFromBody,
|
|
11
11
|
parseModelRateLimitsFromHeaders,
|
|
12
|
+
PERMISSION_DISABLED_COOLDOWN_SECONDS,
|
|
12
13
|
readRateLimit,
|
|
13
14
|
writeModelRateLimit,
|
|
14
15
|
writeRateLimit,
|
|
16
|
+
writeSubscriptionDisabled,
|
|
15
17
|
} from './RateLimitCache';
|
|
18
|
+
import { OauthTokenSelectUseCase } from '../../domain/usecases/OauthTokenSelectUseCase';
|
|
19
|
+
|
|
20
|
+
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
21
|
+
v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
22
|
+
|
|
23
|
+
const writeSubscriptionDisabledEpoch = (
|
|
24
|
+
token: string,
|
|
25
|
+
epochSeconds: number,
|
|
26
|
+
): void => {
|
|
27
|
+
const filePath = cachePathForToken(token);
|
|
28
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
29
|
+
fs.writeFileSync(
|
|
30
|
+
filePath,
|
|
31
|
+
JSON.stringify({ subscriptionDisabledEpoch: epochSeconds }),
|
|
32
|
+
);
|
|
33
|
+
};
|
|
16
34
|
|
|
17
35
|
describe('RateLimitCache', () => {
|
|
18
36
|
let tempDir: string;
|
|
@@ -774,4 +792,138 @@ describe('RateLimitCache', () => {
|
|
|
774
792
|
});
|
|
775
793
|
});
|
|
776
794
|
});
|
|
795
|
+
|
|
796
|
+
describe('writeSubscriptionDisabled', () => {
|
|
797
|
+
it('should persist subscriptionDisabledEpoch to the cache file', () => {
|
|
798
|
+
const token = 'sub-disabled-token';
|
|
799
|
+
writeSubscriptionDisabled(token);
|
|
800
|
+
const filePath = cachePathForToken(token);
|
|
801
|
+
const raw: unknown = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
802
|
+
const epoch = isRecord(raw) ? raw.subscriptionDisabledEpoch : undefined;
|
|
803
|
+
expect(typeof epoch).toBe('number');
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
it('should preserve existing cache fields when writing subscriptionDisabledEpoch', () => {
|
|
807
|
+
const token = 'sub-disabled-preserve-token';
|
|
808
|
+
writeRateLimit(token, {
|
|
809
|
+
'anthropic-ratelimit-unified-5h-status': 'allowed',
|
|
810
|
+
'anthropic-ratelimit-unified-5h-reset': '2000100',
|
|
811
|
+
'anthropic-ratelimit-unified-5h-utilization': '0.1',
|
|
812
|
+
'anthropic-ratelimit-unified-7d-status': 'allowed',
|
|
813
|
+
'anthropic-ratelimit-unified-7d-reset': '2000200',
|
|
814
|
+
'anthropic-ratelimit-unified-7d-utilization': '0.1',
|
|
815
|
+
});
|
|
816
|
+
writeSubscriptionDisabled(token);
|
|
817
|
+
const snapshot = readRateLimit(token);
|
|
818
|
+
expect(snapshot?.subscriptionDisabled).toBe(true);
|
|
819
|
+
expect(snapshot?.fiveHourReset).toBe(2000100);
|
|
820
|
+
});
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
describe('readRateLimit subscriptionDisabled', () => {
|
|
824
|
+
it('returns subscriptionDisabled: true after writeSubscriptionDisabled is called', () => {
|
|
825
|
+
const token = 'sub-disabled-read-token';
|
|
826
|
+
writeSubscriptionDisabled(token);
|
|
827
|
+
const snapshot = readRateLimit(token);
|
|
828
|
+
expect(snapshot?.subscriptionDisabled).toBe(true);
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
it('returns subscriptionDisabled: false when no subscriptionDisabledEpoch is in the cache', () => {
|
|
832
|
+
const token = 'sub-not-disabled-token';
|
|
833
|
+
writeRateLimit(token, {
|
|
834
|
+
'anthropic-ratelimit-unified-5h-status': 'allowed',
|
|
835
|
+
'anthropic-ratelimit-unified-5h-reset': '2000100',
|
|
836
|
+
'anthropic-ratelimit-unified-5h-utilization': '0.1',
|
|
837
|
+
'anthropic-ratelimit-unified-7d-status': 'allowed',
|
|
838
|
+
'anthropic-ratelimit-unified-7d-reset': '2000200',
|
|
839
|
+
'anthropic-ratelimit-unified-7d-utilization': '0.1',
|
|
840
|
+
});
|
|
841
|
+
const snapshot = readRateLimit(token);
|
|
842
|
+
expect(snapshot?.subscriptionDisabled).toBe(false);
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it('returns subscriptionDisabled: true while within the cooldown window', () => {
|
|
846
|
+
const token = 'sub-disabled-within-cooldown-token';
|
|
847
|
+
const nowEpochSeconds = Date.now() / 1000;
|
|
848
|
+
writeSubscriptionDisabledEpoch(
|
|
849
|
+
token,
|
|
850
|
+
nowEpochSeconds - (PERMISSION_DISABLED_COOLDOWN_SECONDS - 60),
|
|
851
|
+
);
|
|
852
|
+
const snapshot = readRateLimit(token);
|
|
853
|
+
expect(snapshot?.subscriptionDisabled).toBe(true);
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
it('returns subscriptionDisabled: false once the cooldown window has elapsed', () => {
|
|
857
|
+
const token = 'sub-disabled-cooldown-elapsed-token';
|
|
858
|
+
const nowEpochSeconds = Date.now() / 1000;
|
|
859
|
+
writeSubscriptionDisabledEpoch(
|
|
860
|
+
token,
|
|
861
|
+
nowEpochSeconds - (PERMISSION_DISABLED_COOLDOWN_SECONDS + 60),
|
|
862
|
+
);
|
|
863
|
+
const snapshot = readRateLimit(token);
|
|
864
|
+
expect(snapshot?.subscriptionDisabled).toBe(false);
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
describe('writeRateLimit preserves subscriptionDisabledEpoch across header writes', () => {
|
|
869
|
+
const headerBearingResponse: Record<string, string> = {
|
|
870
|
+
'anthropic-ratelimit-unified-status': 'allowed',
|
|
871
|
+
'anthropic-ratelimit-unified-5h-status': 'allowed',
|
|
872
|
+
'anthropic-ratelimit-unified-5h-reset': '2000100',
|
|
873
|
+
'anthropic-ratelimit-unified-5h-utilization': '0.1',
|
|
874
|
+
'anthropic-ratelimit-unified-7d-status': 'allowed',
|
|
875
|
+
'anthropic-ratelimit-unified-7d-reset': '2000200',
|
|
876
|
+
'anthropic-ratelimit-unified-7d-utilization': '0.1',
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
it('keeps subscriptionDisabled: true after a later header-bearing 200 response', () => {
|
|
880
|
+
const token = 'sub-disabled-then-200-token';
|
|
881
|
+
writeSubscriptionDisabled(token);
|
|
882
|
+
writeRateLimit(token, headerBearingResponse, 200);
|
|
883
|
+
const snapshot = readRateLimit(token);
|
|
884
|
+
expect(snapshot?.subscriptionDisabled).toBe(true);
|
|
885
|
+
expect(snapshot?.fiveHourReset).toBe(2000100);
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
it('keeps subscriptionDisabled: true after a later header-bearing 429 response', () => {
|
|
889
|
+
const token = 'sub-disabled-then-429-token';
|
|
890
|
+
writeSubscriptionDisabled(token);
|
|
891
|
+
writeRateLimit(token, headerBearingResponse, 429);
|
|
892
|
+
const snapshot = readRateLimit(token);
|
|
893
|
+
expect(snapshot?.subscriptionDisabled).toBe(true);
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
it('still excludes the token from selection after a header-bearing response', () => {
|
|
897
|
+
const token = 'sub-disabled-then-200-selection-token';
|
|
898
|
+
writeSubscriptionDisabled(token);
|
|
899
|
+
writeRateLimit(token, headerBearingResponse, 200);
|
|
900
|
+
const snapshot = readRateLimit(token);
|
|
901
|
+
const useCase = new OauthTokenSelectUseCase();
|
|
902
|
+
const result = useCase.run(
|
|
903
|
+
[
|
|
904
|
+
{
|
|
905
|
+
name: 'disabled-token',
|
|
906
|
+
token,
|
|
907
|
+
snapshot:
|
|
908
|
+
snapshot === null
|
|
909
|
+
? null
|
|
910
|
+
: {
|
|
911
|
+
fiveHourUtilization: snapshot.fiveHourUtilization,
|
|
912
|
+
fiveHourReset: snapshot.fiveHourReset,
|
|
913
|
+
sevenDayUtilization: snapshot.sevenDayUtilization,
|
|
914
|
+
sevenDayReset: snapshot.sevenDayReset,
|
|
915
|
+
},
|
|
916
|
+
subscriptionDisabled: snapshot?.subscriptionDisabled ?? false,
|
|
917
|
+
unifiedRejected: snapshot?.unifiedRejected ?? false,
|
|
918
|
+
},
|
|
919
|
+
],
|
|
920
|
+
Date.now() / 1000,
|
|
921
|
+
);
|
|
922
|
+
expect(result.selected).toBeNull();
|
|
923
|
+
expect(result.metrics[0]?.eligible).toBe(false);
|
|
924
|
+
expect(result.metrics[0]?.exclusionReason).toBe(
|
|
925
|
+
'organization has disabled Claude subscription access for Claude Code',
|
|
926
|
+
);
|
|
927
|
+
});
|
|
928
|
+
});
|
|
777
929
|
});
|
|
@@ -23,6 +23,7 @@ export interface RateLimitSnapshot {
|
|
|
23
23
|
modelWeeklyLimits: Record<string, ModelWeeklyLimit>;
|
|
24
24
|
lastUpdatedEpoch: number;
|
|
25
25
|
blockedUntilEpoch: number;
|
|
26
|
+
subscriptionDisabled: boolean;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export const PROXY_PORT = 8787;
|
|
@@ -33,6 +34,8 @@ export const HEADERLESS_429_DEFAULT_COOLDOWN_SECONDS = 90;
|
|
|
33
34
|
|
|
34
35
|
export const HEADERLESS_429_MAX_COOLDOWN_SECONDS = 600;
|
|
35
36
|
|
|
37
|
+
export const PERMISSION_DISABLED_COOLDOWN_SECONDS = 3600;
|
|
38
|
+
|
|
36
39
|
export const cacheDir = (): string => {
|
|
37
40
|
const base = process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), '.cache');
|
|
38
41
|
return path.join(base, 'tdpm', 'ratelimit');
|
|
@@ -77,6 +80,16 @@ const readModelWeeklyLimits = (
|
|
|
77
80
|
return result;
|
|
78
81
|
};
|
|
79
82
|
|
|
83
|
+
const readSubscriptionDisabledEpoch = (
|
|
84
|
+
payload: Record<string, unknown>,
|
|
85
|
+
): { subscriptionDisabledEpoch: number } | Record<string, never> => {
|
|
86
|
+
const stored = payload.subscriptionDisabledEpoch;
|
|
87
|
+
if (typeof stored === 'number') {
|
|
88
|
+
return { subscriptionDisabledEpoch: stored };
|
|
89
|
+
}
|
|
90
|
+
return {};
|
|
91
|
+
};
|
|
92
|
+
|
|
80
93
|
const cooldownEndFromRetryAfter = (
|
|
81
94
|
retryAfterSeconds: number | null,
|
|
82
95
|
nowEpochSeconds: number,
|
|
@@ -138,6 +151,7 @@ export const writeRateLimit = (
|
|
|
138
151
|
}
|
|
139
152
|
const existing = readPayload(filePath);
|
|
140
153
|
const payload = {
|
|
154
|
+
...readSubscriptionDisabledEpoch(existing),
|
|
141
155
|
ts: Date.now() / 1000,
|
|
142
156
|
headers: rateLimitHeaders,
|
|
143
157
|
modelWeeklyLimits: readModelWeeklyLimits(existing),
|
|
@@ -168,6 +182,23 @@ export const writeModelRateLimit = (
|
|
|
168
182
|
fs.writeFileSync(filePath, JSON.stringify(payload));
|
|
169
183
|
};
|
|
170
184
|
|
|
185
|
+
export const writeSubscriptionDisabled = (
|
|
186
|
+
token: string,
|
|
187
|
+
baseDir: string = cacheDir(),
|
|
188
|
+
): void => {
|
|
189
|
+
const dir = baseDir;
|
|
190
|
+
if (!fs.existsSync(dir)) {
|
|
191
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
192
|
+
}
|
|
193
|
+
const filePath = path.join(dir, `${hashToken(token)}.json`);
|
|
194
|
+
const existing = readPayload(filePath);
|
|
195
|
+
const payload = {
|
|
196
|
+
...existing,
|
|
197
|
+
subscriptionDisabledEpoch: Date.now() / 1000,
|
|
198
|
+
};
|
|
199
|
+
fs.writeFileSync(filePath, JSON.stringify(payload));
|
|
200
|
+
};
|
|
201
|
+
|
|
171
202
|
export const parseModelRateLimitsFromBody = (
|
|
172
203
|
body: string,
|
|
173
204
|
): Record<string, ModelWeeklyLimit> => {
|
|
@@ -264,6 +295,16 @@ export const readRateLimit = (
|
|
|
264
295
|
const storedBlockedUntil = parsed.blockedUntilEpoch;
|
|
265
296
|
const blockedUntilEpoch =
|
|
266
297
|
typeof storedBlockedUntil === 'number' ? storedBlockedUntil : 0;
|
|
298
|
+
const storedSubscriptionDisabledEpoch = parsed.subscriptionDisabledEpoch;
|
|
299
|
+
const subscriptionDisabledEpoch =
|
|
300
|
+
typeof storedSubscriptionDisabledEpoch === 'number'
|
|
301
|
+
? storedSubscriptionDisabledEpoch
|
|
302
|
+
: 0;
|
|
303
|
+
const nowEpochSeconds = Date.now() / 1000;
|
|
304
|
+
const subscriptionDisabled =
|
|
305
|
+
subscriptionDisabledEpoch > 0 &&
|
|
306
|
+
nowEpochSeconds - subscriptionDisabledEpoch <
|
|
307
|
+
PERMISSION_DISABLED_COOLDOWN_SECONDS;
|
|
267
308
|
return {
|
|
268
309
|
fiveHourUtilization: num('anthropic-ratelimit-unified-5h-utilization'),
|
|
269
310
|
fiveHourReset: num('anthropic-ratelimit-unified-5h-reset'),
|
|
@@ -285,6 +326,7 @@ export const readRateLimit = (
|
|
|
285
326
|
},
|
|
286
327
|
lastUpdatedEpoch,
|
|
287
328
|
blockedUntilEpoch,
|
|
329
|
+
subscriptionDisabled,
|
|
288
330
|
};
|
|
289
331
|
} catch {
|
|
290
332
|
return null;
|
|
@@ -331,4 +331,96 @@ describe('startProxy', () => {
|
|
|
331
331
|
expect(response.body).toBe('Upstream error');
|
|
332
332
|
expect(writeModelRateLimitSpy).not.toHaveBeenCalled();
|
|
333
333
|
});
|
|
334
|
+
|
|
335
|
+
it('should call writeSubscriptionDisabled when the response body contains the subscription-disabled message', async () => {
|
|
336
|
+
const writeSubscriptionDisabledSpy = jest
|
|
337
|
+
.spyOn(RateLimitCache, 'writeSubscriptionDisabled')
|
|
338
|
+
.mockImplementation(() => undefined);
|
|
339
|
+
|
|
340
|
+
upstreamHandler = (_request, response) => {
|
|
341
|
+
response.writeHead(403, { 'content-type': 'application/json' });
|
|
342
|
+
response.end(
|
|
343
|
+
JSON.stringify({
|
|
344
|
+
error: {
|
|
345
|
+
message:
|
|
346
|
+
'Your organization has disabled Claude subscription access for Claude Code',
|
|
347
|
+
},
|
|
348
|
+
}),
|
|
349
|
+
);
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
await requestThroughProxy('POST', '/v1/messages', null);
|
|
353
|
+
|
|
354
|
+
expect(writeSubscriptionDisabledSpy).toHaveBeenCalledTimes(1);
|
|
355
|
+
expect(writeSubscriptionDisabledSpy).toHaveBeenCalledWith(TOKEN);
|
|
356
|
+
writeSubscriptionDisabledSpy.mockRestore();
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('should call writeSubscriptionDisabled when a 403 response body is a permission_error', async () => {
|
|
360
|
+
const writeSubscriptionDisabledSpy = jest
|
|
361
|
+
.spyOn(RateLimitCache, 'writeSubscriptionDisabled')
|
|
362
|
+
.mockImplementation(() => undefined);
|
|
363
|
+
|
|
364
|
+
upstreamHandler = (_request, response) => {
|
|
365
|
+
response.writeHead(403, { 'content-type': 'application/json' });
|
|
366
|
+
response.end(
|
|
367
|
+
JSON.stringify({
|
|
368
|
+
type: 'error',
|
|
369
|
+
error: {
|
|
370
|
+
type: 'permission_error',
|
|
371
|
+
message:
|
|
372
|
+
'OAuth authentication is currently not allowed for this organization',
|
|
373
|
+
},
|
|
374
|
+
}),
|
|
375
|
+
);
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
await requestThroughProxy('POST', '/v1/messages', null);
|
|
379
|
+
|
|
380
|
+
expect(writeSubscriptionDisabledSpy).toHaveBeenCalledTimes(1);
|
|
381
|
+
expect(writeSubscriptionDisabledSpy).toHaveBeenCalledWith(TOKEN);
|
|
382
|
+
writeSubscriptionDisabledSpy.mockRestore();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('should not call writeSubscriptionDisabled when a permission_error body has a non-403 status', async () => {
|
|
386
|
+
const writeSubscriptionDisabledSpy = jest
|
|
387
|
+
.spyOn(RateLimitCache, 'writeSubscriptionDisabled')
|
|
388
|
+
.mockImplementation(() => undefined);
|
|
389
|
+
|
|
390
|
+
upstreamHandler = (_request, response) => {
|
|
391
|
+
response.writeHead(200, { 'content-type': 'application/json' });
|
|
392
|
+
response.end(
|
|
393
|
+
JSON.stringify({
|
|
394
|
+
type: 'error',
|
|
395
|
+
error: { type: 'permission_error', message: 'noise' },
|
|
396
|
+
}),
|
|
397
|
+
);
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
await requestThroughProxy('POST', '/v1/messages', null);
|
|
401
|
+
|
|
402
|
+
expect(writeSubscriptionDisabledSpy).not.toHaveBeenCalled();
|
|
403
|
+
writeSubscriptionDisabledSpy.mockRestore();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('should not call writeSubscriptionDisabled when a 403 body is not a permission_error', async () => {
|
|
407
|
+
const writeSubscriptionDisabledSpy = jest
|
|
408
|
+
.spyOn(RateLimitCache, 'writeSubscriptionDisabled')
|
|
409
|
+
.mockImplementation(() => undefined);
|
|
410
|
+
|
|
411
|
+
upstreamHandler = (_request, response) => {
|
|
412
|
+
response.writeHead(403, { 'content-type': 'application/json' });
|
|
413
|
+
response.end(
|
|
414
|
+
JSON.stringify({
|
|
415
|
+
type: 'error',
|
|
416
|
+
error: { type: 'authentication_error', message: 'bad token' },
|
|
417
|
+
}),
|
|
418
|
+
);
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
await requestThroughProxy('POST', '/v1/messages', null);
|
|
422
|
+
|
|
423
|
+
expect(writeSubscriptionDisabledSpy).not.toHaveBeenCalled();
|
|
424
|
+
writeSubscriptionDisabledSpy.mockRestore();
|
|
425
|
+
});
|
|
334
426
|
});
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
parseModelRateLimitsFromBody,
|
|
7
7
|
writeModelRateLimit,
|
|
8
8
|
writeRateLimit,
|
|
9
|
+
writeSubscriptionDisabled,
|
|
9
10
|
} from './RateLimitCache';
|
|
10
11
|
import { ClaudeMessageResponseRepository } from '../../domain/usecases/adapter-interfaces/ClaudeMessageResponseRepository';
|
|
11
12
|
import { parseClaudeMessageResponse } from './ClaudeMessageResponseParser';
|
|
@@ -29,6 +30,22 @@ const extractToken = (
|
|
|
29
30
|
return token.length > 0 ? token : null;
|
|
30
31
|
};
|
|
31
32
|
|
|
33
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
34
|
+
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
35
|
+
|
|
36
|
+
const isPermissionError = (body: string): boolean => {
|
|
37
|
+
let parsed: unknown;
|
|
38
|
+
try {
|
|
39
|
+
parsed = JSON.parse(body);
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (!isRecord(parsed)) return false;
|
|
44
|
+
const error = parsed.error;
|
|
45
|
+
if (!isRecord(error)) return false;
|
|
46
|
+
return error.type === 'permission_error';
|
|
47
|
+
};
|
|
48
|
+
|
|
32
49
|
const startProxy = (
|
|
33
50
|
port: number,
|
|
34
51
|
claudeMessageResponseRepository: ClaudeMessageResponseRepository | null = null,
|
|
@@ -73,6 +90,21 @@ const startProxy = (
|
|
|
73
90
|
} catch (error) {
|
|
74
91
|
console.error('Failed to write model rate limit cache:', error);
|
|
75
92
|
}
|
|
93
|
+
if (
|
|
94
|
+
body.includes(
|
|
95
|
+
'Your organization has disabled Claude subscription access for Claude Code',
|
|
96
|
+
) ||
|
|
97
|
+
(upstreamResponse.statusCode === 403 && isPermissionError(body))
|
|
98
|
+
) {
|
|
99
|
+
try {
|
|
100
|
+
writeSubscriptionDisabled(token);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error(
|
|
103
|
+
'Failed to write subscription disabled cache:',
|
|
104
|
+
error,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
76
108
|
if (claudeMessageResponseRepository !== null) {
|
|
77
109
|
try {
|
|
78
110
|
const response = parseClaudeMessageResponse(
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { ConfigurableSilentSessionMessageComposer } from './ConfigurableSilentSessionMessageComposer';
|
|
2
|
+
import { SilentSessionMessageComposer } from '../../domain/usecases/adapter-interfaces/SilentSessionMessageComposer';
|
|
3
|
+
|
|
4
|
+
type Mocked<T> = jest.Mocked<T> & jest.MockedObject<T>;
|
|
5
|
+
|
|
6
|
+
const createFallback = (): Mocked<SilentSessionMessageComposer> => ({
|
|
7
|
+
composeMainStalledSection: jest.fn().mockReturnValue('FALLBACK_MAIN'),
|
|
8
|
+
composeSubAgentSection: jest.fn().mockReturnValue('FALLBACK_SUB'),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('ConfigurableSilentSessionMessageComposer', () => {
|
|
12
|
+
it('uses the fallback main section when no main template is configured', () => {
|
|
13
|
+
const fallback = createFallback();
|
|
14
|
+
const composer = new ConfigurableSilentSessionMessageComposer(
|
|
15
|
+
{
|
|
16
|
+
mainStalledMessage: null,
|
|
17
|
+
subAgentMessageHeader: null,
|
|
18
|
+
subAgentMessageFooter: null,
|
|
19
|
+
},
|
|
20
|
+
fallback,
|
|
21
|
+
);
|
|
22
|
+
expect(composer.composeMainStalledSection(600)).toBe('FALLBACK_MAIN');
|
|
23
|
+
expect(fallback.composeMainStalledSection).toHaveBeenCalledWith(600);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('uses the configured main template when provided', () => {
|
|
27
|
+
const fallback = createFallback();
|
|
28
|
+
const composer = new ConfigurableSilentSessionMessageComposer(
|
|
29
|
+
{
|
|
30
|
+
mainStalledMessage: 'CUSTOM_MAIN',
|
|
31
|
+
subAgentMessageHeader: null,
|
|
32
|
+
subAgentMessageFooter: null,
|
|
33
|
+
},
|
|
34
|
+
fallback,
|
|
35
|
+
);
|
|
36
|
+
expect(composer.composeMainStalledSection(600)).toBe('CUSTOM_MAIN');
|
|
37
|
+
expect(fallback.composeMainStalledSection).not.toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('uses the fallback sub-agent section when neither header nor footer is configured', () => {
|
|
41
|
+
const fallback = createFallback();
|
|
42
|
+
const composer = new ConfigurableSilentSessionMessageComposer(
|
|
43
|
+
{
|
|
44
|
+
mainStalledMessage: null,
|
|
45
|
+
subAgentMessageHeader: null,
|
|
46
|
+
subAgentMessageFooter: null,
|
|
47
|
+
},
|
|
48
|
+
fallback,
|
|
49
|
+
);
|
|
50
|
+
expect(
|
|
51
|
+
composer.composeSubAgentSection([
|
|
52
|
+
{ label: 'task-a', silentSeconds: 360, runningSeconds: 1200 },
|
|
53
|
+
]),
|
|
54
|
+
).toBe('FALLBACK_SUB');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('renders the configured sub-agent header, list, and footer', () => {
|
|
58
|
+
const fallback = createFallback();
|
|
59
|
+
const composer = new ConfigurableSilentSessionMessageComposer(
|
|
60
|
+
{
|
|
61
|
+
mainStalledMessage: null,
|
|
62
|
+
subAgentMessageHeader: 'HEADER',
|
|
63
|
+
subAgentMessageFooter: 'FOOTER',
|
|
64
|
+
},
|
|
65
|
+
fallback,
|
|
66
|
+
);
|
|
67
|
+
const section = composer.composeSubAgentSection([
|
|
68
|
+
{ label: 'task-a', silentSeconds: 360, runningSeconds: 1200 },
|
|
69
|
+
]);
|
|
70
|
+
expect(section).toContain('HEADER');
|
|
71
|
+
expect(section).toContain('task-a');
|
|
72
|
+
expect(section).toContain('silent for 6m');
|
|
73
|
+
expect(section).toContain('running for 20m');
|
|
74
|
+
expect(section).toContain('FOOTER');
|
|
75
|
+
expect(fallback.composeSubAgentSection).not.toHaveBeenCalled();
|
|
76
|
+
});
|
|
77
|
+
});
|