github-issue-tower-defence-management 1.148.25 → 1.149.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 +13 -0
- package/README.md +4 -4
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +2 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js +1 -0
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js.map +1 -1
- package/bin/adapter/repositories/FileSystemKevReportWatermarkRepository.js +126 -0
- package/bin/adapter/repositories/FileSystemKevReportWatermarkRepository.js.map +1 -0
- package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js +1 -0
- package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js.map +1 -1
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js +5 -3
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js.map +1 -1
- package/bin/domain/entities/KevReportWatermark.js +3 -0
- package/bin/domain/entities/KevReportWatermark.js.map +1 -0
- package/bin/domain/usecases/DailySecurityScanUseCase.js +56 -7
- package/bin/domain/usecases/DailySecurityScanUseCase.js.map +1 -1
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js +9 -0
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js.map +1 -1
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +12 -11
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
- package/bin/domain/usecases/adapter-interfaces/KevReportWatermarkRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/KevReportWatermarkRepository.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +2 -0
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.test.ts +35 -0
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts +7 -0
- package/src/adapter/repositories/FileSystemKevReportWatermarkRepository.test.ts +253 -0
- package/src/adapter/repositories/FileSystemKevReportWatermarkRepository.ts +130 -0
- package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.test.ts +2 -0
- package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.ts +1 -0
- package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.test.ts +3 -0
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.test.ts +159 -0
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts +7 -1
- package/src/domain/entities/KevReportWatermark.ts +4 -0
- package/src/domain/entities/LiveSessionActivitySnapshot.ts +1 -0
- package/src/domain/usecases/DailySecurityScanUseCase.test.ts +210 -0
- package/src/domain/usecases/DailySecurityScanUseCase.ts +84 -7
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.test.ts +58 -1
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.ts +21 -0
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +146 -8
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +19 -10
- package/src/domain/usecases/adapter-interfaces/KevReportWatermarkRepository.ts +11 -0
- package/src/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.ts +3 -0
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts +2 -0
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts.map +1 -1
- package/types/adapter/repositories/FileSystemKevReportWatermarkRepository.d.ts +10 -0
- package/types/adapter/repositories/FileSystemKevReportWatermarkRepository.d.ts.map +1 -0
- package/types/adapter/repositories/ProcessListSessionSubAgentActivityRepository.d.ts.map +1 -1
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts.map +1 -1
- package/types/domain/entities/KevReportWatermark.d.ts +5 -0
- package/types/domain/entities/KevReportWatermark.d.ts.map +1 -0
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts +1 -0
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts.map +1 -1
- package/types/domain/usecases/DailySecurityScanUseCase.d.ts +3 -1
- package/types/domain/usecases/DailySecurityScanUseCase.d.ts.map +1 -1
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts +2 -0
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts.map +1 -1
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/KevReportWatermarkRepository.d.ts +15 -0
- package/types/domain/usecases/adapter-interfaces/KevReportWatermarkRepository.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts +1 -0
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts.map +1 -1
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { KevReportWatermark } from '../../domain/entities/KevReportWatermark';
|
|
5
|
+
import {
|
|
6
|
+
KevReportWatermarkLoadResult,
|
|
7
|
+
KevReportWatermarkRepository,
|
|
8
|
+
} from '../../domain/usecases/adapter-interfaces/KevReportWatermarkRepository';
|
|
9
|
+
|
|
10
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
11
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
12
|
+
|
|
13
|
+
const errorCode = (error: unknown): string | null => {
|
|
14
|
+
if (!isRecord(error)) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const code = error.code;
|
|
18
|
+
return typeof code === 'string' ? code : null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const isCalendarYmd = (value: string): boolean => {
|
|
22
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const parsed = new Date(`${value}T00:00:00.000Z`);
|
|
26
|
+
return (
|
|
27
|
+
!Number.isNaN(parsed.getTime()) &&
|
|
28
|
+
parsed.toISOString().slice(0, 10) === value
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const defaultStateFilePath = (): string => {
|
|
33
|
+
const base = process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), '.cache');
|
|
34
|
+
return path.join(base, 'tdpm', 'kev-report-watermark.json');
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export class FileSystemKevReportWatermarkRepository implements KevReportWatermarkRepository {
|
|
38
|
+
constructor(
|
|
39
|
+
private readonly stateFilePath: string = defaultStateFilePath(),
|
|
40
|
+
) {}
|
|
41
|
+
|
|
42
|
+
load = async (): Promise<KevReportWatermarkLoadResult> => {
|
|
43
|
+
let raw: string;
|
|
44
|
+
try {
|
|
45
|
+
raw = fs.readFileSync(this.stateFilePath, 'utf8');
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (errorCode(error) === 'ENOENT') {
|
|
48
|
+
return { type: 'absent' };
|
|
49
|
+
}
|
|
50
|
+
return this.unreadable(
|
|
51
|
+
`the stored watermark file could not be read (${String(error)})`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let parsed: unknown;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(raw);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return this.unreadable(
|
|
60
|
+
`the stored watermark file does not contain valid JSON (${String(error)})`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!isRecord(parsed)) {
|
|
65
|
+
return this.unreadable(
|
|
66
|
+
'the stored watermark file does not contain a JSON object',
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const lastReportedDateAdded = parsed.lastReportedDateAdded;
|
|
71
|
+
if (typeof lastReportedDateAdded !== 'string') {
|
|
72
|
+
return this.unreadable(
|
|
73
|
+
`the stored lastReportedDateAdded is not a string (${JSON.stringify(lastReportedDateAdded)})`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (!isCalendarYmd(lastReportedDateAdded)) {
|
|
77
|
+
return this.unreadable(
|
|
78
|
+
`the stored lastReportedDateAdded is not a calendar date in YYYY-MM-DD form (${JSON.stringify(lastReportedDateAdded)})`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const reportedCveIdsOnLastReportedDateAdded =
|
|
83
|
+
parsed.reportedCveIdsOnLastReportedDateAdded;
|
|
84
|
+
if (!Array.isArray(reportedCveIdsOnLastReportedDateAdded)) {
|
|
85
|
+
return this.unreadable(
|
|
86
|
+
'the stored reportedCveIdsOnLastReportedDateAdded is not an array',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (
|
|
90
|
+
!reportedCveIdsOnLastReportedDateAdded.every(
|
|
91
|
+
(cveId) => typeof cveId === 'string',
|
|
92
|
+
)
|
|
93
|
+
) {
|
|
94
|
+
return this.unreadable(
|
|
95
|
+
'the stored reportedCveIdsOnLastReportedDateAdded contains a non-string entry',
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
type: 'stored',
|
|
101
|
+
watermark: {
|
|
102
|
+
lastReportedDateAdded,
|
|
103
|
+
reportedCveIdsOnLastReportedDateAdded:
|
|
104
|
+
reportedCveIdsOnLastReportedDateAdded,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
save = async (watermark: KevReportWatermark): Promise<void> => {
|
|
110
|
+
try {
|
|
111
|
+
const directory = path.dirname(this.stateFilePath);
|
|
112
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
113
|
+
const temporaryPath = `${this.stateFilePath}.${process.pid}.tmp`;
|
|
114
|
+
fs.writeFileSync(temporaryPath, JSON.stringify(watermark));
|
|
115
|
+
fs.renameSync(temporaryPath, this.stateFilePath);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error(
|
|
118
|
+
`Unable to write the KEV report watermark to ${this.stateFilePath}: ${String(error)}`,
|
|
119
|
+
);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
private unreadable = (reason: string): KevReportWatermarkLoadResult => {
|
|
125
|
+
console.error(
|
|
126
|
+
`Unable to use the KEV report watermark stored at ${this.stateFilePath}: ${reason}. The file is left untouched for inspection.`,
|
|
127
|
+
);
|
|
128
|
+
return { type: 'unreadable', reason };
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -59,6 +59,7 @@ describe('ProcessListSessionSubAgentActivityRepository', () => {
|
|
|
59
59
|
silentSeconds: 180,
|
|
60
60
|
runningSeconds: 1200,
|
|
61
61
|
waitingOnExternalProcess: false,
|
|
62
|
+
finishedResultUnconsumed: false,
|
|
62
63
|
},
|
|
63
64
|
]);
|
|
64
65
|
});
|
|
@@ -88,6 +89,7 @@ describe('ProcessListSessionSubAgentActivityRepository', () => {
|
|
|
88
89
|
silentSeconds: 0,
|
|
89
90
|
runningSeconds: 30,
|
|
90
91
|
waitingOnExternalProcess: false,
|
|
92
|
+
finishedResultUnconsumed: false,
|
|
91
93
|
},
|
|
92
94
|
]);
|
|
93
95
|
});
|
|
@@ -44,6 +44,7 @@ export class ProcessListSessionSubAgentActivityRepository implements SessionSubA
|
|
|
44
44
|
silentSeconds: this.silentSecondsResolver.resolveSilentSeconds(label),
|
|
45
45
|
runningSeconds: process.elapsedSeconds,
|
|
46
46
|
waitingOnExternalProcess: false,
|
|
47
|
+
finishedResultUnconsumed: false,
|
|
47
48
|
};
|
|
48
49
|
const existing = result.get(sessionName);
|
|
49
50
|
if (existing === undefined) {
|
|
@@ -940,6 +940,9 @@ describe('TranscriptOwnerCallStatusProvider wired into NotifySilentLiveSessionsU
|
|
|
940
940
|
.fn()
|
|
941
941
|
.mockReturnValue('MAIN_STALLED_STALE'),
|
|
942
942
|
composeSubAgentSection: jest.fn().mockReturnValue('SUBAGENT'),
|
|
943
|
+
composeSubAgentUnconsumedResultSection: jest
|
|
944
|
+
.fn()
|
|
945
|
+
.mockReturnValue('SUBAGENT_UNCONSUMED'),
|
|
943
946
|
},
|
|
944
947
|
{ sleep: jest.fn().mockResolvedValue(undefined) },
|
|
945
948
|
);
|
|
@@ -279,6 +279,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
279
279
|
silentSeconds: 120,
|
|
280
280
|
runningSeconds: 900,
|
|
281
281
|
waitingOnExternalProcess: false,
|
|
282
|
+
finishedResultUnconsumed: false,
|
|
282
283
|
},
|
|
283
284
|
]);
|
|
284
285
|
});
|
|
@@ -400,6 +401,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
400
401
|
silentSeconds: 60,
|
|
401
402
|
runningSeconds: 600,
|
|
402
403
|
waitingOnExternalProcess: false,
|
|
404
|
+
finishedResultUnconsumed: false,
|
|
403
405
|
},
|
|
404
406
|
]);
|
|
405
407
|
});
|
|
@@ -474,6 +476,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
474
476
|
silentSeconds: 60,
|
|
475
477
|
runningSeconds: 600,
|
|
476
478
|
waitingOnExternalProcess: false,
|
|
479
|
+
finishedResultUnconsumed: false,
|
|
477
480
|
},
|
|
478
481
|
]);
|
|
479
482
|
});
|
|
@@ -525,6 +528,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
525
528
|
silentSeconds: 600,
|
|
526
529
|
runningSeconds: 900,
|
|
527
530
|
waitingOnExternalProcess: false,
|
|
531
|
+
finishedResultUnconsumed: false,
|
|
528
532
|
},
|
|
529
533
|
]);
|
|
530
534
|
});
|
|
@@ -555,6 +559,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
555
559
|
silentSeconds: fourDaysSeconds,
|
|
556
560
|
runningSeconds: fourDaysSeconds,
|
|
557
561
|
waitingOnExternalProcess: false,
|
|
562
|
+
finishedResultUnconsumed: false,
|
|
558
563
|
},
|
|
559
564
|
]);
|
|
560
565
|
});
|
|
@@ -592,6 +597,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
592
597
|
silentSeconds: 60,
|
|
593
598
|
runningSeconds: 600,
|
|
594
599
|
waitingOnExternalProcess: false,
|
|
600
|
+
finishedResultUnconsumed: false,
|
|
595
601
|
},
|
|
596
602
|
]);
|
|
597
603
|
fs.rmSync(targetDir, { force: true, recursive: true });
|
|
@@ -680,6 +686,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
680
686
|
silentSeconds: 0,
|
|
681
687
|
runningSeconds: 0,
|
|
682
688
|
waitingOnExternalProcess: false,
|
|
689
|
+
finishedResultUnconsumed: false,
|
|
683
690
|
},
|
|
684
691
|
]);
|
|
685
692
|
});
|
|
@@ -731,6 +738,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
731
738
|
silentSeconds: 120,
|
|
732
739
|
runningSeconds: 900,
|
|
733
740
|
waitingOnExternalProcess: false,
|
|
741
|
+
finishedResultUnconsumed: false,
|
|
734
742
|
},
|
|
735
743
|
]);
|
|
736
744
|
});
|
|
@@ -767,6 +775,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
767
775
|
silentSeconds: twoDaysSeconds,
|
|
768
776
|
runningSeconds: twoDaysSeconds,
|
|
769
777
|
waitingOnExternalProcess: false,
|
|
778
|
+
finishedResultUnconsumed: false,
|
|
770
779
|
},
|
|
771
780
|
]);
|
|
772
781
|
});
|
|
@@ -821,6 +830,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
821
830
|
silentSeconds: 600,
|
|
822
831
|
runningSeconds: 900,
|
|
823
832
|
waitingOnExternalProcess: true,
|
|
833
|
+
finishedResultUnconsumed: false,
|
|
824
834
|
},
|
|
825
835
|
]);
|
|
826
836
|
},
|
|
@@ -956,6 +966,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
956
966
|
silentSeconds: 600,
|
|
957
967
|
runningSeconds: 900,
|
|
958
968
|
waitingOnExternalProcess: true,
|
|
969
|
+
finishedResultUnconsumed: false,
|
|
959
970
|
},
|
|
960
971
|
]);
|
|
961
972
|
});
|
|
@@ -987,6 +998,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
987
998
|
silentSeconds: 600,
|
|
988
999
|
runningSeconds: 900,
|
|
989
1000
|
waitingOnExternalProcess: false,
|
|
1001
|
+
finishedResultUnconsumed: false,
|
|
990
1002
|
},
|
|
991
1003
|
]);
|
|
992
1004
|
});
|
|
@@ -1282,6 +1294,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
1282
1294
|
silentSeconds: 120,
|
|
1283
1295
|
runningSeconds: 900,
|
|
1284
1296
|
waitingOnExternalProcess: false,
|
|
1297
|
+
finishedResultUnconsumed: false,
|
|
1285
1298
|
},
|
|
1286
1299
|
]);
|
|
1287
1300
|
});
|
|
@@ -1337,6 +1350,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
1337
1350
|
silentSeconds: 600,
|
|
1338
1351
|
runningSeconds: 900,
|
|
1339
1352
|
waitingOnExternalProcess: false,
|
|
1353
|
+
finishedResultUnconsumed: false,
|
|
1340
1354
|
},
|
|
1341
1355
|
]);
|
|
1342
1356
|
});
|
|
@@ -1367,6 +1381,151 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
1367
1381
|
silentSeconds: 120,
|
|
1368
1382
|
runningSeconds: 900,
|
|
1369
1383
|
waitingOnExternalProcess: false,
|
|
1384
|
+
finishedResultUnconsumed: false,
|
|
1385
|
+
},
|
|
1386
|
+
]);
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
it('reports a finished sub-agent still listed in the running-subagents record as an unconsumed result carrying its silence age', async () => {
|
|
1390
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
1391
|
+
const unconsumedAgentId = 'aaabbbbcccc30001';
|
|
1392
|
+
writeAgentTranscript(
|
|
1393
|
+
sessionName,
|
|
1394
|
+
unconsumedAgentId,
|
|
1395
|
+
finishedEntries('2026-06-27T10:30:00.000Z'),
|
|
1396
|
+
nowEpochSeconds - 5220,
|
|
1397
|
+
);
|
|
1398
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
1399
|
+
createResolver(),
|
|
1400
|
+
emptyProcessLister(),
|
|
1401
|
+
now,
|
|
1402
|
+
livenessResolverWith([unconsumedAgentId]),
|
|
1403
|
+
);
|
|
1404
|
+
|
|
1405
|
+
const result = await repository.listSubAgentActivitiesBySessionName(
|
|
1406
|
+
[sessionName],
|
|
1407
|
+
transcriptMapFor([sessionName]),
|
|
1408
|
+
);
|
|
1409
|
+
|
|
1410
|
+
expect(result.get(sessionName)).toEqual([
|
|
1411
|
+
{
|
|
1412
|
+
label: `agent-${unconsumedAgentId}`,
|
|
1413
|
+
silentSeconds: 5220,
|
|
1414
|
+
runningSeconds: 5400,
|
|
1415
|
+
waitingOnExternalProcess: false,
|
|
1416
|
+
finishedResultUnconsumed: true,
|
|
1417
|
+
},
|
|
1418
|
+
]);
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
it('reports a finished sub-agent whose tail is a final text answer as an unconsumed result while it stays in the running-subagents record', async () => {
|
|
1422
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
1423
|
+
const unconsumedAgentId = 'aaabbbbcccc30002';
|
|
1424
|
+
writeAgentTranscript(
|
|
1425
|
+
sessionName,
|
|
1426
|
+
unconsumedAgentId,
|
|
1427
|
+
finalTextAnswerEntries('2026-06-27T11:00:00.000Z'),
|
|
1428
|
+
nowEpochSeconds - 360,
|
|
1429
|
+
);
|
|
1430
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
1431
|
+
createResolver(),
|
|
1432
|
+
emptyProcessLister(),
|
|
1433
|
+
now,
|
|
1434
|
+
livenessResolverWith([unconsumedAgentId]),
|
|
1435
|
+
);
|
|
1436
|
+
|
|
1437
|
+
const result = await repository.listSubAgentActivitiesBySessionName(
|
|
1438
|
+
[sessionName],
|
|
1439
|
+
transcriptMapFor([sessionName]),
|
|
1440
|
+
);
|
|
1441
|
+
|
|
1442
|
+
expect(result.get(sessionName)).toEqual([
|
|
1443
|
+
{
|
|
1444
|
+
label: `agent-${unconsumedAgentId}`,
|
|
1445
|
+
silentSeconds: 360,
|
|
1446
|
+
runningSeconds: 3600,
|
|
1447
|
+
waitingOnExternalProcess: false,
|
|
1448
|
+
finishedResultUnconsumed: true,
|
|
1449
|
+
},
|
|
1450
|
+
]);
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
it('discards a finished sub-agent when the running-subagents record cannot be resolved', async () => {
|
|
1454
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
1455
|
+
const finishedAgentId = 'aaabbbbcccc30003';
|
|
1456
|
+
writeAgentTranscript(
|
|
1457
|
+
sessionName,
|
|
1458
|
+
finishedAgentId,
|
|
1459
|
+
finishedEntries('2026-06-27T10:30:00.000Z'),
|
|
1460
|
+
nowEpochSeconds - 5220,
|
|
1461
|
+
);
|
|
1462
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
1463
|
+
createResolver(),
|
|
1464
|
+
emptyProcessLister(),
|
|
1465
|
+
now,
|
|
1466
|
+
livenessResolverWith(null),
|
|
1467
|
+
);
|
|
1468
|
+
|
|
1469
|
+
const result = await repository.listSubAgentActivitiesBySessionName(
|
|
1470
|
+
[sessionName],
|
|
1471
|
+
transcriptMapFor([sessionName]),
|
|
1472
|
+
);
|
|
1473
|
+
|
|
1474
|
+
expect(result.size).toBe(0);
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1477
|
+
it('discards a finished sub-agent whose id has already been removed from the running-subagents record', async () => {
|
|
1478
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
1479
|
+
const finishedAgentId = 'aaabbbbcccc30004';
|
|
1480
|
+
writeAgentTranscript(
|
|
1481
|
+
sessionName,
|
|
1482
|
+
finishedAgentId,
|
|
1483
|
+
finishedEntries('2026-06-27T10:30:00.000Z'),
|
|
1484
|
+
nowEpochSeconds - 5220,
|
|
1485
|
+
);
|
|
1486
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
1487
|
+
createResolver(),
|
|
1488
|
+
emptyProcessLister(),
|
|
1489
|
+
now,
|
|
1490
|
+
livenessResolverWith(['aaabbbbcccc30005']),
|
|
1491
|
+
);
|
|
1492
|
+
|
|
1493
|
+
const result = await repository.listSubAgentActivitiesBySessionName(
|
|
1494
|
+
[sessionName],
|
|
1495
|
+
transcriptMapFor([sessionName]),
|
|
1496
|
+
);
|
|
1497
|
+
|
|
1498
|
+
expect(result.size).toBe(0);
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
it('reports a still-working sub-agent listed in the running-subagents record as having no unconsumed result', async () => {
|
|
1502
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
1503
|
+
const workingAgentId = 'aaabbbbcccc30006';
|
|
1504
|
+
writeAgentTranscript(
|
|
1505
|
+
sessionName,
|
|
1506
|
+
workingAgentId,
|
|
1507
|
+
pendingToolUseEntries('2026-06-27T11:45:00.000Z'),
|
|
1508
|
+
nowEpochSeconds - 600,
|
|
1509
|
+
);
|
|
1510
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
1511
|
+
createResolver(),
|
|
1512
|
+
emptyProcessLister(),
|
|
1513
|
+
now,
|
|
1514
|
+
livenessResolverWith([workingAgentId]),
|
|
1515
|
+
);
|
|
1516
|
+
|
|
1517
|
+
const result = await repository.listSubAgentActivitiesBySessionName(
|
|
1518
|
+
[sessionName],
|
|
1519
|
+
transcriptMapFor([sessionName]),
|
|
1520
|
+
);
|
|
1521
|
+
|
|
1522
|
+
expect(result.get(sessionName)).toEqual([
|
|
1523
|
+
{
|
|
1524
|
+
label: `agent-${workingAgentId}`,
|
|
1525
|
+
silentSeconds: 600,
|
|
1526
|
+
runningSeconds: 900,
|
|
1527
|
+
waitingOnExternalProcess: false,
|
|
1528
|
+
finishedResultUnconsumed: false,
|
|
1370
1529
|
},
|
|
1371
1530
|
]);
|
|
1372
1531
|
});
|
|
@@ -344,6 +344,7 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
344
344
|
filePath,
|
|
345
345
|
fileName,
|
|
346
346
|
nowEpochSeconds,
|
|
347
|
+
liveSubAgentIds !== null,
|
|
347
348
|
loadNormalizedProcessCommandLines,
|
|
348
349
|
);
|
|
349
350
|
if (activity !== null) {
|
|
@@ -357,6 +358,7 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
357
358
|
filePath: string,
|
|
358
359
|
fileName: string,
|
|
359
360
|
nowEpochSeconds: number,
|
|
361
|
+
listedInRunningSubAgentsRecord: boolean,
|
|
360
362
|
loadNormalizedProcessCommandLines: () => Promise<string[]>,
|
|
361
363
|
): Promise<SubAgentActivity | null> => {
|
|
362
364
|
let content: string;
|
|
@@ -368,7 +370,10 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
368
370
|
return null;
|
|
369
371
|
}
|
|
370
372
|
const transcript = parseTranscript(content);
|
|
371
|
-
if (
|
|
373
|
+
if (
|
|
374
|
+
transcript.lastEntryIndicatesCompletion &&
|
|
375
|
+
!listedInRunningSubAgentsRecord
|
|
376
|
+
) {
|
|
372
377
|
return null;
|
|
373
378
|
}
|
|
374
379
|
const silentSeconds = clampToZero(
|
|
@@ -388,6 +393,7 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
388
393
|
transcript.pendingToolCommands,
|
|
389
394
|
loadNormalizedProcessCommandLines,
|
|
390
395
|
)),
|
|
396
|
+
finishedResultUnconsumed: transcript.lastEntryIndicatesCompletion,
|
|
391
397
|
};
|
|
392
398
|
};
|
|
393
399
|
|