github-issue-tower-defence-management 1.148.24 → 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/.github/workflows/publish.yml +40 -7
- package/.prettierignore +1 -0
- package/CHANGELOG.md +20 -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/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +6 -2
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.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/RevertOrphanedPreparationUseCase.js +19 -0
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.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/scripts/defaultBranchTipVerify.sh +23 -0
- package/scripts/testWorkflowRunVerify.sh +47 -0
- package/src/adapter/ci/publishTestWorkflowGate.test.ts +653 -0
- 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/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +108 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +10 -2
- 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/NotifyFinishedIssuePreparationUseCase.test.ts +28 -0
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +146 -8
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +19 -10
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +212 -0
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +32 -0
- 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/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.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/RevertOrphanedPreparationUseCase.d.ts +3 -2
- package/types/domain/usecases/RevertOrphanedPreparationUseCase.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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { LocalCommandRunner } from './adapter-interfaces/LocalCommandRunner';
|
|
2
2
|
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
3
3
|
import { HttpRepository } from './adapter-interfaces/HttpRepository';
|
|
4
|
+
import { KevReportWatermarkRepository } from './adapter-interfaces/KevReportWatermarkRepository';
|
|
5
|
+
import { KevReportWatermark } from '../entities/KevReportWatermark';
|
|
4
6
|
import { Member } from '../entities/Member';
|
|
5
7
|
|
|
6
8
|
export type DailySecurityScanConfig = {
|
|
@@ -122,6 +124,40 @@ const renderScannerFindings = (
|
|
|
122
124
|
const KEV_CATALOG_URL =
|
|
123
125
|
'https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json';
|
|
124
126
|
|
|
127
|
+
const dayBeforeUtcYmd = (date: Date): string => {
|
|
128
|
+
const dayBefore = new Date(date);
|
|
129
|
+
dayBefore.setUTCDate(dayBefore.getUTCDate() - 1);
|
|
130
|
+
return dayBefore.toISOString().slice(0, 10);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const advanceWatermark = (
|
|
134
|
+
storedWatermark: KevReportWatermark | null,
|
|
135
|
+
reportedVulnerabilities: KevVulnerability[],
|
|
136
|
+
): KevReportWatermark => {
|
|
137
|
+
const lastReportedDateAdded = reportedVulnerabilities.reduce(
|
|
138
|
+
(latest, vulnerability) =>
|
|
139
|
+
vulnerability.dateAdded > latest ? vulnerability.dateAdded : latest,
|
|
140
|
+
storedWatermark === null ? '' : storedWatermark.lastReportedDateAdded,
|
|
141
|
+
);
|
|
142
|
+
const reportedCveIdsOnLastReportedDateAdded = new Set<string>(
|
|
143
|
+
storedWatermark !== null &&
|
|
144
|
+
storedWatermark.lastReportedDateAdded === lastReportedDateAdded
|
|
145
|
+
? storedWatermark.reportedCveIdsOnLastReportedDateAdded
|
|
146
|
+
: [],
|
|
147
|
+
);
|
|
148
|
+
for (const vulnerability of reportedVulnerabilities) {
|
|
149
|
+
if (vulnerability.dateAdded === lastReportedDateAdded) {
|
|
150
|
+
reportedCveIdsOnLastReportedDateAdded.add(vulnerability.cveID);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
lastReportedDateAdded,
|
|
155
|
+
reportedCveIdsOnLastReportedDateAdded: Array.from(
|
|
156
|
+
reportedCveIdsOnLastReportedDateAdded,
|
|
157
|
+
),
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
|
|
125
161
|
export class DailySecurityScanUseCase {
|
|
126
162
|
constructor(
|
|
127
163
|
readonly localCommandRunner: LocalCommandRunner,
|
|
@@ -130,6 +166,7 @@ export class DailySecurityScanUseCase {
|
|
|
130
166
|
'createNewIssue' | 'searchIssue' | 'createCommentByUrl'
|
|
131
167
|
>,
|
|
132
168
|
readonly httpRepository: HttpRepository,
|
|
169
|
+
readonly kevReportWatermarkRepository: KevReportWatermarkRepository,
|
|
133
170
|
) {}
|
|
134
171
|
|
|
135
172
|
run = async (input: {
|
|
@@ -344,9 +381,26 @@ export class DailySecurityScanUseCase {
|
|
|
344
381
|
return;
|
|
345
382
|
}
|
|
346
383
|
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
384
|
+
const watermarkLoadResult = await this.kevReportWatermarkRepository.load();
|
|
385
|
+
if (watermarkLoadResult.type === 'unreadable') {
|
|
386
|
+
console.error(
|
|
387
|
+
`Skipping the CISA KEV report for this run because ${watermarkLoadResult.reason}. The stored watermark is left unchanged and no issue was created; reporting resumes once the stored watermark is readable again.`,
|
|
388
|
+
);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const storedWatermark =
|
|
392
|
+
watermarkLoadResult.type === 'stored'
|
|
393
|
+
? watermarkLoadResult.watermark
|
|
394
|
+
: null;
|
|
395
|
+
const reportBoundaryDateAdded =
|
|
396
|
+
storedWatermark === null
|
|
397
|
+
? dayBeforeUtcYmd(lastTargetDate)
|
|
398
|
+
: storedWatermark.lastReportedDateAdded;
|
|
399
|
+
const alreadyReportedCveIdsOnBoundary = new Set<string>(
|
|
400
|
+
storedWatermark === null
|
|
401
|
+
? []
|
|
402
|
+
: storedWatermark.reportedCveIdsOnLastReportedDateAdded,
|
|
403
|
+
);
|
|
350
404
|
|
|
351
405
|
const kevJson = await this.httpRepository.get(KEV_CATALOG_URL);
|
|
352
406
|
const parsedKev: unknown = JSON.parse(kevJson);
|
|
@@ -356,10 +410,30 @@ export class DailySecurityScanUseCase {
|
|
|
356
410
|
);
|
|
357
411
|
}
|
|
358
412
|
|
|
359
|
-
const
|
|
360
|
-
(vulnerability) =>
|
|
413
|
+
const unreportedVulnerabilities = parsedKev.vulnerabilities.filter(
|
|
414
|
+
(vulnerability) =>
|
|
415
|
+
vulnerability.dateAdded > reportBoundaryDateAdded ||
|
|
416
|
+
(vulnerability.dateAdded === reportBoundaryDateAdded &&
|
|
417
|
+
!alreadyReportedCveIdsOnBoundary.has(vulnerability.cveID)),
|
|
418
|
+
);
|
|
419
|
+
if (unreportedVulnerabilities.length === 0) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const advancedWatermark = advanceWatermark(
|
|
423
|
+
storedWatermark,
|
|
424
|
+
unreportedVulnerabilities,
|
|
361
425
|
);
|
|
362
|
-
const
|
|
426
|
+
const saveAdvancedWatermark = async (): Promise<void> => {
|
|
427
|
+
try {
|
|
428
|
+
await this.kevReportWatermarkRepository.save(advancedWatermark);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
console.error(
|
|
431
|
+
`The CISA KEV additions considered in this run were not recorded because the KEV report watermark file did not advance to ${advancedWatermark.lastReportedDateAdded}: ${String(error)}. Those additions will be considered again on the next run, and the rest of the scheduled work continues.`,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const affectingKevEntries = unreportedVulnerabilities
|
|
363
437
|
.map((vulnerability) => ({
|
|
364
438
|
vulnerability,
|
|
365
439
|
affectedPackages: scannedVulnerablePackages.filter((scannedPackage) =>
|
|
@@ -368,13 +442,14 @@ export class DailySecurityScanUseCase {
|
|
|
368
442
|
}))
|
|
369
443
|
.filter((entry) => entry.affectedPackages.length > 0);
|
|
370
444
|
if (affectingKevEntries.length === 0) {
|
|
445
|
+
await saveAdvancedWatermark();
|
|
371
446
|
return;
|
|
372
447
|
}
|
|
373
448
|
|
|
374
449
|
await this.issueRepository.createNewIssue(
|
|
375
450
|
org,
|
|
376
451
|
config.kevReportRepo,
|
|
377
|
-
`CISA KEV new additions since ${
|
|
452
|
+
`CISA KEV new additions since ${reportBoundaryDateAdded}`,
|
|
378
453
|
affectingKevEntries
|
|
379
454
|
.map((entry) =>
|
|
380
455
|
[
|
|
@@ -389,5 +464,7 @@ export class DailySecurityScanUseCase {
|
|
|
389
464
|
[manager],
|
|
390
465
|
[],
|
|
391
466
|
);
|
|
467
|
+
|
|
468
|
+
await saveAdvancedWatermark();
|
|
392
469
|
};
|
|
393
470
|
}
|
|
@@ -15,6 +15,7 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
15
15
|
silentSeconds: 360,
|
|
16
16
|
runningSeconds: 1200,
|
|
17
17
|
waitingOnExternalProcess: false,
|
|
18
|
+
finishedResultUnconsumed: false,
|
|
18
19
|
};
|
|
19
20
|
const section = composer.composeSubAgentSection({
|
|
20
21
|
idleSubAgents: [subAgent],
|
|
@@ -23,6 +24,45 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
23
24
|
expect(section).toContain(SILENT_SESSION_REMINDER_SENTINEL);
|
|
24
25
|
});
|
|
25
26
|
|
|
27
|
+
it('embeds the reminder sentinel in the unconsumed-result section', () => {
|
|
28
|
+
const section = composer.composeSubAgentUnconsumedResultSection([
|
|
29
|
+
{
|
|
30
|
+
label: 'agent-aaabbbbcccc30001',
|
|
31
|
+
silentSeconds: 5220,
|
|
32
|
+
runningSeconds: 5400,
|
|
33
|
+
waitingOnExternalProcess: false,
|
|
34
|
+
finishedResultUnconsumed: true,
|
|
35
|
+
},
|
|
36
|
+
]);
|
|
37
|
+
expect(section).toContain(SILENT_SESSION_REMINDER_SENTINEL);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('names every finished sub-agent and the whole minutes its result has stayed unconsumed', () => {
|
|
41
|
+
const section = composer.composeSubAgentUnconsumedResultSection([
|
|
42
|
+
{
|
|
43
|
+
label: 'agent-aaabbbbcccc30001',
|
|
44
|
+
silentSeconds: 5220,
|
|
45
|
+
runningSeconds: 5400,
|
|
46
|
+
waitingOnExternalProcess: false,
|
|
47
|
+
finishedResultUnconsumed: true,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
label: 'agent-aaabbbbcccc30002',
|
|
51
|
+
silentSeconds: 359,
|
|
52
|
+
runningSeconds: 900,
|
|
53
|
+
waitingOnExternalProcess: false,
|
|
54
|
+
finishedResultUnconsumed: true,
|
|
55
|
+
},
|
|
56
|
+
]);
|
|
57
|
+
expect(section).toContain('This is an automated status check.');
|
|
58
|
+
expect(section).toContain(
|
|
59
|
+
'agent-aaabbbbcccc30001: result unconsumed for 87m',
|
|
60
|
+
);
|
|
61
|
+
expect(section).toContain(
|
|
62
|
+
'agent-aaabbbbcccc30002: result unconsumed for 5m',
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
26
66
|
it('renders the main-stalled message as a neutral automated status notice with the silent minutes substituted', () => {
|
|
27
67
|
const section = composer.composeMainStalledSection(600);
|
|
28
68
|
expect(section).toContain('This is an automated status check.');
|
|
@@ -148,6 +188,7 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
148
188
|
silentSeconds: 360,
|
|
149
189
|
runningSeconds: 1200,
|
|
150
190
|
waitingOnExternalProcess: false,
|
|
191
|
+
finishedResultUnconsumed: false,
|
|
151
192
|
};
|
|
152
193
|
const section = composer.composeSubAgentSection({
|
|
153
194
|
idleSubAgents: [subAgent],
|
|
@@ -258,6 +299,7 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
258
299
|
silentSeconds: 360,
|
|
259
300
|
runningSeconds: 60,
|
|
260
301
|
waitingOnExternalProcess: false,
|
|
302
|
+
finishedResultUnconsumed: false,
|
|
261
303
|
},
|
|
262
304
|
],
|
|
263
305
|
longRunningSubAgents: [],
|
|
@@ -277,6 +319,7 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
277
319
|
silentSeconds: 360,
|
|
278
320
|
runningSeconds: 60,
|
|
279
321
|
waitingOnExternalProcess: false,
|
|
322
|
+
finishedResultUnconsumed: false,
|
|
280
323
|
},
|
|
281
324
|
],
|
|
282
325
|
longRunningSubAgents: [],
|
|
@@ -296,6 +339,7 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
296
339
|
silentSeconds: 360,
|
|
297
340
|
runningSeconds: 60,
|
|
298
341
|
waitingOnExternalProcess: false,
|
|
342
|
+
finishedResultUnconsumed: false,
|
|
299
343
|
},
|
|
300
344
|
],
|
|
301
345
|
longRunningSubAgents: [],
|
|
@@ -317,6 +361,7 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
317
361
|
silentSeconds: 30,
|
|
318
362
|
runningSeconds: 1200,
|
|
319
363
|
waitingOnExternalProcess: false,
|
|
364
|
+
finishedResultUnconsumed: false,
|
|
320
365
|
},
|
|
321
366
|
],
|
|
322
367
|
});
|
|
@@ -340,6 +385,7 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
340
385
|
silentSeconds: 30,
|
|
341
386
|
runningSeconds: 1200,
|
|
342
387
|
waitingOnExternalProcess: false,
|
|
388
|
+
finishedResultUnconsumed: false,
|
|
343
389
|
},
|
|
344
390
|
],
|
|
345
391
|
});
|
|
@@ -353,6 +399,7 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
353
399
|
silentSeconds: 360,
|
|
354
400
|
runningSeconds: 1200,
|
|
355
401
|
waitingOnExternalProcess: false,
|
|
402
|
+
finishedResultUnconsumed: false,
|
|
356
403
|
};
|
|
357
404
|
const section = composer.composeSubAgentSection({
|
|
358
405
|
idleSubAgents: [subAgent],
|
|
@@ -374,6 +421,7 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
374
421
|
silentSeconds: 360,
|
|
375
422
|
runningSeconds: 60,
|
|
376
423
|
waitingOnExternalProcess: false,
|
|
424
|
+
finishedResultUnconsumed: false,
|
|
377
425
|
},
|
|
378
426
|
],
|
|
379
427
|
longRunningSubAgents: [
|
|
@@ -382,6 +430,7 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
382
430
|
silentSeconds: 10,
|
|
383
431
|
runningSeconds: 960,
|
|
384
432
|
waitingOnExternalProcess: false,
|
|
433
|
+
finishedResultUnconsumed: false,
|
|
385
434
|
},
|
|
386
435
|
],
|
|
387
436
|
});
|
|
@@ -411,6 +460,7 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
411
460
|
silentSeconds: 360,
|
|
412
461
|
runningSeconds: 1200,
|
|
413
462
|
waitingOnExternalProcess: false,
|
|
463
|
+
finishedResultUnconsumed: false,
|
|
414
464
|
};
|
|
415
465
|
const composedDefaultTexts = [
|
|
416
466
|
composer.composeMainStalledSection(600),
|
|
@@ -419,6 +469,7 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
419
469
|
idleSubAgents: [subAgent],
|
|
420
470
|
longRunningSubAgents: [subAgent],
|
|
421
471
|
}),
|
|
472
|
+
composer.composeSubAgentUnconsumedResultSection([subAgent]),
|
|
422
473
|
];
|
|
423
474
|
for (const text of composedDefaultTexts) {
|
|
424
475
|
for (const pattern of flaggedPatterns) {
|
|
@@ -435,6 +486,7 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
435
486
|
silentSeconds: 360,
|
|
436
487
|
runningSeconds: 1200,
|
|
437
488
|
waitingOnExternalProcess: false,
|
|
489
|
+
finishedResultUnconsumed: false,
|
|
438
490
|
};
|
|
439
491
|
const composedDefaultTexts = [
|
|
440
492
|
composer.composeMainStalledSection(600),
|
|
@@ -443,6 +495,7 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
443
495
|
idleSubAgents: [subAgent],
|
|
444
496
|
longRunningSubAgents: [subAgent],
|
|
445
497
|
}),
|
|
498
|
+
composer.composeSubAgentUnconsumedResultSection([subAgent]),
|
|
446
499
|
];
|
|
447
500
|
for (const text of composedDefaultTexts) {
|
|
448
501
|
expect(text).not.toContain('<');
|
|
@@ -462,12 +515,16 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
462
515
|
silentSeconds: 360,
|
|
463
516
|
runningSeconds: 1200,
|
|
464
517
|
waitingOnExternalProcess: false,
|
|
518
|
+
finishedResultUnconsumed: false,
|
|
465
519
|
};
|
|
466
520
|
const subSection = composer.composeSubAgentSection({
|
|
467
521
|
idleSubAgents: [subAgent],
|
|
468
522
|
longRunningSubAgents: [subAgent],
|
|
469
523
|
});
|
|
470
|
-
const
|
|
524
|
+
const unconsumedResultSection =
|
|
525
|
+
composer.composeSubAgentUnconsumedResultSection([subAgent]);
|
|
526
|
+
const combined =
|
|
527
|
+
`${mainSection}\n${subSection}\n${unconsumedResultSection}`.toLowerCase();
|
|
471
528
|
expect(combined).not.toContain('claude');
|
|
472
529
|
expect(combined).not.toContain('take ownership');
|
|
473
530
|
expect(combined).not.toContain('/home/');
|
|
@@ -42,6 +42,22 @@ const composeLongRunningSubAgentSection = (
|
|
|
42
42
|
].join('\n');
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
+
const composeUnconsumedResultSubAgentSection = (
|
|
46
|
+
unconsumedResultSubAgents: SubAgentActivity[],
|
|
47
|
+
): string => {
|
|
48
|
+
const lines = unconsumedResultSubAgents.map(
|
|
49
|
+
(subAgent) =>
|
|
50
|
+
`- ${subAgent.label}: result unconsumed for ${formatMinutes(
|
|
51
|
+
subAgent.silentSeconds,
|
|
52
|
+
)}`,
|
|
53
|
+
);
|
|
54
|
+
return [
|
|
55
|
+
`${SILENT_SESSION_REMINDER_SENTINEL} This is an automated status check. The following sub-process(es) have finished and their results have been waiting unread for about the minutes shown:`,
|
|
56
|
+
...lines,
|
|
57
|
+
'For each one: please read its result, act on it, and then mark the sub-process as finished so it is no longer tracked as running. If you have already acted on the result, marking it finished is all that remains.',
|
|
58
|
+
].join('\n');
|
|
59
|
+
};
|
|
60
|
+
|
|
45
61
|
export const composeOwnerCallFormatGuidance = (): string => {
|
|
46
62
|
return 'Please share it through a new owner-call in the format documented for this session, written to be self-contained so the owner can understand the situation from that single message.';
|
|
47
63
|
};
|
|
@@ -98,4 +114,9 @@ export class DefaultSilentSessionMessageComposer implements SilentSessionMessage
|
|
|
98
114
|
}
|
|
99
115
|
return sections.join('\n\n');
|
|
100
116
|
};
|
|
117
|
+
|
|
118
|
+
composeSubAgentUnconsumedResultSection = (
|
|
119
|
+
unconsumedResultSubAgents: SubAgentActivity[],
|
|
120
|
+
): string =>
|
|
121
|
+
composeUnconsumedResultSubAgentSection(unconsumedResultSubAgents);
|
|
101
122
|
}
|
|
@@ -281,6 +281,34 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
281
281
|
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
282
282
|
});
|
|
283
283
|
|
|
284
|
+
it('should read the issue scoped to the project it was given and throw IssueNotFoundError when the issue has no item on that project', async () => {
|
|
285
|
+
const issueUrlOnAnotherProjectOnly =
|
|
286
|
+
'https://github.com/user/repo/issues/7';
|
|
287
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
288
|
+
mockIssueRepository.get.mockImplementation(
|
|
289
|
+
async (issueUrl: string, project: Project) =>
|
|
290
|
+
project.id === mockProject.id
|
|
291
|
+
? null
|
|
292
|
+
: createMockIssue({ url: issueUrl, status: 'Preparation' }),
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
await expect(
|
|
296
|
+
useCase.run({
|
|
297
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
298
|
+
issueUrl: issueUrlOnAnotherProjectOnly,
|
|
299
|
+
thresholdForAutoReject: 3,
|
|
300
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
301
|
+
allowedIssueAuthors: null,
|
|
302
|
+
}),
|
|
303
|
+
).rejects.toThrow(`Issue not found: ${issueUrlOnAnotherProjectOnly}`);
|
|
304
|
+
expect(mockIssueRepository.get.mock.calls).toEqual([
|
|
305
|
+
[issueUrlOnAnotherProjectOnly, mockProject],
|
|
306
|
+
]);
|
|
307
|
+
expect(mockIssueRepository.update).not.toHaveBeenCalled();
|
|
308
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
309
|
+
expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
|
|
310
|
+
});
|
|
311
|
+
|
|
284
312
|
it('should process a pull request URL the same as an issue URL when the project item resolves', async () => {
|
|
285
313
|
const prIssue = createMockIssue({
|
|
286
314
|
url: 'https://github.com/user/repo/pull/77',
|
|
@@ -33,6 +33,7 @@ const MAIN_STALLED_SECTION = 'MAIN_STALLED_SECTION';
|
|
|
33
33
|
const MAIN_STALLED_STALE_OWNER_CALL_SECTION =
|
|
34
34
|
'MAIN_STALLED_STALE_OWNER_CALL_SECTION';
|
|
35
35
|
const SUBAGENT_SECTION = 'SUBAGENT_SECTION';
|
|
36
|
+
const SUBAGENT_UNCONSUMED_RESULT_SECTION = 'SUBAGENT_UNCONSUMED_RESULT_SECTION';
|
|
36
37
|
|
|
37
38
|
class EveryNameRecentSet extends Set<string> {
|
|
38
39
|
override has = (): boolean => true;
|
|
@@ -176,6 +177,9 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
176
177
|
.fn()
|
|
177
178
|
.mockReturnValue(MAIN_STALLED_STALE_OWNER_CALL_SECTION),
|
|
178
179
|
composeSubAgentSection: jest.fn().mockReturnValue(SUBAGENT_SECTION),
|
|
180
|
+
composeSubAgentUnconsumedResultSection: jest
|
|
181
|
+
.fn()
|
|
182
|
+
.mockReturnValue(SUBAGENT_UNCONSUMED_RESULT_SECTION),
|
|
179
183
|
};
|
|
180
184
|
mockSleeper = {
|
|
181
185
|
sleep: jest.fn().mockResolvedValue(undefined),
|
|
@@ -571,6 +575,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
571
575
|
silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
572
576
|
runningSeconds: 60,
|
|
573
577
|
waitingOnExternalProcess: false,
|
|
578
|
+
finishedResultUnconsumed: false,
|
|
574
579
|
},
|
|
575
580
|
];
|
|
576
581
|
mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName.mockResolvedValue(
|
|
@@ -596,6 +601,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
596
601
|
silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
597
602
|
runningSeconds: 600,
|
|
598
603
|
waitingOnExternalProcess: false,
|
|
604
|
+
finishedResultUnconsumed: false,
|
|
599
605
|
},
|
|
600
606
|
];
|
|
601
607
|
mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName.mockResolvedValue(
|
|
@@ -613,7 +619,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
613
619
|
});
|
|
614
620
|
});
|
|
615
621
|
|
|
616
|
-
it('
|
|
622
|
+
it('reports a sub-agent past the running threshold that produced output moments ago', async () => {
|
|
617
623
|
setupLiveInteractiveSession(GITHUB_SESSION);
|
|
618
624
|
const subAgents: SubAgentActivity[] = [
|
|
619
625
|
{
|
|
@@ -621,6 +627,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
621
627
|
silentSeconds: 10,
|
|
622
628
|
runningSeconds: DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
|
|
623
629
|
waitingOnExternalProcess: false,
|
|
630
|
+
finishedResultUnconsumed: false,
|
|
624
631
|
},
|
|
625
632
|
];
|
|
626
633
|
mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName.mockResolvedValue(
|
|
@@ -629,10 +636,13 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
629
636
|
|
|
630
637
|
await useCase.run(runParams());
|
|
631
638
|
|
|
632
|
-
expect(mockMessageComposer.composeSubAgentSection).
|
|
639
|
+
expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalledWith({
|
|
640
|
+
idleSubAgents: [],
|
|
641
|
+
longRunningSubAgents: subAgents,
|
|
642
|
+
});
|
|
633
643
|
expect(
|
|
634
644
|
mockNotificationRepository.sendSelfCheckNotification,
|
|
635
|
-
).
|
|
645
|
+
).toHaveBeenCalledWith(GITHUB_SESSION, SUBAGENT_SECTION);
|
|
636
646
|
});
|
|
637
647
|
|
|
638
648
|
it('excludes an owner-handover spawn from selection so no notification is sent', async () => {
|
|
@@ -981,12 +991,14 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
981
991
|
silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
982
992
|
runningSeconds: 60,
|
|
983
993
|
waitingOnExternalProcess: false,
|
|
994
|
+
finishedResultUnconsumed: false,
|
|
984
995
|
});
|
|
985
996
|
const waitingSubAgent = (label: string): SubAgentActivity => ({
|
|
986
997
|
label,
|
|
987
998
|
silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
988
999
|
runningSeconds: 60,
|
|
989
1000
|
waitingOnExternalProcess: true,
|
|
1001
|
+
finishedResultUnconsumed: false,
|
|
990
1002
|
});
|
|
991
1003
|
const quietLongRunningSubAgent = (
|
|
992
1004
|
label: string,
|
|
@@ -996,12 +1008,14 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
996
1008
|
silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
997
1009
|
runningSeconds: DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
|
|
998
1010
|
waitingOnExternalProcess,
|
|
1011
|
+
finishedResultUnconsumed: false,
|
|
999
1012
|
});
|
|
1000
1013
|
const producingLongRunningSubAgent = (label: string): SubAgentActivity => ({
|
|
1001
1014
|
label,
|
|
1002
1015
|
silentSeconds: 30,
|
|
1003
1016
|
runningSeconds: DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
|
|
1004
1017
|
waitingOnExternalProcess: false,
|
|
1018
|
+
finishedResultUnconsumed: false,
|
|
1005
1019
|
});
|
|
1006
1020
|
|
|
1007
1021
|
const setupSubAgents = (subAgents: SubAgentActivity[]): void => {
|
|
@@ -1011,6 +1025,26 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
1011
1025
|
);
|
|
1012
1026
|
};
|
|
1013
1027
|
|
|
1028
|
+
it('reports every long-running sub-agent on runtime alone, whether it keeps producing output or waits on an external process', async () => {
|
|
1029
|
+
setupSubAgents([
|
|
1030
|
+
producingLongRunningSubAgent('sub-process-producing-output'),
|
|
1031
|
+
quietLongRunningSubAgent('sub-process-waiting-on-external', true),
|
|
1032
|
+
]);
|
|
1033
|
+
|
|
1034
|
+
await useCase.run(runParams());
|
|
1035
|
+
|
|
1036
|
+
expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalledWith({
|
|
1037
|
+
idleSubAgents: [],
|
|
1038
|
+
longRunningSubAgents: [
|
|
1039
|
+
producingLongRunningSubAgent('sub-process-producing-output'),
|
|
1040
|
+
quietLongRunningSubAgent('sub-process-waiting-on-external', true),
|
|
1041
|
+
],
|
|
1042
|
+
});
|
|
1043
|
+
expect(
|
|
1044
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
1045
|
+
).toHaveBeenCalledWith(GITHUB_SESSION, SUBAGENT_SECTION);
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1014
1048
|
it('never selects a waiting sub-agent as a silent-reminder candidate', async () => {
|
|
1015
1049
|
setupSubAgents([waitingSubAgent('sub-process-1')]);
|
|
1016
1050
|
|
|
@@ -1047,19 +1081,119 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
1047
1081
|
).toHaveBeenCalledTimes(2);
|
|
1048
1082
|
});
|
|
1049
1083
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1084
|
+
const unconsumedResultSubAgent = (label: string): SubAgentActivity => ({
|
|
1085
|
+
label,
|
|
1086
|
+
silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
1087
|
+
runningSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS + 60,
|
|
1088
|
+
waitingOnExternalProcess: false,
|
|
1089
|
+
finishedResultUnconsumed: true,
|
|
1090
|
+
});
|
|
1091
|
+
const justFinishedSubAgent = (label: string): SubAgentActivity => ({
|
|
1092
|
+
label,
|
|
1093
|
+
silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS - 1,
|
|
1094
|
+
runningSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS + 60,
|
|
1095
|
+
waitingOnExternalProcess: false,
|
|
1096
|
+
finishedResultUnconsumed: true,
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
const setupRecentlyProducingMainSession = (sessionName: string): void => {
|
|
1100
|
+
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
1101
|
+
[{ sessionName, lastOutputEpochSeconds: nowEpochSeconds - 60 }],
|
|
1102
|
+
);
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1105
|
+
it('notifies a session whose finished sub-agent result stayed unconsumed past the threshold while the main session keeps producing output', async () => {
|
|
1106
|
+
setupSubAgents([unconsumedResultSubAgent('agent-aaabbbbcccc30001')]);
|
|
1107
|
+
setupRecentlyProducingMainSession(GITHUB_SESSION);
|
|
1108
|
+
|
|
1109
|
+
await useCase.run(runParams());
|
|
1110
|
+
|
|
1111
|
+
expect(
|
|
1112
|
+
mockMessageComposer.composeSubAgentUnconsumedResultSection,
|
|
1113
|
+
).toHaveBeenCalledWith([
|
|
1114
|
+
unconsumedResultSubAgent('agent-aaabbbbcccc30001'),
|
|
1115
|
+
]);
|
|
1116
|
+
expect(
|
|
1117
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
1118
|
+
).toHaveBeenCalledWith(
|
|
1119
|
+
GITHUB_SESSION,
|
|
1120
|
+
SUBAGENT_UNCONSUMED_RESULT_SECTION,
|
|
1121
|
+
);
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
it('never selects a finished sub-agent whose result has been unconsumed for less than the threshold', async () => {
|
|
1125
|
+
setupSubAgents([justFinishedSubAgent('agent-aaabbbbcccc30002')]);
|
|
1126
|
+
setupRecentlyProducingMainSession(GITHUB_SESSION);
|
|
1052
1127
|
|
|
1053
1128
|
await useCase.run(runParams());
|
|
1054
1129
|
|
|
1055
|
-
expect(
|
|
1130
|
+
expect(
|
|
1131
|
+
mockMessageComposer.composeSubAgentUnconsumedResultSection,
|
|
1132
|
+
).not.toHaveBeenCalled();
|
|
1056
1133
|
expect(
|
|
1057
1134
|
mockNotificationRepository.sendSelfCheckNotification,
|
|
1058
1135
|
).not.toHaveBeenCalled();
|
|
1059
1136
|
});
|
|
1060
1137
|
|
|
1061
|
-
it('never selects a
|
|
1062
|
-
setupSubAgents([
|
|
1138
|
+
it('never selects a still-working idle sub-agent as an unconsumed result', async () => {
|
|
1139
|
+
setupSubAgents([hungSubAgent('sub-process-1')]);
|
|
1140
|
+
|
|
1141
|
+
await useCase.run(runParams());
|
|
1142
|
+
|
|
1143
|
+
expect(
|
|
1144
|
+
mockMessageComposer.composeSubAgentUnconsumedResultSection,
|
|
1145
|
+
).not.toHaveBeenCalled();
|
|
1146
|
+
expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalledWith({
|
|
1147
|
+
idleSubAgents: [hungSubAgent('sub-process-1')],
|
|
1148
|
+
longRunningSubAgents: [],
|
|
1149
|
+
});
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
it('keeps an unconsumed finished sub-agent out of the idle and long-running sections', async () => {
|
|
1153
|
+
setupSubAgents([unconsumedResultSubAgent('agent-aaabbbbcccc30003')]);
|
|
1154
|
+
|
|
1155
|
+
await useCase.run(runParams());
|
|
1156
|
+
|
|
1157
|
+
expect(mockMessageComposer.composeSubAgentSection).not.toHaveBeenCalled();
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
it('delivers the idle section and the unconsumed-result section together when both conditions hold', async () => {
|
|
1161
|
+
setupSubAgents([
|
|
1162
|
+
hungSubAgent('sub-process-1'),
|
|
1163
|
+
unconsumedResultSubAgent('agent-aaabbbbcccc30004'),
|
|
1164
|
+
]);
|
|
1165
|
+
|
|
1166
|
+
await useCase.run(runParams());
|
|
1167
|
+
|
|
1168
|
+
expect(
|
|
1169
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
1170
|
+
).toHaveBeenCalledWith(
|
|
1171
|
+
GITHUB_SESSION,
|
|
1172
|
+
`${SUBAGENT_SECTION}\n\n${SUBAGENT_UNCONSUMED_RESULT_SECTION}`,
|
|
1173
|
+
);
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
it('reports a waiting long-running sub-agent as long-running while keeping it out of the idle section', async () => {
|
|
1177
|
+
setupSubAgents([quietLongRunningSubAgent('sub-process-1', true)]);
|
|
1178
|
+
|
|
1179
|
+
await useCase.run(runParams());
|
|
1180
|
+
|
|
1181
|
+
expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalledWith({
|
|
1182
|
+
idleSubAgents: [],
|
|
1183
|
+
longRunningSubAgents: [quietLongRunningSubAgent('sub-process-1', true)],
|
|
1184
|
+
});
|
|
1185
|
+
expect(
|
|
1186
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
1187
|
+
).toHaveBeenCalledWith(GITHUB_SESSION, SUBAGENT_SECTION);
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
it('never selects a producing sub-agent that is one second short of the running threshold', async () => {
|
|
1191
|
+
setupSubAgents([
|
|
1192
|
+
{
|
|
1193
|
+
...producingLongRunningSubAgent('sub-process-1'),
|
|
1194
|
+
runningSeconds: DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS - 1,
|
|
1195
|
+
},
|
|
1196
|
+
]);
|
|
1063
1197
|
|
|
1064
1198
|
await useCase.run(runParams());
|
|
1065
1199
|
|
|
@@ -1677,6 +1811,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
1677
1811
|
silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
1678
1812
|
runningSeconds: 60,
|
|
1679
1813
|
waitingOnExternalProcess: false,
|
|
1814
|
+
finishedResultUnconsumed: false,
|
|
1680
1815
|
},
|
|
1681
1816
|
];
|
|
1682
1817
|
mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName.mockResolvedValue(
|
|
@@ -1806,12 +1941,14 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
1806
1941
|
silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
1807
1942
|
runningSeconds: 60,
|
|
1808
1943
|
waitingOnExternalProcess: false,
|
|
1944
|
+
finishedResultUnconsumed: false,
|
|
1809
1945
|
};
|
|
1810
1946
|
const quietLongRunningSubAgent: SubAgentActivity = {
|
|
1811
1947
|
label: 'agent-long-1',
|
|
1812
1948
|
silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
1813
1949
|
runningSeconds: DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
|
|
1814
1950
|
waitingOnExternalProcess: false,
|
|
1951
|
+
finishedResultUnconsumed: false,
|
|
1815
1952
|
};
|
|
1816
1953
|
mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName.mockResolvedValue(
|
|
1817
1954
|
new Map([
|
|
@@ -1838,6 +1975,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
1838
1975
|
silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
1839
1976
|
runningSeconds: 60,
|
|
1840
1977
|
waitingOnExternalProcess: false,
|
|
1978
|
+
finishedResultUnconsumed: false,
|
|
1841
1979
|
},
|
|
1842
1980
|
],
|
|
1843
1981
|
],
|