github-issue-tower-defence-management 1.122.2 → 1.122.4
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 +15 -0
- package/README.md +3 -3
- package/bin/adapter/entry-points/cli/index.js +9 -0
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleDoneStore.js +19 -5
- package/bin/adapter/entry-points/console/consoleDoneStore.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleOperationApi.js +78 -39
- package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleProjectResolver.js +8 -1
- package/bin/adapter/entry-points/console/consoleProjectResolver.js.map +1 -1
- package/bin/adapter/entry-points/console/webServer.js +5 -1
- package/bin/adapter/entry-points/console/webServer.js.map +1 -1
- package/bin/adapter/entry-points/handlers/consoleListsWriter.js +2 -0
- package/bin/adapter/entry-points/handlers/consoleListsWriter.js.map +1 -1
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +1 -1
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -1
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js +6 -7
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js.map +1 -1
- package/bin/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.js +60 -11
- package/bin/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.js.map +1 -1
- package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js +1 -0
- package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js.map +1 -1
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js +64 -7
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +23 -4
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js +5 -7
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js.map +1 -1
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +51 -16
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +3 -1
- package/src/adapter/entry-points/cli/index.ts +11 -0
- package/src/adapter/entry-points/console/consoleDoneStore.test.ts +38 -0
- package/src/adapter/entry-points/console/consoleDoneStore.ts +29 -4
- package/src/adapter/entry-points/console/consoleOperationApi.test.ts +190 -11
- package/src/adapter/entry-points/console/consoleOperationApi.ts +97 -56
- package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +13 -0
- package/src/adapter/entry-points/console/consoleProjectResolver.ts +11 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +3 -0
- package/src/adapter/entry-points/console/webServer.test.ts +4 -0
- package/src/adapter/entry-points/console/webServer.ts +9 -1
- package/src/adapter/entry-points/handlers/consoleListsWriter.test.ts +72 -0
- package/src/adapter/entry-points/handlers/consoleListsWriter.ts +3 -0
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +1 -0
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.test.ts +60 -37
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts +7 -18
- package/src/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.test.ts +223 -0
- package/src/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.ts +94 -10
- package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.test.ts +12 -2
- package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.ts +1 -0
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.test.ts +196 -4
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts +91 -7
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +86 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +34 -7
- package/src/domain/entities/LiveSessionActivitySnapshot.ts +1 -0
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.test.ts +112 -51
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.ts +8 -17
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +250 -18
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +72 -22
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +1 -0
- package/src/domain/usecases/adapter-interfaces/SilentSessionCandidateStateRepository.ts +8 -0
- package/src/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.ts +4 -7
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleDoneStore.d.ts +2 -0
- package/types/adapter/entry-points/console/consoleDoneStore.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts +2 -0
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +2 -1
- package/types/adapter/entry-points/console/consoleProjectResolver.d.ts.map +1 -1
- package/types/adapter/entry-points/console/webServer.d.ts +2 -1
- package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/consoleListsWriter.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -1
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts +2 -3
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts.map +1 -1
- package/types/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.d.ts +13 -2
- package/types/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.d.ts.map +1 -1
- package/types/adapter/repositories/ProcessListSessionSubAgentActivityRepository.d.ts.map +1 -1
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts +5 -1
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +2 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts +1 -0
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts.map +1 -1
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts +2 -3
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts.map +1 -1
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +2 -1
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/SilentSessionCandidateStateRepository.d.ts +8 -0
- package/types/domain/usecases/adapter-interfaces/SilentSessionCandidateStateRepository.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts +4 -4
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts.map +1 -1
|
@@ -8,10 +8,17 @@ type StoredCandidateEntry = {
|
|
|
8
8
|
recordedEpochSeconds: number;
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
type StoredAnnouncedRunningEntry = {
|
|
12
|
+
sessionName: string;
|
|
13
|
+
labels: string[];
|
|
14
|
+
recordedEpochSeconds: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
11
17
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
12
18
|
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13
19
|
|
|
14
20
|
export const DEFAULT_STATE_RETENTION_WINDOW_SECONDS = 60 * 60;
|
|
21
|
+
export const ANNOUNCED_RUNNING_RETENTION_WINDOW_SECONDS = 24 * 60 * 60;
|
|
15
22
|
|
|
16
23
|
const defaultStateFilePath = (): string => {
|
|
17
24
|
const base = process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), '.cache');
|
|
@@ -31,7 +38,7 @@ export class FileSystemSilentSessionCandidateStateRepository implements SilentSe
|
|
|
31
38
|
const nowEpochSeconds = Math.floor(params.now.getTime() / 1000);
|
|
32
39
|
const oldestAllowedEpochSeconds =
|
|
33
40
|
nowEpochSeconds - params.recencyWindowSeconds;
|
|
34
|
-
const entries = this.
|
|
41
|
+
const entries = this.readCandidateEntries();
|
|
35
42
|
const recentSessionNames = new Set<string>();
|
|
36
43
|
for (const entry of entries) {
|
|
37
44
|
if (entry.recordedEpochSeconds >= oldestAllowedEpochSeconds) {
|
|
@@ -50,7 +57,7 @@ export class FileSystemSilentSessionCandidateStateRepository implements SilentSe
|
|
|
50
57
|
recordedEpochSeconds - this.retentionWindowSeconds;
|
|
51
58
|
const currentSessionNames = new Set(params.sessionNames);
|
|
52
59
|
const mergedBySessionName = new Map<string, StoredCandidateEntry>();
|
|
53
|
-
for (const entry of this.
|
|
60
|
+
for (const entry of this.readCandidateEntries()) {
|
|
54
61
|
if (
|
|
55
62
|
entry.recordedEpochSeconds >= oldestRetainedEpochSeconds &&
|
|
56
63
|
!currentSessionNames.has(entry.sessionName)
|
|
@@ -64,26 +71,65 @@ export class FileSystemSilentSessionCandidateStateRepository implements SilentSe
|
|
|
64
71
|
recordedEpochSeconds,
|
|
65
72
|
});
|
|
66
73
|
}
|
|
67
|
-
this.
|
|
74
|
+
this.writeState(
|
|
75
|
+
Array.from(mergedBySessionName.values()),
|
|
76
|
+
this.readAnnouncedRunningEntries(),
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
loadAnnouncedRunningSubAgentLabels = async (params: {
|
|
81
|
+
sessionName: string;
|
|
82
|
+
}): Promise<Set<string>> => {
|
|
83
|
+
const entry = this.readAnnouncedRunningEntries().find(
|
|
84
|
+
(candidate) => candidate.sessionName === params.sessionName,
|
|
85
|
+
);
|
|
86
|
+
return new Set(entry?.labels ?? []);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
saveAnnouncedRunningSubAgentLabels = async (params: {
|
|
90
|
+
sessionName: string;
|
|
91
|
+
labels: string[];
|
|
92
|
+
now: Date;
|
|
93
|
+
}): Promise<void> => {
|
|
94
|
+
const recordedEpochSeconds = Math.floor(params.now.getTime() / 1000);
|
|
95
|
+
const oldestRetainedEpochSeconds =
|
|
96
|
+
recordedEpochSeconds - ANNOUNCED_RUNNING_RETENTION_WINDOW_SECONDS;
|
|
97
|
+
const retainedEntries = this.readAnnouncedRunningEntries().filter(
|
|
98
|
+
(entry) =>
|
|
99
|
+
entry.sessionName !== params.sessionName &&
|
|
100
|
+
entry.recordedEpochSeconds >= oldestRetainedEpochSeconds,
|
|
101
|
+
);
|
|
102
|
+
if (params.labels.length > 0) {
|
|
103
|
+
retainedEntries.push({
|
|
104
|
+
sessionName: params.sessionName,
|
|
105
|
+
labels: params.labels,
|
|
106
|
+
recordedEpochSeconds,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
this.writeState(this.readCandidateEntries(), retainedEntries);
|
|
68
110
|
};
|
|
69
111
|
|
|
70
|
-
private
|
|
112
|
+
private readState = (): Record<string, unknown> => {
|
|
71
113
|
let raw: string;
|
|
72
114
|
try {
|
|
73
115
|
raw = fs.readFileSync(this.stateFilePath, 'utf8');
|
|
74
116
|
} catch {
|
|
75
|
-
return
|
|
117
|
+
return {};
|
|
76
118
|
}
|
|
77
119
|
let parsed: unknown;
|
|
78
120
|
try {
|
|
79
121
|
parsed = JSON.parse(raw);
|
|
80
122
|
} catch {
|
|
81
|
-
return
|
|
123
|
+
return {};
|
|
82
124
|
}
|
|
83
125
|
if (!isRecord(parsed)) {
|
|
84
|
-
return
|
|
126
|
+
return {};
|
|
85
127
|
}
|
|
86
|
-
|
|
128
|
+
return parsed;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
private readCandidateEntries = (): StoredCandidateEntry[] => {
|
|
132
|
+
const storedEntries = this.readState().candidates;
|
|
87
133
|
if (!Array.isArray(storedEntries)) {
|
|
88
134
|
return [];
|
|
89
135
|
}
|
|
@@ -105,11 +151,49 @@ export class FileSystemSilentSessionCandidateStateRepository implements SilentSe
|
|
|
105
151
|
return entries;
|
|
106
152
|
};
|
|
107
153
|
|
|
108
|
-
private
|
|
154
|
+
private readAnnouncedRunningEntries = (): StoredAnnouncedRunningEntry[] => {
|
|
155
|
+
const storedEntries = this.readState().announcedRunningSubAgents;
|
|
156
|
+
if (!Array.isArray(storedEntries)) {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
const entries: StoredAnnouncedRunningEntry[] = [];
|
|
160
|
+
for (const storedEntry of storedEntries) {
|
|
161
|
+
if (!isRecord(storedEntry)) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const sessionName = storedEntry.sessionName;
|
|
165
|
+
const recordedEpochSeconds = storedEntry.recordedEpochSeconds;
|
|
166
|
+
const storedLabels = storedEntry.labels;
|
|
167
|
+
if (
|
|
168
|
+
typeof sessionName !== 'string' ||
|
|
169
|
+
typeof recordedEpochSeconds !== 'number' ||
|
|
170
|
+
!Number.isFinite(recordedEpochSeconds) ||
|
|
171
|
+
!Array.isArray(storedLabels)
|
|
172
|
+
) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const labels = storedLabels.filter(
|
|
176
|
+
(label): label is string => typeof label === 'string',
|
|
177
|
+
);
|
|
178
|
+
if (labels.length !== storedLabels.length) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
entries.push({ sessionName, labels, recordedEpochSeconds });
|
|
182
|
+
}
|
|
183
|
+
return entries;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
private writeState = (
|
|
187
|
+
candidates: StoredCandidateEntry[],
|
|
188
|
+
announcedRunningSubAgents: StoredAnnouncedRunningEntry[],
|
|
189
|
+
): void => {
|
|
109
190
|
const directory = path.dirname(this.stateFilePath);
|
|
110
191
|
fs.mkdirSync(directory, { recursive: true });
|
|
111
192
|
const temporaryPath = `${this.stateFilePath}.${process.pid}.tmp`;
|
|
112
|
-
fs.writeFileSync(
|
|
193
|
+
fs.writeFileSync(
|
|
194
|
+
temporaryPath,
|
|
195
|
+
JSON.stringify({ candidates, announcedRunningSubAgents }),
|
|
196
|
+
);
|
|
113
197
|
fs.renameSync(temporaryPath, this.stateFilePath);
|
|
114
198
|
};
|
|
115
199
|
}
|
|
@@ -54,7 +54,12 @@ describe('ProcessListSessionSubAgentActivityRepository', () => {
|
|
|
54
54
|
);
|
|
55
55
|
const result = await repository.listSubAgentActivitiesBySessionName(['s1']);
|
|
56
56
|
expect(result.get('s1')).toEqual([
|
|
57
|
-
{
|
|
57
|
+
{
|
|
58
|
+
label: 'task-a',
|
|
59
|
+
silentSeconds: 180,
|
|
60
|
+
runningSeconds: 1200,
|
|
61
|
+
waitingOnExternalProcess: false,
|
|
62
|
+
},
|
|
58
63
|
]);
|
|
59
64
|
});
|
|
60
65
|
|
|
@@ -78,7 +83,12 @@ describe('ProcessListSessionSubAgentActivityRepository', () => {
|
|
|
78
83
|
);
|
|
79
84
|
const result = await repository.listSubAgentActivitiesBySessionName(['s1']);
|
|
80
85
|
expect(result.get('s1')).toEqual([
|
|
81
|
-
{
|
|
86
|
+
{
|
|
87
|
+
label: 's1',
|
|
88
|
+
silentSeconds: 0,
|
|
89
|
+
runningSeconds: 30,
|
|
90
|
+
waitingOnExternalProcess: false,
|
|
91
|
+
},
|
|
82
92
|
]);
|
|
83
93
|
});
|
|
84
94
|
|
|
@@ -43,6 +43,7 @@ export class ProcessListSessionSubAgentActivityRepository implements SessionSubA
|
|
|
43
43
|
label,
|
|
44
44
|
silentSeconds: this.silentSecondsResolver.resolveSilentSeconds(label),
|
|
45
45
|
runningSeconds: process.elapsedSeconds,
|
|
46
|
+
waitingOnExternalProcess: false,
|
|
46
47
|
};
|
|
47
48
|
const existing = result.get(sessionName);
|
|
48
49
|
if (existing === undefined) {
|
|
@@ -2,6 +2,10 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as os from 'os';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { SubAgentTranscriptDirectoryResolver } from '../../domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver';
|
|
5
|
+
import {
|
|
6
|
+
SubAgentProcess,
|
|
7
|
+
SubAgentProcessLister,
|
|
8
|
+
} from '../../domain/usecases/adapter-interfaces/SubAgentProcessLister';
|
|
5
9
|
import { FileSystemSubAgentTranscriptDirectoryResolver } from './FileSystemSubAgentTranscriptDirectoryResolver';
|
|
6
10
|
import { TranscriptSessionSubAgentActivityRepository } from './TranscriptSessionSubAgentActivityRepository';
|
|
7
11
|
|
|
@@ -70,6 +74,14 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
70
74
|
const createResolver = (): SubAgentTranscriptDirectoryResolver =>
|
|
71
75
|
new FileSystemSubAgentTranscriptDirectoryResolver(rootDirectory);
|
|
72
76
|
|
|
77
|
+
const processListerWith = (
|
|
78
|
+
processes: SubAgentProcess[],
|
|
79
|
+
): SubAgentProcessLister => ({
|
|
80
|
+
listProcesses: async () => processes,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const emptyProcessLister = (): SubAgentProcessLister => processListerWith([]);
|
|
84
|
+
|
|
73
85
|
const runningEntries = (startTimestamp: string): object[] => [
|
|
74
86
|
{ type: 'user', timestamp: startTimestamp, message: { role: 'user' } },
|
|
75
87
|
{
|
|
@@ -251,6 +263,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
251
263
|
);
|
|
252
264
|
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
253
265
|
createResolver(),
|
|
266
|
+
emptyProcessLister(),
|
|
254
267
|
now,
|
|
255
268
|
);
|
|
256
269
|
|
|
@@ -260,7 +273,12 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
260
273
|
);
|
|
261
274
|
|
|
262
275
|
expect(result.get(sessionName)).toEqual([
|
|
263
|
-
{
|
|
276
|
+
{
|
|
277
|
+
label: 'agent-aaa111',
|
|
278
|
+
silentSeconds: 120,
|
|
279
|
+
runningSeconds: 900,
|
|
280
|
+
waitingOnExternalProcess: false,
|
|
281
|
+
},
|
|
264
282
|
]);
|
|
265
283
|
});
|
|
266
284
|
|
|
@@ -274,6 +292,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
274
292
|
);
|
|
275
293
|
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
276
294
|
createResolver(),
|
|
295
|
+
emptyProcessLister(),
|
|
277
296
|
now,
|
|
278
297
|
);
|
|
279
298
|
|
|
@@ -295,6 +314,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
295
314
|
);
|
|
296
315
|
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
297
316
|
createResolver(),
|
|
317
|
+
emptyProcessLister(),
|
|
298
318
|
now,
|
|
299
319
|
);
|
|
300
320
|
|
|
@@ -316,6 +336,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
316
336
|
);
|
|
317
337
|
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
318
338
|
createResolver(),
|
|
339
|
+
emptyProcessLister(),
|
|
319
340
|
now,
|
|
320
341
|
);
|
|
321
342
|
|
|
@@ -337,6 +358,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
337
358
|
);
|
|
338
359
|
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
339
360
|
createResolver(),
|
|
361
|
+
emptyProcessLister(),
|
|
340
362
|
now,
|
|
341
363
|
);
|
|
342
364
|
|
|
@@ -358,6 +380,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
358
380
|
);
|
|
359
381
|
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
360
382
|
createResolver(),
|
|
383
|
+
emptyProcessLister(),
|
|
361
384
|
now,
|
|
362
385
|
);
|
|
363
386
|
|
|
@@ -379,6 +402,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
379
402
|
);
|
|
380
403
|
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
381
404
|
createResolver(),
|
|
405
|
+
emptyProcessLister(),
|
|
382
406
|
now,
|
|
383
407
|
);
|
|
384
408
|
|
|
@@ -400,6 +424,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
400
424
|
);
|
|
401
425
|
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
402
426
|
createResolver(),
|
|
427
|
+
emptyProcessLister(),
|
|
403
428
|
now,
|
|
404
429
|
);
|
|
405
430
|
|
|
@@ -421,6 +446,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
421
446
|
);
|
|
422
447
|
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
423
448
|
createResolver(),
|
|
449
|
+
emptyProcessLister(),
|
|
424
450
|
now,
|
|
425
451
|
);
|
|
426
452
|
|
|
@@ -430,7 +456,12 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
430
456
|
);
|
|
431
457
|
|
|
432
458
|
expect(result.get(sessionName)).toEqual([
|
|
433
|
-
{
|
|
459
|
+
{
|
|
460
|
+
label: 'agent-pending',
|
|
461
|
+
silentSeconds: 600,
|
|
462
|
+
runningSeconds: 900,
|
|
463
|
+
waitingOnExternalProcess: false,
|
|
464
|
+
},
|
|
434
465
|
]);
|
|
435
466
|
});
|
|
436
467
|
|
|
@@ -445,6 +476,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
445
476
|
);
|
|
446
477
|
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
447
478
|
createResolver(),
|
|
479
|
+
emptyProcessLister(),
|
|
448
480
|
now,
|
|
449
481
|
);
|
|
450
482
|
|
|
@@ -458,6 +490,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
458
490
|
label: 'agent-longstall',
|
|
459
491
|
silentSeconds: fourDaysSeconds,
|
|
460
492
|
runningSeconds: fourDaysSeconds,
|
|
493
|
+
waitingOnExternalProcess: false,
|
|
461
494
|
},
|
|
462
495
|
]);
|
|
463
496
|
});
|
|
@@ -480,6 +513,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
480
513
|
fs.symlinkSync(targetPath, path.join(dir, 'agent-link1.jsonl'));
|
|
481
514
|
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
482
515
|
createResolver(),
|
|
516
|
+
emptyProcessLister(),
|
|
483
517
|
now,
|
|
484
518
|
);
|
|
485
519
|
|
|
@@ -489,7 +523,12 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
489
523
|
);
|
|
490
524
|
|
|
491
525
|
expect(result.get(sessionName)).toEqual([
|
|
492
|
-
{
|
|
526
|
+
{
|
|
527
|
+
label: 'agent-link1',
|
|
528
|
+
silentSeconds: 60,
|
|
529
|
+
runningSeconds: 600,
|
|
530
|
+
waitingOnExternalProcess: false,
|
|
531
|
+
},
|
|
493
532
|
]);
|
|
494
533
|
fs.rmSync(targetDir, { force: true, recursive: true });
|
|
495
534
|
});
|
|
@@ -497,6 +536,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
497
536
|
it('returns an empty map when the resolver returns null', async () => {
|
|
498
537
|
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
499
538
|
{ resolveSubAgentsDirectory: () => null },
|
|
539
|
+
emptyProcessLister(),
|
|
500
540
|
now,
|
|
501
541
|
);
|
|
502
542
|
|
|
@@ -511,6 +551,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
511
551
|
it('ignores a session whose subagents directory does not exist', async () => {
|
|
512
552
|
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
513
553
|
createResolver(),
|
|
554
|
+
emptyProcessLister(),
|
|
514
555
|
now,
|
|
515
556
|
);
|
|
516
557
|
|
|
@@ -538,6 +579,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
538
579
|
);
|
|
539
580
|
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
540
581
|
createResolver(),
|
|
582
|
+
emptyProcessLister(),
|
|
541
583
|
now,
|
|
542
584
|
);
|
|
543
585
|
|
|
@@ -559,6 +601,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
559
601
|
);
|
|
560
602
|
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
561
603
|
createResolver(),
|
|
604
|
+
emptyProcessLister(),
|
|
562
605
|
now,
|
|
563
606
|
);
|
|
564
607
|
|
|
@@ -568,7 +611,12 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
568
611
|
);
|
|
569
612
|
|
|
570
613
|
expect(result.get(sessionName)).toEqual([
|
|
571
|
-
{
|
|
614
|
+
{
|
|
615
|
+
label: 'agent-future',
|
|
616
|
+
silentSeconds: 0,
|
|
617
|
+
runningSeconds: 0,
|
|
618
|
+
waitingOnExternalProcess: false,
|
|
619
|
+
},
|
|
572
620
|
]);
|
|
573
621
|
});
|
|
574
622
|
|
|
@@ -604,6 +652,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
604
652
|
|
|
605
653
|
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
606
654
|
new FileSystemSubAgentTranscriptDirectoryResolver(projectsRoot),
|
|
655
|
+
emptyProcessLister(),
|
|
607
656
|
now,
|
|
608
657
|
);
|
|
609
658
|
|
|
@@ -617,6 +666,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
617
666
|
label: 'agent-afcbe335fdbec0a28',
|
|
618
667
|
silentSeconds: 120,
|
|
619
668
|
runningSeconds: 900,
|
|
669
|
+
waitingOnExternalProcess: false,
|
|
620
670
|
},
|
|
621
671
|
]);
|
|
622
672
|
});
|
|
@@ -638,6 +688,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
638
688
|
);
|
|
639
689
|
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
640
690
|
createResolver(),
|
|
691
|
+
emptyProcessLister(),
|
|
641
692
|
now,
|
|
642
693
|
);
|
|
643
694
|
|
|
@@ -651,7 +702,148 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
651
702
|
label: 'agent-stalledold',
|
|
652
703
|
silentSeconds: twoDaysSeconds,
|
|
653
704
|
runningSeconds: twoDaysSeconds,
|
|
705
|
+
waitingOnExternalProcess: false,
|
|
654
706
|
},
|
|
655
707
|
]);
|
|
656
708
|
});
|
|
709
|
+
|
|
710
|
+
describe('external-wait classification from the pending tool command', () => {
|
|
711
|
+
const CI_WATCH_COMMAND =
|
|
712
|
+
'gh pr checks 123 --watch -i 60 -R owner/repo 2>&1 | tail -5';
|
|
713
|
+
|
|
714
|
+
const pendingCommandEntries = (
|
|
715
|
+
startTimestamp: string,
|
|
716
|
+
command: string,
|
|
717
|
+
): object[] => [
|
|
718
|
+
{ type: 'user', timestamp: startTimestamp, message: { role: 'user' } },
|
|
719
|
+
{
|
|
720
|
+
type: 'assistant',
|
|
721
|
+
timestamp: startTimestamp,
|
|
722
|
+
message: {
|
|
723
|
+
role: 'assistant',
|
|
724
|
+
stop_reason: 'tool_use',
|
|
725
|
+
content: [{ type: 'tool_use', name: 'Bash', input: { command } }],
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
];
|
|
729
|
+
|
|
730
|
+
it('classifies a sub-agent as waiting when its pending tool command matches a live process', async () => {
|
|
731
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
732
|
+
writeAgentTranscript(
|
|
733
|
+
sessionName,
|
|
734
|
+
'ciwait',
|
|
735
|
+
pendingCommandEntries('2026-06-27T11:45:00.000Z', CI_WATCH_COMMAND),
|
|
736
|
+
nowEpochSeconds - 600,
|
|
737
|
+
);
|
|
738
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
739
|
+
createResolver(),
|
|
740
|
+
processListerWith([
|
|
741
|
+
{
|
|
742
|
+
commandLine: `/bin/bash -c ${CI_WATCH_COMMAND}`,
|
|
743
|
+
elapsedSeconds: 600,
|
|
744
|
+
},
|
|
745
|
+
]),
|
|
746
|
+
now,
|
|
747
|
+
);
|
|
748
|
+
|
|
749
|
+
const result = await repository.listSubAgentActivitiesBySessionName(
|
|
750
|
+
[sessionName],
|
|
751
|
+
transcriptMapFor([sessionName]),
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
expect(result.get(sessionName)).toEqual([
|
|
755
|
+
{
|
|
756
|
+
label: 'agent-ciwait',
|
|
757
|
+
silentSeconds: 600,
|
|
758
|
+
runningSeconds: 900,
|
|
759
|
+
waitingOnExternalProcess: true,
|
|
760
|
+
},
|
|
761
|
+
]);
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it('classifies a sub-agent as hung when no live process matches its pending tool command', async () => {
|
|
765
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
766
|
+
writeAgentTranscript(
|
|
767
|
+
sessionName,
|
|
768
|
+
'dead',
|
|
769
|
+
pendingCommandEntries('2026-06-27T11:45:00.000Z', CI_WATCH_COMMAND),
|
|
770
|
+
nowEpochSeconds - 600,
|
|
771
|
+
);
|
|
772
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
773
|
+
createResolver(),
|
|
774
|
+
processListerWith([
|
|
775
|
+
{ commandLine: 'node unrelated-server.js', elapsedSeconds: 5000 },
|
|
776
|
+
]),
|
|
777
|
+
now,
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
const result = await repository.listSubAgentActivitiesBySessionName(
|
|
781
|
+
[sessionName],
|
|
782
|
+
transcriptMapFor([sessionName]),
|
|
783
|
+
);
|
|
784
|
+
|
|
785
|
+
expect(result.get(sessionName)).toEqual([
|
|
786
|
+
{
|
|
787
|
+
label: 'agent-dead',
|
|
788
|
+
silentSeconds: 600,
|
|
789
|
+
runningSeconds: 900,
|
|
790
|
+
waitingOnExternalProcess: false,
|
|
791
|
+
},
|
|
792
|
+
]);
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
it('classifies a sub-agent as hung when its pending tool call carries no command string', async () => {
|
|
796
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
797
|
+
writeAgentTranscript(
|
|
798
|
+
sessionName,
|
|
799
|
+
'nocmd',
|
|
800
|
+
pendingToolUseEntries('2026-06-27T11:45:00.000Z'),
|
|
801
|
+
nowEpochSeconds - 600,
|
|
802
|
+
);
|
|
803
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
804
|
+
createResolver(),
|
|
805
|
+
processListerWith([
|
|
806
|
+
{ commandLine: 'any process at all', elapsedSeconds: 1 },
|
|
807
|
+
]),
|
|
808
|
+
now,
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
const result = await repository.listSubAgentActivitiesBySessionName(
|
|
812
|
+
[sessionName],
|
|
813
|
+
transcriptMapFor([sessionName]),
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
expect(result.get(sessionName)?.[0]?.waitingOnExternalProcess).toBe(
|
|
817
|
+
false,
|
|
818
|
+
);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
it('matches on the whitespace-normalized first 60 characters of a long multi-line command', async () => {
|
|
822
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
823
|
+
const longCommand =
|
|
824
|
+
'OWNER=owner; REPO=repo; PR=123\n SHA=$(gh api repos/$OWNER/$REPO/pulls/$PR --jq .head.sha)\n sleep 600';
|
|
825
|
+
writeAgentTranscript(
|
|
826
|
+
sessionName,
|
|
827
|
+
'longcmd',
|
|
828
|
+
pendingCommandEntries('2026-06-27T11:45:00.000Z', longCommand),
|
|
829
|
+
nowEpochSeconds - 600,
|
|
830
|
+
);
|
|
831
|
+
const normalizedProcessLine =
|
|
832
|
+
'bash -c OWNER=owner; REPO=repo; PR=123 SHA=$(gh api repos/$OWNER/$REPO/pulls/$PR --jq .head.sha) sleep 600';
|
|
833
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
834
|
+
createResolver(),
|
|
835
|
+
processListerWith([
|
|
836
|
+
{ commandLine: normalizedProcessLine, elapsedSeconds: 600 },
|
|
837
|
+
]),
|
|
838
|
+
now,
|
|
839
|
+
);
|
|
840
|
+
|
|
841
|
+
const result = await repository.listSubAgentActivitiesBySessionName(
|
|
842
|
+
[sessionName],
|
|
843
|
+
transcriptMapFor([sessionName]),
|
|
844
|
+
);
|
|
845
|
+
|
|
846
|
+
expect(result.get(sessionName)?.[0]?.waitingOnExternalProcess).toBe(true);
|
|
847
|
+
});
|
|
848
|
+
});
|
|
657
849
|
});
|