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.
Files changed (30) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +3 -2
  3. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +15 -2
  4. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  5. package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +5 -4
  6. package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -1
  7. package/bin/adapter/repositories/FileSystemSubAgentLivenessResolver.js +81 -0
  8. package/bin/adapter/repositories/FileSystemSubAgentLivenessResolver.js.map +1 -0
  9. package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js +30 -12
  10. package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js.map +1 -1
  11. package/bin/domain/usecases/adapter-interfaces/SubAgentLivenessResolver.js +3 -0
  12. package/bin/domain/usecases/adapter-interfaces/SubAgentLivenessResolver.js.map +1 -0
  13. package/package.json +1 -1
  14. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +11 -0
  15. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +1 -0
  16. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +6 -0
  17. package/src/adapter/repositories/FileSystemSubAgentLivenessResolver.test.ts +87 -0
  18. package/src/adapter/repositories/FileSystemSubAgentLivenessResolver.ts +57 -0
  19. package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.test.ts +136 -6
  20. package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts +37 -12
  21. package/src/domain/usecases/adapter-interfaces/SubAgentLivenessResolver.ts +6 -0
  22. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  23. package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts +1 -0
  24. package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -1
  25. package/types/adapter/repositories/FileSystemSubAgentLivenessResolver.d.ts +11 -0
  26. package/types/adapter/repositories/FileSystemSubAgentLivenessResolver.d.ts.map +1 -0
  27. package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts +4 -2
  28. package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts.map +1 -1
  29. package/types/domain/usecases/adapter-interfaces/SubAgentLivenessResolver.d.ts +7 -0
  30. 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('does not exclude a sub-agent whose parent transcript records only a completed notification for its id', async () => {
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.get(sessionName)).toEqual([
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
- label: `agent-${completedAgentId}`,
1167
- silentSeconds: 120,
1168
- runningSeconds: 900,
1169
- waitingOnExternalProcess: false,
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 parseKilledOrFailedAgentIds = (content: string): Set<string> => {
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 (!taskIdMatch || !statusMatch) {
232
+ if (statusMatch === null) {
222
233
  continue;
223
234
  }
224
- const status = statusMatch[1];
225
- if (status === 'killed' || status === 'failed') {
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 killedOrFailedAgentIds =
266
- this.loadKilledOrFailedAgentIds(mainTranscriptPath);
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
- killedOrFailedAgentIds,
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 loadKilledOrFailedAgentIds = (
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 parseKilledOrFailedAgentIds(content);
313
+ return parseTerminalAgentIds(content);
293
314
  };
294
315
 
295
316
  private collectActivities = async (
296
317
  directory: string,
297
318
  nowEpochSeconds: number,
298
- killedOrFailedAgentIds: Set<string>,
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 (killedOrFailedAgentIds.has(agentId)) {
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);
@@ -0,0 +1,6 @@
1
+ export interface SubAgentLivenessResolver {
2
+ resolveLiveSubAgentIds: (params: {
3
+ sessionName: string;
4
+ mainTranscriptPath: string | null;
5
+ }) => Promise<Set<string> | null>;
6
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"HandleScheduledEventUseCaseHandler.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts"],"names":[],"mappings":"AAkCA,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,CAylBP;CACH"}
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;AAelE,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,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;AAqCF,eAAO,MAAM,wBAAwB,GACnC,QAAQ,8BAA8B,KACrC,OAAO,CAAC,IAAI,CA2Ed,CAAC;AAEF,eAAO,MAAM,0CAA0C;;;;;;;;CAS7C,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
- constructor(directoryResolver: SubAgentTranscriptDirectoryResolver, processLister: SubAgentProcessLister, now: Date);
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 loadKilledOrFailedAgentIds;
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;AAuBnI,eAAO,MAAM,wBAAwB,GAAI,SAAS,MAAM,KAAG,MAIR,CAAC;AAuMpD,qBAAa,2CAA4C,YAAW,iCAAiC;IAEjG,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAFH,iBAAiB,EAAE,mCAAmC,EACtD,aAAa,EAAE,qBAAqB,EACpC,GAAG,EAAE,IAAI;IAG5B,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,CAoCzC;IAEF,OAAO,CAAC,0BAA0B,CAahC;IAEF,OAAO,CAAC,iBAAiB,CAkCvB;IAEF,OAAO,CAAC,UAAU,CAoChB;IAEF,OAAO,CAAC,sBAAsB,CAiB5B;CACH"}
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,7 @@
1
+ export interface SubAgentLivenessResolver {
2
+ resolveLiveSubAgentIds: (params: {
3
+ sessionName: string;
4
+ mainTranscriptPath: string | null;
5
+ }) => Promise<Set<string> | null>;
6
+ }
7
+ //# sourceMappingURL=SubAgentLivenessResolver.d.ts.map
@@ -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"}