github-issue-tower-defence-management 1.122.4 → 1.122.5

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 (53) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +5 -1
  3. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +14 -2
  4. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  5. package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +3 -1
  6. package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -1
  7. package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js +6 -0
  8. package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js.map +1 -1
  9. package/bin/adapter/repositories/NoUnansweredOwnerCallStatusProvider.js +2 -2
  10. package/bin/adapter/repositories/NoUnansweredOwnerCallStatusProvider.js.map +1 -1
  11. package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js +14 -11
  12. package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js.map +1 -1
  13. package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js +13 -0
  14. package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js.map +1 -1
  15. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +15 -5
  16. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
  17. package/package.json +1 -1
  18. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +11 -0
  19. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +46 -7
  20. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +5 -0
  21. package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.test.ts +42 -0
  22. package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts +16 -0
  23. package/src/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.test.ts +1 -1
  24. package/src/adapter/repositories/NoUnansweredOwnerCallStatusProvider.test.ts +7 -6
  25. package/src/adapter/repositories/NoUnansweredOwnerCallStatusProvider.ts +3 -3
  26. package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.test.ts +99 -49
  27. package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.ts +24 -14
  28. package/src/domain/entities/LiveSessionActivitySnapshot.ts +1 -1
  29. package/src/domain/usecases/DefaultSilentSessionMessageComposer.test.ts +75 -0
  30. package/src/domain/usecases/DefaultSilentSessionMessageComposer.ts +26 -0
  31. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +106 -10
  32. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +24 -6
  33. package/src/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.ts +2 -2
  34. package/src/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.ts +4 -0
  35. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  36. package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts +2 -0
  37. package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -1
  38. package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts +2 -0
  39. package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts.map +1 -1
  40. package/types/adapter/repositories/NoUnansweredOwnerCallStatusProvider.d.ts +1 -1
  41. package/types/adapter/repositories/NoUnansweredOwnerCallStatusProvider.d.ts.map +1 -1
  42. package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts +2 -2
  43. package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts.map +1 -1
  44. package/types/domain/entities/LiveSessionActivitySnapshot.d.ts +1 -1
  45. package/types/domain/entities/LiveSessionActivitySnapshot.d.ts.map +1 -1
  46. package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts +1 -0
  47. package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts.map +1 -1
  48. package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +2 -0
  49. package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
  50. package/types/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.d.ts +1 -1
  51. package/types/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.d.ts.map +1 -1
  52. package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts +1 -0
  53. package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts.map +1 -1
