github-issue-tower-defence-management 1.138.0 → 1.139.1

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 (52) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +3 -2
  3. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +22 -2
  4. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  5. package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +9 -5
  6. package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -1
  7. package/bin/adapter/repositories/FileSystemSilentSessionNotifiedStateRepository.js +125 -0
  8. package/bin/adapter/repositories/FileSystemSilentSessionNotifiedStateRepository.js.map +1 -0
  9. package/bin/adapter/repositories/FileSystemSubAgentLivenessResolver.js +81 -0
  10. package/bin/adapter/repositories/FileSystemSubAgentLivenessResolver.js.map +1 -0
  11. package/bin/adapter/repositories/TmuxSilentSessionNotificationRepository.js +4 -1
  12. package/bin/adapter/repositories/TmuxSilentSessionNotificationRepository.js.map +1 -1
  13. package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js +30 -12
  14. package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js.map +1 -1
  15. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +48 -1
  16. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
  17. package/bin/domain/usecases/adapter-interfaces/SilentSessionNotifiedStateRepository.js +3 -0
  18. package/bin/domain/usecases/adapter-interfaces/SilentSessionNotifiedStateRepository.js.map +1 -0
  19. package/bin/domain/usecases/adapter-interfaces/SubAgentLivenessResolver.js +3 -0
  20. package/bin/domain/usecases/adapter-interfaces/SubAgentLivenessResolver.js.map +1 -0
  21. package/package.json +1 -1
  22. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +16 -0
  23. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +31 -0
  24. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +14 -0
  25. package/src/adapter/repositories/FileSystemSilentSessionNotifiedStateRepository.test.ts +127 -0
  26. package/src/adapter/repositories/FileSystemSilentSessionNotifiedStateRepository.ts +108 -0
  27. package/src/adapter/repositories/FileSystemSubAgentLivenessResolver.test.ts +87 -0
  28. package/src/adapter/repositories/FileSystemSubAgentLivenessResolver.ts +57 -0
  29. package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.test.ts +23 -2
  30. package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.ts +5 -1
  31. package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.test.ts +136 -6
  32. package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts +37 -12
  33. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +218 -8
  34. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +60 -0
  35. package/src/domain/usecases/adapter-interfaces/SilentSessionNotifiedStateRepository.ts +10 -0
  36. package/src/domain/usecases/adapter-interfaces/SubAgentLivenessResolver.ts +6 -0
  37. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  38. package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts +2 -0
  39. package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -1
  40. package/types/adapter/repositories/FileSystemSilentSessionNotifiedStateRepository.d.ts +16 -0
  41. package/types/adapter/repositories/FileSystemSilentSessionNotifiedStateRepository.d.ts.map +1 -0
  42. package/types/adapter/repositories/FileSystemSubAgentLivenessResolver.d.ts +11 -0
  43. package/types/adapter/repositories/FileSystemSubAgentLivenessResolver.d.ts.map +1 -0
  44. package/types/adapter/repositories/TmuxSilentSessionNotificationRepository.d.ts.map +1 -1
  45. package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts +4 -2
  46. package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts.map +1 -1
  47. package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +3 -1
  48. package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
  49. package/types/domain/usecases/adapter-interfaces/SilentSessionNotifiedStateRepository.d.ts +11 -0
  50. package/types/domain/usecases/adapter-interfaces/SilentSessionNotifiedStateRepository.d.ts.map +1 -0
  51. package/types/domain/usecases/adapter-interfaces/SubAgentLivenessResolver.d.ts +7 -0
  52. package/types/domain/usecases/adapter-interfaces/SubAgentLivenessResolver.d.ts.map +1 -0
@@ -2,9 +2,14 @@ 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 { SubAgentLivenessResolver } from '../../domain/usecases/adapter-interfaces/SubAgentLivenessResolver';
5
6
  import { SubAgentProcessLister } from '../../domain/usecases/adapter-interfaces/SubAgentProcessLister';
6
7
  import { SubAgentTranscriptDirectoryResolver } from '../../domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver';
7
8
 
9
+ const alwaysIndeterminateLivenessResolver: SubAgentLivenessResolver = {
10
+ resolveLiveSubAgentIds: async () => null,
11
+ };
12
+
8
13
  const isRecord = (value: unknown): value is Record<string, unknown> =>
9
14
  typeof value === 'object' && value !== null;
10
15
 
