github-issue-tower-defence-management 1.140.0 → 1.140.2

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 (23) hide show
  1. package/.github/workflows/umino-project.yml +2 -1
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +1 -1
  4. package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js +6 -40
  5. package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js.map +1 -1
  6. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +29 -51
  7. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +33 -3
  10. package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.test.ts +24 -11
  11. package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.ts +8 -50
  12. package/src/domain/entities/LiveSessionActivitySnapshot.ts +0 -1
  13. package/src/domain/entities/LiveSessionOutputActivity.ts +0 -1
  14. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +127 -122
  15. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +33 -62
  16. package/types/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts +1 -1
  17. package/types/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts.map +1 -1
  18. package/types/domain/entities/LiveSessionActivitySnapshot.d.ts +0 -1
  19. package/types/domain/entities/LiveSessionActivitySnapshot.d.ts.map +1 -1
  20. package/types/domain/entities/LiveSessionOutputActivity.d.ts +0 -1
  21. package/types/domain/entities/LiveSessionOutputActivity.d.ts.map +1 -1
  22. package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +0 -1
  23. package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
@@ -21,54 +21,34 @@ const parseEpochMilliseconds = (timestamp: string | null): number | null => {
21
21
  return Number.isNaN(parsed) ? null : parsed;
22
22
  };
23
23
 
