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.
Files changed (224) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +29 -3
  3. package/bin/adapter/entry-points/cli/index.js +10 -10
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/console/dashboardComposeService.js +7 -6
  6. package/bin/adapter/entry-points/console/dashboardComposeService.js.map +1 -1
  7. package/bin/adapter/entry-points/console/ui-dist/assets/{index-CO-f_j4f.css → index-BPH6W1yn.css} +1 -1
  8. package/bin/adapter/entry-points/console/ui-dist/assets/index-CT3t4Yje.js +101 -0
  9. package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
  10. package/bin/adapter/entry-points/console/webServer.js +5 -4
  11. package/bin/adapter/entry-points/console/webServer.js.map +1 -1
  12. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +125 -2
  13. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  14. package/bin/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.js +2 -0
  15. package/bin/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.js.map +1 -1
  16. package/bin/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.js +2 -0
  17. package/bin/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.js.map +1 -1
  18. package/bin/adapter/entry-points/handlers/OauthTokenSelectHandler.js +2 -0
  19. package/bin/adapter/entry-points/handlers/OauthTokenSelectHandler.js.map +1 -1
  20. package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +62 -0
  21. package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -0
  22. package/bin/adapter/entry-points/handlers/tokenStatusWriter.js +88 -3
  23. package/bin/adapter/entry-points/handlers/tokenStatusWriter.js.map +1 -1
  24. package/bin/adapter/proxy/RateLimitCache.js +25 -1
  25. package/bin/adapter/proxy/RateLimitCache.js.map +1 -1
  26. package/bin/adapter/proxy/proxyEntry.js +25 -0
  27. package/bin/adapter/proxy/proxyEntry.js.map +1 -1
  28. package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js +37 -0
  29. package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js.map +1 -0
  30. package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js +76 -0
  31. package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js.map +1 -0
  32. package/bin/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.js +67 -0
  33. package/bin/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.js.map +1 -0
  34. package/bin/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.js +50 -0
  35. package/bin/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.js.map +1 -0
  36. package/bin/adapter/repositories/NoUnansweredOwnerCallStatusProvider.js +12 -0
  37. package/bin/adapter/repositories/NoUnansweredOwnerCallStatusProvider.js.map +1 -0
  38. package/bin/adapter/repositories/NodeSubAgentProcessLister.js +37 -0
  39. package/bin/adapter/repositories/NodeSubAgentProcessLister.js.map +1 -0
  40. package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js +48 -0
  41. package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js.map +1 -0
  42. package/bin/adapter/repositories/RealSleeper.js +17 -0
  43. package/bin/adapter/repositories/RealSleeper.js.map +1 -0
  44. package/bin/adapter/repositories/TmuxSilentSessionNotificationRepository.js +56 -0
  45. package/bin/adapter/repositories/TmuxSilentSessionNotificationRepository.js.map +1 -0
  46. package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js +150 -0
  47. package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js.map +1 -0
  48. package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js +152 -0
  49. package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js.map +1 -0
  50. package/bin/domain/entities/LiveSessionActivitySnapshot.js +3 -0
  51. package/bin/domain/entities/LiveSessionActivitySnapshot.js.map +1 -0
  52. package/bin/domain/entities/LiveSessionOutputActivity.js +3 -0
  53. package/bin/domain/entities/LiveSessionOutputActivity.js.map +1 -0
  54. package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js +35 -0
  55. package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js.map +1 -0
  56. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +113 -0
  57. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -0
  58. package/bin/domain/usecases/OauthTokenSelectUseCase.js +8 -2
  59. package/bin/domain/usecases/OauthTokenSelectUseCase.js.map +1 -1
  60. package/bin/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.js +3 -0
  61. package/bin/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.js.map +1 -0
  62. package/bin/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.js +3 -0
  63. package/bin/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.js.map +1 -0
  64. package/bin/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.js +3 -0
  65. package/bin/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.js.map +1 -0
  66. package/bin/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.js +3 -0
  67. package/bin/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.js.map +1 -0
  68. package/bin/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.js +3 -0
  69. package/bin/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.js.map +1 -0
  70. package/bin/domain/usecases/adapter-interfaces/Sleeper.js +3 -0
  71. package/bin/domain/usecases/adapter-interfaces/Sleeper.js.map +1 -0
  72. package/bin/domain/usecases/adapter-interfaces/SubAgentProcessLister.js +3 -0
  73. package/bin/domain/usecases/adapter-interfaces/SubAgentProcessLister.js.map +1 -0
  74. package/bin/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.js +3 -0
  75. package/bin/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.js.map +1 -0
  76. package/bin/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.js +3 -0
  77. package/bin/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.js.map +1 -0
  78. package/bin/domain/usecases/dashboard/DashboardProjectCode.js +19 -0
  79. package/bin/domain/usecases/dashboard/DashboardProjectCode.js.map +1 -0
  80. package/package.json +1 -1
  81. package/src/adapter/entry-points/cli/index.ts +14 -14
  82. package/src/adapter/entry-points/console/dashboardComposeService.test.ts +31 -28
  83. package/src/adapter/entry-points/console/dashboardComposeService.ts +13 -10
  84. package/src/adapter/entry-points/console/dashboardEmitterComposerKey.test.ts +175 -0
  85. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +1 -1
  86. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCopyUrlButton.stories.tsx +24 -0
  87. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCopyUrlButton.test.tsx +63 -0
  88. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCopyUrlButton.tsx +51 -0
  89. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.test.tsx +7 -0
  90. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +2 -0
  91. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.test.tsx +18 -0
  92. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.tsx +2 -0
  93. package/src/adapter/entry-points/console/ui/src/index.css +14 -0
  94. package/src/adapter/entry-points/console/ui-dist/assets/{index-CO-f_j4f.css → index-BPH6W1yn.css} +1 -1
  95. package/src/adapter/entry-points/console/ui-dist/assets/index-CT3t4Yje.js +101 -0
  96. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  97. package/src/adapter/entry-points/console/webServer.test.ts +23 -23
  98. package/src/adapter/entry-points/console/webServer.ts +5 -4
  99. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +122 -0
  100. package/src/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.ts +2 -0
  101. package/src/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.test.ts +112 -0
  102. package/src/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.ts +2 -0
  103. package/src/adapter/entry-points/handlers/OauthTokenSelectHandler.test.ts +131 -0
  104. package/src/adapter/entry-points/handlers/OauthTokenSelectHandler.ts +2 -0
  105. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +338 -0
  106. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +174 -0
  107. package/src/adapter/entry-points/handlers/tokenStatusWriter.test.ts +79 -0
  108. package/src/adapter/entry-points/handlers/tokenStatusWriter.ts +117 -3
  109. package/src/adapter/proxy/RateLimitCache.test.ts +89 -0
  110. package/src/adapter/proxy/RateLimitCache.ts +31 -0
  111. package/src/adapter/proxy/proxyEntry.test.ts +92 -0
  112. package/src/adapter/proxy/proxyEntry.ts +32 -0
  113. package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.test.ts +77 -0
  114. package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts +51 -0
  115. package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.test.ts +92 -0
  116. package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.ts +48 -0
  117. package/src/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.test.ts +56 -0
  118. package/src/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.ts +32 -0
  119. package/src/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.test.ts +32 -0
  120. package/src/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.ts +17 -0
  121. package/src/adapter/repositories/NoUnansweredOwnerCallStatusProvider.test.ts +12 -0
  122. package/src/adapter/repositories/NoUnansweredOwnerCallStatusProvider.ts +9 -0
  123. package/src/adapter/repositories/NodeSubAgentProcessLister.test.ts +43 -0
  124. package/src/adapter/repositories/NodeSubAgentProcessLister.ts +40 -0
  125. package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.test.ts +109 -0
  126. package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.ts +56 -0
  127. package/src/adapter/repositories/RealSleeper.test.ts +19 -0
  128. package/src/adapter/repositories/RealSleeper.ts +12 -0
  129. package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.test.ts +153 -0
  130. package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.ts +83 -0
  131. package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.test.ts +220 -0
  132. package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.ts +139 -0
  133. package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.test.ts +217 -0
  134. package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts +145 -0
  135. package/src/domain/entities/LiveSessionActivitySnapshot.ts +12 -0
  136. package/src/domain/entities/LiveSessionOutputActivity.ts +4 -0
  137. package/src/domain/usecases/DefaultSilentSessionMessageComposer.test.ts +52 -0
  138. package/src/domain/usecases/DefaultSilentSessionMessageComposer.ts +39 -0
  139. package/src/domain/usecases/InTmuxByHumanSessionTokenCountUseCase.test.ts +2 -0
  140. package/src/domain/usecases/LiveSessionOauthTokenSelectUseCase.test.ts +40 -0
  141. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +612 -0
  142. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +223 -0
  143. package/src/domain/usecases/OauthTokenSelectUseCase.test.ts +36 -0
  144. package/src/domain/usecases/OauthTokenSelectUseCase.ts +12 -0
  145. package/src/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.ts +5 -0
  146. package/src/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.ts +7 -0
  147. package/src/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.ts +7 -0
  148. package/src/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.ts +6 -0
  149. package/src/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.ts +11 -0
  150. package/src/domain/usecases/adapter-interfaces/Sleeper.ts +3 -0
  151. package/src/domain/usecases/adapter-interfaces/SubAgentProcessLister.ts +8 -0
  152. package/src/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.ts +3 -0
  153. package/src/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.ts +3 -0
  154. package/src/domain/usecases/dashboard/DashboardProjectCode.test.ts +32 -0
  155. package/src/domain/usecases/dashboard/DashboardProjectCode.ts +20 -0
  156. package/types/adapter/entry-points/console/dashboardComposeService.d.ts +1 -1
  157. package/types/adapter/entry-points/console/dashboardComposeService.d.ts.map +1 -1
  158. package/types/adapter/entry-points/console/webServer.d.ts +2 -2
  159. package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
  160. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  161. package/types/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.d.ts.map +1 -1
  162. package/types/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.d.ts.map +1 -1
  163. package/types/adapter/entry-points/handlers/OauthTokenSelectHandler.d.ts.map +1 -1
  164. package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts +34 -0
  165. package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -0
  166. package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts +1 -0
  167. package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts.map +1 -1
  168. package/types/adapter/proxy/RateLimitCache.d.ts +3 -0
  169. package/types/adapter/proxy/RateLimitCache.d.ts.map +1 -1
  170. package/types/adapter/proxy/proxyEntry.d.ts.map +1 -1
  171. package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts +15 -0
  172. package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts.map +1 -0
  173. package/types/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts +10 -0
  174. package/types/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts.map +1 -0
  175. package/types/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.d.ts +9 -0
  176. package/types/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.d.ts.map +1 -0
  177. package/types/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.d.ts +7 -0
  178. package/types/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.d.ts.map +1 -0
  179. package/types/adapter/repositories/NoUnansweredOwnerCallStatusProvider.d.ts +5 -0
  180. package/types/adapter/repositories/NoUnansweredOwnerCallStatusProvider.d.ts.map +1 -0
  181. package/types/adapter/repositories/NodeSubAgentProcessLister.d.ts +8 -0
  182. package/types/adapter/repositories/NodeSubAgentProcessLister.d.ts.map +1 -0
  183. package/types/adapter/repositories/ProcessListSessionSubAgentActivityRepository.d.ts +12 -0
  184. package/types/adapter/repositories/ProcessListSessionSubAgentActivityRepository.d.ts.map +1 -0
  185. package/types/adapter/repositories/RealSleeper.d.ts +5 -0
  186. package/types/adapter/repositories/RealSleeper.d.ts.map +1 -0
  187. package/types/adapter/repositories/TmuxSilentSessionNotificationRepository.d.ts +13 -0
  188. package/types/adapter/repositories/TmuxSilentSessionNotificationRepository.d.ts.map +1 -0
  189. package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts +10 -0
  190. package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts.map +1 -0
  191. package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts +12 -0
  192. package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts.map +1 -0
  193. package/types/domain/entities/LiveSessionActivitySnapshot.d.ts +12 -0
  194. package/types/domain/entities/LiveSessionActivitySnapshot.d.ts.map +1 -0
  195. package/types/domain/entities/LiveSessionOutputActivity.d.ts +5 -0
  196. package/types/domain/entities/LiveSessionOutputActivity.d.ts.map +1 -0
  197. package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts +7 -0
  198. package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts.map +1 -0
  199. package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +41 -0
  200. package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -0
  201. package/types/domain/usecases/OauthTokenSelectUseCase.d.ts +2 -0
  202. package/types/domain/usecases/OauthTokenSelectUseCase.d.ts.map +1 -1
  203. package/types/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.d.ts +4 -0
  204. package/types/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.d.ts.map +1 -0
  205. package/types/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.d.ts +5 -0
  206. package/types/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.d.ts.map +1 -0
  207. package/types/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.d.ts +5 -0
  208. package/types/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.d.ts.map +1 -0
  209. package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts +6 -0
  210. package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts.map +1 -0
  211. package/types/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.d.ts +6 -0
  212. package/types/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.d.ts.map +1 -0
  213. package/types/domain/usecases/adapter-interfaces/Sleeper.d.ts +4 -0
  214. package/types/domain/usecases/adapter-interfaces/Sleeper.d.ts.map +1 -0
  215. package/types/domain/usecases/adapter-interfaces/SubAgentProcessLister.d.ts +8 -0
  216. package/types/domain/usecases/adapter-interfaces/SubAgentProcessLister.d.ts.map +1 -0
  217. package/types/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.d.ts +4 -0
  218. package/types/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.d.ts.map +1 -0
  219. package/types/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.d.ts +4 -0
  220. package/types/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.d.ts.map +1 -0
  221. package/types/domain/usecases/dashboard/DashboardProjectCode.d.ts +4 -0
  222. package/types/domain/usecases/dashboard/DashboardProjectCode.d.ts.map +1 -0
  223. package/bin/adapter/entry-points/console/ui-dist/assets/index-BpccigBS.js +0 -101
  224. 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
- issues,
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
+ }