github-issue-tower-defence-management 1.139.1 → 1.140.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +6 -0
  3. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +43 -2
  4. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  5. package/bin/adapter/entry-points/handlers/tokenExhaustionHandover.js +31 -0
  6. package/bin/adapter/entry-points/handlers/tokenExhaustionHandover.js.map +1 -0
  7. package/bin/adapter/repositories/FileHandoverStateRepository.js +99 -0
  8. package/bin/adapter/repositories/FileHandoverStateRepository.js.map +1 -0
  9. package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js +6 -40
  10. package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js.map +1 -1
  11. package/bin/adapter/repositories/NodeProcessSignalRepository.js +44 -0
  12. package/bin/adapter/repositories/NodeProcessSignalRepository.js.map +1 -0
  13. package/bin/adapter/repositories/NodeTmuxSessionRepository.js +67 -1
  14. package/bin/adapter/repositories/NodeTmuxSessionRepository.js.map +1 -1
  15. package/bin/adapter/repositories/ProcClaudeHandoverSessionRepository.js +206 -0
  16. package/bin/adapter/repositories/ProcClaudeHandoverSessionRepository.js.map +1 -0
  17. package/bin/adapter/repositories/RateLimitSnapshotRepository.js +44 -0
  18. package/bin/adapter/repositories/RateLimitSnapshotRepository.js.map +1 -0
  19. package/bin/domain/entities/ClaudeHandoverSession.js +3 -0
  20. package/bin/domain/entities/ClaudeHandoverSession.js.map +1 -0
  21. package/bin/domain/entities/TokenExhaustionHandoverState.js +3 -0
  22. package/bin/domain/entities/TokenExhaustionHandoverState.js.map +1 -0
  23. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +9 -46
  24. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
  25. package/bin/domain/usecases/TokenExhaustionHandoverUseCase.js +255 -0
  26. package/bin/domain/usecases/TokenExhaustionHandoverUseCase.js.map +1 -0
  27. package/bin/domain/usecases/adapter-interfaces/ClaudeHandoverSessionRepository.js +3 -0
  28. package/bin/domain/usecases/adapter-interfaces/ClaudeHandoverSessionRepository.js.map +1 -0
  29. package/bin/domain/usecases/adapter-interfaces/ProcessSignalRepository.js +3 -0
  30. package/bin/domain/usecases/adapter-interfaces/ProcessSignalRepository.js.map +1 -0
  31. package/bin/domain/usecases/adapter-interfaces/TokenExhaustionHandoverStateRepository.js +3 -0
  32. package/bin/domain/usecases/adapter-interfaces/TokenExhaustionHandoverStateRepository.js.map +1 -0
  33. package/bin/domain/usecases/adapter-interfaces/TokenRateLimitSnapshotRepository.js +3 -0
  34. package/bin/domain/usecases/adapter-interfaces/TokenRateLimitSnapshotRepository.js.map +1 -0
  35. package/package.json +1 -1
  36. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +32 -0
  37. package/src/adapter/entry-points/handlers/tokenExhaustionHandover.ts +79 -0
  38. package/src/adapter/repositories/FileHandoverStateRepository.test.ts +114 -0
  39. package/src/adapter/repositories/FileHandoverStateRepository.ts +76 -0
  40. package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.test.ts +24 -11
  41. package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.ts +8 -50
  42. package/src/adapter/repositories/NodeProcessSignalRepository.test.ts +82 -0
  43. package/src/adapter/repositories/NodeProcessSignalRepository.ts +47 -0
  44. package/src/adapter/repositories/NodeTmuxSessionRepository.test.ts +132 -0
  45. package/src/adapter/repositories/NodeTmuxSessionRepository.ts +100 -0
  46. package/src/adapter/repositories/ProcClaudeHandoverSessionRepository.test.ts +194 -0
  47. package/src/adapter/repositories/ProcClaudeHandoverSessionRepository.ts +208 -0
  48. package/src/adapter/repositories/RateLimitSnapshotRepository.ts +50 -0
  49. package/src/domain/entities/ClaudeHandoverSession.ts +11 -0
  50. package/src/domain/entities/LiveSessionActivitySnapshot.ts +0 -1
  51. package/src/domain/entities/LiveSessionOutputActivity.ts +0 -1
  52. package/src/domain/entities/TokenExhaustionHandoverState.ts +8 -0
  53. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +7 -114
  54. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +8 -53
  55. package/src/domain/usecases/TokenExhaustionHandoverUseCase.test.ts +696 -0
  56. package/src/domain/usecases/TokenExhaustionHandoverUseCase.ts +413 -0
  57. package/src/domain/usecases/adapter-interfaces/ClaudeHandoverSessionRepository.ts +5 -0
  58. package/src/domain/usecases/adapter-interfaces/ProcessSignalRepository.ts +5 -0
  59. package/src/domain/usecases/adapter-interfaces/TmuxSessionRepository.ts +2 -0
  60. package/src/domain/usecases/adapter-interfaces/TokenExhaustionHandoverStateRepository.ts +6 -0
  61. package/src/domain/usecases/adapter-interfaces/TokenRateLimitSnapshotRepository.ts +22 -0
  62. package/src/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.test.ts +2 -0
  63. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  64. package/types/adapter/entry-points/handlers/tokenExhaustionHandover.d.ts +14 -0
  65. package/types/adapter/entry-points/handlers/tokenExhaustionHandover.d.ts.map +1 -0
  66. package/types/adapter/repositories/FileHandoverStateRepository.d.ts +11 -0
  67. package/types/adapter/repositories/FileHandoverStateRepository.d.ts.map +1 -0
  68. package/types/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts +1 -1
  69. package/types/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts.map +1 -1
  70. package/types/adapter/repositories/NodeProcessSignalRepository.d.ts +8 -0
  71. package/types/adapter/repositories/NodeProcessSignalRepository.d.ts.map +1 -0
  72. package/types/adapter/repositories/NodeTmuxSessionRepository.d.ts +7 -1
  73. package/types/adapter/repositories/NodeTmuxSessionRepository.d.ts.map +1 -1
  74. package/types/adapter/repositories/ProcClaudeHandoverSessionRepository.d.ts +13 -0
  75. package/types/adapter/repositories/ProcClaudeHandoverSessionRepository.d.ts.map +1 -0
  76. package/types/adapter/repositories/RateLimitSnapshotRepository.d.ts +9 -0
  77. package/types/adapter/repositories/RateLimitSnapshotRepository.d.ts.map +1 -0
  78. package/types/domain/entities/ClaudeHandoverSession.d.ts +10 -0
  79. package/types/domain/entities/ClaudeHandoverSession.d.ts.map +1 -0
  80. package/types/domain/entities/LiveSessionActivitySnapshot.d.ts +0 -1
  81. package/types/domain/entities/LiveSessionActivitySnapshot.d.ts.map +1 -1
  82. package/types/domain/entities/LiveSessionOutputActivity.d.ts +0 -1
  83. package/types/domain/entities/LiveSessionOutputActivity.d.ts.map +1 -1
  84. package/types/domain/entities/TokenExhaustionHandoverState.d.ts +8 -0
  85. package/types/domain/entities/TokenExhaustionHandoverState.d.ts.map +1 -0
  86. package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +0 -1
  87. package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
  88. package/types/domain/usecases/TokenExhaustionHandoverUseCase.d.ts +50 -0
  89. package/types/domain/usecases/TokenExhaustionHandoverUseCase.d.ts.map +1 -0
  90. package/types/domain/usecases/adapter-interfaces/ClaudeHandoverSessionRepository.d.ts +5 -0
  91. package/types/domain/usecases/adapter-interfaces/ClaudeHandoverSessionRepository.d.ts.map +1 -0
  92. package/types/domain/usecases/adapter-interfaces/ProcessSignalRepository.d.ts +6 -0
  93. package/types/domain/usecases/adapter-interfaces/ProcessSignalRepository.d.ts.map +1 -0
  94. package/types/domain/usecases/adapter-interfaces/TmuxSessionRepository.d.ts +2 -0
  95. package/types/domain/usecases/adapter-interfaces/TmuxSessionRepository.d.ts.map +1 -1
  96. package/types/domain/usecases/adapter-interfaces/TokenExhaustionHandoverStateRepository.d.ts +6 -0
  97. package/types/domain/usecases/adapter-interfaces/TokenExhaustionHandoverStateRepository.d.ts.map +1 -0
  98. package/types/domain/usecases/adapter-interfaces/TokenRateLimitSnapshotRepository.d.ts +21 -0
  99. package/types/domain/usecases/adapter-interfaces/TokenRateLimitSnapshotRepository.d.ts.map +1 -0