24
- const readContentBlocks = (
25
- message: Record<string, unknown>,
26
- ): Record<string, unknown>[] => {
27
- const content = message.content;
28
- if (!Array.isArray(content)) {
29
- return [];
30
- }
31
- return content.filter(isRecord);
32
- };
33
-
34
- type TranscriptActivity = {
35
- lastAssistantOutputEpochSeconds: number | null;
36
- hasInProgressToolCall: boolean;
37
- };
38
-
39
24
  export class FileSystemSessionOutputActivityRepository implements SessionOutputActivityRepository {
40
25
  listSessionOutputActivities = async (
41
26
  transcriptPathBySessionName: Map<string, string>,
42
27
  ): Promise<LiveSessionOutputActivity[]> => {
43
28
  const activities: LiveSessionOutputActivity[] = [];
44
29
  for (const [sessionName, transcriptPath] of transcriptPathBySessionName) {
45
- const { lastAssistantOutputEpochSeconds, hasInProgressToolCall } =
46
- this.readTranscriptActivity(transcriptPath);
30
+ const lastAssistantOutputEpochSeconds =
31
+ this.readLastAssistantOutputEpochSeconds(transcriptPath);
47
32
  if (lastAssistantOutputEpochSeconds !== null) {
48
33
  activities.push({
49
34
  sessionName,
50
35
  lastOutputEpochSeconds: lastAssistantOutputEpochSeconds,
51
- hasInProgressToolCall,
52
36
  });
53
37
  }
54
38
  }
55
39
  return activities;
56
40
  };
57
41
 
58
- private readTranscriptActivity = (
42
+ private readLastAssistantOutputEpochSeconds = (
59
43
  transcriptPath: string,
60
- ): TranscriptActivity => {
44
+ ): number | null => {
61
45
  let content: string;
62
46
  try {
63
47
  content = fs.readFileSync(transcriptPath, 'utf8');
64
48
  } catch {
65
- return {
66
- lastAssistantOutputEpochSeconds: null,
67
- hasInProgressToolCall: false,
68
- };
49
+ return null;
69
50
  }
70
51
  let lastAssistantOutputEpochMs: number | null = null;
71
- const pendingToolUseIds = new Set<string>();
72
52
  for (const line of content.split('\n')) {
73
53
  const trimmed = line.trim();
74
54
  if (trimmed.length === 0) {
@@ -83,37 +63,15 @@ export class FileSystemSessionOutputActivityRepository implements SessionOutputA
83
63
  if (!isRecord(parsed)) {
84
64
  continue;
85
65
  }
86
- const message = parsed.message;
87
66
  if (readString(parsed, 'type') === 'assistant') {
88
67
  const epochMs = parseEpochMilliseconds(readString(parsed, 'timestamp'));
89
68
  if (epochMs !== null) {
90
69
  lastAssistantOutputEpochMs = epochMs;
91
70
  }
92
71
  }
93
- if (!isRecord(message)) {
94
- continue;
95
- }
96
- for (const block of readContentBlocks(message)) {
97
- const blockType = readString(block, 'type');
98
- if (blockType === 'tool_use') {
99
- const toolUseId = readString(block, 'id');
100
- if (toolUseId !== null) {
101
- pendingToolUseIds.add(toolUseId);
102
- }
103
- } else if (blockType === 'tool_result') {
104
- const toolUseId = readString(block, 'tool_use_id');
105
- if (toolUseId !== null) {
106
- pendingToolUseIds.delete(toolUseId);
107
- }
108
- }
109
- }
110
72
  }
111
- return {
112
- lastAssistantOutputEpochSeconds:
113
- lastAssistantOutputEpochMs === null
114
- ? null
115
- : Math.floor(lastAssistantOutputEpochMs / 1000),
116
- hasInProgressToolCall: pendingToolUseIds.size > 0,
117
- };
73
+ return lastAssistantOutputEpochMs === null
74
+ ? null
75
+ : Math.floor(lastAssistantOutputEpochMs / 1000);
118
76
  };
119
77
  }
@@ -8,7 +8,6 @@ export type SubAgentActivity = {
8
8
  export type LiveSessionActivitySnapshot = {
9
9
  sessionName: string;
10
10
  mainSilentSeconds: number | null;
11
- mainHasInProgressToolCall: boolean;
12
11
  subAgents: SubAgentActivity[];
13
12
  unansweredOwnerCallAgeSeconds: number | null;
14
13
  };
@@ -1,5 +1,4 @@
1
1
  export type LiveSessionOutputActivity = {
2
2
  sessionName: string;
3
3
  lastOutputEpochSeconds: number;
4
- hasInProgressToolCall: boolean;
5
4
  };
@@ -10,7 +10,6 @@ import {
10
10
  DEFAULT_NOTIFICATION_STAGGER_SECONDS,
11
11
  DEFAULT_CANDIDATE_DEBOUNCE_RECENCY_WINDOW_SECONDS,
12
12
  DEFAULT_HUB_TASK_STATUS_CACHE_TTL_SECONDS,
13
- IN_PROGRESS_TOOL_CALL_MAX_SUPPRESS_SECONDS,
14
13
  } from './NotifySilentLiveSessionsUseCase';
15
14
  import { Issue } from '../entities/Issue';
16
15
  import { LiveSessionProcessSnapshotProvider } from './adapter-interfaces/LiveSessionProcessSnapshotProvider';
@@ -268,7 +267,6 @@ describe('NotifySilentLiveSessionsUseCase', () => {
268
267
  sessionName,
269
268
  lastOutputEpochSeconds:
270
269
  nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
271
- hasInProgressToolCall: false,
272
270
  },
273
271
  ],
274
272
  );
@@ -299,7 +297,6 @@ describe('NotifySilentLiveSessionsUseCase', () => {
299
297
  sessionName: GITHUB_SESSION,
300
298
  lastOutputEpochSeconds:
301
299
  nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
302
- hasInProgressToolCall: false,
303
300
  },
304
301
  ],
305
302
  );
@@ -323,7 +320,6 @@ describe('NotifySilentLiveSessionsUseCase', () => {
323
320
  sessionName: pullRequestSession,
324
321
  lastOutputEpochSeconds:
325
322
  nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
326
- hasInProgressToolCall: false,
327
323
  },
328
324
  ],
329
325
  );
@@ -335,7 +331,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
335
331
  ).toHaveBeenCalledWith(pullRequestSession, MAIN_STALLED_SECTION);
336
332
  });
337
333
 
338
- it('excludes a non-github-named session from selection so it is never notified', async () => {
334
+ it('excludes a non-github-named session that has no resolvable transcript so it is never notified', async () => {
339
335
  mockSnapshotProvider.getSnapshot.mockResolvedValue(
340
336
  snapshotWithSessions(['workbench']),
341
337
  );
@@ -345,16 +341,15 @@ describe('NotifySilentLiveSessionsUseCase', () => {
345
341
  sessionName: 'workbench',
346
342
  lastOutputEpochSeconds:
347
343
  nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
348
- hasInProgressToolCall: false,
349
344
  },
350
345
  ],
351
346
  );
352
347
 
353
348
  await useCase.run(runParams());
354
349
 
355
- expect(mockTranscriptResolver.resolveTranscriptPaths).toHaveBeenCalledWith(
356
- [],
357
- );
350
+ expect(mockTranscriptResolver.resolveTranscriptPaths).toHaveBeenCalledWith([
351
+ sessionFor('workbench'),
352
+ ]);
358
353
  expect(
359
354
  mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName,
360
355
  ).toHaveBeenCalledWith([], new Map());
@@ -366,7 +361,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
366
361
  ).not.toHaveBeenCalled();
367
362
  });
