github-issue-tower-defence-management 1.112.1 → 1.112.3

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 (46) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +5 -8
  3. package/bin/adapter/entry-points/console/ui-dist/assets/index-BNPHMdzv.css +1 -0
  4. package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
  5. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +2 -9
  6. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  7. package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js +0 -6
  8. package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js.map +1 -1
  9. package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js +26 -1
  10. package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js.map +1 -1
  11. package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js +0 -9
  12. package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js.map +1 -1
  13. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +3 -13
  14. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
  15. package/package.json +1 -1
  16. package/src/adapter/entry-points/console/ui/src/index.css +2 -4
  17. package/src/adapter/entry-points/console/ui-dist/assets/index-BNPHMdzv.css +1 -0
  18. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  19. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +0 -5
  20. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +2 -42
  21. package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.test.ts +0 -44
  22. package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts +0 -8
  23. package/src/adapter/repositories/GoogleSpreadsheetRepository.integration.test.ts +4 -4
  24. package/src/adapter/repositories/GraphqlProjectRepository.test.ts +4 -2
  25. package/src/adapter/repositories/KySlackRepository.test.ts +5 -4
  26. package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.test.ts +90 -0
  27. package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.ts +29 -1
  28. package/src/adapter/repositories/issue/ApiV3IssueRepository.test.ts +6 -2
  29. package/src/domain/usecases/DefaultSilentSessionMessageComposer.test.ts +1 -17
  30. package/src/domain/usecases/DefaultSilentSessionMessageComposer.ts +0 -11
  31. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +2 -62
  32. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +7 -20
  33. package/src/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.ts +0 -1
  34. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  35. package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts +0 -2
  36. package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts.map +1 -1
  37. package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts.map +1 -1
  38. package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts +0 -1
  39. package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts.map +1 -1
  40. package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
  41. package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts +0 -1
  42. package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts.map +1 -1
  43. package/bin/adapter/entry-points/console/ui-dist/assets/index-BaVsPpWs.css +0 -1
  44. package/src/adapter/entry-points/console/ui-dist/assets/index-BaVsPpWs.css +0 -1
  45. /package/bin/adapter/entry-points/console/ui-dist/assets/{index-ITb3VBcs.js → index-BMjm7L0w.js} +0 -0
  46. /package/src/adapter/entry-points/console/ui-dist/assets/{index-ITb3VBcs.js → index-BMjm7L0w.js} +0 -0