@@ -0,0 +1,255 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TokenExhaustionHandoverUseCase = exports.TOKEN_EXHAUSTION_SEVEN_DAY_WARNING_FREE_THRESHOLD = exports.TOKEN_EXHAUSTION_FIVE_HOUR_WARNING_FREE_THRESHOLD = exports.TOKEN_EXHAUSTION_SEVEN_DAY_FREE_THRESHOLD = exports.TOKEN_EXHAUSTION_FIVE_HOUR_FREE_THRESHOLD = exports.TOKEN_EXHAUSTION_SNAPSHOT_HARD_STALE_THRESHOLD_SECONDS = exports.TOKEN_EXHAUSTION_SNAPSHOT_STALE_THRESHOLD_SECONDS = exports.DEFAULT_TOKEN_EXHAUSTION_GRACE_PERIOD_SECONDS = exports.DEFAULT_TOKEN_EXHAUSTION_HANDOVER_MESSAGE_BARE_NAME_LEADER = exports.DEFAULT_TOKEN_EXHAUSTION_HANDOVER_MESSAGE = void 0;
4
+ exports.DEFAULT_TOKEN_EXHAUSTION_HANDOVER_MESSAGE = 'TOKEN NEAR EXHAUSTION. Within 2 minutes, write a CHECKPOINT to your assigned task issue: the in-flight subagents and what each is doing, their durable refs (branch and pull request URLs), the working directory and branch, the single concrete next action to resume, and the reason (token near exhaustion). Do this WITHOUT waiting for long-running or CI-watching subagents. Then self-kill your own tmux session with `tmux kill-session`.';
5
+ exports.DEFAULT_TOKEN_EXHAUSTION_HANDOVER_MESSAGE_BARE_NAME_LEADER = 'TOKEN NEAR EXHAUSTION. You are a resident leader with no assigned task issue. Within 2 minutes, write a CHECKPOINT for every in-flight subagent (what each is doing and its durable refs = branch and pull request URLs, plus the single concrete next action to resume and the working directory and branch) into the GitHub issue that each subagent is working on, WITHOUT waiting for long-running or CI-watching subagents. Then stop and stay idle; do NOT self-kill. This session will be terminated and relaunched on a token that still has quota so its work is preserved.';
6
+ exports.DEFAULT_TOKEN_EXHAUSTION_GRACE_PERIOD_SECONDS = 180;
7
+ exports.TOKEN_EXHAUSTION_SNAPSHOT_STALE_THRESHOLD_SECONDS = 900;
8
+ exports.TOKEN_EXHAUSTION_SNAPSHOT_HARD_STALE_THRESHOLD_SECONDS = 3600;
9
+ exports.TOKEN_EXHAUSTION_FIVE_HOUR_FREE_THRESHOLD = 0.1;
10
+ exports.TOKEN_EXHAUSTION_SEVEN_DAY_FREE_THRESHOLD = 0.05;
11
+ exports.TOKEN_EXHAUSTION_FIVE_HOUR_WARNING_FREE_THRESHOLD = 0.25;
12
+ exports.TOKEN_EXHAUSTION_SEVEN_DAY_WARNING_FREE_THRESHOLD = 0.15;
13
+ class TokenExhaustionHandoverUseCase {
14
+ constructor(handoverSessionRepository, snapshotRepository, tmuxSessionRepository, processSignalRepository) {
15
+ this.handoverSessionRepository = handoverSessionRepository;
16
+ this.snapshotRepository = snapshotRepository;
17
+ this.tmuxSessionRepository = tmuxSessionRepository;
18
+ this.processSignalRepository = processSignalRepository;
19
+ this.run = async (input) => {
20
+ const nowEpochSeconds = Math.floor(input.now.getTime() / 1000);
21
+ const sessions = this.handoverSessionRepository.listHandoverSessions();
22
+ const snapshots = this.snapshotRepository.listSnapshots();
23
+ const snapshotByToken = new Map(snapshots.map((snapshot) => [snapshot.token, snapshot]));
24
+ const liveSessionNames = new Set(await this.tmuxSessionRepository.listLiveSessionNames());
25
+ const nextEntries = {
26
+ ...input.state.entries,
27
+ };
28
+ const newlyHandoverSentSessionNames = [];
29
+ const killedSessionNames = [];
30
+ const terminatedPids = [];
31
+ const relaunchedLeaderNames = [];
32
+ const leftAliveSessionNames = [];
33
+ for (const session of sessions) {
34
+ try {
35
+ const snapshot = snapshotByToken.get(session.token);
36
+ if (snapshot === undefined) {
37
+ continue;
38
+ }
39
+ const hasTmux = session.kind !== 'implSubagent';
40
+ const verdict = this.evaluateSnapshot(snapshot, nowEpochSeconds, hasTmux);
41
+ const stateKey = this.stateKeyFor(session);
42
+ if (verdict.stale) {
43
+ continue;
44
+ }
45
+ if (!verdict.exhausted) {
46
+ delete nextEntries[stateKey];
47
+ continue;
48
+ }
49
+ if (!this.isFresherTokenAvailable(session.token, snapshots, nowEpochSeconds)) {
50
+ console.log(`Token exhaustion handover: leaving ${this.displayName(session)} alive (token pool globally exhausted, no fresher token available)`);
51
+ leftAliveSessionNames.push(this.displayName(session));
52
+ continue;
53
+ }
54
+ const entry = nextEntries[stateKey];
55
+ if (entry === undefined) {
56
+ if (input.enabled) {
57
+ await this.sendHandover(session, input);
58
+ }
59
+ console.log(`Token exhaustion handover: signaled ${this.displayName(session)} kind=${session.kind} reason=${verdict.reason} enabled=${input.enabled}`);
60
+ nextEntries[stateKey] = {
61
+ signaledAtEpoch: nowEpochSeconds,
62
+ pid: session.pid,
63
+ };
64
+ newlyHandoverSentSessionNames.push(this.displayName(session));
65
+ continue;
66
+ }
67
+ if (nowEpochSeconds - entry.signaledAtEpoch <
68
+ input.gracePeriodSeconds) {
69
+ console.log(`Token exhaustion handover: waiting for grace period for ${this.displayName(session)} remainingSeconds=${input.gracePeriodSeconds - (nowEpochSeconds - entry.signaledAtEpoch)}`);
70
+ continue;
71
+ }
72
+ const alive = hasTmux
73
+ ? session.sessionName !== null &&
74
+ liveSessionNames.has(session.sessionName)
75
+ : this.processSignalRepository.isProcessAlive(entry.pid);
76
+ if (!alive) {
77
+ if (input.enabled && this.needsRelaunch(session)) {
78
+ await this.relaunchBareNameLeader(session, relaunchedLeaderNames);
79
+ }
80
+ delete nextEntries[stateKey];
81
+ continue;
82
+ }
83
+ if (!input.enabled) {
84
+ console.log(`Token exhaustion handover: would force-kill ${this.displayName(session)} kind=${session.kind} (dry-run, enabled=false)`);
85
+ delete nextEntries[stateKey];
86
+ continue;
87
+ }
88
+ await this.forceKill(session, entry);
89
+ if (this.needsRelaunch(session)) {
90
+ await this.relaunchBareNameLeader(session, relaunchedLeaderNames);
91
+ }
92
+ console.log(`Token exhaustion handover: force-killed ${this.displayName(session)} kind=${session.kind}`);
93
+ if (hasTmux) {
94
+ killedSessionNames.push(this.displayName(session));
95
+ }
96
+ else {
97
+ terminatedPids.push(entry.pid);
98
+ }
99
+ delete nextEntries[stateKey];
100
+ }
101
+ catch (error) {
102
+ console.error(`Token exhaustion handover: error processing ${this.displayName(session)}: ${error instanceof Error ? error.message : String(error)}`);
103
+ }
104
+ }
105
+ console.log(`Token exhaustion handover: cycle summary evaluated=${sessions.length} enabled=${input.enabled} signaled=${newlyHandoverSentSessionNames.length} killed=${killedSessionNames.length} terminatedPids=${terminatedPids.length} relaunched=${relaunchedLeaderNames.length} leftAlive=${leftAliveSessionNames.length}`);
106
+ return {
107
+ newlyHandoverSentSessionNames,
108
+ killedSessionNames,
109
+ terminatedPids,
110
+ relaunchedLeaderNames,
111
+ leftAliveSessionNames,
112
+ state: { entries: nextEntries },
113
+ };
114
+ };
115
+ this.needsRelaunch = (session) => session.kind === 'bareNameLeader' && session.name !== null;
116
+ this.relaunchBareNameLeader = async (session, relaunchedLeaderNames) => {
117
+ if (session.name === null) {
118
+ return;
119
+ }
120
+ await this.tmuxSessionRepository.launchBareNameLeaderSession(session.name);
121
+ relaunchedLeaderNames.push(session.name);
122
+ };
123
+ this.sendHandover = async (session, input) => {
124
+ if (session.kind === 'implSubagent') {
125
+ this.processSignalRepository.terminateProcess(session.pid);
126
+ return;
127
+ }
128
+ if (session.sessionName === null) {
129
+ return;
130
+ }
131
+ const message = session.kind === 'issueUrlLeader'
132
+ ? input.issueUrlLeaderMessage
133
+ : input.bareNameLeaderMessage;
134
+ await this.tmuxSessionRepository.sendKeys(session.sessionName, message);
135
+ };
136
+ this.forceKill = async (session, entry) => {
137
+ if (session.kind === 'implSubagent') {
138
+ this.processSignalRepository.killProcess(entry.pid);
139
+ return;
140
+ }
141
+ if (session.sessionName !== null) {
142
+ try {
143
+ await this.tmuxSessionRepository.killSession(session.sessionName);
144
+ }
145
+ catch (error) {
146
+ console.error(`Token exhaustion handover: killSession failed for ${session.sessionName}: ${error instanceof Error ? error.message : String(error)}`);
147
+ }
148
+ }
149
+ this.processSignalRepository.killProcess(entry.pid);
150
+ };
151
+ this.stateKeyFor = (session) => session.kind === 'implSubagent'
152
+ ? `pid:${session.pid}`
153
+ : (session.sessionName ?? `pid:${session.pid}`);
154
+ this.displayName = (session) => session.sessionName ?? `pid:${session.pid}`;
155
+ this.isFresherTokenAvailable = (currentToken, snapshots, nowEpochSeconds) => {
156
+ for (const snapshot of snapshots) {
157
+ if (snapshot.token === currentToken) {
158
+ continue;
159
+ }
160
+ const verdict = this.evaluateSnapshot(snapshot, nowEpochSeconds, true);
161
+ if (!verdict.stale && !verdict.exhausted) {
162
+ return true;
163
+ }
164
+ }
165
+ return false;
166
+ };
167
+ this.evaluateSnapshot = (snapshot, nowEpochSeconds, hasTmux) => {
168
+ const age = nowEpochSeconds - snapshot.lastUpdatedEpoch;
169
+ const fiveHourFree = this.freeRatio(snapshot.fiveHourUtilization, snapshot.fiveHourReset, nowEpochSeconds);
170
+ const sevenDayFree = this.freeRatio(snapshot.sevenDayUtilization, snapshot.sevenDayReset, nowEpochSeconds);
171
+ const blocked = snapshot.blocked ||
172
+ snapshot.rejected ||
173
+ snapshot.blockedUntilEpoch > nowEpochSeconds;
174
+ const weeklyCapped = this.isWeeklyCapped(snapshot.modelWeeklyLimits, nowEpochSeconds);
175
+ if (age > exports.TOKEN_EXHAUSTION_SNAPSHOT_HARD_STALE_THRESHOLD_SECONDS) {
176
+ return {
177
+ stale: true,
178
+ exhausted: false,
179
+ reason: 'hard_stale',
180
+ fiveHourFree,
181
+ sevenDayFree,
182
+ blocked,
183
+ weeklyCapped,
184
+ };
185
+ }
186
+ const inWarningBand = fiveHourFree < exports.TOKEN_EXHAUSTION_FIVE_HOUR_WARNING_FREE_THRESHOLD ||
187
+ sevenDayFree < exports.TOKEN_EXHAUSTION_SEVEN_DAY_WARNING_FREE_THRESHOLD ||
188
+ blocked ||
189
+ weeklyCapped;
190
+ if (age > exports.TOKEN_EXHAUSTION_SNAPSHOT_STALE_THRESHOLD_SECONDS &&
191
+ !inWarningBand) {
192
+ return {
193
+ stale: true,
194
+ exhausted: false,
195
+ reason: 'stale_healthy',
196
+ fiveHourFree,
197
+ sevenDayFree,
198
+ blocked,
199
+ weeklyCapped,
200
+ };
201
+ }
202
+ const exhausted = fiveHourFree < exports.TOKEN_EXHAUSTION_FIVE_HOUR_FREE_THRESHOLD ||
203
+ (hasTmux && sevenDayFree < exports.TOKEN_EXHAUSTION_SEVEN_DAY_FREE_THRESHOLD) ||
204
+ blocked ||
205
+ weeklyCapped;
206
+ const reason = weeklyCapped
207
+ ? 'weekly_hard_cap_rejected'
208
+ : fiveHourFree < exports.TOKEN_EXHAUSTION_FIVE_HOUR_FREE_THRESHOLD
209
+ ? 'five_hour_free_below_threshold'
210
+ : hasTmux && sevenDayFree < exports.TOKEN_EXHAUSTION_SEVEN_DAY_FREE_THRESHOLD
211
+ ? 'seven_day_free_below_threshold'
212
+ : blocked
213
+ ? 'blocked_or_rejected'
214
+ : 'healthy';
215
+ return {
216
+ stale: false,
217
+ exhausted,
218
+ reason,
219
+ fiveHourFree,
220
+ sevenDayFree,
221
+ blocked,
222
+ weeklyCapped,
223
+ };
224
+ };
225
+ this.freeRatio = (utilization, resetEpoch, nowEpochSeconds) => {
226
+ if (resetEpoch > 0 && nowEpochSeconds > resetEpoch) {
227
+ return 1;
228
+ }
229
+ return 1 - this.clamp01(utilization);
230
+ };
231
+ this.clamp01 = (value) => {
232
+ if (value < 0) {
233
+ return 0;
234
+ }
235
+ if (value > 1) {
236
+ return 1;
237
+ }
238
+ return value;
239
+ };
240
+ this.isWeeklyCapped = (modelWeeklyLimits, nowEpochSeconds) => {
241
+ for (const limit of modelWeeklyLimits) {
242
+ if (!limit.rejected) {
243
+ continue;
244
+ }
245
+ if (limit.resetsAt > 0 && nowEpochSeconds > limit.resetsAt) {
246
+ continue;
247
+ }
248
+ return true;
249
+ }
250
+ return false;
251
+ };
252
+ }
253
+ }
254
+ exports.TokenExhaustionHandoverUseCase = TokenExhaustionHandoverUseCase;
255
+ //# sourceMappingURL=TokenExhaustionHandoverUseCase.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TokenExhaustionHandoverUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/TokenExhaustionHandoverUseCase.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=ClaudeHandoverSessionRepository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClaudeHandoverSessionRepository.js","sourceRoot":"","sources":["../../../../src/domain/usecases/adapter-interfaces/ClaudeHandoverSessionRepository.ts"],"names":[],"mappings":""}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=ProcessSignalRepository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProcessSignalRepository.js","sourceRoot":"","sources":["../../../../src/domain/usecases/adapter-interfaces/ProcessSignalRepository.ts"],"names":[],"mappings":""}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=TokenExhaustionHandoverStateRepository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TokenExhaustionHandoverStateRepository.js","sourceRoot":"","sources":["../../../../src/domain/usecases/adapter-interfaces/TokenExhaustionHandoverStateRepository.ts"],"names":[],"mappings":""}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=TokenRateLimitSnapshotRepository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TokenRateLimitSnapshotRepository.js","sourceRoot":"","sources":["../../../../src/domain/usecases/adapter-interfaces/TokenRateLimitSnapshotRepository.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-issue-tower-defence-management",
3
- "version": "1.139.1",
3
+ "version": "1.140.1",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -10,6 +10,7 @@ import { writeMachineStatus } from './machineStatusWriter';
10
10
  import { writeTokenStatus } from './tokenStatusWriter';
