github-issue-tower-defence-management 1.137.2 → 1.139.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 +14 -0
- package/README.md +3 -2
- package/bin/adapter/entry-points/cli/index.js +2 -1
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +17 -3
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +5 -4
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -1
- package/bin/adapter/repositories/FileSystemSubAgentLivenessResolver.js +81 -0
- package/bin/adapter/repositories/FileSystemSubAgentLivenessResolver.js.map +1 -0
- package/bin/adapter/repositories/ProcTakeOwnershipSpawnRepository.js +15 -0
- package/bin/adapter/repositories/ProcTakeOwnershipSpawnRepository.js.map +1 -1
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js +30 -12
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js.map +1 -1
- package/bin/domain/usecases/StartPreparationUseCase.js +7 -1
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/adapter-interfaces/SubAgentLivenessResolver.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/SubAgentLivenessResolver.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.ts +2 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +13 -0
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +1 -0
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +6 -0
- package/src/adapter/entry-points/handlers/tokenStatusWriter.test.ts +7 -4
- package/src/adapter/repositories/FileSystemSubAgentLivenessResolver.test.ts +87 -0
- package/src/adapter/repositories/FileSystemSubAgentLivenessResolver.ts +57 -0
- package/src/adapter/repositories/ProcTakeOwnershipSpawnRepository.test.ts +101 -0
- package/src/adapter/repositories/ProcTakeOwnershipSpawnRepository.ts +16 -0
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.test.ts +136 -6
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts +37 -12
- package/src/domain/usecases/StartPreparationUseCase.test.ts +54 -0
- package/src/domain/usecases/StartPreparationUseCase.ts +9 -0
- package/src/domain/usecases/adapter-interfaces/SubAgentLivenessResolver.ts +6 -0
- package/src/domain/usecases/adapter-interfaces/TakeOwnershipSpawnRepository.ts +1 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts +1 -0
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -1
- package/types/adapter/repositories/FileSystemSubAgentLivenessResolver.d.ts +11 -0
- package/types/adapter/repositories/FileSystemSubAgentLivenessResolver.d.ts.map +1 -0
- package/types/adapter/repositories/ProcTakeOwnershipSpawnRepository.d.ts +1 -0
- package/types/adapter/repositories/ProcTakeOwnershipSpawnRepository.d.ts.map +1 -1
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts +4 -2
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts.map +1 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts +3 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/SubAgentLivenessResolver.d.ts +7 -0
- package/types/domain/usecases/adapter-interfaces/SubAgentLivenessResolver.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/TakeOwnershipSpawnRepository.d.ts +1 -0
- package/types/domain/usecases/adapter-interfaces/TakeOwnershipSpawnRepository.d.ts.map +1 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { SubAgentLivenessResolver } from '../../domain/usecases/adapter-interfaces/SubAgentLivenessResolver';
|
|
4
|
+
|
|
5
|
+
const RUNNING_SUBAGENTS_FILE_NAME = 'running-subagents.txt';
|
|
6
|
+
|
|
7
|
+
export class FileSystemSubAgentLivenessResolver implements SubAgentLivenessResolver {
|
|
8
|
+
constructor(private readonly runtimeRootDirectory: string | null) {}
|
|
9
|
+
|
|
10
|
+
resolveLiveSubAgentIds = async (params: {
|
|
11
|
+
sessionName: string;
|
|
12
|
+
mainTranscriptPath: string | null;
|
|
13
|
+
}): Promise<Set<string> | null> => {
|
|
14
|
+
const runningFilePath = this.resolveRunningSubAgentsFilePath(
|
|
15
|
+
params.mainTranscriptPath,
|
|
16
|
+
);
|
|
17
|
+
if (runningFilePath === null) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
let content: string;
|
|
21
|
+
try {
|
|
22
|
+
content = fs.readFileSync(runningFilePath, 'utf8');
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const liveIds = new Set<string>();
|
|
27
|
+
for (const line of content.split('\n')) {
|
|
28
|
+
const trimmed = line.trim();
|
|
29
|
+
if (trimmed.length > 0) {
|
|
30
|
+
liveIds.add(trimmed);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return liveIds;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
private resolveRunningSubAgentsFilePath = (
|
|
37
|
+
mainTranscriptPath: string | null,
|
|
38
|
+
): string | null => {
|
|
39
|
+
if (this.runtimeRootDirectory === null || mainTranscriptPath === null) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const sessionUuid = path.basename(mainTranscriptPath, '.jsonl');
|
|
43
|
+
if (sessionUuid.length === 0 || sessionUuid.endsWith('.jsonl')) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const cwdSlug = path.basename(path.dirname(mainTranscriptPath));
|
|
47
|
+
if (cwdSlug.length === 0 || cwdSlug === '.') {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return path.join(
|
|
51
|
+
this.runtimeRootDirectory,
|
|
52
|
+
cwdSlug,
|
|
53
|
+
sessionUuid,
|
|
54
|
+
RUNNING_SUBAGENTS_FILE_NAME,
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -127,4 +127,105 @@ describe('ProcTakeOwnershipSpawnRepository', () => {
|
|
|
127
127
|
|
|
128
128
|
expect(repository.listSpawns()).toEqual([]);
|
|
129
129
|
});
|
|
130
|
+
|
|
131
|
+
describe('listRunningIssueUrls', () => {
|
|
132
|
+
it('extracts issue URL from a bash-c style Take ownership spawn', () => {
|
|
133
|
+
writeProcess({
|
|
134
|
+
pid: 401,
|
|
135
|
+
cmdline: argv(
|
|
136
|
+
'bash',
|
|
137
|
+
'-c',
|
|
138
|
+
`claude-agent -p "Take ownership of ${issueUrl}" | tee /home/user/logs-aw/20260626_120000_abc.log`,
|
|
139
|
+
),
|
|
140
|
+
environ: { CLAUDE_CODE_OAUTH_TOKEN: 'token-a' },
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const repository = new ProcTakeOwnershipSpawnRepository(procDirectory);
|
|
144
|
+
|
|
145
|
+
expect(repository.listRunningIssueUrls()).toEqual([issueUrl]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('extracts issue URL from a null-separated claude-agent spawn', () => {
|
|
149
|
+
writeProcess({
|
|
150
|
+
pid: 402,
|
|
151
|
+
cmdline: argv(
|
|
152
|
+
'timeout',
|
|
153
|
+
'--kill-after=60s',
|
|
154
|
+
'3h',
|
|
155
|
+
'claude-agent',
|
|
156
|
+
'--agent',
|
|
157
|
+
'impl',
|
|
158
|
+
'-p',
|
|
159
|
+
`Take ownership of ${issueUrl} and finish it`,
|
|
160
|
+
'--model',
|
|
161
|
+
'claude-sonnet-4-6',
|
|
162
|
+
),
|
|
163
|
+
environ: { CLAUDE_CODE_OAUTH_TOKEN: 'token-b' },
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const repository = new ProcTakeOwnershipSpawnRepository(procDirectory);
|
|
167
|
+
|
|
168
|
+
expect(repository.listRunningIssueUrls()).toEqual([issueUrl]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('returns empty list when no Take ownership process is running', () => {
|
|
172
|
+
writeProcess({
|
|
173
|
+
pid: 403,
|
|
174
|
+
cmdline: argv('claude', '--name', issueUrl),
|
|
175
|
+
environ: { CLAUDE_CODE_OAUTH_TOKEN: 'token-c' },
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const repository = new ProcTakeOwnershipSpawnRepository(procDirectory);
|
|
179
|
+
|
|
180
|
+
expect(repository.listRunningIssueUrls()).toEqual([]);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('returns URLs from all matching processes', () => {
|
|
184
|
+
const issueUrl2 = 'https://github.com/HiromiShikata/example/issues/2';
|
|
185
|
+
writeProcess({
|
|
186
|
+
pid: 404,
|
|
187
|
+
cmdline: argv(
|
|
188
|
+
'timeout',
|
|
189
|
+
'--kill-after=60s',
|
|
190
|
+
'3h',
|
|
191
|
+
'claude-agent',
|
|
192
|
+
'--agent',
|
|
193
|
+
'impl',
|
|
194
|
+
'-p',
|
|
195
|
+
`Take ownership of ${issueUrl} and finish it`,
|
|
196
|
+
'--model',
|
|
197
|
+
'claude-sonnet-4-6',
|
|
198
|
+
),
|
|
199
|
+
environ: { CLAUDE_CODE_OAUTH_TOKEN: 'token-d' },
|
|
200
|
+
});
|
|
201
|
+
writeProcess({
|
|
202
|
+
pid: 405,
|
|
203
|
+
cmdline: argv(
|
|
204
|
+
'timeout',
|
|
205
|
+
'--kill-after=60s',
|
|
206
|
+
'3h',
|
|
207
|
+
'claude-agent',
|
|
208
|
+
'--agent',
|
|
209
|
+
'impl',
|
|
210
|
+
'-p',
|
|
211
|
+
`Take ownership of ${issueUrl2} and finish it`,
|
|
212
|
+
'--model',
|
|
213
|
+
'claude-sonnet-4-6',
|
|
214
|
+
),
|
|
215
|
+
environ: { CLAUDE_CODE_OAUTH_TOKEN: 'token-e' },
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const repository = new ProcTakeOwnershipSpawnRepository(procDirectory);
|
|
219
|
+
|
|
220
|
+
expect(repository.listRunningIssueUrls()).toEqual([issueUrl, issueUrl2]);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('returns empty list when the proc directory does not exist', () => {
|
|
224
|
+
const repository = new ProcTakeOwnershipSpawnRepository(
|
|
225
|
+
path.join(procDirectory, 'missing'),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
expect(repository.listRunningIssueUrls()).toEqual([]);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
130
231
|
});
|
|
@@ -9,6 +9,7 @@ const DEFAULT_PROC_DIRECTORY = '/proc';
|
|
|
9
9
|
const OAUTH_TOKEN_ENVIRON_KEY = 'CLAUDE_CODE_OAUTH_TOKEN';
|
|
10
10
|
const TAKE_OWNERSHIP_MARKER = 'Take ownership';
|
|
11
11
|
const LOG_PATH_PATTERN = /\/logs-aw\/[^\0"]+\.log/;
|
|
12
|
+
const ISSUE_URL_PATTERN = /Take ownership of (https:\/\/github\.com\/[^ \0"]+)/;
|
|
12
13
|
|
|
13
14
|
const parseCommandLineArguments = (cmdline: string): string[] =>
|
|
14
15
|
cmdline.split('\0').filter((argument) => argument.length > 0);
|
|
@@ -37,6 +38,21 @@ export class ProcTakeOwnershipSpawnRepository implements TakeOwnershipSpawnRepos
|
|
|
37
38
|
return spawns;
|
|
38
39
|
};
|
|
39
40
|
|
|
41
|
+
listRunningIssueUrls = (): string[] => {
|
|
42
|
+
const urls: string[] = [];
|
|
43
|
+
for (const pidDirectory of this.listProcessIdDirectories()) {
|
|
44
|
+
const rawCmdline = this.readRawCmdline(pidDirectory);
|
|
45
|
+
if (rawCmdline === null) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const match = rawCmdline.match(ISSUE_URL_PATTERN);
|
|
49
|
+
if (match !== null) {
|
|
50
|
+
urls.push(match[1]);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return urls;
|
|
54
|
+
};
|
|
55
|
+
|
|
40
56
|
private listProcessIdDirectories = (): string[] => {
|
|
41
57
|
let entries: string[];
|
|
42
58
|
try {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
SubAgentProcess,
|
|
7
7
|
SubAgentProcessLister,
|
|
8
8
|
} from '../../domain/usecases/adapter-interfaces/SubAgentProcessLister';
|
|
9
|
+
import { SubAgentLivenessResolver } from '../../domain/usecases/adapter-interfaces/SubAgentLivenessResolver';
|
|
9
10
|
import { FileSystemSubAgentTranscriptDirectoryResolver } from './FileSystemSubAgentTranscriptDirectoryResolver';
|
|
10
11
|
import { TranscriptSessionSubAgentActivityRepository } from './TranscriptSessionSubAgentActivityRepository';
|
|
11
12
|
|
|
@@ -1085,6 +1086,23 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
1085
1086
|
content: `<task-notification>\n<task-id>${agentId}</task-id>\n<status>completed</status>\n</task-notification>`,
|
|
1086
1087
|
});
|
|
1087
1088
|
|
|
1089
|
+
const stoppedNotificationEntry = (
|
|
1090
|
+
agentId: string,
|
|
1091
|
+
timestamp: string,
|
|
1092
|
+
): object => ({
|
|
1093
|
+
type: 'queue-operation',
|
|
1094
|
+
operation: 'enqueue',
|
|
1095
|
+
timestamp,
|
|
1096
|
+
content: `<task-notification>\n<task-id>${agentId}</task-id>\n<status>stopped</status>\n</task-notification>`,
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
const livenessResolverWith = (
|
|
1100
|
+
liveIds: string[] | null,
|
|
1101
|
+
): SubAgentLivenessResolver => ({
|
|
1102
|
+
resolveLiveSubAgentIds: async () =>
|
|
1103
|
+
liveIds === null ? null : new Set(liveIds),
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1088
1106
|
it('excludes a sub-agent killed via TaskStop when the parent transcript records a killed notification for its id', async () => {
|
|
1089
1107
|
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
1090
1108
|
const killedAgentId = 'aaabbbbcccc00001';
|
|
@@ -1137,7 +1155,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
1137
1155
|
expect(result.size).toBe(0);
|
|
1138
1156
|
});
|
|
1139
1157
|
|
|
1140
|
-
it('
|
|
1158
|
+
it('excludes a sub-agent whose parent transcript records a completed notification for its id', async () => {
|
|
1141
1159
|
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
1142
1160
|
const startTimestamp = '2026-06-27T11:45:00.000Z';
|
|
1143
1161
|
const completedAgentId = 'aaabbbbcccc00003';
|
|
@@ -1161,14 +1179,71 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
1161
1179
|
transcriptMapFor([sessionName]),
|
|
1162
1180
|
);
|
|
1163
1181
|
|
|
1164
|
-
expect(result.
|
|
1182
|
+
expect(result.size).toBe(0);
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
it('excludes a sub-agent whose parent transcript records a stopped notification for its id', async () => {
|
|
1186
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
1187
|
+
const stoppedAgentId = 'aaabbbbcccc00006';
|
|
1188
|
+
writeAgentTranscript(
|
|
1189
|
+
sessionName,
|
|
1190
|
+
stoppedAgentId,
|
|
1191
|
+
pendingToolUseEntries('2026-06-27T11:00:00.000Z'),
|
|
1192
|
+
nowEpochSeconds - 300,
|
|
1193
|
+
);
|
|
1194
|
+
writeMainTranscript(sessionName, [
|
|
1195
|
+
stoppedNotificationEntry(stoppedAgentId, '2026-06-27T11:55:00.000Z'),
|
|
1196
|
+
]);
|
|
1197
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
1198
|
+
createResolver(),
|
|
1199
|
+
emptyProcessLister(),
|
|
1200
|
+
now,
|
|
1201
|
+
);
|
|
1202
|
+
|
|
1203
|
+
const result = await repository.listSubAgentActivitiesBySessionName(
|
|
1204
|
+
[sessionName],
|
|
1205
|
+
transcriptMapFor([sessionName]),
|
|
1206
|
+
);
|
|
1207
|
+
|
|
1208
|
+
expect(result.size).toBe(0);
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
it('excludes every task id listed in a single multi-id terminal notification', async () => {
|
|
1212
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
1213
|
+
const firstAgentId = 'aaabbbbcccc10001';
|
|
1214
|
+
const secondAgentId = 'aaabbbbcccc10002';
|
|
1215
|
+
writeAgentTranscript(
|
|
1216
|
+
sessionName,
|
|
1217
|
+
firstAgentId,
|
|
1218
|
+
pendingToolUseEntries('2026-06-27T11:00:00.000Z'),
|
|
1219
|
+
nowEpochSeconds - 300,
|
|
1220
|
+
);
|
|
1221
|
+
writeAgentTranscript(
|
|
1222
|
+
sessionName,
|
|
1223
|
+
secondAgentId,
|
|
1224
|
+
pendingToolUseEntries('2026-06-27T11:00:00.000Z'),
|
|
1225
|
+
nowEpochSeconds - 300,
|
|
1226
|
+
);
|
|
1227
|
+
writeMainTranscript(sessionName, [
|
|
1165
1228
|
{
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1229
|
+
type: 'queue-operation',
|
|
1230
|
+
operation: 'enqueue',
|
|
1231
|
+
timestamp: '2026-06-27T11:55:00.000Z',
|
|
1232
|
+
content: `<task-notification>\n<task-id>${firstAgentId}</task-id>\n<task-id>${secondAgentId}</task-id>\n<status>completed</status>\n</task-notification>`,
|
|
1170
1233
|
},
|
|
1171
1234
|
]);
|
|
1235
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
1236
|
+
createResolver(),
|
|
1237
|
+
emptyProcessLister(),
|
|
1238
|
+
now,
|
|
1239
|
+
);
|
|
1240
|
+
|
|
1241
|
+
const result = await repository.listSubAgentActivitiesBySessionName(
|
|
1242
|
+
[sessionName],
|
|
1243
|
+
transcriptMapFor([sessionName]),
|
|
1244
|
+
);
|
|
1245
|
+
|
|
1246
|
+
expect(result.size).toBe(0);
|
|
1172
1247
|
});
|
|
1173
1248
|
|
|
1174
1249
|
it('excludes a killed sub-agent but reports a still-active sibling in the same session', async () => {
|
|
@@ -1211,6 +1286,61 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
1211
1286
|
]);
|
|
1212
1287
|
});
|
|
1213
1288
|
|
|
1289
|
+
it('excludes a stale sub-agent whose tail is a plain tool result when no live process exists for its id', async () => {
|
|
1290
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
1291
|
+
const deadAgentId = 'aaabbbbcccc20001';
|
|
1292
|
+
writeAgentTranscript(
|
|
1293
|
+
sessionName,
|
|
1294
|
+
deadAgentId,
|
|
1295
|
+
inFlightToolResultTailEntries('2026-06-27T11:00:00.000Z'),
|
|
1296
|
+
nowEpochSeconds - 3600,
|
|
1297
|
+
);
|
|
1298
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
1299
|
+
createResolver(),
|
|
1300
|
+
emptyProcessLister(),
|
|
1301
|
+
now,
|
|
1302
|
+
livenessResolverWith(['aaabbbbcccc20002']),
|
|
1303
|
+
);
|
|
1304
|
+
|
|
1305
|
+
const result = await repository.listSubAgentActivitiesBySessionName(
|
|
1306
|
+
[sessionName],
|
|
1307
|
+
transcriptMapFor([sessionName]),
|
|
1308
|
+
);
|
|
1309
|
+
|
|
1310
|
+
expect(result.size).toBe(0);
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
it('flags a genuinely-running silent sub-agent whose id is present in the live process set', async () => {
|
|
1314
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
1315
|
+
const liveAgentId = 'aaabbbbcccc20003';
|
|
1316
|
+
writeAgentTranscript(
|
|
1317
|
+
sessionName,
|
|
1318
|
+
liveAgentId,
|
|
1319
|
+
pendingToolUseEntries('2026-06-27T11:45:00.000Z'),
|
|
1320
|
+
nowEpochSeconds - 600,
|
|
1321
|
+
);
|
|
1322
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
1323
|
+
createResolver(),
|
|
1324
|
+
emptyProcessLister(),
|
|
1325
|
+
now,
|
|
1326
|
+
livenessResolverWith([liveAgentId]),
|
|
1327
|
+
);
|
|
1328
|
+
|
|
1329
|
+
const result = await repository.listSubAgentActivitiesBySessionName(
|
|
1330
|
+
[sessionName],
|
|
1331
|
+
transcriptMapFor([sessionName]),
|
|
1332
|
+
);
|
|
1333
|
+
|
|
1334
|
+
expect(result.get(sessionName)).toEqual([
|
|
1335
|
+
{
|
|
1336
|
+
label: `agent-${liveAgentId}`,
|
|
1337
|
+
silentSeconds: 600,
|
|
1338
|
+
runningSeconds: 900,
|
|
1339
|
+
waitingOnExternalProcess: false,
|
|
1340
|
+
},
|
|
1341
|
+
]);
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1214
1344
|
it('reports a sub-agent even when the parent transcript path does not exist', async () => {
|
|
1215
1345
|
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
1216
1346
|
const startTimestamp = '2026-06-27T11:45:00.000Z';
|
|
@@ -2,9 +2,14 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { SubAgentActivity } from '../../domain/entities/LiveSessionActivitySnapshot';
|
|
4
4
|
import { SessionSubAgentActivityRepository } from '../../domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository';
|
|
5
|
+
import { SubAgentLivenessResolver } from '../../domain/usecases/adapter-interfaces/SubAgentLivenessResolver';
|
|
5
6
|
import { SubAgentProcessLister } from '../../domain/usecases/adapter-interfaces/SubAgentProcessLister';
|
|
6
7
|
import { SubAgentTranscriptDirectoryResolver } from '../../domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver';
|
|
7
8
|
|
|
9
|
+
const alwaysIndeterminateLivenessResolver: SubAgentLivenessResolver = {
|
|
10
|
+
resolveLiveSubAgentIds: async () => null,
|
|
11
|
+
};
|
|
12
|
+
|
|
8
13
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
9
14
|
typeof value === 'object' && value !== null;
|
|
10
15
|
|
|
@@ -190,7 +195,14 @@ const parseTranscript = (content: string): ParsedTranscript => {
|
|
|
190
195
|
|
|
191
196
|
const clampToZero = (value: number): number => (value > 0 ? value : 0);
|
|
192
197
|
|
|
193
|
-
const
|
|
198
|
+
const TERMINAL_TASK_NOTIFICATION_STATUSES = new Set([
|
|
199
|
+
'completed',
|
|
200
|
+
'killed',
|
|
201
|
+
'failed',
|
|
202
|
+
'stopped',
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
const parseTerminalAgentIds = (content: string): Set<string> => {
|
|
194
206
|
const result = new Set<string>();
|
|
195
207
|
for (const line of content.split('\n')) {
|
|
196
208
|
const trimmed = line.trim();
|
|
@@ -216,13 +228,16 @@ const parseKilledOrFailedAgentIds = (content: string): Set<string> => {
|
|
|
216
228
|
if (notifContent === null) {
|
|
217
229
|
continue;
|
|
218
230
|
}
|
|
219
|
-
const taskIdMatch = notifContent.match(/<task-id>(a[0-9a-f]+)<\/task-id>/);
|
|
220
231
|
const statusMatch = notifContent.match(/<status>([^<]+)<\/status>/);
|
|
221
|
-
if (
|
|
232
|
+
if (statusMatch === null) {
|
|
222
233
|
continue;
|
|
223
234
|
}
|
|
224
|
-
|
|
225
|
-
|
|
235
|
+
if (!TERMINAL_TASK_NOTIFICATION_STATUSES.has(statusMatch[1])) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
for (const taskIdMatch of notifContent.matchAll(
|
|
239
|
+
/<task-id>(a[0-9a-f]+)<\/task-id>/g,
|
|
240
|
+
)) {
|
|
226
241
|
result.add(taskIdMatch[1]);
|
|
227
242
|
}
|
|
228
243
|
}
|
|
@@ -234,6 +249,7 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
234
249
|
private readonly directoryResolver: SubAgentTranscriptDirectoryResolver,
|
|
235
250
|
private readonly processLister: SubAgentProcessLister,
|
|
236
251
|
private readonly now: Date,
|
|
252
|
+
private readonly livenessResolver: SubAgentLivenessResolver = alwaysIndeterminateLivenessResolver,
|
|
237
253
|
) {}
|
|
238
254
|
|
|
239
255
|
listSubAgentActivitiesBySessionName = async (
|
|
@@ -262,12 +278,17 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
262
278
|
if (directory === null) {
|
|
263
279
|
continue;
|
|
264
280
|
}
|
|
265
|
-
const
|
|
266
|
-
|
|
281
|
+
const terminalAgentIds = this.loadTerminalAgentIds(mainTranscriptPath);
|
|
282
|
+
const liveSubAgentIds =
|
|
283
|
+
await this.livenessResolver.resolveLiveSubAgentIds({
|
|
284
|
+
sessionName,
|
|
285
|
+
mainTranscriptPath,
|
|
286
|
+
});
|
|
267
287
|
const activities = await this.collectActivities(
|
|
268
288
|
directory,
|
|
269
289
|
nowEpochSeconds,
|
|
270
|
-
|
|
290
|
+
terminalAgentIds,
|
|
291
|
+
liveSubAgentIds,
|
|
271
292
|
loadNormalizedProcessCommandLines,
|
|
272
293
|
);
|
|
273
294
|
if (activities.length > 0) {
|
|
@@ -277,7 +298,7 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
277
298
|
return result;
|
|
278
299
|
};
|
|
279
300
|
|
|
280
|
-
private
|
|
301
|
+
private loadTerminalAgentIds = (
|
|
281
302
|
mainTranscriptPath: string | null,
|
|
282
303
|
): Set<string> => {
|
|
283
304
|
if (mainTranscriptPath === null) {
|
|
@@ -289,13 +310,14 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
289
310
|
} catch {
|
|
290
311
|
return new Set();
|
|
291
312
|
}
|
|
292
|
-
return
|
|
313
|
+
return parseTerminalAgentIds(content);
|
|
293
314
|
};
|
|
294
315
|
|
|
295
316
|
private collectActivities = async (
|
|
296
317
|
directory: string,
|
|
297
318
|
nowEpochSeconds: number,
|
|
298
|
-
|
|
319
|
+
terminalAgentIds: Set<string>,
|
|
320
|
+
liveSubAgentIds: Set<string> | null,
|
|
299
321
|
loadNormalizedProcessCommandLines: () => Promise<string[]>,
|
|
300
322
|
): Promise<SubAgentActivity[]> => {
|
|
301
323
|
let entries: fs.Dirent[];
|
|
@@ -311,7 +333,10 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
311
333
|
continue;
|
|
312
334
|
}
|
|
313
335
|
const agentId = fileName.slice('agent-'.length, -'.jsonl'.length);
|
|
314
|
-
if (
|
|
336
|
+
if (terminalAgentIds.has(agentId)) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (liveSubAgentIds !== null && !liveSubAgentIds.has(agentId)) {
|
|
315
340
|
continue;
|
|
316
341
|
}
|
|
317
342
|
const filePath = path.join(directory, fileName);
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
7
7
|
import { LocalCommandRunner } from './adapter-interfaces/LocalCommandRunner';
|
|
8
8
|
import { ClaudeTokenUsageRepository } from './adapter-interfaces/ClaudeTokenUsageRepository';
|
|
9
|
+
import { TakeOwnershipSpawnRepository } from './adapter-interfaces/TakeOwnershipSpawnRepository';
|
|
9
10
|
import { ClaudeTokenUsage } from '../entities/ClaudeTokenUsage';
|
|
10
11
|
import { Issue } from '../entities/Issue';
|
|
11
12
|
import { Project } from '../entities/Project';
|
|
@@ -95,6 +96,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
95
96
|
>;
|
|
96
97
|
let mockLocalCommandRunner: Mocked<LocalCommandRunner>;
|
|
97
98
|
let mockClaudeTokenUsageRepository: Mocked<ClaudeTokenUsageRepository>;
|
|
99
|
+
let mockTakeOwnershipSpawnRepository: Mocked<TakeOwnershipSpawnRepository>;
|
|
98
100
|
let mockProject: Project;
|
|
99
101
|
beforeEach(() => {
|
|
100
102
|
jest.resetAllMocks();
|
|
@@ -121,11 +123,16 @@ describe('StartPreparationUseCase', () => {
|
|
|
121
123
|
getTokenInFlightCounts: jest.fn().mockResolvedValue({}),
|
|
122
124
|
proxyBaseUrl: jest.fn().mockReturnValue('http://127.0.0.1:8787'),
|
|
123
125
|
};
|
|
126
|
+
mockTakeOwnershipSpawnRepository = {
|
|
127
|
+
listSpawns: jest.fn().mockReturnValue([]),
|
|
128
|
+
listRunningIssueUrls: jest.fn().mockReturnValue([]),
|
|
129
|
+
};
|
|
124
130
|
useCase = new StartPreparationUseCase(
|
|
125
131
|
mockProjectRepository,
|
|
126
132
|
mockIssueRepository,
|
|
127
133
|
mockLocalCommandRunner,
|
|
128
134
|
mockClaudeTokenUsageRepository,
|
|
135
|
+
mockTakeOwnershipSpawnRepository,
|
|
129
136
|
);
|
|
130
137
|
});
|
|
131
138
|
it('should run aw command for awaiting workspace issues', async () => {
|
|
@@ -181,6 +188,45 @@ describe('StartPreparationUseCase', () => {
|
|
|
181
188
|
],
|
|
182
189
|
]);
|
|
183
190
|
});
|
|
191
|
+
|
|
192
|
+
it('skips an issue whose URL is already in the running worker list', async () => {
|
|
193
|
+
const runningUrl = 'https://github.com/user/repo/issues/1';
|
|
194
|
+
const awaitingIssues: Issue[] = [
|
|
195
|
+
createMockIssue({
|
|
196
|
+
url: runningUrl,
|
|
197
|
+
title: 'Issue 1',
|
|
198
|
+
labels: ['category:impl'],
|
|
199
|
+
status: 'Awaiting Workspace',
|
|
200
|
+
author: 'testuser',
|
|
201
|
+
}),
|
|
202
|
+
];
|
|
203
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
204
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
205
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
206
|
+
);
|
|
207
|
+
mockTakeOwnershipSpawnRepository.listRunningIssueUrls.mockReturnValue([
|
|
208
|
+
runningUrl,
|
|
209
|
+
]);
|
|
210
|
+
|
|
211
|
+
await useCase.run({
|
|
212
|
+
projectUrl: 'https://github.com/user/repo',
|
|
213
|
+
defaultAgentName: 'agent1',
|
|
214
|
+
defaultLlmModelName: 'claude-opus',
|
|
215
|
+
fallbackLlmModelName: null,
|
|
216
|
+
defaultLlmAgentName: null,
|
|
217
|
+
configFilePath: '/path/to/config.yml',
|
|
218
|
+
maximumPreparingIssuesCount: null,
|
|
219
|
+
utilizationPercentageThreshold: 90,
|
|
220
|
+
allowedIssueAuthors: ['testuser'],
|
|
221
|
+
manager: 'manager-user',
|
|
222
|
+
codexHomeCandidates: null,
|
|
223
|
+
labelsAsLlmAgentName: null,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
227
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(0);
|
|
228
|
+
});
|
|
229
|
+
|
|
184
230
|
it('should pass --branch to aw command when issue has an existing linked PR', async () => {
|
|
185
231
|
const awaitingIssues: Issue[] = [
|
|
186
232
|
createMockIssue({
|
|
@@ -6275,6 +6321,10 @@ describe('StartPreparationUseCase.buildRotationOrder', () => {
|
|
|
6275
6321
|
mockIssueRepositoryForRotation,
|
|
6276
6322
|
mockLocalCommandRunnerForRotation,
|
|
6277
6323
|
mockClaudeTokenUsageRepositoryForRotation,
|
|
6324
|
+
{
|
|
6325
|
+
listSpawns: jest.fn().mockReturnValue([]),
|
|
6326
|
+
listRunningIssueUrls: jest.fn().mockReturnValue([]),
|
|
6327
|
+
},
|
|
6278
6328
|
);
|
|
6279
6329
|
|
|
6280
6330
|
it('lists selected tokens first in ascending 7-day reset deadline order then excluded tokens', () => {
|
|
@@ -6564,6 +6614,10 @@ describe('StartPreparationUseCase.getTokenConcurrentLimit', () => {
|
|
|
6564
6614
|
getTokenInFlightCounts: jest.fn(),
|
|
6565
6615
|
proxyBaseUrl: jest.fn(),
|
|
6566
6616
|
},
|
|
6617
|
+
{
|
|
6618
|
+
listSpawns: jest.fn().mockReturnValue([]),
|
|
6619
|
+
listRunningIssueUrls: jest.fn().mockReturnValue([]),
|
|
6620
|
+
},
|
|
6567
6621
|
);
|
|
6568
6622
|
});
|
|
6569
6623
|
|
|
@@ -2,6 +2,7 @@ import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
|
2
2
|
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
3
3
|
import { LocalCommandRunner } from './adapter-interfaces/LocalCommandRunner';
|
|
4
4
|
import { ClaudeTokenUsageRepository } from './adapter-interfaces/ClaudeTokenUsageRepository';
|
|
5
|
+
import { TakeOwnershipSpawnRepository } from './adapter-interfaces/TakeOwnershipSpawnRepository';
|
|
5
6
|
import { ClaudeTokenUsage } from '../entities/ClaudeTokenUsage';
|
|
6
7
|
import {
|
|
7
8
|
AWAITING_WORKSPACE_STATUS_NAME,
|
|
@@ -38,6 +39,7 @@ export class StartPreparationUseCase {
|
|
|
38
39
|
>,
|
|
39
40
|
private readonly localCommandRunner: LocalCommandRunner,
|
|
40
41
|
private readonly claudeTokenUsageRepository: ClaudeTokenUsageRepository,
|
|
42
|
+
private readonly takeOwnershipSpawnRepository: TakeOwnershipSpawnRepository,
|
|
41
43
|
) {}
|
|
42
44
|
|
|
43
45
|
private weeklyLimitTypeForModel = (modelName: string | null): string => {
|
|
@@ -369,6 +371,9 @@ export class StartPreparationUseCase {
|
|
|
369
371
|
return { rotationOrder };
|
|
370
372
|
}
|
|
371
373
|
|
|
374
|
+
const runningIssueUrls = new Set(
|
|
375
|
+
this.takeOwnershipSpawnRepository.listRunningIssueUrls(),
|
|
376
|
+
);
|
|
372
377
|
const awaitingWorkspaceIssues = allOpenedIssues
|
|
373
378
|
.filter(
|
|
374
379
|
(issue) =>
|
|
@@ -428,6 +433,10 @@ export class StartPreparationUseCase {
|
|
|
428
433
|
exclusionCounts.dependedIssueUrls++;
|
|
429
434
|
continue;
|
|
430
435
|
}
|
|
436
|
+
if (runningIssueUrls.has(issue.url)) {
|
|
437
|
+
console.warn(`Skipping ${issue.url}: worker already running.`);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
431
440
|
if (
|
|
432
441
|
issue.nextActionDate !== null &&
|
|
433
442
|
issue.nextActionDate >= tomorrowStart
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/cli/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EACL,UAAU,EACV,cAAc,EACd,wBAAwB,EACxB,YAAY,EACZ,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/cli/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EACL,UAAU,EACV,cAAc,EACd,wBAAwB,EACxB,YAAY,EACZ,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AAuJzB,eAAO,MAAM,OAAO,SAAgB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HandleScheduledEventUseCaseHandler.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"HandleScheduledEventUseCaseHandler.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts"],"names":[],"mappings":"AAoCA,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAkD3D,qBAAa,kCAAkC;IAC7C,MAAM,GACJ,gBAAgB,MAAM,EACtB,UAAU,OAAO,EACjB,6BAA4B,MAAM,EAAE,GAAG,IAAW,KACjD,OAAO,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,EAAE,KAAK,EAAE,CAAC;QAChB,SAAS,EAAE,OAAO,CAAC;QACnB,eAAe,EAAE,IAAI,EAAE,CAAC;KACzB,GAAG,IAAI,CAAC,CAkmBP;CACH"}
|
|
@@ -10,6 +10,7 @@ export type NotifySilentTmuxSessionsParams = {
|
|
|
10
10
|
subAgentOutputRootDirectory: string | null;
|
|
11
11
|
subAgentProcessMatchPattern: string | null;
|
|
12
12
|
subAgentTranscriptRootDirectory: string | null;
|
|
13
|
+
subAgentRuntimeRootDirectory: string | null;
|
|
13
14
|
mainSilentThresholdSeconds: number;
|
|
14
15
|
unansweredOwnerCallGraceSeconds: number;
|
|
15
16
|
subAgentSilentThresholdSeconds: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notifySilentTmuxSessions.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gEAAgE,CAAC;AAGpG,OAAO,EAAE,oBAAoB,EAAE,MAAM,kEAAkE,CAAC;AACxG,OAAO,EAEL,qBAAqB,EAQtB,MAAM,0DAA0D,CAAC;
|
|
1
|
+
{"version":3,"file":"notifySilentTmuxSessions.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gEAAgE,CAAC;AAGpG,OAAO,EAAE,oBAAoB,EAAE,MAAM,kEAAkE,CAAC;AACxG,OAAO,EAEL,qBAAqB,EAQtB,MAAM,0DAA0D,CAAC;AAgBlE,OAAO,EAEL,6BAA6B,EAC9B,MAAM,6DAA6D,CAAC;AAKrE,MAAM,MAAM,8BAA8B,GAAG;IAC3C,OAAO,EAAE,OAAO,CAAC;IACjB,kBAAkB,EAAE,kBAAkB,CAAC;IACvC,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,2BAA2B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,2BAA2B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,+BAA+B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,4BAA4B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,0BAA0B,EAAE,MAAM,CAAC;IACnC,+BAA+B,EAAE,MAAM,CAAC;IACxC,8BAA8B,EAAE,MAAM,CAAC;IACvC,+BAA+B,EAAE,MAAM,CAAC;IACxC,cAAc,EAAE,MAAM,CAAC;IACvB,qCAAqC,EAAE,MAAM,CAAC;IAC9C,8BAA8B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9C,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,qBAAqB,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACpD,+BAA+B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,4BAA4B,EAAE,MAAM,CAAC;IACrC,gBAAgB,EAAE,6BAA6B,CAAC;IAChD,GAAG,EAAE,IAAI,CAAC;CACX,CAAC;AAuCF,eAAO,MAAM,wBAAwB,GACnC,QAAQ,8BAA8B,KACrC,OAAO,CAAC,IAAI,CA6Ed,CAAC;AAEF,eAAO,MAAM,0CAA0C;;;;;;;;CAS7C,CAAC"}
|