368
363
 
369
- it('monitors only the github-named session when mixed with a non-github-named session', async () => {
364
+ it('monitors the github-named session and excludes a non-github-named session that has no resolvable transcript', async () => {
370
365
  mockSnapshotProvider.getSnapshot.mockResolvedValue(
371
366
  snapshotWithSessions([GITHUB_SESSION, 'orchestrator']),
372
367
  );
@@ -379,13 +374,11 @@ describe('NotifySilentLiveSessionsUseCase', () => {
379
374
  sessionName: GITHUB_SESSION,
380
375
  lastOutputEpochSeconds:
381
376
  nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
382
- hasInProgressToolCall: false,
383
377
  },
384
378
  {
385
379
  sessionName: 'orchestrator',
386
380
  lastOutputEpochSeconds:
387
381
  nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
388
- hasInProgressToolCall: false,
389
382
  },
390
383
  ],
391
384
  );
@@ -394,6 +387,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
394
387
 
395
388
  expect(mockTranscriptResolver.resolveTranscriptPaths).toHaveBeenCalledWith([
396
389
  sessionFor(GITHUB_SESSION),
390
+ sessionFor('orchestrator'),
397
391
  ]);
398
392
  expect(
399
393
  mockNotificationRepository.sendSelfCheckNotification,
@@ -501,7 +495,6 @@ describe('NotifySilentLiveSessionsUseCase', () => {
501
495
  sessionName: GITHUB_SESSION,
502
496
  lastOutputEpochSeconds:
503
497
  nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS + 1,
504
- hasInProgressToolCall: false,
505
498
  },
506
499
  ],
507
500
  );
@@ -529,7 +522,6 @@ describe('NotifySilentLiveSessionsUseCase', () => {
529
522
  sessionName: GITHUB_SESSION,
530
523
  lastOutputEpochSeconds:
531
524
  nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
532
- hasInProgressToolCall: false,
533
525
  },
534
526
  ],
535
527
  );
@@ -550,7 +542,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
550
542
  ).toHaveBeenCalledWith(GITHUB_SESSION, MAIN_STALLED_SECTION);
551
543
  });
552
544
 