11
11
  import { writeInTmuxByHumanData } from './inTmuxByHumanDataWriter';
12
12
  import { reconcileInTmuxByHumanSessions } from './inTmuxByHumanSessionReconciler';
13
+ import { handleTokenExhaustionHandover } from './tokenExhaustionHandover';
13
14
  import { cleanStaleTmuxSessions } from './staleTmuxSessionCleaner';
14
15
  import {
15
16
  notifySilentTmuxSessions,
@@ -115,6 +116,12 @@ export class HandleScheduledEventUseCaseHandler {
115
116
  inTmuxConsoleToken?: string;
116
117
  inTmuxProjectOrder?: string[];
117
118
  inTmuxLauncherCommand?: string;
119
+ tokenExhaustionHandoverEnabled?: boolean;
120
+ tokenExhaustionHandoverMessage?: string;
121
+ tokenExhaustionHandoverBareNameLeaderMessage?: string;
122
+ tokenRateLimitSnapshotBaseDir?: string;
123
+ tokenExhaustionGracePeriodSeconds?: number;
124
+ tokenExhaustionHandoverStateFilePath?: string;
118
125
  silentNotificationEnabled?: boolean;
119
126
  ownerCallMarker?: string;
120
127
  subAgentOutputRootDirectory?: string;
@@ -554,6 +561,31 @@ export class HandleScheduledEventUseCaseHandler {
554
561
  );
555
562
  }
556
563
 
564
+ try {
565
+ await handleTokenExhaustionHandover({
566
+ enabled: mergedInput.tokenExhaustionHandoverEnabled ?? false,
567
+ tokenListJsonPath:
568
+ mergedInput.claudeCodeOauthTokenListJsonPath ?? null,
569
+ handoverMessage: mergedInput.tokenExhaustionHandoverMessage ?? null,
570
+ bareNameLeaderHandoverMessage:
571
+ mergedInput.tokenExhaustionHandoverBareNameLeaderMessage ?? null,
572
+ tokenRateLimitSnapshotBaseDir:
573
+ mergedInput.tokenRateLimitSnapshotBaseDir ?? null,
574
+ gracePeriodSeconds:
575
+ mergedInput.tokenExhaustionGracePeriodSeconds ?? null,
576
+ stateFilePath:
577
+ mergedInput.tokenExhaustionHandoverStateFilePath ?? null,
578
+ localCommandRunner: nodeLocalCommandRunner,
579
+ now: inTmuxNow,
580
+ });
581
+ } catch (error) {
582
+ console.error(
583
+ `Failed to handle token exhaustion handover: ${
584
+ error instanceof Error ? error.message : String(error)
585
+ }`,
586
+ );
587
+ }
588
+
557
589
  try {
558
590
  await reconcileInTmuxByHumanSessions({
559
591
  inTmuxLauncherCommand: mergedInput.inTmuxLauncherCommand ?? null,
@@ -0,0 +1,79 @@
1
+ import { LocalCommandRunner } from '../../../domain/usecases/adapter-interfaces/LocalCommandRunner';
2
+ import {
3
+ TokenExhaustionHandoverUseCase,
4
+ DEFAULT_TOKEN_EXHAUSTION_HANDOVER_MESSAGE,
5
+ DEFAULT_TOKEN_EXHAUSTION_HANDOVER_MESSAGE_BARE_NAME_LEADER,
6
+ DEFAULT_TOKEN_EXHAUSTION_GRACE_PERIOD_SECONDS,
7
+ } from '../../../domain/usecases/TokenExhaustionHandoverUseCase';
8
+ import { NodeTmuxSessionRepository } from '../../repositories/NodeTmuxSessionRepository';
9
+ import { RateLimitSnapshotRepository } from '../../repositories/RateLimitSnapshotRepository';
10
+ import { ProcClaudeHandoverSessionRepository } from '../../repositories/ProcClaudeHandoverSessionRepository';
11
+ import { NodeProcessSignalRepository } from '../../repositories/NodeProcessSignalRepository';
12
+ import {
13
+ FileHandoverStateRepository,
14
+ defaultHandoverStateFilePath,
15
+ } from '../../repositories/FileHandoverStateRepository';
16
+
17
+ export type TokenExhaustionHandoverParams = {
18
+ enabled: boolean;
19
+ tokenListJsonPath: string | null;
20
+ handoverMessage?: string | null;
21
+ bareNameLeaderHandoverMessage?: string | null;
22
+ tokenRateLimitSnapshotBaseDir?: string | null;
23
+ gracePeriodSeconds?: number | null;
24
+ stateFilePath?: string | null;
25
+ localCommandRunner: LocalCommandRunner;
26
+ now: Date;
27
+ };
28
+
29
+ export const handleTokenExhaustionHandover = async (
30
+ params: TokenExhaustionHandoverParams,
31
+ ): Promise<void> => {
32
+ const {
33
+ enabled,
34
+ tokenListJsonPath,
35
+ handoverMessage,
36
+ bareNameLeaderHandoverMessage,
37
+ tokenRateLimitSnapshotBaseDir,
38
+ gracePeriodSeconds,
39
+ stateFilePath,
40
+ localCommandRunner,
41
+ now,
42
+ } = params;
43
+
44
+ if (tokenListJsonPath === null) {
45
+ console.log(
46
+ 'Token exhaustion handover: skipped (no claudeCodeOauthTokenListJsonPath configured).',
47
+ );
48
+ return;
49
+ }
50
+
51
+ const snapshotRepository = new RateLimitSnapshotRepository(
52
+ tokenListJsonPath,
53
+ tokenRateLimitSnapshotBaseDir ?? undefined,
54
+ );
55
+ const stateRepository = new FileHandoverStateRepository(
56
+ stateFilePath ?? defaultHandoverStateFilePath(),
57
+ );
58
+ const useCase = new TokenExhaustionHandoverUseCase(
59
+ new ProcClaudeHandoverSessionRepository(),
60
+ snapshotRepository,
61
+ new NodeTmuxSessionRepository(localCommandRunner),
62
+ new NodeProcessSignalRepository(),
63
+ );
64
+
65
+ const result = await useCase.run({
66
+ enabled,
67
+ issueUrlLeaderMessage:
68
+ handoverMessage ?? DEFAULT_TOKEN_EXHAUSTION_HANDOVER_MESSAGE,
69
+ bareNameLeaderMessage:
70
+ bareNameLeaderHandoverMessage ??
71
+ DEFAULT_TOKEN_EXHAUSTION_HANDOVER_MESSAGE_BARE_NAME_LEADER,
72
+ gracePeriodSeconds:
73
+ gracePeriodSeconds ?? DEFAULT_TOKEN_EXHAUSTION_GRACE_PERIOD_SECONDS,
74
+ state: stateRepository.load(),
75
+ now,
76
+ });
77
+
78
+ stateRepository.save(result.state);
79
+ };
@@ -0,0 +1,114 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ import {
5
+ FileHandoverStateRepository,
6
+ defaultHandoverStateFilePath,
7
+ } from './FileHandoverStateRepository';
8
+
9
+ describe('FileHandoverStateRepository', () => {
10
+ let directory: string;
11
+ let filePath: string;
12
+
13
+ beforeEach(() => {
14
+ directory = fs.mkdtempSync(path.join(os.tmpdir(), 'handover-state-'));
15
+ filePath = path.join(directory, 'nested', 'state.json');
16
+ });
17
+
18
+ afterEach(() => {
19
+ fs.rmSync(directory, { recursive: true, force: true });
20
+ });
21
+
22
+ it('returns an empty state when the file does not exist', () => {
23
+ const repository = new FileHandoverStateRepository(filePath);
24
+
25
+ expect(repository.load()).toEqual({ entries: {} });
26
+ });
27
+
28
+ it('saves and reloads state, creating parent directories', () => {
29
+ const repository = new FileHandoverStateRepository(filePath);
30
+ const state = {
31
+ entries: {
32
+ 'session-a': { signaledAtEpoch: 1700000000, pid: 4242 },
33
+ },
34
+ };
35
+
36
+ repository.save(state);
37
+
38
+ expect(fs.existsSync(filePath)).toBe(true);
39
+ expect(new FileHandoverStateRepository(filePath).load()).toEqual(state);
40
+ });
41
+
42
+ it('writes atomically via a temporary file and leaves no temp file behind', () => {
43
+ const repository = new FileHandoverStateRepository(filePath);
44
+
45
+ repository.save({
46
+ entries: { 'session-a': { signaledAtEpoch: 1, pid: 2 } },
47
+ });
48
+
49
+ expect(fs.existsSync(`${filePath}.tmp`)).toBe(false);
50
+ });
51
+
52
+ it('returns an empty state when the file contains invalid JSON', () => {
53
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
54
+ fs.writeFileSync(filePath, 'not json');
55
+ const repository = new FileHandoverStateRepository(filePath);
56
+
57
+ expect(repository.load()).toEqual({ entries: {} });
58
+ });
59
+
60
+ it('drops entries with malformed values while keeping valid ones', () => {
61
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
62
+ fs.writeFileSync(
63
+ filePath,
64
+ JSON.stringify({
65
+ entries: {
66
+ valid: { signaledAtEpoch: 5, pid: 6 },
67
+ missingPid: { signaledAtEpoch: 5 },
68
+ notAnObject: 7,
69
+ },
70
+ }),
71
+ );
72
+ const repository = new FileHandoverStateRepository(filePath);
73
+
74
+ expect(repository.load()).toEqual({
75
+ entries: { valid: { signaledAtEpoch: 5, pid: 6 } },
76
+ });
77
+ });
78
+ });
79
+
80
+ describe('defaultHandoverStateFilePath', () => {
81
+ const originalXdgCacheHome = process.env.XDG_CACHE_HOME;
82
+
83
+ afterEach(() => {
84
+ if (originalXdgCacheHome === undefined) {
85
+ delete process.env.XDG_CACHE_HOME;
86
+ } else {
87
+ process.env.XDG_CACHE_HOME = originalXdgCacheHome;
88
+ }
89
+ });
90
+
91
+ it('uses a TDPM-native filename distinct from the standalone monitor state file', () => {
92
+ process.env.XDG_CACHE_HOME = '/custom/cache';
93
+
94
+ expect(defaultHandoverStateFilePath()).toBe(
95
+ '/custom/cache/tdpm/token-exhaustion-handover-state-tdpm-native.json',
96
+ );
97
+ expect(defaultHandoverStateFilePath()).not.toBe(
98
+ '/custom/cache/tdpm/token-exhaustion-handover-state.json',
99
+ );
100
+ });
101
+
102
+ it('falls back to ~/.cache when XDG_CACHE_HOME is unset', () => {
103
+ delete process.env.XDG_CACHE_HOME;
104
+
105
+ expect(defaultHandoverStateFilePath()).toBe(
106
+ path.join(
107
+ os.homedir(),
108
+ '.cache',
109
+ 'tdpm',
110
+ 'token-exhaustion-handover-state-tdpm-native.json',
111
+ ),
112
+ );
113
+ });
114
+ });
@@ -0,0 +1,76 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ import {
5
+ TokenExhaustionHandoverState,
6
+ TokenExhaustionHandoverStateEntry,
7
+ } from '../../domain/entities/TokenExhaustionHandoverState';
8
+ import { TokenExhaustionHandoverStateRepository } from '../../domain/usecases/adapter-interfaces/TokenExhaustionHandoverStateRepository';
9
+
10
+ export const defaultHandoverStateFilePath = (): string => {
11
+ const base = process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), '.cache');
12
+ return path.join(
13
+ base,
14
+ 'tdpm',
15
+ 'token-exhaustion-handover-state-tdpm-native.json',
16
+ );
17
+ };
18
+
19
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
20
+ value !== null && typeof value === 'object' && !Array.isArray(value);
21
+
22
+ export class FileHandoverStateRepository implements TokenExhaustionHandoverStateRepository {
23
+ constructor(
24
+ private readonly filePath: string = defaultHandoverStateFilePath(),
25
+ ) {}
26
+
27
+ load = (): TokenExhaustionHandoverState => {
28
+ let raw: string;
29
+ try {
30
+ raw = fs.readFileSync(this.filePath, 'utf8');
31
+ } catch {
32
+ return { entries: {} };
33
+ }
34
+ let parsed: unknown;
35
+ try {
36
+ parsed = JSON.parse(raw);
37
+ } catch {
38
+ return { entries: {} };
39
+ }
40
+ return { entries: this.parseEntries(parsed) };
41
+ };
42
+
43
+ save = (state: TokenExhaustionHandoverState): void => {
44
+ const directory = path.dirname(this.filePath);
45
+ if (!fs.existsSync(directory)) {
46
+ fs.mkdirSync(directory, { recursive: true });
47
+ }
48
+ const temporaryPath = `${this.filePath}.tmp`;
49
+ fs.writeFileSync(temporaryPath, JSON.stringify(state));
50
+ fs.renameSync(temporaryPath, this.filePath);
51
+ };
52
+
53
+ private parseEntries = (
54
+ parsed: unknown,
55
+ ): Record<string, TokenExhaustionHandoverStateEntry> => {
56
+ const entries: Record<string, TokenExhaustionHandoverStateEntry> = {};
57
+ if (!isRecord(parsed)) {
58
+ return entries;
59
+ }
60
+ const stored = parsed.entries;
61
+ if (!isRecord(stored)) {
62
+ return entries;
63
+ }
64
+ for (const [key, value] of Object.entries(stored)) {
65
+ if (!isRecord(value)) {
66
+ continue;
67
+ }
68
+ const signaledAtEpoch = value.signaledAtEpoch;
69
+ const pid = value.pid;
70
+ if (typeof signaledAtEpoch === 'number' && typeof pid === 'number') {
71
+ entries[key] = { signaledAtEpoch, pid };
72
+ }
73
+ }
74
+ return entries;
75
+ };
76
+ }