@@ -91,31 +91,41 @@ const hasOwnerTextReply = (content: unknown): boolean => {
91
91
  export class TranscriptOwnerCallStatusProvider implements OwnerCallStatusProvider {
92
92
  constructor(private readonly ownerCallMarker: string | null) {}
93
93
 
94
- listSessionNamesWithUnansweredOwnerCall = async (
94
+ listUnansweredOwnerCallEpochSecondsBySessionName = async (
95
95
  transcriptPathBySessionName: Map<string, string>,
96
- ): Promise<Set<string>> => {
97
- const waiting = new Set<string>();
96
+ ): Promise<Map<string, number>> => {
97
+ const unansweredOwnerCallEpochSecondsBySessionName = new Map<
98
+ string,
99
+ number
100
+ >();
98
101
  if (this.ownerCallMarker === null || this.ownerCallMarker.length === 0) {
99
- return waiting;
102
+ return unansweredOwnerCallEpochSecondsBySessionName;
100
103
  }
101
104
  const marker = this.ownerCallMarker;
102
105
  for (const [sessionName, transcriptPath] of transcriptPathBySessionName) {
103
- if (this.isWaitingForOwnerReply(transcriptPath, marker)) {
104
- waiting.add(sessionName);
106
+ const unansweredOwnerCallEpochMs = this.findUnansweredOwnerCallEpochMs(
107
+ transcriptPath,
108
+ marker,
109
+ );
110
+ if (unansweredOwnerCallEpochMs !== null) {
111
+ unansweredOwnerCallEpochSecondsBySessionName.set(
112
+ sessionName,
113
+ Math.floor(unansweredOwnerCallEpochMs / 1000),
114
+ );
105
115
  }
106
116
  }
107
- return waiting;
117
+ return unansweredOwnerCallEpochSecondsBySessionName;
108
118
  };
109
119
 
110
- private isWaitingForOwnerReply = (
120
+ private findUnansweredOwnerCallEpochMs = (
111
121
  transcriptPath: string,
112
122
  marker: string,
113
- ): boolean => {
123
+ ): number | null => {
114
124
  let content: string;
115
125
  try {
116
126
  content = fs.readFileSync(transcriptPath, 'utf8');
117
127
  } catch {
118
- return false;
128
+ return null;
119
129
  }
120
130
  let lastOwnerCallEpochMs: number | null = null;
121
131
  let lastOwnerReplyEpochMs: number | null = null;
@@ -155,11 +165,11 @@ export class TranscriptOwnerCallStatusProvider implements OwnerCallStatusProvide
155
165
  }
156
166
  }
157
167
  if (lastOwnerCallEpochMs === null) {
158
- return false;
168
+ return null;
159
169
  }
160
- return (
161
- lastOwnerReplyEpochMs === null ||
170
+ return lastOwnerReplyEpochMs === null ||
162
171
  lastOwnerCallEpochMs > lastOwnerReplyEpochMs
163
- );
172
+ ? lastOwnerCallEpochMs
173
+ : null;
164
174
  };
165
175
  }
@@ -9,5 +9,5 @@ export type LiveSessionActivitySnapshot = {
9
9
  sessionName: string;
10
10
  mainSilentSeconds: number | null;
11
11
  subAgents: SubAgentActivity[];
12
- hasUnansweredOwnerCall: boolean;
12
+ unansweredOwnerCallAgeSeconds: number | null;
13
13
  };
@@ -116,6 +116,81 @@ describe('DefaultSilentSessionMessageComposer', () => {
116
116
  expect(section).not.toContain('""');
117
117
  });
118
118
 
119
+ it('embeds the reminder sentinel in the stale-owner-call main-stalled section', () => {
120
+ const section = composer.composeMainStalledWithStaleOwnerCallSection(
121
+ 600,
122
+ 3600,
123
+ );
124
+ expect(section).toContain(SILENT_SESSION_REMINDER_SENTINEL);
125
+ });
126
+
127
+ it('renders the stale-owner-call section with the silent and owner-call-age minutes substituted', () => {
128
+ const section = composer.composeMainStalledWithStaleOwnerCallSection(
129
+ 659,
130
+ 3659,
131
+ );
132
+ expect(section).toContain(
133
+ 'You have produced no output for 10 minutes, and the owner call you raised 60 minutes ago is still unanswered.',
134
+ );
135
+ });
136
+
137
+ it('directs the agent to re-raise its pending ask as a fresh fully self-contained owner call', () => {
138
+ const section = composer.composeMainStalledWithStaleOwnerCallSection(
139
+ 600,
140
+ 3600,
141
+ );
142
+ expect(section).toContain(
143
+ 'If you are still blocked on the owner, re-raise your pending ask NOW as a fresh owner call.',
144
+ );
145
+ expect(section).toContain(
146
+ 'Make it fully self-contained: restate the whole situation — what happened, what you are asking, and any decision needed — inside the new owner call itself',
147
+ );
148
+ });
149
+
150
+ it('directs the agent to continue autonomously when it is no longer blocked', () => {
151
+ const section = composer.composeMainStalledWithStaleOwnerCallSection(
152
+ 600,
153
+ 3600,
154
+ );
155
+ expect(section).toContain(
156
+ 'If you are no longer blocked — the answer became unnecessary, you can proceed safely, or the information arrived another way — resume immediately and drive all remaining tasks to completion.',
157
+ );
158
+ });
159
+
160
+ it('states that the stale-owner-call reminder itself does not notify the owner', () => {
161
+ const section = composer.composeMainStalledWithStaleOwnerCallSection(
162
+ 600,
163
+ 3600,
164
+ );
165
+ expect(section).toContain(
166
+ 'This reminder does not notify the owner; only a fresh owner call from you surfaces this session to the owner.',
167
+ );
168
+ });
169
+
170
+ it('interpolates the configured owner-call marker into the stale-owner-call format guidance', () => {
171
+ const markedComposer = new DefaultSilentSessionMessageComposer(
172
+ '<<OWNER_CALL>>',
173
+ );
174
+ const section = markedComposer.composeMainStalledWithStaleOwnerCallSection(
175
+ 600,
176
+ 3600,
177
+ );
178
+ expect(section).toContain(
179
+ 'the configured owner-call marker tag "<<OWNER_CALL>>" as a complete matching pair',
180
+ );
181
+ expect(section).toContain('🔴');
182
+ });
183
+
184
+ it('composes the stale-owner-call section distinctly from the plain main-stalled section', () => {
185
+ const plainSection = composer.composeMainStalledSection(600);
186
+ const staleSection = composer.composeMainStalledWithStaleOwnerCallSection(
187
+ 600,
188
+ 3600,
189
+ );
190
+ expect(staleSection).not.toBe(plainSection);
191
+ expect(plainSection).not.toContain('is still unanswered');
192
+ });
193
+
119
194
  it('emits a distinct idle message for a sub-agent that is only output-idle', () => {
120
195
  const section = composer.composeSubAgentSection({
121
196
  idleSubAgents: [
@@ -67,6 +67,21 @@ const composeMainStalledMessage = (
67
67
  ].join('\n');
68
68
  };
69
69
 
70
+ const composeMainStalledWithStaleOwnerCallMessage = (
71
+ mainSilentSeconds: number,
72
+ unansweredOwnerCallAgeSeconds: number,
73
+ ownerCallMarker: string | null,
74
+ ): string => {
75
+ const silentMinutes = Math.floor(mainSilentSeconds / 60);
76
+ const ownerCallAgeMinutes = Math.floor(unansweredOwnerCallAgeSeconds / 60);
77
+ return [
78
+ `${SILENT_SESSION_REMINDER_SENTINEL} You have produced no output for ${silentMinutes} minutes, and the owner call you raised ${ownerCallAgeMinutes} minutes ago is still unanswered. An owner call outstanding this long may have been missed, may have scrolled out of the owner's view, or may no longer reflect your current situation, so do not keep waiting on it passively. Act now, choosing one of the following:`,
79
+ `1. If you are still blocked on the owner, re-raise your pending ask NOW as a fresh owner call. Make it fully self-contained: restate the whole situation — what happened, what you are asking, and any decision needed — inside the new owner call itself, so the owner understands everything from that single latest message without scrolling back. ${composeOwnerCallFormatGuidance(ownerCallMarker)}`,
80
+ `2. If you are no longer blocked — the answer became unnecessary, you can proceed safely, or the information arrived another way — resume immediately and drive all remaining tasks to completion.`,
81
+ `This reminder does not notify the owner; only a fresh owner call from you surfaces this session to the owner. Also, in your next output, report an estimate of how many minutes you expect to need to finish all remaining tasks.`,
82
+ ].join('\n');
83
+ };
84
+
70
85
  export class DefaultSilentSessionMessageComposer implements SilentSessionMessageComposer {
71
86
  constructor(private readonly ownerCallMarker: string | null = null) {}
72
87
 
@@ -74,6 +89,17 @@ export class DefaultSilentSessionMessageComposer implements SilentSessionMessage
74
89
  return composeMainStalledMessage(mainSilentSeconds, this.ownerCallMarker);
75
90
  };
76
91
 
92
+ composeMainStalledWithStaleOwnerCallSection = (
93
+ mainSilentSeconds: number,
94
+ unansweredOwnerCallAgeSeconds: number,
95
+ ): string => {
96
+ return composeMainStalledWithStaleOwnerCallMessage(
97
+ mainSilentSeconds,
98
+ unansweredOwnerCallAgeSeconds,
99
+ this.ownerCallMarker,
100
+ );
101
+ };
102
+
77
103
  composeSubAgentSection = (stallSections: SubAgentStallSections): string => {
78
104
  const sections: string[] = [];
79
105
  if (stallSections.idleSubAgents.length > 0) {
@@ -4,6 +4,7 @@ import {
4
4
  parseHubTaskIssueUrlFromSessionName,
5
5
  isGitHubIssueOrPullRequestSessionName,
6
6
  DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
7
+ DEFAULT_UNANSWERED_OWNER_CALL_GRACE_SECONDS,
7
8
  DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
8
9
  DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
9
10
  DEFAULT_NOTIFICATION_STAGGER_SECONDS,
@@ -28,6 +29,8 @@ import { InteractiveLiveSession } from '../entities/InteractiveLiveSession';
28
29
  type Mocked<T> = jest.Mocked<T> & jest.MockedObject<T>;
29
30
 
30
31
  const MAIN_STALLED_SECTION = 'MAIN_STALLED_SECTION';
32
+ const MAIN_STALLED_STALE_OWNER_CALL_SECTION =
33
+ 'MAIN_STALLED_STALE_OWNER_CALL_SECTION';
31
34
  const SUBAGENT_SECTION = 'SUBAGENT_SECTION';
32
35
 
33
36
  class EveryNameRecentSet extends Set<string> {
@@ -66,6 +69,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
66
69
  }>,
67
70
  ): {
68
71
  mainSilentThresholdSeconds: number;
72
+ unansweredOwnerCallGraceSeconds: number;
69
73
  subAgentSilentThresholdSeconds: number;
70
74
  subAgentRunningThresholdSeconds: number;
71
75
  staggerSeconds: number;
@@ -75,6 +79,8 @@ describe('NotifySilentLiveSessionsUseCase', () => {
75
79
  now: Date;
76
80
  } => ({
77
81
  mainSilentThresholdSeconds: DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
82
+ unansweredOwnerCallGraceSeconds:
83
+ DEFAULT_UNANSWERED_OWNER_CALL_GRACE_SECONDS,
78
84
  subAgentSilentThresholdSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
79
85
  subAgentRunningThresholdSeconds: DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
80
86
  staggerSeconds: DEFAULT_NOTIFICATION_STAGGER_SECONDS,
@@ -147,9 +153,9 @@ describe('NotifySilentLiveSessionsUseCase', () => {
147
153
  .mockResolvedValue(new Map<string, SubAgentActivity[]>()),
148
154
  };
149
155
  mockOwnerCallStatusProvider = {
150
- listSessionNamesWithUnansweredOwnerCall: jest
156
+ listUnansweredOwnerCallEpochSecondsBySessionName: jest
151
157
  .fn()
152
- .mockResolvedValue(new Set<string>()),
158
+ .mockResolvedValue(new Map<string, number>()),
153
159
  };
154
160
  mockNotificationRepository = {
155
161
  sendSelfCheckNotification: jest.fn().mockResolvedValue(undefined),
@@ -170,6 +176,9 @@ describe('NotifySilentLiveSessionsUseCase', () => {
170
176
  composeMainStalledSection: jest
171
177
  .fn()
172
178
  .mockReturnValue(MAIN_STALLED_SECTION),
179
+ composeMainStalledWithStaleOwnerCallSection: jest
180
+ .fn()
181
+ .mockReturnValue(MAIN_STALLED_STALE_OWNER_CALL_SECTION),
173
182
  composeSubAgentSection: jest.fn().mockReturnValue(SUBAGENT_SECTION),
174
183
  };
175
184
  mockSleeper = {
@@ -249,6 +258,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
249
258
 
250
259
  it('exposes the default thresholds as named constants', () => {
251
260
  expect(DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS).toBe(600);
261
+ expect(DEFAULT_UNANSWERED_OWNER_CALL_GRACE_SECONDS).toBe(3600);
252
262
  expect(DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS).toBe(300);
253
263
  expect(DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS).toBe(900);
254
264
  expect(DEFAULT_NOTIFICATION_STAGGER_SECONDS).toBe(25);
@@ -371,36 +381,119 @@ describe('NotifySilentLiveSessionsUseCase', () => {
371
381
  mockSessionOutputActivityRepository.listSessionOutputActivities,
372
382
  ).toHaveBeenCalledWith(expectedMap);
373
383
  expect(
374
- mockOwnerCallStatusProvider.listSessionNamesWithUnansweredOwnerCall,
384
+ mockOwnerCallStatusProvider.listUnansweredOwnerCallEpochSecondsBySessionName,
375
385
  ).toHaveBeenCalledWith(expectedMap);
376
386
  expect(
377
387
  mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName,
378
388
  ).toHaveBeenCalledWith([GITHUB_SESSION], expectedMap);
379
389
  });
380
390
 
381
- it('suppresses the stalled section and sends nothing when an owner call is pending past the threshold', async () => {
391
+ it('suppresses the stalled section and sends nothing while the unanswered owner call is younger than the grace period', async () => {
392
+ setupSilentMainSession(GITHUB_SESSION);
393
+ mockOwnerCallStatusProvider.listUnansweredOwnerCallEpochSecondsBySessionName.mockResolvedValue(
394
+ new Map([
395
+ [
396
+ GITHUB_SESSION,
397
+ nowEpochSeconds - DEFAULT_UNANSWERED_OWNER_CALL_GRACE_SECONDS + 1,
398
+ ],
399
+ ]),
400
+ );
401
+
402
+ await useCase.run(runParams());
403
+
404
+ expect(
405
+ mockMessageComposer.composeMainStalledSection,
406
+ ).not.toHaveBeenCalled();
407
+ expect(
408
+ mockMessageComposer.composeMainStalledWithStaleOwnerCallSection,
409
+ ).not.toHaveBeenCalled();
410
+ expect(
411
+ mockNotificationRepository.sendSelfCheckNotification,
412
+ ).not.toHaveBeenCalled();
413
+ });
414
+
415
+ it('sends the stale-owner-call stalled section once the unanswered owner call reaches the grace period', async () => {
416
+ setupSilentMainSession(GITHUB_SESSION);
417
+ mockOwnerCallStatusProvider.listUnansweredOwnerCallEpochSecondsBySessionName.mockResolvedValue(
418
+ new Map([
419
+ [
420
+ GITHUB_SESSION,
421
+ nowEpochSeconds - DEFAULT_UNANSWERED_OWNER_CALL_GRACE_SECONDS,
422
+ ],
423
+ ]),
424
+ );
425
+
426
+ await useCase.run(runParams());
427
+
428
+ expect(
429
+ mockMessageComposer.composeMainStalledWithStaleOwnerCallSection,
430
+ ).toHaveBeenCalledWith(
431
+ DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
432
+ DEFAULT_UNANSWERED_OWNER_CALL_GRACE_SECONDS,
433
+ );
434
+ expect(
435
+ mockMessageComposer.composeMainStalledSection,
436
+ ).not.toHaveBeenCalled();
437
+ expect(
438
+ mockNotificationRepository.sendSelfCheckNotification,
439
+ ).toHaveBeenCalledWith(
440
+ GITHUB_SESSION,
441
+ MAIN_STALLED_STALE_OWNER_CALL_SECTION,
442
+ );
443
+ });
444
+
445
+ it('does not send the stale-owner-call section when the owner call is past the grace period but the main output is not silent past the threshold', async () => {
382
446
  setupLiveInteractiveSession(GITHUB_SESSION);
383
447
  mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
384
448
  [
385
449
  {
386
450
  sessionName: GITHUB_SESSION,
387
451
  lastOutputEpochSeconds:
388
- nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
452
+ nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS + 1,
389
453
  },
390
454
  ],
391
455
  );
392
- mockOwnerCallStatusProvider.listSessionNamesWithUnansweredOwnerCall.mockResolvedValue(
393
- new Set([GITHUB_SESSION]),
456
+ mockOwnerCallStatusProvider.listUnansweredOwnerCallEpochSecondsBySessionName.mockResolvedValue(
457
+ new Map([
458
+ [
459
+ GITHUB_SESSION,
460
+ nowEpochSeconds - DEFAULT_UNANSWERED_OWNER_CALL_GRACE_SECONDS,
461
+ ],
462
+ ]),
394
463
  );
395
464
 
396
465
  await useCase.run(runParams());
397
466
 
398
467
  expect(
399
- mockMessageComposer.composeMainStalledSection,
468
+ mockNotificationRepository.sendSelfCheckNotification,
400
469
  ).not.toHaveBeenCalled();
470
+ });
471
+
472
+ it('defers a first-cycle stale-owner-call candidate until it persists into the next cycle', async () => {
473
+ setupSilentMainSession(GITHUB_SESSION);
474
+ mockOwnerCallStatusProvider.listUnansweredOwnerCallEpochSecondsBySessionName.mockResolvedValue(
475
+ new Map([
476
+ [
477
+ GITHUB_SESSION,
478
+ nowEpochSeconds - DEFAULT_UNANSWERED_OWNER_CALL_GRACE_SECONDS,
479
+ ],
480
+ ]),
481
+ );
482
+ mockCandidateStateRepository.loadRecentCandidateSessionNames.mockResolvedValue(
483
+ new Set<string>(),
484
+ );
485
+
486
+ await useCase.run(runParams());
487
+
401
488
  expect(
402
489
  mockNotificationRepository.sendSelfCheckNotification,
403
490
  ).not.toHaveBeenCalled();
491
+ expect(
492
+ mockCandidateStateRepository.saveCandidateSessionNames,
493
+ ).toHaveBeenCalledWith({
494
+ sessionNames: [GITHUB_SESSION],
495
+ now,
496
+ });
404
497
  });
405
498
 
406
499
  it('sends the main stalled section when the session is silent past the threshold and not waiting on the owner', async () => {
@@ -414,8 +507,8 @@ describe('NotifySilentLiveSessionsUseCase', () => {
414
507
  },
415
508
  ],
416
509
  );
417
- mockOwnerCallStatusProvider.listSessionNamesWithUnansweredOwnerCall.mockResolvedValue(
418
- new Set<string>(),
510
+ mockOwnerCallStatusProvider.listUnansweredOwnerCallEpochSecondsBySessionName.mockResolvedValue(
511
+ new Map<string, number>(),
419
512
  );
420
513
 
421
514
  await useCase.run(runParams());
@@ -423,6 +516,9 @@ describe('NotifySilentLiveSessionsUseCase', () => {
423
516
  expect(mockMessageComposer.composeMainStalledSection).toHaveBeenCalledWith(
424
517
  DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
425
518
  );
519
+ expect(
520
+ mockMessageComposer.composeMainStalledWithStaleOwnerCallSection,
521
+ ).not.toHaveBeenCalled();
426
522
  expect(
427
523
  mockNotificationRepository.sendSelfCheckNotification,
428
524
  ).toHaveBeenCalledWith(GITHUB_SESSION, MAIN_STALLED_SECTION);
@@ -14,6 +14,7 @@ import { IssueRepository } from './adapter-interfaces/IssueRepository';
14
14
  import { ResolveInteractiveLiveSessionsUseCase } from './ResolveInteractiveLiveSessionsUseCase';
15
15
 
16
16
  export const DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS = 10 * 60;
17
+ export const DEFAULT_UNANSWERED_OWNER_CALL_GRACE_SECONDS = 60 * 60;
17
18
  export const DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS = 5 * 60;
18
19
  export const DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS = 15 * 60;
19
20
  export const DEFAULT_NOTIFICATION_STAGGER_SECONDS = 25;
@@ -78,6 +79,7 @@ export class NotifySilentLiveSessionsUseCase {
78
79
 
79
80
  run = async (params: {
80
81
  mainSilentThresholdSeconds: number;
82
+ unansweredOwnerCallGraceSeconds: number;
81
83
  subAgentSilentThresholdSeconds: number;
82
84
  subAgentRunningThresholdSeconds: number;
83
85
  staggerSeconds: number;
@@ -324,8 +326,8 @@ export class NotifySilentLiveSessionsUseCase {
324
326
  transcriptPathBySessionName,
325
327
  );
326
328
 
327
- const sessionNamesWithUnansweredOwnerCall =
328
- await this.ownerCallStatusProvider.listSessionNamesWithUnansweredOwnerCall(
329
+ const unansweredOwnerCallEpochSecondsBySessionName =
330
+ await this.ownerCallStatusProvider.listUnansweredOwnerCallEpochSecondsBySessionName(
329
331
  transcriptPathBySessionName,
330
332
  );
331
333
 
@@ -336,12 +338,16 @@ export class NotifySilentLiveSessionsUseCase {
336
338
  lastOutputEpochSeconds === undefined
337
339
  ? null
338
340
  : nowEpochSeconds - lastOutputEpochSeconds;
341
+ const unansweredOwnerCallEpochSeconds =
342
+ unansweredOwnerCallEpochSecondsBySessionName.get(sessionName);
339
343
  return {
340
344
  sessionName,
341
345
  mainSilentSeconds,
342
346
  subAgents: subAgentsBySessionName.get(sessionName) ?? [],
343
- hasUnansweredOwnerCall:
344
- sessionNamesWithUnansweredOwnerCall.has(sessionName),
347
+ unansweredOwnerCallAgeSeconds:
348
+ unansweredOwnerCallEpochSeconds === undefined
349
+ ? null
350
+ : nowEpochSeconds - unansweredOwnerCallEpochSeconds,
345
351
  };
346
352
  });
347
353
  };
@@ -350,6 +356,7 @@ export class NotifySilentLiveSessionsUseCase {
350
356
  snapshot: LiveSessionActivitySnapshot,
351
357
  thresholds: {
352
358
  mainSilentThresholdSeconds: number;
359
+ unansweredOwnerCallGraceSeconds: number;
353
360
  subAgentSilentThresholdSeconds: number;
354
361
  subAgentRunningThresholdSeconds: number;
355
362
  now: Date;
@@ -358,13 +365,24 @@ export class NotifySilentLiveSessionsUseCase {
358
365
  const sections: string[] = [];
359
366
 
360
367
  const mainSilentSeconds = snapshot.mainSilentSeconds;
368
+ const unansweredOwnerCallAgeSeconds =
369
+ snapshot.unansweredOwnerCallAgeSeconds;
370
+ const suppressedByRecentOwnerCall =
371
+ unansweredOwnerCallAgeSeconds !== null &&
372
+ unansweredOwnerCallAgeSeconds <
373
+ thresholds.unansweredOwnerCallGraceSeconds;
361
374
  const mainTriggered =
362
375
  mainSilentSeconds !== null &&
363
376
  mainSilentSeconds >= thresholds.mainSilentThresholdSeconds &&
364
- !snapshot.hasUnansweredOwnerCall;
377
+ !suppressedByRecentOwnerCall;
365
378
  if (mainTriggered) {
366
379
  sections.push(
367
- this.messageComposer.composeMainStalledSection(mainSilentSeconds),
380
+ unansweredOwnerCallAgeSeconds !== null
381
+ ? this.messageComposer.composeMainStalledWithStaleOwnerCallSection(
382
+ mainSilentSeconds,
383
+ unansweredOwnerCallAgeSeconds,
384
+ )
385
+ : this.messageComposer.composeMainStalledSection(mainSilentSeconds),
368
386
  );
369
387
  }
370
388
 
@@ -1,5 +1,5 @@
1
1
  export interface OwnerCallStatusProvider {
2
- listSessionNamesWithUnansweredOwnerCall: (
2
+ listUnansweredOwnerCallEpochSecondsBySessionName: (
3
3
  transcriptPathBySessionName: Map<string, string>,
4
- ) => Promise<Set<string>>;
4
+ ) => Promise<Map<string, number>>;
5
5
  }
@@ -7,5 +7,9 @@ export type SubAgentStallSections = {
7
7
 
8
8
  export interface SilentSessionMessageComposer {
9
9
  composeMainStalledSection: (mainSilentSeconds: number) => string;
10
+ composeMainStalledWithStaleOwnerCallSection: (
11
+ mainSilentSeconds: number,
12
+ unansweredOwnerCallAgeSeconds: number,
13
+ ) => string;
10
14
  composeSubAgentSection: (sections: SubAgentStallSections) => string;
11
15
  }
@@ -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;AAiD3D,qBAAa,kCAAkC;IAC7C,MAAM,GACJ,gBAAgB,MAAM,EACtB,UAAU,OAAO,KAChB,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,CAmkBP;CACH"}
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;AAiD3D,qBAAa,kCAAkC;IAC7C,MAAM,GACJ,gBAAgB,MAAM,EACtB,UAAU,OAAO,KAChB,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,CA8kBP;CACH"}
@@ -11,6 +11,7 @@ export type NotifySilentTmuxSessionsParams = {
11
11
  subAgentProcessMatchPattern: string | null;
12
12
  subAgentTranscriptRootDirectory: string | null;
13
13
  mainSilentThresholdSeconds: number;
14
+ unansweredOwnerCallGraceSeconds: number;
14
15
  subAgentSilentThresholdSeconds: number;
15
16
  subAgentRunningThresholdSeconds: number;
16
17
  staggerSeconds: number;
@@ -26,6 +27,7 @@ export type NotifySilentTmuxSessionsParams = {
26
27
  export declare const notifySilentTmuxSessions: (params: NotifySilentTmuxSessionsParams) => Promise<void>;
27
28
  export declare const DEFAULT_NOTIFY_SILENT_TMUX_SESSIONS_PARAMS: {
28
29
  readonly mainSilentThresholdSeconds: number;
30
+ readonly unansweredOwnerCallGraceSeconds: number;
29
31
  readonly subAgentSilentThresholdSeconds: number;
30
32
  readonly subAgentRunningThresholdSeconds: number;
31
33
  readonly staggerSeconds: 25;
@@ -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,EAOtB,MAAM,0DAA0D,CAAC;AAclE,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,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,CAyEd,CAAC;AAEF,eAAO,MAAM,0CAA0C;;;;;;;CAQ7C,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;AAclE,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,6 +1,7 @@
1
1
  import { SilentSessionMessageComposer, SubAgentStallSections } from '../../domain/usecases/adapter-interfaces/SilentSessionMessageComposer';
2
2
  export type SilentSessionMessageTemplates = {
3
3
  mainStalledMessage: string | null;
4
+ mainStalledStaleOwnerCallMessage: string | null;
4
5
  subAgentIdleMessageHeader: string | null;
5
6
  subAgentIdleMessageFooter: string | null;
6
7
  subAgentLongRunningMessageHeader: string | null;
@@ -12,6 +13,7 @@ export declare class ConfigurableSilentSessionMessageComposer implements SilentS
12
13
  private readonly ownerCallMarker;
13
14
  constructor(templates: SilentSessionMessageTemplates, fallback: SilentSessionMessageComposer, ownerCallMarker?: string | null);
14
15
  composeMainStalledSection: (mainSilentSeconds: number) => string;
16
+ composeMainStalledWithStaleOwnerCallSection: (mainSilentSeconds: number, unansweredOwnerCallAgeSeconds: number) => string;
15
17
  composeSubAgentSection: (stallSections: SubAgentStallSections) => string;
16
18
  private composeIdleSection;
17
19
  private composeLongRunningSection;
@@ -1 +1 @@
1
- {"version":3,"file":"ConfigurableSilentSessionMessageComposer.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,4BAA4B,EAC5B,qBAAqB,EACtB,MAAM,uEAAuE,CAAC;AAQ/E,MAAM,MAAM,6BAA6B,GAAG;IAC1C,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,yBAAyB,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,yBAAyB,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,gCAAgC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,gCAAgC,EAAE,MAAM,GAAG,IAAI,CAAC;CACjD,CAAC;AAOF,qBAAa,wCAAyC,YAAW,4BAA4B;IAEzF,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,eAAe;gBAFf,SAAS,EAAE,6BAA6B,EACxC,QAAQ,EAAE,4BAA4B,EACtC,eAAe,GAAE,MAAM,GAAG,IAAW;IAGxD,yBAAyB,GAAI,mBAAmB,MAAM,KAAG,MAAM,CAO7D;IAEF,sBAAsB,GAAI,eAAe,qBAAqB,KAAG,MAAM,CA4CrE;IAEF,OAAO,CAAC,kBAAkB,CAkBxB;IAEF,OAAO,CAAC,yBAAyB,CAkB/B;CACH"}
1
+ {"version":3,"file":"ConfigurableSilentSessionMessageComposer.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,4BAA4B,EAC5B,qBAAqB,EACtB,MAAM,uEAAuE,CAAC;AAQ/E,MAAM,MAAM,6BAA6B,GAAG;IAC1C,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,gCAAgC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,yBAAyB,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,yBAAyB,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,gCAAgC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,gCAAgC,EAAE,MAAM,GAAG,IAAI,CAAC;CACjD,CAAC;AAOF,qBAAa,wCAAyC,YAAW,4BAA4B;IAEzF,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,eAAe;gBAFf,SAAS,EAAE,6BAA6B,EACxC,QAAQ,EAAE,4BAA4B,EACtC,eAAe,GAAE,MAAM,GAAG,IAAW;IAGxD,yBAAyB,GAAI,mBAAmB,MAAM,KAAG,MAAM,CAO7D;IAEF,2CAA2C,GACzC,mBAAmB,MAAM,EACzB,+BAA+B,MAAM,KACpC,MAAM,CAUP;IAEF,sBAAsB,GAAI,eAAe,qBAAqB,KAAG,MAAM,CA4CrE;IAEF,OAAO,CAAC,kBAAkB,CAkBxB;IAEF,OAAO,CAAC,yBAAyB,CAkB/B;CACH"}
@@ -1,5 +1,5 @@
1
1
  import { OwnerCallStatusProvider } from '../../domain/usecases/adapter-interfaces/OwnerCallStatusProvider';
2
2
  export declare class NoUnansweredOwnerCallStatusProvider implements OwnerCallStatusProvider {
3
- listSessionNamesWithUnansweredOwnerCall: (_transcriptPathBySessionName: Map<string, string>) => Promise<Set<string>>;
3
+ listUnansweredOwnerCallEpochSecondsBySessionName: (_transcriptPathBySessionName: Map<string, string>) => Promise<Map<string, number>>;
4
4
  }
5
5
  //# sourceMappingURL=NoUnansweredOwnerCallStatusProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"NoUnansweredOwnerCallStatusProvider.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/NoUnansweredOwnerCallStatusProvider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,kEAAkE,CAAC;AAE3G,qBAAa,mCAAoC,YAAW,uBAAuB;IACjF,uCAAuC,GACrC,8BAA8B,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,KAChD,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAErB;CACH"}
1
+ {"version":3,"file":"NoUnansweredOwnerCallStatusProvider.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/NoUnansweredOwnerCallStatusProvider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,kEAAkE,CAAC;AAE3G,qBAAa,mCAAoC,YAAW,uBAAuB;IACjF,gDAAgD,GAC9C,8BAA8B,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,KAChD,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAE7B;CACH"}
@@ -2,7 +2,7 @@ import { OwnerCallStatusProvider } from '../../domain/usecases/adapter-interface
2
2
  export declare class TranscriptOwnerCallStatusProvider implements OwnerCallStatusProvider {
3
3
  private readonly ownerCallMarker;
4
4
  constructor(ownerCallMarker: string | null);
5
- listSessionNamesWithUnansweredOwnerCall: (transcriptPathBySessionName: Map<string, string>) => Promise<Set<string>>;
6
- private isWaitingForOwnerReply;
5
+ listUnansweredOwnerCallEpochSecondsBySessionName: (transcriptPathBySessionName: Map<string, string>) => Promise<Map<string, number>>;
6
+ private findUnansweredOwnerCallEpochMs;
7
7
  }
8
8
  //# sourceMappingURL=TranscriptOwnerCallStatusProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TranscriptOwnerCallStatusProvider.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/TranscriptOwnerCallStatusProvider.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,kEAAkE,CAAC;AAyF3G,qBAAa,iCAAkC,YAAW,uBAAuB;IACnE,OAAO,CAAC,QAAQ,CAAC,eAAe;gBAAf,eAAe,EAAE,MAAM,GAAG,IAAI;IAE3D,uCAAuC,GACrC,6BAA6B,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,KAC/C,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAYrB;IAEF,OAAO,CAAC,sBAAsB,CAsD5B;CACH"}
1
+ {"version":3,"file":"TranscriptOwnerCallStatusProvider.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/TranscriptOwnerCallStatusProvider.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,kEAAkE,CAAC;AAyF3G,qBAAa,iCAAkC,YAAW,uBAAuB;IACnE,OAAO,CAAC,QAAQ,CAAC,eAAe;gBAAf,eAAe,EAAE,MAAM,GAAG,IAAI;IAE3D,gDAAgD,GAC9C,6BAA6B,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,KAC/C,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAsB7B;IAEF,OAAO,CAAC,8BAA8B,CAsDpC;CACH"}
@@ -8,6 +8,6 @@ export type LiveSessionActivitySnapshot = {
8
8
  sessionName: string;
9
9
  mainSilentSeconds: number | null;
10
10
  subAgents: SubAgentActivity[];
11
- hasUnansweredOwnerCall: boolean;
11
+ unansweredOwnerCallAgeSeconds: number | null;
12
12
  };
13
13
  //# sourceMappingURL=LiveSessionActivitySnapshot.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"LiveSessionActivitySnapshot.d.ts","sourceRoot":"","sources":["../../../src/domain/entities/LiveSessionActivitySnapshot.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,wBAAwB,EAAE,OAAO,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,sBAAsB,EAAE,OAAO,CAAC;CACjC,CAAC"}
1
+ {"version":3,"file":"LiveSessionActivitySnapshot.d.ts","sourceRoot":"","sources":["../../../src/domain/entities/LiveSessionActivitySnapshot.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,wBAAwB,EAAE,OAAO,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,6BAA6B,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9C,CAAC"}
@@ -4,6 +4,7 @@ export declare class DefaultSilentSessionMessageComposer implements SilentSessio
4
4
  private readonly ownerCallMarker;
5
5
  constructor(ownerCallMarker?: string | null);
6
6
  composeMainStalledSection: (mainSilentSeconds: number) => string;
7
+ composeMainStalledWithStaleOwnerCallSection: (mainSilentSeconds: number, unansweredOwnerCallAgeSeconds: number) => string;
7
8
  composeSubAgentSection: (stallSections: SubAgentStallSections) => string;
8
9
  }
9
10
  //# sourceMappingURL=DefaultSilentSessionMessageComposer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultSilentSessionMessageComposer.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/DefaultSilentSessionMessageComposer.ts"],"names":[],"mappings":"AACA,OAAO,EACL,4BAA4B,EAC5B,qBAAqB,EACtB,MAAM,mDAAmD,CAAC;AAwC3D,eAAO,MAAM,8BAA8B,GACzC,iBAAiB,MAAM,GAAG,IAAI,KAC7B,MAMF,CAAC;AAiBF,qBAAa,mCAAoC,YAAW,4BAA4B;IAC1E,OAAO,CAAC,QAAQ,CAAC,eAAe;gBAAf,eAAe,GAAE,MAAM,GAAG,IAAW;IAElE,yBAAyB,GAAI,mBAAmB,MAAM,KAAG,MAAM,CAE7D;IAEF,sBAAsB,GAAI,eAAe,qBAAqB,KAAG,MAAM,CAWrE;CACH"}
1
+ {"version":3,"file":"DefaultSilentSessionMessageComposer.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/DefaultSilentSessionMessageComposer.ts"],"names":[],"mappings":"AACA,OAAO,EACL,4BAA4B,EAC5B,qBAAqB,EACtB,MAAM,mDAAmD,CAAC;AAwC3D,eAAO,MAAM,8BAA8B,GACzC,iBAAiB,MAAM,GAAG,IAAI,KAC7B,MAMF,CAAC;AAgCF,qBAAa,mCAAoC,YAAW,4BAA4B;IAC1E,OAAO,CAAC,QAAQ,CAAC,eAAe;gBAAf,eAAe,GAAE,MAAM,GAAG,IAAW;IAElE,yBAAyB,GAAI,mBAAmB,MAAM,KAAG,MAAM,CAE7D;IAEF,2CAA2C,GACzC,mBAAmB,MAAM,EACzB,+BAA+B,MAAM,KACpC,MAAM,CAMP;IAEF,sBAAsB,GAAI,eAAe,qBAAqB,KAAG,MAAM,CAWrE;CACH"}
@@ -10,6 +10,7 @@ import { SilentSessionHubTaskStatusCacheRepository } from './adapter-interfaces/
10
10
  import { Sleeper } from './adapter-interfaces/Sleeper';
11
11
  import { IssueRepository } from './adapter-interfaces/IssueRepository';
12
12
  export declare const DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS: number;
13
+ export declare const DEFAULT_UNANSWERED_OWNER_CALL_GRACE_SECONDS: number;
13
14
  export declare const DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS: number;
14
15
  export declare const DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS: number;
15
16
  export declare const DEFAULT_NOTIFICATION_STAGGER_SECONDS = 25;
@@ -34,6 +35,7 @@ export declare class NotifySilentLiveSessionsUseCase {
34
35
  constructor(liveSessionProcessSnapshotProvider: LiveSessionProcessSnapshotProvider, interactiveLiveSessionTranscriptResolver: InteractiveLiveSessionTranscriptResolver, sessionOutputActivityRepository: SessionOutputActivityRepository, subAgentActivityRepository: SessionSubAgentActivityRepository, ownerCallStatusProvider: OwnerCallStatusProvider, notificationRepository: SilentSessionNotificationRepository, candidateStateRepository: SilentSessionCandidateStateRepository, messageComposer: SilentSessionMessageComposer, sleeper: Sleeper, hubTaskStatusResolver?: HubTaskStatusResolver | null, hubTaskStatusCacheRepository?: SilentSessionHubTaskStatusCacheRepository | null);
35
36
  run: (params: {
36
37
  mainSilentThresholdSeconds: number;
38
+ unansweredOwnerCallGraceSeconds: number;
37
39
  subAgentSilentThresholdSeconds: number;
38
40
  subAgentRunningThresholdSeconds: number;
39
41
  staggerSeconds: number;