553
- it('does not send the main stalled section when the session is silent past the threshold but is waiting on a running tool call', async () => {
545
+ it('sends the main stalled section for a session silent past the threshold regardless of any in-progress tool call, because apparent busyness never suppresses the reminder', async () => {
554
546
  setupLiveInteractiveSession(GITHUB_SESSION);
555
547
  mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
556
548
  [
@@ -558,34 +550,6 @@ describe('NotifySilentLiveSessionsUseCase', () => {
558
550
  sessionName: GITHUB_SESSION,
559
551
  lastOutputEpochSeconds:
560
552
  nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
561
- hasInProgressToolCall: true,
562
- },
563
- ],
564
- );
565
- mockOwnerCallStatusProvider.listUnansweredOwnerCallEpochSecondsBySessionName.mockResolvedValue(
566
- new Map<string, number>(),
567
- );
568
-
569
- await useCase.run(runParams());
570
-
571
- expect(
572
- mockMessageComposer.composeMainStalledSection,
573
- ).not.toHaveBeenCalled();
574
- expect(
575
- mockNotificationRepository.sendSelfCheckNotification,
576
- ).not.toHaveBeenCalled();
577
- });
578
-
579
- it('sends the main stalled section when the in-progress tool call has been pending longer than the suppression bound', async () => {
580
- setupLiveInteractiveSession(GITHUB_SESSION);
581
- const pendingToolCallAgeSeconds =
582
- IN_PROGRESS_TOOL_CALL_MAX_SUPPRESS_SECONDS + 60 * 60;
583
- mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
584
- [
585
- {
586
- sessionName: GITHUB_SESSION,
587
- lastOutputEpochSeconds: nowEpochSeconds - pendingToolCallAgeSeconds,
588
- hasInProgressToolCall: true,
589
553
  },
590
554
  ],
591
555
  );
@@ -596,7 +560,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
596
560
  await useCase.run(runParams());
597
561
 
598
562
  expect(mockMessageComposer.composeMainStalledSection).toHaveBeenCalledWith(
599
- pendingToolCallAgeSeconds,
563
+ DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
600
564
  );
601
565
  expect(
602
566
  mockNotificationRepository.sendSelfCheckNotification,
@@ -611,7 +575,6 @@ describe('NotifySilentLiveSessionsUseCase', () => {
611
575
  sessionName: GITHUB_SESSION,
612
576
  lastOutputEpochSeconds:
613
577
  nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS + 1,
614
- hasInProgressToolCall: false,
615
578
  },
616
579
  ],
617
580
  );
@@ -729,6 +692,117 @@ describe('NotifySilentLiveSessionsUseCase', () => {
729
692
  ).not.toHaveBeenCalled();
730
693
  });
731
694
 
695
+ describe('role-named resident leader and PM agent sessions', () => {
696
+ it('reminds a role-named leader agent session that has a resolvable transcript and is silent past the threshold', async () => {
697
+ setupLiveInteractiveSession('secretary');
698
+ mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
699
+ [
700
+ {
701
+ sessionName: 'secretary',
702
+ lastOutputEpochSeconds:
703
+ nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
704
+ },
705
+ ],
706
+ );
707
+
708
+ await useCase.run(runParams());
709
+
710
+ expect(
711
+ mockMessageComposer.composeMainStalledSection,
712
+ ).toHaveBeenCalledWith(DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS);
713
+ expect(
714
+ mockNotificationRepository.sendSelfCheckNotification,
715
+ ).toHaveBeenCalledWith('secretary', MAIN_STALLED_SECTION);
716
+ });
717
+
718
+ it('excludes a non-agent interactive session with no resolvable transcript (for example a viewer named sso_login)', async () => {
719
+ mockSnapshotProvider.getSnapshot.mockResolvedValue(
720
+ snapshotWithSessions(['sso_login']),
721
+ );
722
+ mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
723
+ [
724
+ {
725
+ sessionName: 'sso_login',
726
+ lastOutputEpochSeconds:
727
+ nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
728
+ },
729
+ ],
730
+ );
731
+
732
+ await useCase.run(runParams());
733
+
734
+ expect(
735
+ mockTranscriptResolver.resolveTranscriptPaths,
736
+ ).toHaveBeenCalledWith([sessionFor('sso_login')]);
737
+ expect(
738
+ mockMessageComposer.composeMainStalledSection,
739
+ ).not.toHaveBeenCalled();
740
+ expect(
741
+ mockNotificationRepository.sendSelfCheckNotification,
742
+ ).not.toHaveBeenCalled();
743
+ });
744
+
745
+ it('reminds both a github-named session and a role-named leader session in the same cycle', async () => {
746
+ mockSnapshotProvider.getSnapshot.mockResolvedValue(
747
+ snapshotWithSessions([GITHUB_SESSION, 'app']),
748
+ );
749
+ mockTranscriptResolver.resolveTranscriptPaths.mockReturnValue(
750
+ transcriptMapFor([GITHUB_SESSION, 'app']),
751
+ );
752
+ mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
753
+ [
754
+ {
755
+ sessionName: GITHUB_SESSION,
756
+ lastOutputEpochSeconds:
757
+ nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
758
+ },
759
+ {
760
+ sessionName: 'app',
761
+ lastOutputEpochSeconds:
762
+ nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
763
+ },
764
+ ],
765
+ );
766
+
767
+ await useCase.run(runParams());
768
+
769
+ expect(
770
+ mockNotificationRepository.sendSelfCheckNotification,
771
+ ).toHaveBeenCalledTimes(2);
772
+ expect(
773
+ mockNotificationRepository.sendSelfCheckNotification,
774
+ ).toHaveBeenCalledWith(GITHUB_SESSION, MAIN_STALLED_SECTION);
775
+ expect(
776
+ mockNotificationRepository.sendSelfCheckNotification,
777
+ ).toHaveBeenCalledWith('app', MAIN_STALLED_SECTION);
778
+ });
779
+
780
+ it('parks a role-named leader session while its latest owner call is unanswered', async () => {
781
+ setupLiveInteractiveSession('uminopm');
782
+ mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
783
+ [
784
+ {
785
+ sessionName: 'uminopm',
786
+ lastOutputEpochSeconds:
787
+ nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
788
+ },
789
+ ],
790
+ );
791
+ mockOwnerCallStatusProvider.listUnansweredOwnerCallEpochSecondsBySessionName.mockResolvedValue(
792
+ new Map([['uminopm', nowEpochSeconds - 60]]),
793
+ );
794
+
795
+ await useCase.run(runParams());
796
+
797
+ expect(
798
+ mockMessageComposer.composeMainStalledSection,
799
+ ).not.toHaveBeenCalled();
800
+ expect(
801
+ mockNotificationRepository.sendSelfCheckNotification,
802
+ ).not.toHaveBeenCalled();
803
+ });
804
+ });
805
+
732
806
  it('notifies a persistent stall only once per silent episode instead of every cycle', async () => {
733
807
  wireStatefulNotifiedLatch();
734
808
  setupLiveInteractiveSession(GITHUB_SESSION);
@@ -738,7 +812,6 @@ describe('NotifySilentLiveSessionsUseCase', () => {
738
812
  sessionName: GITHUB_SESSION,
739
813
  lastOutputEpochSeconds:
740
814
  nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
741
- hasInProgressToolCall: false,
742
815
  },
743
816
  ],
744
817
  );
@@ -763,14 +836,12 @@ describe('NotifySilentLiveSessionsUseCase', () => {
763
836
  sessionName: GITHUB_SESSION,
764
837
  lastOutputEpochSeconds:
765
838
  nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
766
- hasInProgressToolCall: false,
767
839
  },
768
840
  ];
769
841
  const active = [
770
842
  {
771
843
  sessionName: GITHUB_SESSION,
772
844
  lastOutputEpochSeconds: nowEpochSeconds,
773
- hasInProgressToolCall: false,
774
845
  },
775
846
  ];
776
847
 
@@ -808,7 +879,6 @@ describe('NotifySilentLiveSessionsUseCase', () => {
808
879
  sessionName: GITHUB_SESSION,
809
880
  lastOutputEpochSeconds:
810
881
  nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
811
- hasInProgressToolCall: false,
812
882
  },
813
883
  ],
814
884
  );