@@ -128,7 +128,6 @@ export class HandleScheduledEventUseCaseHandler {
128
128
  silentNotificationCooldownSeconds?: number;
129
129
  silentNotificationStaggerSeconds?: number;
130
130
  silentMainStalledMessage?: string;
131
- silentOwnerReNotificationMessage?: string;
132
131
  silentSubAgentMessageHeader?: string;
133
132
  silentSubAgentMessageFooter?: string;
134
133
  credentials: {
@@ -634,10 +633,6 @@ export class HandleScheduledEventUseCaseHandler {
634
633
  mergedInput.silentMainStalledMessage ??
635
634
  process.env.TDPM_SILENT_MAIN_STALLED_MESSAGE ??
636
635
  null,
637
- ownerReNotificationMessage:
638
- mergedInput.silentOwnerReNotificationMessage ??
639
- process.env.TDPM_SILENT_OWNER_RE_NOTIFICATION_MESSAGE ??
640
- null,
641
636
  subAgentMessageHeader:
642
637
  mergedInput.silentSubAgentMessageHeader ??
643
638
  process.env.TDPM_SILENT_SUBAGENT_MESSAGE_HEADER ??
@@ -21,7 +21,6 @@ const CLAUDE_PID = 201;
21
21
 
22
22
  const EMPTY_TEMPLATES: SilentSessionMessageTemplates = {
23
23
  mainStalledMessage: null,
24
- ownerReNotificationMessage: null,
25
24
  subAgentMessageHeader: null,
26
25
  subAgentMessageFooter: null,
27
26
  };
@@ -169,7 +168,6 @@ describe('notifySilentTmuxSessions', () => {
169
168
  ...baseParams(runner),
170
169
  messageTemplates: {
171
170
  mainStalledMessage: 'CUSTOM_MAIN_TEMPLATE',
172
- ownerReNotificationMessage: null,
173
171
  subAgentMessageHeader: null,
174
172
  subAgentMessageFooter: null,
175
173
  },
@@ -183,42 +181,7 @@ describe('notifySilentTmuxSessions', () => {
183
181
  );
184
182
  });
185
183
 
186
- it('sends no notification while an owner call is unanswered and the wait is within the threshold', async () => {
187
- const recentOwnerCall = new Date(
188
- (NOW_EPOCH_SECONDS - 5 * 60) * 1000,
189
- ).toISOString();
190
- writeTranscript([
191
- {
192
- type: 'user',
193
- timestamp: '2026-06-25T23:00:00.000Z',
194
- message: { role: 'user', content: 'go ahead' },
195
- },
196
- {
197
- type: 'assistant',
198
- timestamp: recentOwnerCall,
199
- message: {
200
- role: 'assistant',
201
- stop_reason: 'end_turn',
202
- content: [
203
- { type: 'text', text: 'waiting <<OWNER_CALL>> please decide' },
204
- ],
205
- },
206
- },
207
- ]);
208
- const runner = liveSessionRunner();
209
-
210
- await notifySilentTmuxSessions({
211
- ...baseParams(runner),
212
- ownerCallMarker: '<<OWNER_CALL>>',
213
- });
214
-
215
- const sendCall = runner.runCommand.mock.calls.find(
216
- (call) => call[0] === 'tmux' && call[1][0] === 'send-keys',
217
- );
218
- expect(sendCall).toBeUndefined();
219
- });
220
-
221
- it('re-notifies the owner when an owner call has been unanswered past the threshold', async () => {
184
+ it('suppresses the notification whenever an owner call is unanswered, regardless of how long the session has been silent', async () => {
222
185
  const stalePendingOwnerCall = new Date(
223
186
  (NOW_EPOCH_SECONDS - 11 * 60) * 1000,
224
187
  ).toISOString();
@@ -250,10 +213,7 @@ describe('notifySilentTmuxSessions', () => {
250
213
  const sendCall = runner.runCommand.mock.calls.find(
251
214
  (call) => call[0] === 'tmux' && call[1][0] === 'send-keys',
252
215
  );
253
- expect(sendCall?.[1][2]).toBe(SESSION_NAME);
254
- expect(sendCall?.[1][4]).toContain(SILENT_SESSION_REMINDER_SENTINEL);
255
- expect(sendCall?.[1][4]).toContain('Re-raise your pending call-to-user');
256
- expect(sendCall?.[1][4]).not.toContain('You have produced no output for');
216
+ expect(sendCall).toBeUndefined();
257
217
  });
258
218
 
259
219
  it('does not re-notify the same silent session on the next cycle within cooldown', async () => {
@@ -6,9 +6,6 @@ type Mocked<T> = jest.Mocked<T> & jest.MockedObject<T>;
6
6
 
7
7
  const createFallback = (): Mocked<SilentSessionMessageComposer> => ({
8
8
  composeMainStalledSection: jest.fn().mockReturnValue('FALLBACK_MAIN'),
9
- composeOwnerReNotificationSection: jest
10
- .fn()
11
- .mockReturnValue('FALLBACK_OWNER'),
12
9
  composeSubAgentSection: jest.fn().mockReturnValue('FALLBACK_SUB'),
13
10
  });
14
11
 
@@ -18,7 +15,6 @@ describe('ConfigurableSilentSessionMessageComposer', () => {
18
15
  const composer = new ConfigurableSilentSessionMessageComposer(
19
16
  {
20
17
  mainStalledMessage: null,
21
- ownerReNotificationMessage: null,
22
18
  subAgentMessageHeader: null,
23
19
  subAgentMessageFooter: null,
24
20
  },
@@ -33,7 +29,6 @@ describe('ConfigurableSilentSessionMessageComposer', () => {
33
29
  const composer = new ConfigurableSilentSessionMessageComposer(
34
30
  {
35
31
  mainStalledMessage: 'CUSTOM_MAIN',
36
- ownerReNotificationMessage: null,
37
32
  subAgentMessageHeader: null,
38
33
  subAgentMessageFooter: null,
39
34
  },
@@ -50,7 +45,6 @@ describe('ConfigurableSilentSessionMessageComposer', () => {
50
45
  const composer = new ConfigurableSilentSessionMessageComposer(
51
46
  {
52
47
  mainStalledMessage: null,
53
- ownerReNotificationMessage: null,
54
48
  subAgentMessageHeader: null,
55
49
  subAgentMessageFooter: null,
56
50
  },
@@ -68,7 +62,6 @@ describe('ConfigurableSilentSessionMessageComposer', () => {
68
62
  const composer = new ConfigurableSilentSessionMessageComposer(
69
63
  {
70
64
  mainStalledMessage: null,
71
- ownerReNotificationMessage: null,
72
65
  subAgentMessageHeader: 'HEADER',
73
66
  subAgentMessageFooter: 'FOOTER',
74
67
  },
@@ -86,48 +79,11 @@ describe('ConfigurableSilentSessionMessageComposer', () => {
86
79
  expect(fallback.composeSubAgentSection).not.toHaveBeenCalled();
87
80
  });
88
81
 
89
- it('uses the fallback owner-re-notification section when no template is configured', () => {
90
- const fallback = createFallback();
91
- const composer = new ConfigurableSilentSessionMessageComposer(
92
- {
93
- mainStalledMessage: null,
94
- ownerReNotificationMessage: null,
95
- subAgentMessageHeader: null,
96
- subAgentMessageFooter: null,
97
- },
98
- fallback,
99
- );
100
- expect(composer.composeOwnerReNotificationSection(600)).toBe(
101
- 'FALLBACK_OWNER',
102
- );
103
- expect(fallback.composeOwnerReNotificationSection).toHaveBeenCalledWith(
104
- 600,
105
- );
106
- });
107
-
108
- it('uses the configured owner-re-notification template when provided', () => {
109
- const fallback = createFallback();
110
- const composer = new ConfigurableSilentSessionMessageComposer(
111
- {
112
- mainStalledMessage: null,
113
- ownerReNotificationMessage: 'CUSTOM_OWNER',
114
- subAgentMessageHeader: null,
115
- subAgentMessageFooter: null,
116
- },
117
- fallback,
118
- );
119
- const section = composer.composeOwnerReNotificationSection(600);
120
- expect(section).toContain('CUSTOM_OWNER');
121
- expect(section).toContain(SILENT_SESSION_REMINDER_SENTINEL);
122
- expect(fallback.composeOwnerReNotificationSection).not.toHaveBeenCalled();
123
- });
124
-
125
82
  it('does not double-prepend the sentinel when the template already carries it', () => {
126
83
  const fallback = createFallback();
127
84
  const composer = new ConfigurableSilentSessionMessageComposer(
128
85
  {
129
86
  mainStalledMessage: `${SILENT_SESSION_REMINDER_SENTINEL} CUSTOM_MAIN`,
130
- ownerReNotificationMessage: null,
131
87
  subAgentMessageHeader: null,
132
88
  subAgentMessageFooter: null,
133
89
  },
@@ -9,7 +9,6 @@ const withReminderSentinel = (message: string): string =>
9
9
 
10
10
  export type SilentSessionMessageTemplates = {
11
11
  mainStalledMessage: string | null;
12
- ownerReNotificationMessage: string | null;
13
12
  subAgentMessageHeader: string | null;
14
13
  subAgentMessageFooter: string | null;
15
14
  };
@@ -32,13 +31,6 @@ export class ConfigurableSilentSessionMessageComposer implements SilentSessionMe
32
31
  return withReminderSentinel(this.templates.mainStalledMessage);
33
32
  };
34
33
 
35
- composeOwnerReNotificationSection = (waitingSeconds: number): string => {
36
- if (this.templates.ownerReNotificationMessage === null) {
37
- return this.fallback.composeOwnerReNotificationSection(waitingSeconds);
38
- }
39
- return withReminderSentinel(this.templates.ownerReNotificationMessage);
40
- };
41
-
42
34
  composeSubAgentSection = (subAgents: SubAgentActivity[]): string => {
43
35
  if (
44
36
  this.templates.subAgentMessageHeader === null &&
@@ -6,11 +6,11 @@ dotenv.config();
6
6
 
7
7
  const GOOGLE_SERVICE_ACCOUNT_KEY = process.env.GOOGLE_SERVICE_ACCOUNT_KEY;
8
8
 
9
- if (!GOOGLE_SERVICE_ACCOUNT_KEY) {
10
- throw new Error('GOOGLE_SERVICE_ACCOUNT_KEY is required');
11
- }
9
+ const describeWhenCredentials = GOOGLE_SERVICE_ACCOUNT_KEY
10
+ ? describe
11
+ : describe.skip;
12
12
 
13
- describe('GoogleSpreadsheetRepository integration tests', () => {
13
+ describeWhenCredentials('GoogleSpreadsheetRepository integration tests', () => {
14
14
  jest.setTimeout(60 * 1000);
15
15
  jest.retryTimes(3, { logErrorsBeforeRetry: true });
16
16
 
@@ -28,10 +28,12 @@ describe('convertToFieldOptionColor', () => {
28
28
  });
29
29
  });
30
30
 
31
- describe('GraphqlProjectRepository', () => {
31
+ const token = process.env.GH_TOKEN;
32
+ const describeWhenCredentials = token ? describe : describe.skip;
33
+
34
+ describeWhenCredentials('GraphqlProjectRepository', () => {
32
35
  const localStorageRepository = new LocalStorageRepository();
33
36
  let repository: GraphqlProjectRepository;
34
- const token = process.env.GH_TOKEN;
35
37
  const login = 'HiromiShikata';
36
38
  const projectUrl = `https://github.com/users/HiromiShikata/projects/49`;
37
39
  const projectNumber = 49;
@@ -12,16 +12,17 @@ const TEST_USER_NAME = 'shikata.hiromi_test2';
12
12
  const TEST_IMAGE_URL = 'https://i.imgur.com/Zi3qToQ.jpeg';
13
13
  const TEST_IMAGE_PATH = './tmp/test/fixtures/test-image.png';
14
14
 
15
- if (!SLACK_USER_TOKEN) {
16
- throw new Error('SLACK_USER_TOKEN is required');
17
- }
15
+ const describeWhenCredentials = SLACK_USER_TOKEN ? describe : describe.skip;
18
16
 
19
- describe('KySlackRepository Integration Tests', () => {
17
+ describeWhenCredentials('KySlackRepository Integration Tests', () => {
20
18
  jest.setTimeout(60 * 1000);
21
19
  jest.retryTimes(3, { logErrorsBeforeRetry: true });
22
20
  let slackRepository: KySlackRepository;
23
21
 
24
22
  beforeAll(() => {
23
+ if (!SLACK_USER_TOKEN) {
24
+ return;
25
+ }
25
26
  slackRepository = new KySlackRepository(SLACK_USER_TOKEN);
26
27
  });
27
28
  beforeEach(async () => {
@@ -52,9 +52,42 @@ describe('TranscriptOwnerCallStatusProvider', () => {
52
52
  const ownerReply = (timestamp: string): object => ({
53
53
  type: 'user',
54
54
  timestamp,
55
+ origin: { kind: 'human' },
56
+ promptSource: 'typed',
55
57
  message: { role: 'user', content: 'go ahead' },
56
58
  });
57
59
 
60
+ const ownerReplyQueuedNoOrigin = (timestamp: string): object => ({
61
+ type: 'user',
62
+ timestamp,
63
+ promptSource: 'queued',
64
+ message: { role: 'user', content: 'go ahead' },
65
+ });
66
+
67
+ const taskNotification = (timestamp: string): object => ({
68
+ type: 'user',
69
+ timestamp,
70
+ origin: { kind: 'task-notification' },
71
+ promptSource: 'system',
72
+ message: {
73
+ role: 'user',
74
+ content:
75
+ '<task-notification>\n<task-id>abc123</task-id>\nA sub-agent finished.\n</task-notification>',
76
+ },
77
+ });
78
+
79
+ const peerAgentMessage = (timestamp: string): object => ({
80
+ type: 'user',
81
+ timestamp,
82
+ origin: { kind: 'peer' },
83
+ promptSource: 'system',
84
+ isMeta: true,
85
+ message: {
86
+ role: 'user',
87
+ content: 'Another Claude session sent a message: please continue.',
88
+ },
89
+ });
90
+
58
91
  const toolResult = (timestamp: string): object => ({
59
92
  type: 'user',
60
93
  timestamp,
@@ -188,6 +221,63 @@ describe('TranscriptOwnerCallStatusProvider', () => {
188
221
  expect(result.has(sessionName)).toBe(false);
189
222
  });
190
223
 
224
+ it('does not count a system-injected task-notification user entry as an owner reply', async () => {
225
+ const transcriptPath = writeTranscript('workbench.jsonl', [
226
+ assistantWithMarker('2026-06-27T10:00:00.000Z'),
227
+ taskNotification('2026-06-27T10:10:00.000Z'),
228
+ ]);
229
+ const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
230
+
231
+ const result = await provider.listSessionNamesWithUnansweredOwnerCall(
232
+ new Map([[sessionName, transcriptPath]]),
233
+ );
234
+
235
+ expect(result.has(sessionName)).toBe(true);
236
+ });
237
+
238
+ it('does not count a cross-session peer agent message as an owner reply', async () => {
239
+ const transcriptPath = writeTranscript('workbench.jsonl', [
240
+ assistantWithMarker('2026-06-27T10:00:00.000Z'),
241
+ peerAgentMessage('2026-06-27T10:10:00.000Z'),
242
+ ]);
243
+ const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
244
+
245
+ const result = await provider.listSessionNamesWithUnansweredOwnerCall(
246
+ new Map([[sessionName, transcriptPath]]),
247
+ );
248
+
249
+ expect(result.has(sessionName)).toBe(true);
250
+ });
251
+
252
+ it('counts a genuine human reply that has promptSource queued but no origin field', async () => {
253
+ const transcriptPath = writeTranscript('workbench.jsonl', [
254
+ assistantWithMarker('2026-06-27T10:00:00.000Z'),
255
+ ownerReplyQueuedNoOrigin('2026-06-27T10:10:00.000Z'),
256
+ ]);
257
+ const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
258
+
259
+ const result = await provider.listSessionNamesWithUnansweredOwnerCall(
260
+ new Map([[sessionName, transcriptPath]]),
261
+ );
262
+
263
+ expect(result.has(sessionName)).toBe(false);
264
+ });
265
+
266
+ it('counts a genuine human-origin reply that arrives after a task-notification', async () => {
267
+ const transcriptPath = writeTranscript('workbench.jsonl', [
268
+ assistantWithMarker('2026-06-27T10:00:00.000Z'),
269
+ taskNotification('2026-06-27T10:10:00.000Z'),
270
+ ownerReply('2026-06-27T10:20:00.000Z'),
271
+ ]);
272
+ const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
273
+
274
+ const result = await provider.listSessionNamesWithUnansweredOwnerCall(
275
+ new Map([[sessionName, transcriptPath]]),
276
+ );
277
+
278
+ expect(result.has(sessionName)).toBe(false);
279
+ });
280
+
191
281
  it('does not report a session that never raised an owner call', async () => {
192
282
  const transcriptPath = writeTranscript('workbench.jsonl', [
193
283
  assistantPlain('2026-06-27T10:00:00.000Z'),
@@ -40,6 +40,30 @@ const extractText = (content: unknown): string => {
40
40
  return texts.join('\n');
41
41
  };
42
42
 
43
+ // A transcript user entry is only a genuine owner reply when it was actually
44
+ // typed (or queued) by the human owner. Claude Code records the provenance of
45
+ // each user entry on the top-level `origin` and `promptSource` fields. A
46
+ // genuine human prompt is identified by EITHER `origin.kind === 'human'` OR a
47
+ // `promptSource` of `typed`/`queued`; the second condition is required because
48
+ // older-format transcripts record genuine human replies with a `promptSource`
49
+ // of `queued`/`typed` but no `origin` field, and dropping those would leave a
50
+ // real owner reply uncounted and the session waiting forever. Every
51
+ // system-injected user entry, by contrast, uses a `promptSource` of `system`
52
+ // (sub-agent `task-notification` notices, cross-session `peer` messages) or
53
+ // `sdk` (spawn prompts), or has neither field (tool results, skill/meta
54
+ // entries) — none of which match. Only a genuine human entry may clear an
55
+ // outstanding call-to-user; otherwise a system-injected entry would be
56
+ // miscounted as the owner answering and a genuinely waiting session would stop
57
+ // being suppressed.
58
+ const isGenuineHumanEntry = (parsed: Record<string, unknown>): boolean => {
59
+ const origin = parsed.origin;
60
+ if (isRecord(origin) && readString(origin, 'kind') === 'human') {
61
+ return true;
62
+ }
63
+ const promptSource = readString(parsed, 'promptSource');
64
+ return promptSource === 'typed' || promptSource === 'queued';
65
+ };
66
+
43
67
  const hasOwnerTextReply = (content: unknown): boolean => {
44
68
  if (typeof content === 'string') {
45
69
  if (content.length === 0) {
@@ -122,7 +146,11 @@ export class TranscriptOwnerCallStatusProvider implements OwnerCallStatusProvide
122
146
  ) {
123
147
  lastOwnerCallEpochMs = epochMs;
124
148
  }
125
- if (type === 'user' && hasOwnerTextReply(messageContent)) {
149
+ if (
150
+ type === 'user' &&
151
+ isGenuineHumanEntry(parsed) &&
152
+ hasOwnerTextReply(messageContent)
153
+ ) {
126
154
  lastOwnerReplyEpochMs = epochMs;
127
155
  }
128
156
  }
@@ -3,11 +3,15 @@ import { LocalStorageRepository } from '../LocalStorageRepository';
3
3
  import dotenv from 'dotenv';
4
4
 
5
5
  dotenv.config();
6
- describe('ApiV3IssueRepository', () => {
6
+
7
+ const githubToken = process.env.GH_TOKEN;
8
+ const describeWhenCredentials = githubToken ? describe : describe.skip;
9
+
10
+ describeWhenCredentials('ApiV3IssueRepository', () => {
7
11
  const localStorageRepository = new LocalStorageRepository();
8
12
  const repository = new ApiV3IssueRepository(
9
13
  localStorageRepository,
10
- process.env.GH_TOKEN || 'dummy',
14
+ githubToken,
11
15
  );
12
16
  test('searchIssue', async () => {
13
17
  const result = await repository.searchIssue({
@@ -9,20 +9,6 @@ describe('DefaultSilentSessionMessageComposer', () => {
9
9
  expect(section).toContain(SILENT_SESSION_REMINDER_SENTINEL);
10
10
  });
11
11
 
12
- it('embeds the reminder sentinel in the owner-re-notification section', () => {
13
- const section = composer.composeOwnerReNotificationSection(1800);
14
- expect(section).toContain(SILENT_SESSION_REMINDER_SENTINEL);
15
- });
16
-
17
- it('instructs the agent to re-raise its pending call-to-user without treating the reminder as an owner reply', () => {
18
- const section = composer.composeOwnerReNotificationSection(1800);
19
- expect(section).toContain('waiting on the owner for 30 minutes');
20
- expect(section).toContain('NOT the owner replying');
21
- expect(section).toContain('Re-raise your pending call-to-user');
22
- expect(section).toContain('you are not stalled or idle');
23
- expect(section.toLowerCase()).not.toContain('no output for');
24
- });
25
-
26
12
  it('embeds the reminder sentinel in the sub-agent section', () => {
27
13
  const section = composer.composeSubAgentSection([
28
14
  { label: 'sub-process-1', silentSeconds: 360, runningSeconds: 1200 },
@@ -67,12 +53,10 @@ describe('DefaultSilentSessionMessageComposer', () => {
67
53
 
68
54
  it('does not contain any host-specific or internal identifiers', () => {
69
55
  const mainSection = composer.composeMainStalledSection(600);
70
- const ownerSection = composer.composeOwnerReNotificationSection(1800);
71
56
  const subSection = composer.composeSubAgentSection([
72
57
  { label: 'sub-process-1', silentSeconds: 360, runningSeconds: 1200 },
73
58
  ]);
74
- const combined =
75
- `${mainSection}\n${ownerSection}\n${subSection}`.toLowerCase();
59
+ const combined = `${mainSection}\n${subSection}`.toLowerCase();
76
60
  expect(combined).not.toContain('claude');
77
61
  expect(combined).not.toContain('take ownership');
78
62
  expect(combined).not.toContain('/home/');
@@ -19,22 +19,11 @@ const composeMainStalledMessage = (mainSilentSeconds: number): string => {
19
19
  ].join('\n');
20
20
  };
21
21
 
22
- const composeOwnerReNotificationMessage = (waitingSeconds: number): string => {
23
- const minutes = Math.floor(waitingSeconds / 60);
24
- return [
25
- `${SILENT_SESSION_REMINDER_SENTINEL} This is an automated monitor reminder, NOT the owner replying. Do NOT treat this message as an owner answer to your outstanding call-to-user; your pending question is still unanswered. You have been waiting on the owner for ${minutes} minutes and the owner has not yet replied. The owner is only notified when a fresh call-to-user is raised, so a single past call-to-user that has gone quiet will never reach them. Re-raise your pending call-to-user to the owner now: re-emit it so a new call-to-user marker fires and the owner is notified again. Keep your question and proposed options unchanged unless you have new information. You are correctly waiting on the owner — you are not stalled or idle — so do not abandon the wait and do not invent an owner decision yourself; simply re-surface the same request so the owner sees it.`,
26
- ].join('\n');
27
- };
28
-
29
22
  export class DefaultSilentSessionMessageComposer implements SilentSessionMessageComposer {
30
23
  composeMainStalledSection = (mainSilentSeconds: number): string => {
31
24
  return composeMainStalledMessage(mainSilentSeconds);
32
25
  };
33
26
 
34
- composeOwnerReNotificationSection = (waitingSeconds: number): string => {
35
- return composeOwnerReNotificationMessage(waitingSeconds);
36
- };
37
-
38
27
  composeSubAgentSection = (subAgents: SubAgentActivity[]): string => {
39
28
  const lines = subAgents.map(
40
29
  (subAgent) =>
@@ -21,7 +21,6 @@ import { InteractiveLiveSession } from '../entities/InteractiveLiveSession';
21
21
  type Mocked<T> = jest.Mocked<T> & jest.MockedObject<T>;
22
22
 
23
23
  const MAIN_STALLED_SECTION = 'MAIN_STALLED_SECTION';
24
- const OWNER_RENOTIFICATION_SECTION = 'OWNER_RENOTIFICATION_SECTION';
25
24
  const SUBAGENT_SECTION = 'SUBAGENT_SECTION';
26
25
 
27
26
  describe('NotifySilentLiveSessionsUseCase', () => {
@@ -125,9 +124,6 @@ describe('NotifySilentLiveSessionsUseCase', () => {
125
124
  composeMainStalledSection: jest
126
125
  .fn()
127
126
  .mockReturnValue(MAIN_STALLED_SECTION),
128
- composeOwnerReNotificationSection: jest
129
- .fn()
130
- .mockReturnValue(OWNER_RENOTIFICATION_SECTION),
131
127
  composeSubAgentSection: jest.fn().mockReturnValue(SUBAGENT_SECTION),
132
128
  };
133
129
  mockSleeper = {
@@ -201,7 +197,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
201
197
  ).toHaveBeenCalledWith(['workbench']);
202
198
  });
203
199
 
204
- it('sends the owner-re-notification section instead of the stalled section when an owner call is pending past the threshold', async () => {
200
+ it('suppresses the stalled section and sends nothing when an owner call is pending past the threshold', async () => {
205
201
  setupLiveInteractiveSession('workbench');
206
202
  mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
207
203
  [
@@ -221,65 +217,12 @@ describe('NotifySilentLiveSessionsUseCase', () => {
221
217
  expect(
222
218
  mockMessageComposer.composeMainStalledSection,
223
219
  ).not.toHaveBeenCalled();
224
- expect(
225
- mockMessageComposer.composeOwnerReNotificationSection,
226
- ).toHaveBeenCalledWith(DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS);
227
- expect(
228
- mockNotificationRepository.sendSelfCheckNotification,
229
- ).toHaveBeenCalledWith('workbench', OWNER_RENOTIFICATION_SECTION);
230
- });
231
-
232
- it('does not send the owner-re-notification section when the owner-call wait is within the threshold', async () => {
233
- setupLiveInteractiveSession('workbench');
234
- mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
235
- [
236
- {
237
- sessionName: 'workbench',
238
- lastOutputEpochSeconds:
239
- nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS + 1,
240
- },
241
- ],
242
- );
243
- mockOwnerCallStatusProvider.listSessionNamesWithUnansweredOwnerCall.mockResolvedValue(
244
- new Set(['workbench']),
245
- );
246
-
247
- await useCase.run(runParams());
248
-
249
- expect(
250
- mockMessageComposer.composeOwnerReNotificationSection,
251
- ).not.toHaveBeenCalled();
252
- expect(
253
- mockNotificationRepository.sendSelfCheckNotification,
254
- ).not.toHaveBeenCalled();
255
- });
256
-
257
- it('does not re-notify the owner within the cooldown window while still waiting on the owner', async () => {
258
- setupLiveInteractiveSession('workbench');
259
- mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
260
- [
261
- {
262
- sessionName: 'workbench',
263
- lastOutputEpochSeconds:
264
- nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
265
- },
266
- ],
267
- );
268
- mockOwnerCallStatusProvider.listSessionNamesWithUnansweredOwnerCall.mockResolvedValue(
269
- new Set(['workbench']),
270
- );
271
- mockNotificationRepository.getLastNotifiedEpochSeconds.mockResolvedValue(
272
- nowEpochSeconds - DEFAULT_NOTIFICATION_COOLDOWN_SECONDS + 1,
273
- );
274
-
275
- await useCase.run(runParams());
276
-
277
220
  expect(
278
221
  mockNotificationRepository.sendSelfCheckNotification,
279
222
  ).not.toHaveBeenCalled();
280
223
  });
281
224
 
282
- it('stops re-notifying the owner once a genuine owner reply clears the unanswered call', async () => {
225
+ it('sends the main stalled section when the session is silent past the threshold and not waiting on the owner', async () => {
283
226
  setupLiveInteractiveSession('workbench');
284
227
  mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
285
228
  [
@@ -296,9 +239,6 @@ describe('NotifySilentLiveSessionsUseCase', () => {
296
239
 
297
240
  await useCase.run(runParams());
298
241
 
299
- expect(
300
- mockMessageComposer.composeOwnerReNotificationSection,
301
- ).not.toHaveBeenCalled();
302
242
  expect(mockMessageComposer.composeMainStalledSection).toHaveBeenCalledWith(
303
243
  DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
304
244
  );
@@ -172,27 +172,14 @@ export class NotifySilentLiveSessionsUseCase {
172
172
 
173
173
  if (
174
174
  snapshot.mainSilentSeconds !== null &&
175
- snapshot.mainSilentSeconds >= thresholds.mainSilentThresholdSeconds
175
+ snapshot.mainSilentSeconds >= thresholds.mainSilentThresholdSeconds &&
176
+ !snapshot.hasUnansweredOwnerCall
176
177
  ) {
177
- if (snapshot.hasUnansweredOwnerCall) {
178
- // The session is correctly waiting on the owner, but it has been silent
179
- // long enough that the owner has effectively not been re-notified. Rather
180
- // than suppress, instruct the agent to re-raise its pending call-to-user
181
- // so the owner's marker re-fires. The per-session cooldown applied in
182
- // run() limits this to roughly once per cooldown window, and it stops
183
- // automatically once the owner replies (hasUnansweredOwnerCall clears).
184
- sections.push(
185
- this.messageComposer.composeOwnerReNotificationSection(
186
- snapshot.mainSilentSeconds,
187
- ),
188
- );
189
- } else {
190
- sections.push(
191
- this.messageComposer.composeMainStalledSection(
192
- snapshot.mainSilentSeconds,
193
- ),
194
- );
195
- }
178
+ sections.push(
179
+ this.messageComposer.composeMainStalledSection(
180
+ snapshot.mainSilentSeconds,
181
+ ),
182
+ );
196
183
  }
197
184
 
198
185
  const stalledSubAgents = snapshot.subAgents.filter(
@@ -2,6 +2,5 @@ import { SubAgentActivity } from '../../entities/LiveSessionActivitySnapshot';
2
2
 
3
3
  export interface SilentSessionMessageComposer {
4
4
  composeMainStalledSection: (mainSilentSeconds: number) => string;
5
- composeOwnerReNotificationSection: (waitingSeconds: number) => string;
6
5
  composeSubAgentSection: (subAgents: SubAgentActivity[]) => string;
7
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;AA0D3D,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,CA8iBP;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;AA0D3D,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,CAyiBP;CACH"}
@@ -2,7 +2,6 @@ import { SubAgentActivity } from '../../domain/entities/LiveSessionActivitySnaps
2
2
  import { SilentSessionMessageComposer } from '../../domain/usecases/adapter-interfaces/SilentSessionMessageComposer';
3
3
  export type SilentSessionMessageTemplates = {
4
4
  mainStalledMessage: string | null;
5
- ownerReNotificationMessage: string | null;
6
5
  subAgentMessageHeader: string | null;
7
6
  subAgentMessageFooter: string | null;
8
7
  };
@@ -11,7 +10,6 @@ export declare class ConfigurableSilentSessionMessageComposer implements SilentS
11
10
  private readonly fallback;
12
11
  constructor(templates: SilentSessionMessageTemplates, fallback: SilentSessionMessageComposer);
13
12
  composeMainStalledSection: (mainSilentSeconds: number) => string;
14
- composeOwnerReNotificationSection: (waitingSeconds: number) => string;
15
13
  composeSubAgentSection: (subAgents: SubAgentActivity[]) => string;
16
14
  }
17
15
  //# sourceMappingURL=ConfigurableSilentSessionMessageComposer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ConfigurableSilentSessionMessageComposer.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mDAAmD,CAAC;AACrF,OAAO,EAAE,4BAA4B,EAAE,MAAM,uEAAuE,CAAC;AAQrH,MAAM,MAAM,6BAA6B,GAAG;IAC1C,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,0BAA0B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC,CAAC;AAOF,qBAAa,wCAAyC,YAAW,4BAA4B;IAEzF,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBADR,SAAS,EAAE,6BAA6B,EACxC,QAAQ,EAAE,4BAA4B;IAGzD,yBAAyB,GAAI,mBAAmB,MAAM,KAAG,MAAM,CAK7D;IAEF,iCAAiC,GAAI,gBAAgB,MAAM,KAAG,MAAM,CAKlE;IAEF,sBAAsB,GAAI,WAAW,gBAAgB,EAAE,KAAG,MAAM,CAsB9D;CACH"}
1
+ {"version":3,"file":"ConfigurableSilentSessionMessageComposer.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mDAAmD,CAAC;AACrF,OAAO,EAAE,4BAA4B,EAAE,MAAM,uEAAuE,CAAC;AAQrH,MAAM,MAAM,6BAA6B,GAAG;IAC1C,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC,CAAC;AAOF,qBAAa,wCAAyC,YAAW,4BAA4B;IAEzF,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBADR,SAAS,EAAE,6BAA6B,EACxC,QAAQ,EAAE,4BAA4B;IAGzD,yBAAyB,GAAI,mBAAmB,MAAM,KAAG,MAAM,CAK7D;IAEF,sBAAsB,GAAI,WAAW,gBAAgB,EAAE,KAAG,MAAM,CAsB9D;CACH"}