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.
Files changed (62) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +3 -3
  3. package/bin/adapter/entry-points/console/consoleDoneStore.js +19 -5
  4. package/bin/adapter/entry-points/console/consoleDoneStore.js.map +1 -1
  5. package/bin/adapter/entry-points/handlers/consoleListsWriter.js +2 -0
  6. package/bin/adapter/entry-points/handlers/consoleListsWriter.js.map +1 -1
  7. package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +1 -1
  8. package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -1
  9. package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js +6 -7
  10. package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js.map +1 -1
  11. package/bin/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.js +60 -11
  12. package/bin/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.js.map +1 -1
  13. package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js +1 -0
  14. package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js.map +1 -1
  15. package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js +64 -7
  16. package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js.map +1 -1
  17. package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js +5 -7
  18. package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js.map +1 -1
  19. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +51 -16
  20. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
  21. package/package.json +1 -1
  22. package/src/adapter/entry-points/console/consoleDoneStore.test.ts +38 -0
  23. package/src/adapter/entry-points/console/consoleDoneStore.ts +29 -4
  24. package/src/adapter/entry-points/handlers/consoleListsWriter.test.ts +72 -0
  25. package/src/adapter/entry-points/handlers/consoleListsWriter.ts +3 -0
  26. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +1 -0
  27. package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.test.ts +60 -37
  28. package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts +7 -18
  29. package/src/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.test.ts +223 -0
  30. package/src/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.ts +94 -10
  31. package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.test.ts +12 -2
  32. package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.ts +1 -0
  33. package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.test.ts +196 -4
  34. package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts +91 -7
  35. package/src/domain/entities/LiveSessionActivitySnapshot.ts +1 -0
  36. package/src/domain/usecases/DefaultSilentSessionMessageComposer.test.ts +112 -51
  37. package/src/domain/usecases/DefaultSilentSessionMessageComposer.ts +8 -17
  38. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +250 -18
  39. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +72 -22
  40. package/src/domain/usecases/adapter-interfaces/SilentSessionCandidateStateRepository.ts +8 -0
  41. package/src/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.ts +4 -7
  42. package/types/adapter/entry-points/console/consoleDoneStore.d.ts +2 -0
  43. package/types/adapter/entry-points/console/consoleDoneStore.d.ts.map +1 -1
  44. package/types/adapter/entry-points/handlers/consoleListsWriter.d.ts.map +1 -1
  45. package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -1
  46. package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts +2 -3
  47. package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts.map +1 -1
  48. package/types/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.d.ts +13 -2
  49. package/types/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.d.ts.map +1 -1
  50. package/types/adapter/repositories/ProcessListSessionSubAgentActivityRepository.d.ts.map +1 -1
  51. package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts +5 -1
  52. package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts.map +1 -1
  53. package/types/domain/entities/LiveSessionActivitySnapshot.d.ts +1 -0
  54. package/types/domain/entities/LiveSessionActivitySnapshot.d.ts.map +1 -1
  55. package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts +2 -3
  56. package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts.map +1 -1
  57. package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +2 -1
  58. package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
  59. package/types/domain/usecases/adapter-interfaces/SilentSessionCandidateStateRepository.d.ts +8 -0
  60. package/types/domain/usecases/adapter-interfaces/SilentSessionCandidateStateRepository.d.ts.map +1 -1
  61. package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts +4 -4
  62. 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
- { label: 'agent-aaa111', silentSeconds: 120, runningSeconds: 900 },
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
- { label: 'agent-pending', silentSeconds: 600, runningSeconds: 900 },
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
- { label: 'agent-link1', silentSeconds: 60, runningSeconds: 600 },
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
- { label: 'agent-future', silentSeconds: 0, runningSeconds: 0 },
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 { firstEntryEpochSeconds, lastEntryIndicatesCompletion };
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(directory, nowEpochSeconds);
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
- ): SubAgentActivity[] => {
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(filePath, fileName, nowEpochSeconds);
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
- ): SubAgentActivity | null => {
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
  }
@@ -2,6 +2,7 @@ export type SubAgentActivity = {
2
2
  label: string;
3
3
  silentSeconds: number;
4
4
  runningSeconds: number;
5
+ waitingOnExternalProcess: boolean;
5
6
  };
6
7
 
7
8
  export type LiveSessionActivitySnapshot = {