@@ -898,7 +968,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
898
968
  });
899
969
  });
900
970
 
901
- describe('fire-once latch and input-state gating', () => {
971
+ describe('fire-once latch', () => {
902
972
  const setupSilentGithubSession = (): void => {
903
973
  setupLiveInteractiveSession(GITHUB_SESSION);
904
974
  mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
@@ -907,19 +977,11 @@ describe('NotifySilentLiveSessionsUseCase', () => {
907
977
  sessionName: GITHUB_SESSION,
908
978
  lastOutputEpochSeconds:
909
979
  nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
910
- hasInProgressToolCall: false,
911
980
  },
912
981
  ],
913
982
  );
914
983
  };
915
984
 
916
- const idleSubAgent = (label: string): SubAgentActivity => ({
917
- label,
918
- silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
919
- runningSeconds: 60,
920
- waitingOnExternalProcess: false,
921
- });
922
-
923
985
  it('loads the fire-once latch using the configured recency window', async () => {
924
986
  setupSilentGithubSession();
925
987
 
@@ -973,69 +1035,17 @@ describe('NotifySilentLiveSessionsUseCase', () => {
973
1035
  ).toHaveBeenCalledWith({ sessionNames: [], now });
974
1036
  });
975
1037
 
976
- it('does not inject into a session whose main REPL has an in-progress tool call even when a sub-agent advisory qualifies', async () => {
977
- setupLiveInteractiveSession(GITHUB_SESSION);
978
- mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
979
- [
980
- {
981
- sessionName: GITHUB_SESSION,
982
- lastOutputEpochSeconds: nowEpochSeconds,
983
- hasInProgressToolCall: true,
984
- },
985
- ],
986
- );
987
- mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName.mockResolvedValue(
988
- new Map([[GITHUB_SESSION, [idleSubAgent('sub-process-1')]]]),
989
- );
1038
+ it('sends the reminder for a silent session even when its main REPL is mid-turn, because apparent busyness no longer defers delivery', async () => {
1039
+ setupSilentGithubSession();
990
1040
 
991
1041
  await useCase.run(runParams());
992
1042
 
993
- expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalled();
994
1043
  expect(
995
1044
  mockNotificationRepository.sendSelfCheckNotification,
996
- ).not.toHaveBeenCalled();
1045
+ ).toHaveBeenCalledWith(GITHUB_SESSION, MAIN_STALLED_SECTION);
997
1046
  expect(
998
1047
  mockNotifiedStateRepository.saveNotifiedSessionNames,
999
- ).toHaveBeenCalledWith({ sessionNames: [], now });
1000
- });
1001
-
1002
- it('delivers the deferred reminder once the main REPL becomes input-ready', async () => {
1003
- wireStatefulNotifiedLatch();
1004
- setupLiveInteractiveSession(GITHUB_SESSION);
1005
- mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName.mockResolvedValue(
1006
- new Map([[GITHUB_SESSION, [idleSubAgent('sub-process-1')]]]),
1007
- );
1008
-
1009
- mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
1010
- [
1011
- {
1012
- sessionName: GITHUB_SESSION,
1013
- lastOutputEpochSeconds: nowEpochSeconds,
1014
- hasInProgressToolCall: true,
1015
- },
1016
- ],
1017
- );
1018
- await useCase.run(runParams());
1019
- expect(
1020
- mockNotificationRepository.sendSelfCheckNotification,
1021
- ).not.toHaveBeenCalled();
1022
-
1023
- mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
1024
- [
1025
- {
1026
- sessionName: GITHUB_SESSION,
1027
- lastOutputEpochSeconds: nowEpochSeconds,
1028
- hasInProgressToolCall: false,
1029
- },
1030
- ],
1031
- );
1032
- await useCase.run(runParams());
1033
- expect(
1034
- mockNotificationRepository.sendSelfCheckNotification,
1035
- ).toHaveBeenCalledTimes(1);
1036
- expect(
1037
- mockNotificationRepository.sendSelfCheckNotification,
1038
- ).toHaveBeenCalledWith(GITHUB_SESSION, SUBAGENT_SECTION);
1048
+ ).toHaveBeenCalledWith({ sessionNames: [GITHUB_SESSION], now });
1039
1049
  });
1040
1050
  });
