github-issue-tower-defence-management 1.122.3 → 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 +8 -0
- package/README.md +3 -3
- 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/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/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/console/consoleDoneStore.test.ts +38 -0
- package/src/adapter/entry-points/console/consoleDoneStore.ts +29 -4
- 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/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/SilentSessionCandidateStateRepository.ts +8 -0
- package/src/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.ts +4 -7
- 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/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/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/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
|
@@ -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
|
});
|
|
@@ -2,6 +2,7 @@ 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 { SubAgentProcessLister } from '../../domain/usecases/adapter-interfaces/SubAgentProcessLister';
|
|
5
6
|
import { SubAgentTranscriptDirectoryResolver } from '../../domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver';
|
|
6
7
|
|
|
7
8
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
@@ -23,9 +24,18 @@ const parseEpochSeconds = (timestamp: string | null): number | null => {
|
|
|
23
24
|
return Number.isNaN(parsed) ? null : Math.floor(parsed / 1000);
|
|
24
25
|
};
|
|
25
26
|
|
|
27
|
+
const PENDING_TOOL_COMMAND_FRAGMENT_LENGTH = 60;
|
|
28
|
+
|
|
29
|
+
export const normalizeCommandFragment = (command: string): string =>
|
|
30
|
+
command
|
|
31
|
+
.replace(/\s+/g, ' ')
|
|
32
|
+
.trim()
|
|
33
|
+
.slice(0, PENDING_TOOL_COMMAND_FRAGMENT_LENGTH);
|
|
34
|
+
|
|
26
35
|
type ParsedTranscript = {
|
|
27
36
|
firstEntryEpochSeconds: number | null;
|
|
28
37
|
lastEntryIndicatesCompletion: boolean;
|
|
38
|
+
pendingToolCommands: string[];
|
|
29
39
|
};
|
|
30
40
|
|
|
31
41
|
const readContentBlocks = (
|
|
@@ -65,9 +75,33 @@ const entryIndicatesCompletion = (entry: Record<string, unknown>): boolean => {
|
|
|
65
75
|
return false;
|
|
66
76
|
};
|
|
67
77
|
|
|
78
|
+
const entryPendingToolCommands = (entry: Record<string, unknown>): string[] => {
|
|
79
|
+
const type = readString(entry, 'type');
|
|
80
|
+
const message = entry.message;
|
|
81
|
+
if (type !== 'assistant' || !isRecord(message)) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
const commands: string[] = [];
|
|
85
|
+
for (const block of readContentBlocks(message)) {
|
|
86
|
+
if (readString(block, 'type') !== 'tool_use') {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const input = block.input;
|
|
90
|
+
if (!isRecord(input)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const command = readString(input, 'command');
|
|
94
|
+
if (command !== null && command.trim().length > 0) {
|
|
95
|
+
commands.push(command);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return commands;
|
|
99
|
+
};
|
|
100
|
+
|
|
68
101
|
const parseTranscript = (content: string): ParsedTranscript => {
|
|
69
102
|
let firstEntryEpochSeconds: number | null = null;
|
|
70
103
|
let lastEntryIndicatesCompletion = false;
|
|
104
|
+
let pendingToolCommands: string[] = [];
|
|
71
105
|
for (const line of content.split('\n')) {
|
|
72
106
|
const trimmed = line.trim();
|
|
73
107
|
if (trimmed.length === 0) {
|
|
@@ -88,9 +122,14 @@ const parseTranscript = (content: string): ParsedTranscript => {
|
|
|
88
122
|
}
|
|
89
123
|
if (isRecord(parsed.message)) {
|
|
90
124
|
lastEntryIndicatesCompletion = entryIndicatesCompletion(parsed);
|
|
125
|
+
pendingToolCommands = entryPendingToolCommands(parsed);
|
|
91
126
|
}
|
|
92
127
|
}
|
|
93
|
-
return {
|
|
128
|
+
return {
|
|
129
|
+
firstEntryEpochSeconds,
|
|
130
|
+
lastEntryIndicatesCompletion,
|
|
131
|
+
pendingToolCommands,
|
|
132
|
+
};
|
|
94
133
|
};
|
|
95
134
|
|
|
96
135
|
const clampToZero = (value: number): number => (value > 0 ? value : 0);
|
|
@@ -98,6 +137,7 @@ const clampToZero = (value: number): number => (value > 0 ? value : 0);
|
|
|
98
137
|
export class TranscriptSessionSubAgentActivityRepository implements SessionSubAgentActivityRepository {
|
|
99
138
|
constructor(
|
|
100
139
|
private readonly directoryResolver: SubAgentTranscriptDirectoryResolver,
|
|
140
|
+
private readonly processLister: SubAgentProcessLister,
|
|
101
141
|
private readonly now: Date,
|
|
102
142
|
) {}
|
|
103
143
|
|
|
@@ -107,6 +147,16 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
107
147
|
): Promise<Map<string, SubAgentActivity[]>> => {
|
|
108
148
|
const result = new Map<string, SubAgentActivity[]>();
|
|
109
149
|
const nowEpochSeconds = Math.floor(this.now.getTime() / 1000);
|
|
150
|
+
let normalizedProcessCommandLines: string[] | null = null;
|
|
151
|
+
const loadNormalizedProcessCommandLines = async (): Promise<string[]> => {
|
|
152
|
+
if (normalizedProcessCommandLines === null) {
|
|
153
|
+
const processes = await this.processLister.listProcesses();
|
|
154
|
+
normalizedProcessCommandLines = processes.map((process) =>
|
|
155
|
+
process.commandLine.replace(/\s+/g, ' ').trim(),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return normalizedProcessCommandLines;
|
|
159
|
+
};
|
|
110
160
|
for (const sessionName of sessionNames) {
|
|
111
161
|
const mainTranscriptPath =
|
|
112
162
|
transcriptPathBySessionName.get(sessionName) ?? null;
|
|
@@ -117,7 +167,11 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
117
167
|
if (directory === null) {
|
|
118
168
|
continue;
|
|
119
169
|
}
|
|
120
|
-
const activities = this.collectActivities(
|
|
170
|
+
const activities = await this.collectActivities(
|
|
171
|
+
directory,
|
|
172
|
+
nowEpochSeconds,
|
|
173
|
+
loadNormalizedProcessCommandLines,
|
|
174
|
+
);
|
|
121
175
|
if (activities.length > 0) {
|
|
122
176
|
result.set(sessionName, activities);
|
|
123
177
|
}
|
|
@@ -125,10 +179,11 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
125
179
|
return result;
|
|
126
180
|
};
|
|
127
181
|
|
|
128
|
-
private collectActivities = (
|
|
182
|
+
private collectActivities = async (
|
|
129
183
|
directory: string,
|
|
130
184
|
nowEpochSeconds: number,
|
|
131
|
-
|
|
185
|
+
loadNormalizedProcessCommandLines: () => Promise<string[]>,
|
|
186
|
+
): Promise<SubAgentActivity[]> => {
|
|
132
187
|
let entries: fs.Dirent[];
|
|
133
188
|
try {
|
|
134
189
|
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
@@ -142,7 +197,12 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
142
197
|
continue;
|
|
143
198
|
}
|
|
144
199
|
const filePath = path.join(directory, fileName);
|
|
145
|
-
const activity = this.toActivity(
|
|
200
|
+
const activity = await this.toActivity(
|
|
201
|
+
filePath,
|
|
202
|
+
fileName,
|
|
203
|
+
nowEpochSeconds,
|
|
204
|
+
loadNormalizedProcessCommandLines,
|
|
205
|
+
);
|
|
146
206
|
if (activity !== null) {
|
|
147
207
|
activities.push(activity);
|
|
148
208
|
}
|
|
@@ -150,11 +210,12 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
150
210
|
return activities;
|
|
151
211
|
};
|
|
152
212
|
|
|
153
|
-
private toActivity = (
|
|
213
|
+
private toActivity = async (
|
|
154
214
|
filePath: string,
|
|
155
215
|
fileName: string,
|
|
156
216
|
nowEpochSeconds: number,
|
|
157
|
-
|
|
217
|
+
loadNormalizedProcessCommandLines: () => Promise<string[]>,
|
|
218
|
+
): Promise<SubAgentActivity | null> => {
|
|
158
219
|
let content: string;
|
|
159
220
|
let stats: fs.Stats;
|
|
160
221
|
try {
|
|
@@ -178,6 +239,29 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
178
239
|
label: fileName.replace(/\.jsonl$/, ''),
|
|
179
240
|
silentSeconds,
|
|
180
241
|
runningSeconds,
|
|
242
|
+
waitingOnExternalProcess: await this.hasLiveMatchingProcess(
|
|
243
|
+
transcript.pendingToolCommands,
|
|
244
|
+
loadNormalizedProcessCommandLines,
|
|
245
|
+
),
|
|
181
246
|
};
|
|
182
247
|
};
|
|
248
|
+
|
|
249
|
+
private hasLiveMatchingProcess = async (
|
|
250
|
+
pendingToolCommands: string[],
|
|
251
|
+
loadNormalizedProcessCommandLines: () => Promise<string[]>,
|
|
252
|
+
): Promise<boolean> => {
|
|
253
|
+
if (pendingToolCommands.length === 0) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
const fragments = pendingToolCommands
|
|
257
|
+
.map(normalizeCommandFragment)
|
|
258
|
+
.filter((fragment) => fragment.length > 0);
|
|
259
|
+
if (fragments.length === 0) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
const processCommandLines = await loadNormalizedProcessCommandLines();
|
|
263
|
+
return fragments.some((fragment) =>
|
|
264
|
+
processCommandLines.some((commandLine) => commandLine.includes(fragment)),
|
|
265
|
+
);
|
|
266
|
+
};
|
|
183
267
|
}
|