github-issue-tower-defence-management 1.138.0 → 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 +7 -0
- package/README.md +3 -2
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +15 -2
- 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/TranscriptSessionSubAgentActivityRepository.js +30 -12
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.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/handlers/HandleScheduledEventUseCaseHandler.ts +11 -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/repositories/FileSystemSubAgentLivenessResolver.test.ts +87 -0
- package/src/adapter/repositories/FileSystemSubAgentLivenessResolver.ts +57 -0
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.test.ts +136 -6
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts +37 -12
- package/src/domain/usecases/adapter-interfaces/SubAgentLivenessResolver.ts +6 -0
- 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/TranscriptSessionSubAgentActivityRepository.d.ts +4 -2
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import YAML from 'yaml';
|
|
2
2
|
import TYPIA from 'typia';
|
|
3
3
|
import fs from 'fs';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import path from 'path';
|
|
4
6
|
import { writeSituationFile } from './situationFileWriter';
|
|
5
7
|
import { writeConsoleLists } from './consoleListsWriter';
|
|
6
8
|
import { writeDashboardRow } from './dashboardRowWriter';
|
|
@@ -118,6 +120,7 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
118
120
|
subAgentOutputRootDirectory?: string;
|
|
119
121
|
subAgentProcessMatchPattern?: string;
|
|
120
122
|
subAgentTranscriptRootDirectory?: string;
|
|
123
|
+
subAgentRuntimeRootDirectory?: string;
|
|
121
124
|
mainSilentThresholdSeconds?: number;
|
|
122
125
|
unansweredOwnerCallGraceSeconds?: number;
|
|
123
126
|
subAgentSilentThresholdSeconds?: number;
|
|
@@ -602,6 +605,13 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
602
605
|
mergedInput.subAgentTranscriptRootDirectory ??
|
|
603
606
|
process.env.TDPM_SUBAGENT_TRANSCRIPT_ROOT_DIRECTORY ??
|
|
604
607
|
null;
|
|
608
|
+
const getuid = process.getuid?.bind(process);
|
|
609
|
+
const subAgentRuntimeRootDirectory =
|
|
610
|
+
mergedInput.subAgentRuntimeRootDirectory ??
|
|
611
|
+
process.env.TDPM_SUBAGENT_RUNTIME_ROOT_DIRECTORY ??
|
|
612
|
+
(getuid === undefined
|
|
613
|
+
? null
|
|
614
|
+
: path.join(os.tmpdir(), `claude-${getuid()}`));
|
|
605
615
|
await notifySilentTmuxSessions({
|
|
606
616
|
enabled: silentNotificationEnabled,
|
|
607
617
|
localCommandRunner: nodeLocalCommandRunner,
|
|
@@ -609,6 +619,7 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
609
619
|
subAgentOutputRootDirectory,
|
|
610
620
|
subAgentProcessMatchPattern,
|
|
611
621
|
subAgentTranscriptRootDirectory,
|
|
622
|
+
subAgentRuntimeRootDirectory,
|
|
612
623
|
mainSilentThresholdSeconds: readSilentSeconds(
|
|
613
624
|
mergedInput.mainSilentThresholdSeconds,
|
|
614
625
|
process.env.TDPM_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
@@ -139,6 +139,7 @@ describe('notifySilentTmuxSessions', () => {
|
|
|
139
139
|
subAgentOutputRootDirectory: null,
|
|
140
140
|
subAgentProcessMatchPattern: null,
|
|
141
141
|
subAgentTranscriptRootDirectory: null,
|
|
142
|
+
subAgentRuntimeRootDirectory: null,
|
|
142
143
|
candidateDebounceStateFilePath: candidateStateFilePath,
|
|
143
144
|
activeHubTaskStatus: null,
|
|
144
145
|
hubTaskStatusResolver: null,
|
|
@@ -25,6 +25,7 @@ import { TranscriptRefusalTailStatusProvider } from '../../repositories/Transcri
|
|
|
25
25
|
import { ProcessListSessionSubAgentActivityRepository } from '../../repositories/ProcessListSessionSubAgentActivityRepository';
|
|
26
26
|
import { TranscriptSessionSubAgentActivityRepository } from '../../repositories/TranscriptSessionSubAgentActivityRepository';
|
|
27
27
|
import { FileSystemSubAgentTranscriptDirectoryResolver } from '../../repositories/FileSystemSubAgentTranscriptDirectoryResolver';
|
|
28
|
+
import { FileSystemSubAgentLivenessResolver } from '../../repositories/FileSystemSubAgentLivenessResolver';
|
|
28
29
|
import { NodeSubAgentProcessLister } from '../../repositories/NodeSubAgentProcessLister';
|
|
29
30
|
import { FileSystemSubAgentSilentSecondsResolver } from '../../repositories/FileSystemSubAgentSilentSecondsResolver';
|
|
30
31
|
import {
|
|
@@ -43,6 +44,7 @@ export type NotifySilentTmuxSessionsParams = {
|
|
|
43
44
|
subAgentOutputRootDirectory: string | null;
|
|
44
45
|
subAgentProcessMatchPattern: string | null;
|
|
45
46
|
subAgentTranscriptRootDirectory: string | null;
|
|
47
|
+
subAgentRuntimeRootDirectory: string | null;
|
|
46
48
|
mainSilentThresholdSeconds: number;
|
|
47
49
|
unansweredOwnerCallGraceSeconds: number;
|
|
48
50
|
subAgentSilentThresholdSeconds: number;
|
|
@@ -69,6 +71,7 @@ const createOwnerCallStatusProvider = (
|
|
|
69
71
|
|
|
70
72
|
const createSubAgentActivityRepository = (
|
|
71
73
|
subAgentTranscriptRootDirectory: string | null,
|
|
74
|
+
subAgentRuntimeRootDirectory: string | null,
|
|
72
75
|
subAgentProcessMatchPattern: string | null,
|
|
73
76
|
subAgentOutputRootDirectory: string | null,
|
|
74
77
|
localCommandRunner: LocalCommandRunner,
|
|
@@ -81,6 +84,7 @@ const createSubAgentActivityRepository = (
|
|
|
81
84
|
),
|
|
82
85
|
new NodeSubAgentProcessLister(localCommandRunner),
|
|
83
86
|
now,
|
|
87
|
+
new FileSystemSubAgentLivenessResolver(subAgentRuntimeRootDirectory),
|
|
84
88
|
);
|
|
85
89
|
}
|
|
86
90
|
return new ProcessListSessionSubAgentActivityRepository(
|
|
@@ -104,6 +108,7 @@ export const notifySilentTmuxSessions = async (
|
|
|
104
108
|
subAgentOutputRootDirectory,
|
|
105
109
|
subAgentProcessMatchPattern,
|
|
106
110
|
subAgentTranscriptRootDirectory,
|
|
111
|
+
subAgentRuntimeRootDirectory,
|
|
107
112
|
mainSilentThresholdSeconds,
|
|
108
113
|
unansweredOwnerCallGraceSeconds,
|
|
109
114
|
subAgentSilentThresholdSeconds,
|
|
@@ -137,6 +142,7 @@ export const notifySilentTmuxSessions = async (
|
|
|
137
142
|
new FileSystemSessionOutputActivityRepository(),
|
|
138
143
|
createSubAgentActivityRepository(
|
|
139
144
|
subAgentTranscriptRootDirectory,
|
|
145
|
+
subAgentRuntimeRootDirectory,
|
|
140
146
|
subAgentProcessMatchPattern,
|
|
141
147
|
subAgentOutputRootDirectory,
|
|
142
148
|
localCommandRunner,
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { FileSystemSubAgentLivenessResolver } from './FileSystemSubAgentLivenessResolver';
|
|
5
|
+
|
|
6
|
+
describe('FileSystemSubAgentLivenessResolver', () => {
|
|
7
|
+
let runtimeRoot: string;
|
|
8
|
+
const cwdSlug = '-home-user-worktrees-issue-9';
|
|
9
|
+
const sessionUuid = 'ba0637e1-9ff1-41a8-b13c-f45e6a71efc5';
|
|
10
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
11
|
+
const mainTranscriptPath = path.join(
|
|
12
|
+
'/home/user/.config/projects',
|
|
13
|
+
cwdSlug,
|
|
14
|
+
`${sessionUuid}.jsonl`,
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
runtimeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'subagent-live-'));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
fs.rmSync(runtimeRoot, { force: true, recursive: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const writeRunningFile = (contents: string): void => {
|
|
26
|
+
const dir = path.join(runtimeRoot, cwdSlug, sessionUuid);
|
|
27
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
28
|
+
fs.writeFileSync(path.join(dir, 'running-subagents.txt'), contents, 'utf8');
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
it('returns the trimmed non-empty ids listed in running-subagents.txt', async () => {
|
|
32
|
+
writeRunningFile('a70a08e0587d6f484\n\n ad5d15d239d7e72e8 \n');
|
|
33
|
+
const resolver = new FileSystemSubAgentLivenessResolver(runtimeRoot);
|
|
34
|
+
|
|
35
|
+
const result = await resolver.resolveLiveSubAgentIds({
|
|
36
|
+
sessionName,
|
|
37
|
+
mainTranscriptPath,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(result).toEqual(new Set(['a70a08e0587d6f484', 'ad5d15d239d7e72e8']));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('returns an empty set when the running file exists but lists no ids', async () => {
|
|
44
|
+
writeRunningFile('\n \n');
|
|
45
|
+
const resolver = new FileSystemSubAgentLivenessResolver(runtimeRoot);
|
|
46
|
+
|
|
47
|
+
const result = await resolver.resolveLiveSubAgentIds({
|
|
48
|
+
sessionName,
|
|
49
|
+
mainTranscriptPath,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(result).toEqual(new Set());
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('returns null when the running file does not exist', async () => {
|
|
56
|
+
const resolver = new FileSystemSubAgentLivenessResolver(runtimeRoot);
|
|
57
|
+
|
|
58
|
+
const result = await resolver.resolveLiveSubAgentIds({
|
|
59
|
+
sessionName,
|
|
60
|
+
mainTranscriptPath,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(result).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('returns null when the runtime root directory is null', async () => {
|
|
67
|
+
const resolver = new FileSystemSubAgentLivenessResolver(null);
|
|
68
|
+
|
|
69
|
+
const result = await resolver.resolveLiveSubAgentIds({
|
|
70
|
+
sessionName,
|
|
71
|
+
mainTranscriptPath,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(result).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('returns null when the main transcript path is null', async () => {
|
|
78
|
+
const resolver = new FileSystemSubAgentLivenessResolver(runtimeRoot);
|
|
79
|
+
|
|
80
|
+
const result = await resolver.resolveLiveSubAgentIds({
|
|
81
|
+
sessionName,
|
|
82
|
+
mainTranscriptPath: null,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(result).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -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
|
+
}
|
|
@@ -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);
|
|
@@ -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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { SubAgentLivenessResolver } from '../../domain/usecases/adapter-interfaces/SubAgentLivenessResolver';
|
|
2
|
+
export declare class FileSystemSubAgentLivenessResolver implements SubAgentLivenessResolver {
|
|
3
|
+
private readonly runtimeRootDirectory;
|
|
4
|
+
constructor(runtimeRootDirectory: string | null);
|
|
5
|
+
resolveLiveSubAgentIds: (params: {
|
|
6
|
+
sessionName: string;
|
|
7
|
+
mainTranscriptPath: string | null;
|
|
8
|
+
}) => Promise<Set<string> | null>;
|
|
9
|
+
private resolveRunningSubAgentsFilePath;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=FileSystemSubAgentLivenessResolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileSystemSubAgentLivenessResolver.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/FileSystemSubAgentLivenessResolver.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,wBAAwB,EAAE,MAAM,mEAAmE,CAAC;AAI7G,qBAAa,kCAAmC,YAAW,wBAAwB;IACrE,OAAO,CAAC,QAAQ,CAAC,oBAAoB;gBAApB,oBAAoB,EAAE,MAAM,GAAG,IAAI;IAEhE,sBAAsB,GAAU,QAAQ;QACtC,WAAW,EAAE,MAAM,CAAC;QACpB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;KACnC,KAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAqB7B;IAEF,OAAO,CAAC,+BAA+B,CAoBrC;CACH"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SubAgentActivity } from '../../domain/entities/LiveSessionActivitySnapshot';
|
|
2
2
|
import { SessionSubAgentActivityRepository } from '../../domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository';
|
|
3
|
+
import { SubAgentLivenessResolver } from '../../domain/usecases/adapter-interfaces/SubAgentLivenessResolver';
|
|
3
4
|
import { SubAgentProcessLister } from '../../domain/usecases/adapter-interfaces/SubAgentProcessLister';
|
|
4
5
|
import { SubAgentTranscriptDirectoryResolver } from '../../domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver';
|
|
5
6
|
export declare const normalizeCommandFragment: (command: string) => string;
|
|
@@ -7,9 +8,10 @@ export declare class TranscriptSessionSubAgentActivityRepository implements Sess
|
|
|
7
8
|
private readonly directoryResolver;
|
|
8
9
|
private readonly processLister;
|
|
9
10
|
private readonly now;
|
|
10
|
-
|
|
11
|
+
private readonly livenessResolver;
|
|
12
|
+
constructor(directoryResolver: SubAgentTranscriptDirectoryResolver, processLister: SubAgentProcessLister, now: Date, livenessResolver?: SubAgentLivenessResolver);
|
|
11
13
|
listSubAgentActivitiesBySessionName: (sessionNames: string[], transcriptPathBySessionName: Map<string, string>) => Promise<Map<string, SubAgentActivity[]>>;
|
|
12
|
-
private
|
|
14
|
+
private loadTerminalAgentIds;
|
|
13
15
|
private collectActivities;
|
|
14
16
|
private toActivity;
|
|
15
17
|
private hasLiveMatchingProcess;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TranscriptSessionSubAgentActivityRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mDAAmD,CAAC;AACrF,OAAO,EAAE,iCAAiC,EAAE,MAAM,4EAA4E,CAAC;AAC/H,OAAO,EAAE,qBAAqB,EAAE,MAAM,gEAAgE,CAAC;AACvG,OAAO,EAAE,mCAAmC,EAAE,MAAM,8EAA8E,CAAC;
|
|
1
|
+
{"version":3,"file":"TranscriptSessionSubAgentActivityRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mDAAmD,CAAC;AACrF,OAAO,EAAE,iCAAiC,EAAE,MAAM,4EAA4E,CAAC;AAC/H,OAAO,EAAE,wBAAwB,EAAE,MAAM,mEAAmE,CAAC;AAC7G,OAAO,EAAE,qBAAqB,EAAE,MAAM,gEAAgE,CAAC;AACvG,OAAO,EAAE,mCAAmC,EAAE,MAAM,8EAA8E,CAAC;AA2BnI,eAAO,MAAM,wBAAwB,GAAI,SAAS,MAAM,KAAG,MAIR,CAAC;AAiNpD,qBAAa,2CAA4C,YAAW,iCAAiC;IAEjG,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,gBAAgB;gBAHhB,iBAAiB,EAAE,mCAAmC,EACtD,aAAa,EAAE,qBAAqB,EACpC,GAAG,EAAE,IAAI,EACT,gBAAgB,GAAE,wBAA8D;IAGnG,mCAAmC,GACjC,cAAc,MAAM,EAAE,EACtB,6BAA6B,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,KAC/C,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC,CAyCzC;IAEF,OAAO,CAAC,oBAAoB,CAa1B;IAEF,OAAO,CAAC,iBAAiB,CAsCvB;IAEF,OAAO,CAAC,UAAU,CAoChB;IAEF,OAAO,CAAC,sBAAsB,CAiB5B;CACH"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SubAgentLivenessResolver.d.ts","sourceRoot":"","sources":["../../../../src/domain/usecases/adapter-interfaces/SubAgentLivenessResolver.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,wBAAwB;IACvC,sBAAsB,EAAE,CAAC,MAAM,EAAE;QAC/B,WAAW,EAAE,MAAM,CAAC;QACpB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;KACnC,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;CACnC"}
|