1041
1051
 
@@ -1218,13 +1228,11 @@ describe('NotifySilentLiveSessionsUseCase', () => {
1218
1228
  sessionName: GITHUB_SESSION_ALPHA,
1219
1229
  lastOutputEpochSeconds:
1220
1230
  nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
1221
- hasInProgressToolCall: false,
1222
1231
  },
1223
1232
  {
1224
1233
  sessionName: GITHUB_SESSION_BRAVO,
1225
1234
  lastOutputEpochSeconds:
1226
1235
  nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
1227
- hasInProgressToolCall: false,
1228
1236
  },
1229
1237
  ],
1230
1238
  );
@@ -1260,7 +1268,6 @@ describe('NotifySilentLiveSessionsUseCase', () => {
1260
1268
  sessionName: GITHUB_SESSION,
1261
1269
  lastOutputEpochSeconds:
1262
1270
  nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
1263
- hasInProgressToolCall: false,
1264
1271
  },
1265
1272
  ],
1266
1273
  );
@@ -1326,15 +1333,15 @@ describe('NotifySilentLiveSessionsUseCase', () => {
1326
1333
  ).not.toHaveBeenCalled();
1327
1334
  });
1328
1335
 
1329
- it('excludes a non-github-named session before the hub-task gate so the resolver is never consulted', async () => {
1330
- setupSilentMainSession('workbench');
1336
+ it('does not consult the hub-task resolver for a role-named leader session that has no hub task URL, and still reminds it', async () => {
1337
+ setupSilentMainSession('tdpm-cli');
1331
1338
 
1332
1339
  await useCase.run(runParams({ activeHubTaskStatus: ACTIVE_STATUS }));
1333
1340
 
1334
1341
  expect(mockHubTaskStatusResolver.getIssueByUrl).not.toHaveBeenCalled();
1335
1342
  expect(
1336
1343
  mockNotificationRepository.sendSelfCheckNotification,
1337
- ).not.toHaveBeenCalled();
1344
+ ).toHaveBeenCalledWith('tdpm-cli', MAIN_STALLED_SECTION);
1338
1345
  });
1339
1346
 
1340
1347
  it('does not call the resolver at all when the active status is unconfigured', async () => {
@@ -1802,13 +1809,11 @@ describe('NotifySilentLiveSessionsUseCase', () => {
1802
1809
  sessionName: GITHUB_SESSION_ALPHA,
1803
1810
  lastOutputEpochSeconds:
1804
1811
  nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
1805
- hasInProgressToolCall: false,
1806
1812
  },
1807
1813
  {
1808
1814
  sessionName: GITHUB_SESSION_BRAVO,
1809
1815
  lastOutputEpochSeconds:
1810
1816
  nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
1811
- hasInProgressToolCall: false,
1812
1817
  },
1813
1818
  ],
1814
1819
  );