github-issue-tower-defence-management 1.109.1 → 1.110.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/CHANGELOG.md +19 -0
- package/README.md +29 -3
- package/bin/adapter/entry-points/cli/index.js +10 -10
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/console/dashboardComposeService.js +7 -6
- package/bin/adapter/entry-points/console/dashboardComposeService.js.map +1 -1
- 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/console/webServer.js +5 -4
- package/bin/adapter/entry-points/console/webServer.js.map +1 -1
- 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 +25 -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/bin/domain/usecases/dashboard/DashboardProjectCode.js +19 -0
- package/bin/domain/usecases/dashboard/DashboardProjectCode.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.ts +14 -14
- package/src/adapter/entry-points/console/dashboardComposeService.test.ts +31 -28
- package/src/adapter/entry-points/console/dashboardComposeService.ts +13 -10
- package/src/adapter/entry-points/console/dashboardEmitterComposerKey.test.ts +175 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +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/console/webServer.test.ts +23 -23
- package/src/adapter/entry-points/console/webServer.ts +5 -4
- 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 +89 -0
- package/src/adapter/proxy/RateLimitCache.ts +31 -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/src/domain/usecases/dashboard/DashboardProjectCode.test.ts +32 -0
- package/src/domain/usecases/dashboard/DashboardProjectCode.ts +20 -0
- package/types/adapter/entry-points/console/dashboardComposeService.d.ts +1 -1
- package/types/adapter/entry-points/console/dashboardComposeService.d.ts.map +1 -1
- package/types/adapter/entry-points/console/webServer.d.ts +2 -2
- package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
- 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/types/domain/usecases/dashboard/DashboardProjectCode.d.ts +4 -0
- package/types/domain/usecases/dashboard/DashboardProjectCode.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,11 +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';
|
|
16
18
|
|
|
19
|
+
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
20
|
+
v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
21
|
+
|
|
22
|
+
const writeSubscriptionDisabledEpoch = (
|
|
23
|
+
token: string,
|
|
24
|
+
epochSeconds: number,
|
|
25
|
+
): void => {
|
|
26
|
+
const filePath = cachePathForToken(token);
|
|
27
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
28
|
+
fs.writeFileSync(
|
|
29
|
+
filePath,
|
|
30
|
+
JSON.stringify({ subscriptionDisabledEpoch: epochSeconds }),
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
17
34
|
describe('RateLimitCache', () => {
|
|
18
35
|
let tempDir: string;
|
|
19
36
|
const originalXdg = process.env.XDG_CACHE_HOME;
|
|
@@ -774,4 +791,76 @@ describe('RateLimitCache', () => {
|
|
|
774
791
|
});
|
|
775
792
|
});
|
|
776
793
|
});
|
|
794
|
+
|
|
795
|
+
describe('writeSubscriptionDisabled', () => {
|
|
796
|
+
it('should persist subscriptionDisabledEpoch to the cache file', () => {
|
|
797
|
+
const token = 'sub-disabled-token';
|
|
798
|
+
writeSubscriptionDisabled(token);
|
|
799
|
+
const filePath = cachePathForToken(token);
|
|
800
|
+
const raw: unknown = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
801
|
+
const epoch = isRecord(raw) ? raw.subscriptionDisabledEpoch : undefined;
|
|
802
|
+
expect(typeof epoch).toBe('number');
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
it('should preserve existing cache fields when writing subscriptionDisabledEpoch', () => {
|
|
806
|
+
const token = 'sub-disabled-preserve-token';
|
|
807
|
+
writeRateLimit(token, {
|
|
808
|
+
'anthropic-ratelimit-unified-5h-status': 'allowed',
|
|
809
|
+
'anthropic-ratelimit-unified-5h-reset': '2000100',
|
|
810
|
+
'anthropic-ratelimit-unified-5h-utilization': '0.1',
|
|
811
|
+
'anthropic-ratelimit-unified-7d-status': 'allowed',
|
|
812
|
+
'anthropic-ratelimit-unified-7d-reset': '2000200',
|
|
813
|
+
'anthropic-ratelimit-unified-7d-utilization': '0.1',
|
|
814
|
+
});
|
|
815
|
+
writeSubscriptionDisabled(token);
|
|
816
|
+
const snapshot = readRateLimit(token);
|
|
817
|
+
expect(snapshot?.subscriptionDisabled).toBe(true);
|
|
818
|
+
expect(snapshot?.fiveHourReset).toBe(2000100);
|
|
819
|
+
});
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
describe('readRateLimit subscriptionDisabled', () => {
|
|
823
|
+
it('returns subscriptionDisabled: true after writeSubscriptionDisabled is called', () => {
|
|
824
|
+
const token = 'sub-disabled-read-token';
|
|
825
|
+
writeSubscriptionDisabled(token);
|
|
826
|
+
const snapshot = readRateLimit(token);
|
|
827
|
+
expect(snapshot?.subscriptionDisabled).toBe(true);
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
it('returns subscriptionDisabled: false when no subscriptionDisabledEpoch is in the cache', () => {
|
|
831
|
+
const token = 'sub-not-disabled-token';
|
|
832
|
+
writeRateLimit(token, {
|
|
833
|
+
'anthropic-ratelimit-unified-5h-status': 'allowed',
|
|
834
|
+
'anthropic-ratelimit-unified-5h-reset': '2000100',
|
|
835
|
+
'anthropic-ratelimit-unified-5h-utilization': '0.1',
|
|
836
|
+
'anthropic-ratelimit-unified-7d-status': 'allowed',
|
|
837
|
+
'anthropic-ratelimit-unified-7d-reset': '2000200',
|
|
838
|
+
'anthropic-ratelimit-unified-7d-utilization': '0.1',
|
|
839
|
+
});
|
|
840
|
+
const snapshot = readRateLimit(token);
|
|
841
|
+
expect(snapshot?.subscriptionDisabled).toBe(false);
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
it('returns subscriptionDisabled: true while within the cooldown window', () => {
|
|
845
|
+
const token = 'sub-disabled-within-cooldown-token';
|
|
846
|
+
const nowEpochSeconds = Date.now() / 1000;
|
|
847
|
+
writeSubscriptionDisabledEpoch(
|
|
848
|
+
token,
|
|
849
|
+
nowEpochSeconds - (PERMISSION_DISABLED_COOLDOWN_SECONDS - 60),
|
|
850
|
+
);
|
|
851
|
+
const snapshot = readRateLimit(token);
|
|
852
|
+
expect(snapshot?.subscriptionDisabled).toBe(true);
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
it('returns subscriptionDisabled: false once the cooldown window has elapsed', () => {
|
|
856
|
+
const token = 'sub-disabled-cooldown-elapsed-token';
|
|
857
|
+
const nowEpochSeconds = Date.now() / 1000;
|
|
858
|
+
writeSubscriptionDisabledEpoch(
|
|
859
|
+
token,
|
|
860
|
+
nowEpochSeconds - (PERMISSION_DISABLED_COOLDOWN_SECONDS + 60),
|
|
861
|
+
);
|
|
862
|
+
const snapshot = readRateLimit(token);
|
|
863
|
+
expect(snapshot?.subscriptionDisabled).toBe(false);
|
|
864
|
+
});
|
|
865
|
+
});
|
|
777
866
|
});
|
|
@@ -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');
|
|
@@ -168,6 +171,23 @@ export const writeModelRateLimit = (
|
|
|
168
171
|
fs.writeFileSync(filePath, JSON.stringify(payload));
|
|
169
172
|
};
|
|
170
173
|
|
|
174
|
+
export const writeSubscriptionDisabled = (
|
|
175
|
+
token: string,
|
|
176
|
+
baseDir: string = cacheDir(),
|
|
177
|
+
): void => {
|
|
178
|
+
const dir = baseDir;
|
|
179
|
+
if (!fs.existsSync(dir)) {
|
|
180
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
181
|
+
}
|
|
182
|
+
const filePath = path.join(dir, `${hashToken(token)}.json`);
|
|
183
|
+
const existing = readPayload(filePath);
|
|
184
|
+
const payload = {
|
|
185
|
+
...existing,
|
|
186
|
+
subscriptionDisabledEpoch: Date.now() / 1000,
|
|
187
|
+
};
|
|
188
|
+
fs.writeFileSync(filePath, JSON.stringify(payload));
|
|
189
|
+
};
|
|
190
|
+
|
|
171
191
|
export const parseModelRateLimitsFromBody = (
|
|
172
192
|
body: string,
|
|
173
193
|
): Record<string, ModelWeeklyLimit> => {
|
|
@@ -264,6 +284,16 @@ export const readRateLimit = (
|
|
|
264
284
|
const storedBlockedUntil = parsed.blockedUntilEpoch;
|
|
265
285
|
const blockedUntilEpoch =
|
|
266
286
|
typeof storedBlockedUntil === 'number' ? storedBlockedUntil : 0;
|
|
287
|
+
const storedSubscriptionDisabledEpoch = parsed.subscriptionDisabledEpoch;
|
|
288
|
+
const subscriptionDisabledEpoch =
|
|
289
|
+
typeof storedSubscriptionDisabledEpoch === 'number'
|
|
290
|
+
? storedSubscriptionDisabledEpoch
|
|
291
|
+
: 0;
|
|
292
|
+
const nowEpochSeconds = Date.now() / 1000;
|
|
293
|
+
const subscriptionDisabled =
|
|
294
|
+
subscriptionDisabledEpoch > 0 &&
|
|
295
|
+
nowEpochSeconds - subscriptionDisabledEpoch <
|
|
296
|
+
PERMISSION_DISABLED_COOLDOWN_SECONDS;
|
|
267
297
|
return {
|
|
268
298
|
fiveHourUtilization: num('anthropic-ratelimit-unified-5h-utilization'),
|
|
269
299
|
fiveHourReset: num('anthropic-ratelimit-unified-5h-reset'),
|
|
@@ -285,6 +315,7 @@ export const readRateLimit = (
|
|
|
285
315
|
},
|
|
286
316
|
lastUpdatedEpoch,
|
|
287
317
|
blockedUntilEpoch,
|
|
318
|
+
subscriptionDisabled,
|
|
288
319
|
};
|
|
289
320
|
} catch {
|
|
290
321
|
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
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { SubAgentActivity } from '../../domain/entities/LiveSessionActivitySnapshot';
|
|
2
|
+
import { SilentSessionMessageComposer } from '../../domain/usecases/adapter-interfaces/SilentSessionMessageComposer';
|
|
3
|
+
|
|
4
|
+
export type SilentSessionMessageTemplates = {
|
|
5
|
+
mainStalledMessage: string | null;
|
|
6
|
+
subAgentMessageHeader: string | null;
|
|
7
|
+
subAgentMessageFooter: string | null;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const formatMinutes = (seconds: number): string => {
|
|
11
|
+
const minutes = Math.floor(seconds / 60);
|
|
12
|
+
return `${minutes}m`;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export class ConfigurableSilentSessionMessageComposer implements SilentSessionMessageComposer {
|
|
16
|
+
constructor(
|
|
17
|
+
private readonly templates: SilentSessionMessageTemplates,
|
|
18
|
+
private readonly fallback: SilentSessionMessageComposer,
|
|
19
|
+
) {}
|
|
20
|
+
|
|
21
|
+
composeMainStalledSection = (mainSilentSeconds: number): string => {
|
|
22
|
+
if (this.templates.mainStalledMessage === null) {
|
|
23
|
+
return this.fallback.composeMainStalledSection(mainSilentSeconds);
|
|
24
|
+
}
|
|
25
|
+
return this.templates.mainStalledMessage;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
composeSubAgentSection = (subAgents: SubAgentActivity[]): string => {
|
|
29
|
+
if (
|
|
30
|
+
this.templates.subAgentMessageHeader === null &&
|
|
31
|
+
this.templates.subAgentMessageFooter === null
|
|
32
|
+
) {
|
|
33
|
+
return this.fallback.composeSubAgentSection(subAgents);
|
|
34
|
+
}
|
|
35
|
+
const lines = subAgents.map(
|
|
36
|
+
(subAgent) =>
|
|
37
|
+
`- ${subAgent.label}: silent for ${formatMinutes(
|
|
38
|
+
subAgent.silentSeconds,
|
|
39
|
+
)}, running for ${formatMinutes(subAgent.runningSeconds)}`,
|
|
40
|
+
);
|
|
41
|
+
const sections: string[] = [];
|
|
42
|
+
if (this.templates.subAgentMessageHeader !== null) {
|
|
43
|
+
sections.push(this.templates.subAgentMessageHeader);
|
|
44
|
+
}
|
|
45
|
+
sections.push(...lines);
|
|
46
|
+
if (this.templates.subAgentMessageFooter !== null) {
|
|
47
|
+
sections.push(this.templates.subAgentMessageFooter);
|
|
48
|
+
}
|
|
49
|
+
return sections.join('\n');
|
|
50
|
+
};
|
|
51
|
+
}
|