@@ -190,7 +195,14 @@ const parseTranscript = (content: string): ParsedTranscript => {
190
195
 
191
196
  const clampToZero = (value: number): number => (value > 0 ? value : 0);
192
197
 
193
- const parseKilledOrFailedAgentIds = (content: string): Set<string> => {
198
+ const TERMINAL_TASK_NOTIFICATION_STATUSES = new Set([
199
+ 'completed',
200
+ 'killed',
201
+ 'failed',
202
+ 'stopped',
203
+ ]);
204
+
205
+ const parseTerminalAgentIds = (content: string): Set<string> => {
194
206
  const result = new Set<string>();
195
207
  for (const line of content.split('\n')) {
196
208
  const trimmed = line.trim();
@@ -216,13 +228,16 @@ const parseKilledOrFailedAgentIds = (content: string): Set<string> => {
216
228
  if (notifContent === null) {
217
229
  continue;
218
230
  }
219
- const taskIdMatch = notifContent.match(/<task-id>(a[0-9a-f]+)<\/task-id>/);
220
231
  const statusMatch = notifContent.match(/<status>([^<]+)<\/status>/);
221
- if (!taskIdMatch || !statusMatch) {
232
+ if (statusMatch === null) {
222
233
  continue;
223
234
  }
224
- const status = statusMatch[1];
225
- if (status === 'killed' || status === 'failed') {
235
+ if (!TERMINAL_TASK_NOTIFICATION_STATUSES.has(statusMatch[1])) {
236
+ continue;
237
+ }
238
+ for (const taskIdMatch of notifContent.matchAll(
239
+ /<task-id>(a[0-9a-f]+)<\/task-id>/g,
240
+ )) {
226
241
  result.add(taskIdMatch[1]);
227
242
  }
228
243
  }
@@ -234,6 +249,7 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
234
249
  private readonly directoryResolver: SubAgentTranscriptDirectoryResolver,
235
250
  private readonly processLister: SubAgentProcessLister,
236
251
  private readonly now: Date,
252
+ private readonly livenessResolver: SubAgentLivenessResolver = alwaysIndeterminateLivenessResolver,
237
253
  ) {}
238
254
 
239
255
  listSubAgentActivitiesBySessionName = async (
@@ -262,12 +278,17 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
262
278
  if (directory === null) {
263
279
  continue;
264
280
  }
265
- const killedOrFailedAgentIds =
266
- this.loadKilledOrFailedAgentIds(mainTranscriptPath);
281
+ const terminalAgentIds = this.loadTerminalAgentIds(mainTranscriptPath);
282
+ const liveSubAgentIds =
283
+ await this.livenessResolver.resolveLiveSubAgentIds({
284
+ sessionName,
285
+ mainTranscriptPath,
286
+ });
267
287
  const activities = await this.collectActivities(
268
288
  directory,
269
289
  nowEpochSeconds,
270
- killedOrFailedAgentIds,
290
+ terminalAgentIds,
291
+ liveSubAgentIds,
271
292
  loadNormalizedProcessCommandLines,
272
293
  );
273
294
  if (activities.length > 0) {
@@ -277,7 +298,7 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
277
298
  return result;
278
299
  };
279
300
 
280
- private loadKilledOrFailedAgentIds = (
301
+ private loadTerminalAgentIds = (
281
302
  mainTranscriptPath: string | null,
282
303
  ): Set<string> => {
283
304
  if (mainTranscriptPath === null) {
@@ -289,13 +310,14 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
289
310
  } catch {
290
311
  return new Set();
291
312
  }
292
- return parseKilledOrFailedAgentIds(content);
313
+ return parseTerminalAgentIds(content);
293
314
  };
294
315
 
295
316
  private collectActivities = async (
296
317
  directory: string,
297
318
  nowEpochSeconds: number,
298
- killedOrFailedAgentIds: Set<string>,
319
+ terminalAgentIds: Set<string>,
320
+ liveSubAgentIds: Set<string> | null,
299
321
  loadNormalizedProcessCommandLines: () => Promise<string[]>,
300
322
  ): Promise<SubAgentActivity[]> => {
301
323
  let entries: fs.Dirent[];
@@ -311,7 +333,10 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
311
333
  continue;
312
334
  }
313
335
  const agentId = fileName.slice('agent-'.length, -'.jsonl'.length);
314
- if (killedOrFailedAgentIds.has(agentId)) {
336
+ if (terminalAgentIds.has(agentId)) {
337
+ continue;
338
+ }
339
+ if (liveSubAgentIds !== null && !liveSubAgentIds.has(agentId)) {
315
340
  continue;
316
341
  }
317
342
  const filePath = path.join(directory, fileName);
@@ -21,6 +21,7 @@ import { OwnerCallStatusProvider } from './adapter-interfaces/OwnerCallStatusPro
21
21
  import { RefusalTailStatusProvider } from './adapter-interfaces/RefusalTailStatusProvider';
22
22
  import { SilentSessionNotificationRepository } from './adapter-interfaces/SilentSessionNotificationRepository';
23
23
  import { SilentSessionCandidateStateRepository } from './adapter-interfaces/SilentSessionCandidateStateRepository';
24
+ import { SilentSessionNotifiedStateRepository } from './adapter-interfaces/SilentSessionNotifiedStateRepository';
24
25
  import { SilentSessionHubTaskStatusCacheRepository } from './adapter-interfaces/SilentSessionHubTaskStatusCacheRepository';
25
26
  import { SilentSessionMessageComposer } from './adapter-interfaces/SilentSessionMessageComposer';
26
27
  import { Sleeper } from './adapter-interfaces/Sleeper';
@@ -50,6 +51,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
50
51
  let mockOwnerCallStatusProvider: Mocked<OwnerCallStatusProvider>;
51
52
  let mockNotificationRepository: Mocked<SilentSessionNotificationRepository>;
52
53
  let mockCandidateStateRepository: Mocked<SilentSessionCandidateStateRepository>;
54
+ let mockNotifiedStateRepository: Mocked<SilentSessionNotifiedStateRepository>;
53
55
  let mockMessageComposer: Mocked<SilentSessionMessageComposer>;
54
56
  let mockSleeper: Mocked<Sleeper>;
55
57
  let mockHubTaskStatusResolver: Mocked<HubTaskStatusResolver>;
@@ -169,6 +171,12 @@ describe('NotifySilentLiveSessionsUseCase', () => {
169
171
  .mockResolvedValue(everyNameRecentSet()),
170
172
  saveCandidateSessionNames: jest.fn().mockResolvedValue(undefined),
171
173
  };
174
+ mockNotifiedStateRepository = {
175
+ loadRecentNotifiedSessionNames: jest
176
+ .fn()
177
+ .mockResolvedValue(new Set<string>()),
178
+ saveNotifiedSessionNames: jest.fn().mockResolvedValue(undefined),
179
+ };
172
180
  mockMessageComposer = {
173
181
  composeMainStalledSection: jest
174
182
  .fn()
@@ -201,6 +209,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
201
209
  mockOwnerCallStatusProvider,
202
210
  mockNotificationRepository,
203
211
  mockCandidateStateRepository,
212
+ mockNotifiedStateRepository,
204
213
  mockMessageComposer,
205
214
  mockSleeper,
206
215
  mockHubTaskStatusResolver,
@@ -209,6 +218,20 @@ describe('NotifySilentLiveSessionsUseCase', () => {
209
218
  );
210
219
  });
211
220
 
221
+ // Simulates the on-disk fire-once latch across the per-cycle fresh monitor
222
+ // process: load returns the currently persisted set, save overwrites it.
223
+ const wireStatefulNotifiedLatch = (): void => {
224
+ let persistedNotifiedSessionNames = new Set<string>();
225
+ mockNotifiedStateRepository.loadRecentNotifiedSessionNames.mockImplementation(
226
+ async () => new Set(persistedNotifiedSessionNames),
227
+ );
228
+ mockNotifiedStateRepository.saveNotifiedSessionNames.mockImplementation(
229
+ async ({ sessionNames }) => {
230
+ persistedNotifiedSessionNames = new Set(sessionNames);
231
+ },
232
+ );
233
+ };
234
+
212
235
  const issueFor = (overrides: Partial<Issue>): Issue => ({
213
236
  nameWithOwner: 'HiromiShikata/repo',
214
237
  number: 42,
@@ -706,7 +729,8 @@ describe('NotifySilentLiveSessionsUseCase', () => {
706
729
  ).not.toHaveBeenCalled();
707
730
  });
708
731
 
709
- it('re-notifies a persistent stall on every cycle once it has been a candidate in the previous cycle', async () => {
732
+ it('notifies a persistent stall only once per silent episode instead of every cycle', async () => {
733
+ wireStatefulNotifiedLatch();
710
734
  setupLiveInteractiveSession(GITHUB_SESSION);
711
735
  mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
712
736
  [
@@ -719,18 +743,60 @@ describe('NotifySilentLiveSessionsUseCase', () => {
719
743
  ],
720
744
  );
721
745
 
746
+ await useCase.run(runParams());
722
747
  await useCase.run(runParams());
723
748
  await useCase.run(runParams());
724
749
 
725
750
  expect(
726
751
  mockNotificationRepository.sendSelfCheckNotification,
727
- ).toHaveBeenCalledTimes(2);
752
+ ).toHaveBeenCalledTimes(1);
753
+ expect(
754
+ mockNotificationRepository.sendSelfCheckNotification,
755
+ ).toHaveBeenCalledWith(GITHUB_SESSION, MAIN_STALLED_SECTION);
756
+ });
757
+
758
+ it('re-notifies a session after its silent episode resolves and a new one begins', async () => {
759
+ wireStatefulNotifiedLatch();
760
+ setupLiveInteractiveSession(GITHUB_SESSION);
761
+ const stalled = [
762
+ {
763
+ sessionName: GITHUB_SESSION,
764
+ lastOutputEpochSeconds:
765
+ nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
766
+ hasInProgressToolCall: false,
767
+ },
768
+ ];
769
+ const active = [
770
+ {
771
+ sessionName: GITHUB_SESSION,
772
+ lastOutputEpochSeconds: nowEpochSeconds,
773
+ hasInProgressToolCall: false,
774
+ },
775
+ ];
776
+
777
+ mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
778
+ stalled,
779
+ );
780
+ await useCase.run(runParams());
781
+ expect(
782
+ mockNotificationRepository.sendSelfCheckNotification,
783
+ ).toHaveBeenCalledTimes(1);
784
+
785
+ mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
786
+ active,
787
+ );
788
+ await useCase.run(runParams());
728
789
  expect(
729
790
  mockNotificationRepository.sendSelfCheckNotification,
730
- ).toHaveBeenNthCalledWith(1, GITHUB_SESSION, MAIN_STALLED_SECTION);
791
+ ).toHaveBeenCalledTimes(1);
792
+
793
+ mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
794
+ stalled,
795
+ );
796
+ await useCase.run(runParams());
731
797
  expect(
732
798
  mockNotificationRepository.sendSelfCheckNotification,
733
- ).toHaveBeenNthCalledWith(2, GITHUB_SESSION, MAIN_STALLED_SECTION);
799
+ ).toHaveBeenCalledTimes(2);
734
800
  });
735
801
 
736
802
  describe('two-consecutive-cycle debounce', () => {
@@ -832,6 +898,147 @@ describe('NotifySilentLiveSessionsUseCase', () => {
832
898
  });
833
899
  });
834
900
 
901
+ describe('fire-once latch and input-state gating', () => {
902
+ const setupSilentGithubSession = (): void => {
903
+ setupLiveInteractiveSession(GITHUB_SESSION);
904
+ mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
905
+ [
906
+ {
907
+ sessionName: GITHUB_SESSION,
908
+ lastOutputEpochSeconds:
909
+ nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
910
+ hasInProgressToolCall: false,
911
+ },
912
+ ],
913
+ );
914
+ };
915
+
916
+ const idleSubAgent = (label: string): SubAgentActivity => ({
917
+ label,
918
+ silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
919
+ runningSeconds: 60,
920
+ waitingOnExternalProcess: false,
921
+ });
922
+
923
+ it('loads the fire-once latch using the configured recency window', async () => {
924
+ setupSilentGithubSession();
925
+
926
+ await useCase.run(
927
+ runParams({ candidateDebounceRecencyWindowSeconds: 900 }),
928
+ );
929
+
930
+ expect(
931
+ mockNotifiedStateRepository.loadRecentNotifiedSessionNames,
932
+ ).toHaveBeenCalledWith({ now, recencyWindowSeconds: 900 });
933
+ });
934
+
935
+ it('persists the sent session in the fire-once latch on a send cycle', async () => {
936
+ setupSilentGithubSession();
937
+
938
+ await useCase.run(runParams());
939
+
940
+ expect(
941
+ mockNotificationRepository.sendSelfCheckNotification,
942
+ ).toHaveBeenCalledWith(GITHUB_SESSION, MAIN_STALLED_SECTION);
943
+ expect(
944
+ mockNotifiedStateRepository.saveNotifiedSessionNames,
945
+ ).toHaveBeenCalledWith({ sessionNames: [GITHUB_SESSION], now });
946
+ });
947
+
948
+ it('does not re-send but keeps the latch for a session already notified this episode', async () => {
949
+ setupSilentGithubSession();
950
+ mockNotifiedStateRepository.loadRecentNotifiedSessionNames.mockResolvedValue(
951
+ new Set([GITHUB_SESSION]),
952
+ );
953
+
954
+ await useCase.run(runParams());
955
+
956
+ expect(
957
+ mockNotificationRepository.sendSelfCheckNotification,
958
+ ).not.toHaveBeenCalled();
959
+ expect(
960
+ mockNotifiedStateRepository.saveNotifiedSessionNames,
961
+ ).toHaveBeenCalledWith({ sessionNames: [GITHUB_SESSION], now });
962
+ });
963
+
964
+ it('prunes the fire-once latch for a previously notified session that is no longer a candidate', async () => {
965
+ mockNotifiedStateRepository.loadRecentNotifiedSessionNames.mockResolvedValue(
966
+ new Set([GITHUB_SESSION]),
967
+ );
968
+
969
+ await useCase.run(runParams());
970
+
971
+ expect(
972
+ mockNotifiedStateRepository.saveNotifiedSessionNames,
973
+ ).toHaveBeenCalledWith({ sessionNames: [], now });
974
+ });
975
+
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
+ );
990
+
991
+ await useCase.run(runParams());
992
+
993
+ expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalled();
994
+ expect(
995
+ mockNotificationRepository.sendSelfCheckNotification,
996
+ ).not.toHaveBeenCalled();
997
+ expect(
998
+ 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);
1039
+ });
1040
+ });
1041
+
835
1042
  describe('sub-agent state-based reminder judgment', () => {
836
1043
  const hungSubAgent = (label: string): SubAgentActivity => ({
837
1044
  label,
@@ -893,7 +1100,8 @@ describe('NotifySilentLiveSessionsUseCase', () => {
893
1100
  ).toHaveBeenCalledWith(GITHUB_SESSION, SUBAGENT_SECTION);
894
1101
  });
895
1102
 
896
- it('keeps notifying a hung sub-agent on every cycle while the condition holds', async () => {
1103
+ it('notifies a hung sub-agent only once per episode instead of every cycle', async () => {
1104
+ wireStatefulNotifiedLatch();
897
1105
  setupSubAgents([hungSubAgent('sub-process-1')]);
898
1106
 
899
1107
  await useCase.run(runParams());
@@ -901,7 +1109,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
901
1109
 
902
1110
  expect(
903
1111
  mockNotificationRepository.sendSelfCheckNotification,
904
- ).toHaveBeenCalledTimes(2);
1112
+ ).toHaveBeenCalledTimes(1);
905
1113
  });
906
1114
 
907
1115
  it('never selects a waiting sub-agent for the long-running section even when quiet past both thresholds', async () => {
@@ -942,7 +1150,8 @@ describe('NotifySilentLiveSessionsUseCase', () => {
942
1150
  ).toHaveBeenCalledWith(GITHUB_SESSION, SUBAGENT_SECTION);
943
1151
  });
944
1152
 
945
- it('keeps notifying a quiet long-running sub-agent on every cycle while the condition holds', async () => {
1153
+ it('notifies a quiet long-running sub-agent only once per episode while still evaluating it every cycle', async () => {
1154
+ wireStatefulNotifiedLatch();
946
1155
  setupSubAgents([quietLongRunningSubAgent('sub-process-1', false)]);
947
1156
 
948
1157
  await useCase.run(runParams());
@@ -950,7 +1159,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
950
1159
 
951
1160
  expect(
952
1161
  mockNotificationRepository.sendSelfCheckNotification,
953
- ).toHaveBeenCalledTimes(2);
1162
+ ).toHaveBeenCalledTimes(1);
954
1163
  expect(
955
1164
  mockMessageComposer.composeSubAgentSection,
956
1165
  ).toHaveBeenNthCalledWith(2, {
@@ -1626,6 +1835,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
1626
1835
  mockOwnerCallStatusProvider,
1627
1836
  mockNotificationRepository,
1628
1837
  mockCandidateStateRepository,
1838
+ mockNotifiedStateRepository,
1629
1839
  mockMessageComposer,
1630
1840
  mockSleeper,
1631
1841
  mockHubTaskStatusResolver,
@@ -9,6 +9,7 @@ import { SessionSubAgentActivityRepository } from './adapter-interfaces/SessionS
9
9
  import { SilentSessionMessageComposer } from './adapter-interfaces/SilentSessionMessageComposer';
10
10
  import { SilentSessionNotificationRepository } from './adapter-interfaces/SilentSessionNotificationRepository';
11
11
  import { SilentSessionCandidateStateRepository } from './adapter-interfaces/SilentSessionCandidateStateRepository';
12
+ import { SilentSessionNotifiedStateRepository } from './adapter-interfaces/SilentSessionNotifiedStateRepository';
12
13
  import { SilentSessionHubTaskStatusCacheRepository } from './adapter-interfaces/SilentSessionHubTaskStatusCacheRepository';
13
14
  import { Sleeper } from './adapter-interfaces/Sleeper';
14
15
  import { IssueRepository } from './adapter-interfaces/IssueRepository';
@@ -70,6 +71,7 @@ type NotifyCandidate = {
70
71
  sessionName: string;
71
72
  message: string;
72
73
  sectionLabels: string[];
74
+ mainInputBusy: boolean;
73
75
  };
74
76
 
75
77
  export class NotifySilentLiveSessionsUseCase {
@@ -84,6 +86,7 @@ export class NotifySilentLiveSessionsUseCase {
84
86
  private readonly ownerCallStatusProvider: OwnerCallStatusProvider,
85
87
  private readonly notificationRepository: SilentSessionNotificationRepository,
86
88
  private readonly candidateStateRepository: SilentSessionCandidateStateRepository,
89
+ private readonly notifiedStateRepository: SilentSessionNotifiedStateRepository,
87
90
  private readonly messageComposer: SilentSessionMessageComposer,
88
91
  private readonly sleeper: Sleeper,
89
92
  private readonly hubTaskStatusResolver: HubTaskStatusResolver | null = null,
@@ -185,8 +188,44 @@ export class NotifySilentLiveSessionsUseCase {
185
188
  `Silent live session notification: ${debouncedCandidates.length} debounced candidate(s) of ${candidates.length} current candidate(s) across ${interactiveSessions.length} interactive session(s); ${suppressedFirstCycleCount} first-cycle candidate(s) deferred until they persist into the next cycle.`,
186
189
  );
187
190
 
191
+ // Fire-once latch: the set of sessions already reminded during their
192
+ // current silent episode. A session that is still a candidate this cycle
193
+ // and was already reminded is NOT re-injected, so a continuous silent
194
+ // episode produces exactly one reminder instead of one every schedule
195
+ // cycle. The latch is keyed by the globally-unique session name and
196
+ // persisted across the per-cycle fresh monitor process on disk.
197
+ const currentCandidateSessionNames = new Set(
198
+ candidates.map((candidate) => candidate.sessionName),
199
+ );
200
+ const previouslyNotifiedSessionNames =
201
+ await this.notifiedStateRepository.loadRecentNotifiedSessionNames({
202
+ now: params.now,
203
+ recencyWindowSeconds: params.candidateDebounceRecencyWindowSeconds,
204
+ });
205
+
188
206
  let sentCount = 0;
207
+ const notifiedThisCycleSessionNames: string[] = [];
189
208
  for (const candidate of debouncedCandidates) {
209
+ if (previouslyNotifiedSessionNames.has(candidate.sessionName)) {
210
+ console.log(
211
+ `Skipping ${candidate.sessionName}: the current silent-episode reminder was already delivered; not re-injecting until the condition resolves and re-arises.`,
212
+ );
213
+ continue;
214
+ }
215
+ // Input-state gating: a session whose main REPL is mid-turn — currently
216
+ // running an in-progress tool call, which includes waiting on a
217
+ // long-running sub-agent — cannot accept an injected prompt cleanly, so
218
+ // the reminder is deferred rather than piled into a busy input box. The
219
+ // session stays a candidate, so it is reminded once it becomes
220
+ // input-ready. A tool call pending past
221
+ // IN_PROGRESS_TOOL_CALL_MAX_SUPPRESS_SECONDS is treated as hung and no
222
+ // longer counts as busy (see composeCandidate).
223
+ if (candidate.mainInputBusy) {
224
+ console.log(
225
+ `Skipping ${candidate.sessionName}: main REPL is not in an input-accepting state (in-progress tool call); deferring reminder until it is idle.`,
226
+ );
227
+ continue;
228
+ }
190
229
  if (
191
230
  !(await this.isHubTaskActive(
192
231
  candidate.sessionName,
@@ -205,6 +244,7 @@ export class NotifySilentLiveSessionsUseCase {
205
244
  candidate.message,
206
245
  );
207
246
  sentCount += 1;
247
+ notifiedThisCycleSessionNames.push(candidate.sessionName);
208
248
  // One line per send, grep-stable on the `Notified ` prefix: the
209
249
  // ISO-8601 UTC timestamp disambiguates concurrent schedule runs and
210
250
  // the section list records what the message actually contained.
@@ -212,6 +252,25 @@ export class NotifySilentLiveSessionsUseCase {
212
252
  `Notified ${candidate.sessionName} at=${params.now.toISOString()} sections=[${candidate.sectionLabels.join(',')}]`,
213
253
  );
214
254
  }
255
+
256
+ // Persist the latch for the next cycle: keep every already-latched session
257
+ // that is STILL a candidate (refreshing its timestamp so a continuous
258
+ // episode stays latched) plus the sessions notified this cycle, and prune
259
+ // any latched session that is no longer a candidate so its episode ends and
260
+ // a later re-qualification fires a fresh reminder.
261
+ const retainedNotifiedSessionNames = [
262
+ ...previouslyNotifiedSessionNames,
263
+ ].filter((sessionName) => currentCandidateSessionNames.has(sessionName));
264
+ const notifiedSessionNamesToPersist = Array.from(
265
+ new Set([
266
+ ...retainedNotifiedSessionNames,
267
+ ...notifiedThisCycleSessionNames,
268
+ ]),
269
+ );
270
+ await this.notifiedStateRepository.saveNotifiedSessionNames({
271
+ sessionNames: notifiedSessionNamesToPersist,
272
+ now: params.now,
273
+ });
215
274
  };
216
275
 
217
276
  private isHubTaskActive = async (
@@ -488,6 +547,7 @@ export class NotifySilentLiveSessionsUseCase {
488
547
  sessionName: snapshot.sessionName,
489
548
  message: sections.join('\n\n'),
490
549
  sectionLabels,
550
+ mainInputBusy: suppressedByInProgressToolCall,
491
551
  };
492
552
  };
493
553
  }
@@ -0,0 +1,10 @@
1
+ export interface SilentSessionNotifiedStateRepository {
2
+ loadRecentNotifiedSessionNames: (params: {
3
+ now: Date;
4
+ recencyWindowSeconds: number;
5
+ }) => Promise<Set<string>>;
6
+ saveNotifiedSessionNames: (params: {
7
+ sessionNames: string[];
8
+ now: Date;
9
+ }) => Promise<void>;
10
+ }
@@ -0,0 +1,6 @@
1
+ export interface SubAgentLivenessResolver {
2
+ resolveLiveSubAgentIds: (params: {
3
+ sessionName: string;
4
+ mainTranscriptPath: string | null;
5
+ }) => Promise<Set<string> | null>;
6
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"HandleScheduledEventUseCaseHandler.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts"],"names":[],"mappings":"AAkCA,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAkD3D,qBAAa,kCAAkC;IAC7C,MAAM,GACJ,gBAAgB,MAAM,EACtB,UAAU,OAAO,EACjB,6BAA4B,MAAM,EAAE,GAAG,IAAW,KACjD,OAAO,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,EAAE,KAAK,EAAE,CAAC;QAChB,SAAS,EAAE,OAAO,CAAC;QACnB,eAAe,EAAE,IAAI,EAAE,CAAC;KACzB,GAAG,IAAI,CAAC,CAylBP;CACH"}
1
+ {"version":3,"file":"HandleScheduledEventUseCaseHandler.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts"],"names":[],"mappings":"AAoCA,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAkD3D,qBAAa,kCAAkC;IAC7C,MAAM,GACJ,gBAAgB,MAAM,EACtB,UAAU,OAAO,EACjB,6BAA4B,MAAM,EAAE,GAAG,IAAW,KACjD,OAAO,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,EAAE,KAAK,EAAE,CAAC;QAChB,SAAS,EAAE,OAAO,CAAC;QACnB,eAAe,EAAE,IAAI,EAAE,CAAC;KACzB,GAAG,IAAI,CAAC,CAumBP;CACH"}
@@ -10,6 +10,7 @@ export type NotifySilentTmuxSessionsParams = {
10
10
  subAgentOutputRootDirectory: string | null;
11
11
  subAgentProcessMatchPattern: string | null;
12
12
  subAgentTranscriptRootDirectory: string | null;
13
+ subAgentRuntimeRootDirectory: string | null;
13
14
  mainSilentThresholdSeconds: number;
14
15
  unansweredOwnerCallGraceSeconds: number;
15
16
  subAgentSilentThresholdSeconds: number;
@@ -17,6 +18,7 @@ export type NotifySilentTmuxSessionsParams = {
17
18
  staggerSeconds: number;
18
19
  candidateDebounceRecencyWindowSeconds: number;
19
20
  candidateDebounceStateFilePath: string | null;
21
+ notifiedStateFilePath: string | null;
20
22
  activeHubTaskStatus: string | null;
21
23
  hubTaskStatusResolver: HubTaskStatusResolver | null;
22
24
  hubTaskStatusCacheStateFilePath: string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"notifySilentTmuxSessions.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gEAAgE,CAAC;AAGpG,OAAO,EAAE,oBAAoB,EAAE,MAAM,kEAAkE,CAAC;AACxG,OAAO,EAEL,qBAAqB,EAQtB,MAAM,0DAA0D,CAAC;AAelE,OAAO,EAEL,6BAA6B,EAC9B,MAAM,6DAA6D,CAAC;AAKrE,MAAM,MAAM,8BAA8B,GAAG;IAC3C,OAAO,EAAE,OAAO,CAAC;IACjB,kBAAkB,EAAE,kBAAkB,CAAC;IACvC,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,2BAA2B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,2BAA2B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,+BAA+B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,0BAA0B,EAAE,MAAM,CAAC;IACnC,+BAA+B,EAAE,MAAM,CAAC;IACxC,8BAA8B,EAAE,MAAM,CAAC;IACvC,+BAA+B,EAAE,MAAM,CAAC;IACxC,cAAc,EAAE,MAAM,CAAC;IACvB,qCAAqC,EAAE,MAAM,CAAC;IAC9C,8BAA8B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9C,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,qBAAqB,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACpD,+BAA+B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,4BAA4B,EAAE,MAAM,CAAC;IACrC,gBAAgB,EAAE,6BAA6B,CAAC;IAChD,GAAG,EAAE,IAAI,CAAC;CACX,CAAC;AAqCF,eAAO,MAAM,wBAAwB,GACnC,QAAQ,8BAA8B,KACrC,OAAO,CAAC,IAAI,CA2Ed,CAAC;AAEF,eAAO,MAAM,0CAA0C;;;;;;;;CAS7C,CAAC"}
1
+ {"version":3,"file":"notifySilentTmuxSessions.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gEAAgE,CAAC;AAGpG,OAAO,EAAE,oBAAoB,EAAE,MAAM,kEAAkE,CAAC;AACxG,OAAO,EAEL,qBAAqB,EAQtB,MAAM,0DAA0D,CAAC;AAgBlE,OAAO,EAEL,6BAA6B,EAC9B,MAAM,6DAA6D,CAAC;AAMrE,MAAM,MAAM,8BAA8B,GAAG;IAC3C,OAAO,EAAE,OAAO,CAAC;IACjB,kBAAkB,EAAE,kBAAkB,CAAC;IACvC,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,2BAA2B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,2BAA2B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,+BAA+B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,4BAA4B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,0BAA0B,EAAE,MAAM,CAAC;IACnC,+BAA+B,EAAE,MAAM,CAAC;IACxC,8BAA8B,EAAE,MAAM,CAAC;IACvC,+BAA+B,EAAE,MAAM,CAAC;IACxC,cAAc,EAAE,MAAM,CAAC;IACvB,qCAAqC,EAAE,MAAM,CAAC;IAC9C,8BAA8B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9C,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,qBAAqB,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACpD,+BAA+B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,4BAA4B,EAAE,MAAM,CAAC;IACrC,gBAAgB,EAAE,6BAA6B,CAAC;IAChD,GAAG,EAAE,IAAI,CAAC;CACX,CAAC;AAuCF,eAAO,MAAM,wBAAwB,GACnC,QAAQ,8BAA8B,KACrC,OAAO,CAAC,IAAI,CAmFd,CAAC;AAEF,eAAO,MAAM,0CAA0C;;;;;;;;CAS7C,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { SilentSessionNotifiedStateRepository } from '../../domain/usecases/adapter-interfaces/SilentSessionNotifiedStateRepository';
2
+ export declare class FileSystemSilentSessionNotifiedStateRepository implements SilentSessionNotifiedStateRepository {
3
+ private readonly stateFilePath;
4
+ constructor(stateFilePath?: string);
5
+ loadRecentNotifiedSessionNames: (params: {
6
+ now: Date;
7
+ recencyWindowSeconds: number;
8
+ }) => Promise<Set<string>>;
9
+ saveNotifiedSessionNames: (params: {
10
+ sessionNames: string[];
11
+ now: Date;
12
+ }) => Promise<void>;
13
+ private readNotifiedEntries;
14
+ private writeState;
15
+ }
16
+ //# sourceMappingURL=FileSystemSilentSessionNotifiedStateRepository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FileSystemSilentSessionNotifiedStateRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/FileSystemSilentSessionNotifiedStateRepository.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,oCAAoC,EAAE,MAAM,+EAA+E,CAAC;AAuBrI,qBAAa,8CAA+C,YAAW,oCAAoC;IAEvG,OAAO,CAAC,QAAQ,CAAC,aAAa;gBAAb,aAAa,GAAE,MAA+B;IAGjE,8BAA8B,GAAU,QAAQ;QAC9C,GAAG,EAAE,IAAI,CAAC;QACV,oBAAoB,EAAE,MAAM,CAAC;KAC9B,KAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAWtB;IAEF,wBAAwB,GAAU,QAAQ;QACxC,YAAY,EAAE,MAAM,EAAE,CAAC;QACvB,GAAG,EAAE,IAAI,CAAC;KACX,KAAG,OAAO,CAAC,IAAI,CAAC,CAUf;IAEF,OAAO,CAAC,mBAAmB,CAoCzB;IAEF,OAAO,CAAC,UAAU,CAMhB;CACH"}
@@ -0,0 +1,11 @@
1
+ import { SubAgentLivenessResolver } from '../../domain/usecases/adapter-interfaces/SubAgentLivenessResolver';
2
+ export declare class FileSystemSubAgentLivenessResolver implements SubAgentLivenessResolver {
3
+ private readonly runtimeRootDirectory;
4
+ constructor(runtimeRootDirectory: string | null);
5
+ resolveLiveSubAgentIds: (params: {
6
+ sessionName: string;
7
+ mainTranscriptPath: string | null;
8
+ }) => Promise<Set<string> | null>;
9
+ private resolveRunningSubAgentsFilePath;
10
+ }
11
+ //# sourceMappingURL=FileSystemSubAgentLivenessResolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FileSystemSubAgentLivenessResolver.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/FileSystemSubAgentLivenessResolver.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,wBAAwB,EAAE,MAAM,mEAAmE,CAAC;AAI7G,qBAAa,kCAAmC,YAAW,wBAAwB;IACrE,OAAO,CAAC,QAAQ,CAAC,oBAAoB;gBAApB,oBAAoB,EAAE,MAAM,GAAG,IAAI;IAEhE,sBAAsB,GAAU,QAAQ;QACtC,WAAW,EAAE,MAAM,CAAC;QACpB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;KACnC,KAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAqB7B;IAEF,OAAO,CAAC,+BAA+B,CAoBrC;CACH"}
@@ -1 +1 @@
1
- {"version":3,"file":"TmuxSilentSessionNotificationRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/TmuxSilentSessionNotificationRepository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mCAAmC,EAAE,MAAM,8EAA8E,CAAC;AACnI,OAAO,EAAE,kBAAkB,EAAE,MAAM,6DAA6D,CAAC;AAEjG,qBAAa,uCAAwC,YAAW,mCAAmC;IACrF,OAAO,CAAC,QAAQ,CAAC,kBAAkB;gBAAlB,kBAAkB,EAAE,kBAAkB;IAEnE,yBAAyB,GACvB,aAAa,MAAM,EACnB,SAAS,MAAM,KACd,OAAO,CAAC,IAAI,CAAC,CA4Bd;CACH"}
1
+ {"version":3,"file":"TmuxSilentSessionNotificationRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/TmuxSilentSessionNotificationRepository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mCAAmC,EAAE,MAAM,8EAA8E,CAAC;AACnI,OAAO,EAAE,kBAAkB,EAAE,MAAM,6DAA6D,CAAC;AAKjG,qBAAa,uCAAwC,YAAW,mCAAmC;IACrF,OAAO,CAAC,QAAQ,CAAC,kBAAkB;gBAAlB,kBAAkB,EAAE,kBAAkB;IAEnE,yBAAyB,GACvB,aAAa,MAAM,EACnB,SAAS,MAAM,KACd,OAAO,CAAC,IAAI,CAAC,CA6Bd;CACH"}
@@ -1,5 +1,6 @@
1
1
  import { SubAgentActivity } from '../../domain/entities/LiveSessionActivitySnapshot';
2
2
  import { SessionSubAgentActivityRepository } from '../../domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository';
3
+ import { SubAgentLivenessResolver } from '../../domain/usecases/adapter-interfaces/SubAgentLivenessResolver';
3
4
  import { SubAgentProcessLister } from '../../domain/usecases/adapter-interfaces/SubAgentProcessLister';
4
5
  import { SubAgentTranscriptDirectoryResolver } from '../../domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver';
5
6
  export declare const normalizeCommandFragment: (command: string) => string;
@@ -7,9 +8,10 @@ export declare class TranscriptSessionSubAgentActivityRepository implements Sess
7
8
  private readonly directoryResolver;
8
9
  private readonly processLister;
9
10
  private readonly now;
10
- constructor(directoryResolver: SubAgentTranscriptDirectoryResolver, processLister: SubAgentProcessLister, now: Date);
11
+ private readonly livenessResolver;
12
+ constructor(directoryResolver: SubAgentTranscriptDirectoryResolver, processLister: SubAgentProcessLister, now: Date, livenessResolver?: SubAgentLivenessResolver);
11
13
  listSubAgentActivitiesBySessionName: (sessionNames: string[], transcriptPathBySessionName: Map<string, string>) => Promise<Map<string, SubAgentActivity[]>>;
12
- private loadKilledOrFailedAgentIds;
14
+ private loadTerminalAgentIds;
13
15
  private collectActivities;
14
16
  private toActivity;
15
17
  private hasLiveMatchingProcess;