github-issue-tower-defence-management 1.115.0 → 1.115.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +2 -3
  3. package/bin/adapter/entry-points/console/ui-dist/assets/index-B8K2luHn.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 -17
  6. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  7. package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +2 -4
  8. package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -1
  9. package/bin/adapter/repositories/TmuxSilentSessionNotificationRepository.js +1 -26
  10. package/bin/adapter/repositories/TmuxSilentSessionNotificationRepository.js.map +1 -1
  11. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +1 -10
  12. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
  13. package/package.json +1 -1
  14. package/src/adapter/entry-points/console/ui/src/index.css +1 -7
  15. package/src/adapter/entry-points/console/ui-dist/assets/index-B8K2luHn.css +1 -0
  16. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  17. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +0 -21
  18. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +5 -77
  19. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +1 -12
  20. package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.test.ts +2 -98
  21. package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.ts +1 -45
  22. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +33 -10
  23. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +0 -22
  24. package/src/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.ts +0 -5
  25. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  26. package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts +0 -4
  27. package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -1
  28. package/types/adapter/repositories/TmuxSilentSessionNotificationRepository.d.ts +1 -6
  29. package/types/adapter/repositories/TmuxSilentSessionNotificationRepository.d.ts.map +1 -1
  30. package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +0 -2
  31. package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
  32. package/types/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.d.ts +0 -2
  33. package/types/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.d.ts.map +1 -1
  34. package/bin/adapter/entry-points/console/ui-dist/assets/index-Blb_xqIx.css +0 -1
  35. package/src/adapter/entry-points/console/ui-dist/assets/index-Blb_xqIx.css +0 -1
  36. /package/bin/adapter/entry-points/console/ui-dist/assets/{index-DQyZ8KpG.js → index-BNFY_kIF.js} +0 -0
  37. /package/src/adapter/entry-points/console/ui-dist/assets/{index-DQyZ8KpG.js → index-BNFY_kIF.js} +0 -0
@@ -65,15 +65,6 @@ import {
65
65
 
66
66
  const DEFAULT_DASHBOARD_DATA_DIR: string | null = null;
67
67
 
68
- // The silent-session notification inspects the single, global set of live tmux
69
- // sessions, but the scheduled handler runs once per project. If its cooldown
70
- // were stored in the per-project cache (`./tmp/cache/<projectName>`), the same
71
- // session would get an independent cooldown record per project and be notified
72
- // once per enabled project within a single cooldown window. The cooldown must
73
- // therefore live in a single project-independent cache scope so it is shared
74
- // across every per-project pass.
75
- const SILENT_SESSION_NOTIFICATION_CACHE_PATH = './tmp/cache/shared';
76
-
77
68
  const readSilentSeconds = (
78
69
  configValue: number | undefined,
79
70
  envValue: string | undefined,
@@ -125,7 +116,6 @@ export class HandleScheduledEventUseCaseHandler {
125
116
  mainSilentThresholdSeconds?: number;
126
117
  subAgentSilentThresholdSeconds?: number;
127
118
  subAgentRunningThresholdSeconds?: number;
128
- silentNotificationCooldownSeconds?: number;
129
119
  silentNotificationStaggerSeconds?: number;
130
120
  activeHubTaskStatus?: string;
131
121
  silentMainStalledMessage?: string;
@@ -591,15 +581,9 @@ export class HandleScheduledEventUseCaseHandler {
591
581
  mergedInput.subAgentTranscriptRootDirectory ??
592
582
  process.env.TDPM_SUBAGENT_TRANSCRIPT_ROOT_DIRECTORY ??
593
583
  null;
594
- const silentSessionNotificationCacheRepository =
595
- new LocalStorageCacheRepository(
596
- localStorageRepository,
597
- SILENT_SESSION_NOTIFICATION_CACHE_PATH,
598
- );
599
584
  await notifySilentTmuxSessions({
600
585
  enabled: silentNotificationEnabled,
601
586
  localCommandRunner: nodeLocalCommandRunner,
602
- cacheRepository: silentSessionNotificationCacheRepository,
603
587
  ownerCallMarker,
604
588
  subAgentOutputRootDirectory,
605
589
  subAgentProcessMatchPattern,
@@ -619,11 +603,6 @@ export class HandleScheduledEventUseCaseHandler {
619
603
  process.env.TDPM_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
620
604
  DEFAULT_NOTIFY_SILENT_TMUX_SESSIONS_PARAMS.subAgentRunningThresholdSeconds,
621
605
  ),
622
- cooldownSeconds: readSilentSeconds(
623
- mergedInput.silentNotificationCooldownSeconds,
624
- process.env.TDPM_SILENT_NOTIFICATION_COOLDOWN_SECONDS,
625
- DEFAULT_NOTIFY_SILENT_TMUX_SESSIONS_PARAMS.cooldownSeconds,
626
- ),
627
606
  staggerSeconds: readSilentSeconds(
628
607
  mergedInput.silentNotificationStaggerSeconds,
629
608
  process.env.TDPM_SILENT_NOTIFICATION_STAGGER_SECONDS,
@@ -3,8 +3,6 @@ import * as os from 'os';
3
3
  import * as path from 'path';
4
4
  import { LocalCommandRunner } from '../../../domain/usecases/adapter-interfaces/LocalCommandRunner';
5
5
  import { ProcessEnvironReader } from '../../../domain/usecases/adapter-interfaces/ProcessEnvironReader';
6
- import { LocalStorageCacheRepository } from '../../repositories/LocalStorageCacheRepository';
7
- import { LocalStorageRepository } from '../../repositories/LocalStorageRepository';
8
6
  import { SilentSessionMessageTemplates } from '../../repositories/ConfigurableSilentSessionMessageComposer';
9
7
  import { Issue } from '../../../domain/entities/Issue';
10
8
  import { SILENT_SESSION_REMINDER_SENTINEL } from '../../../domain/usecases/silentSessionReminderSentinel';
@@ -38,18 +36,15 @@ const createMockRunner = (): Mocked<LocalCommandRunner> => ({
38
36
 
39
37
  describe('notifySilentTmuxSessions', () => {
40
38
  let configDir: string;
41
- let cacheDirectory: string;
42
39
 
43
40
  beforeEach(() => {
44
41
  jest.spyOn(console, 'log').mockImplementation(() => undefined);
45
42
  configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'silent-config-'));
46
- cacheDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'silent-cache-'));
47
43
  });
48
44
 
49
45
  afterEach(() => {
50
46
  jest.restoreAllMocks();
51
47
  fs.rmSync(configDir, { force: true, recursive: true });
52
- fs.rmSync(cacheDirectory, { force: true, recursive: true });
53
48
  });
54
49
 
55
50
  const writeTranscript = (lines: object[]): void => {
@@ -78,17 +73,6 @@ describe('notifySilentTmuxSessions', () => {
78
73
  ]);
79
74
  };
80
75
 
81
- const makeCacheRepository = (): LocalStorageCacheRepository =>
82
- new LocalStorageCacheRepository(
83
- new LocalStorageRepository(),
84
- cacheDirectory,
85
- );
86
-
87
- const makeCacheRepositoryAt = (
88
- basePath: string,
89
- ): LocalStorageCacheRepository =>
90
- new LocalStorageCacheRepository(new LocalStorageRepository(), basePath);
91
-
92
76
  const makeEnvironReader = (): ProcessEnvironReader => ({
93
77
  readEnviron: (pid: number) =>
94
78
  pid === CLAUDE_PID
@@ -126,7 +110,6 @@ describe('notifySilentTmuxSessions', () => {
126
110
  enabled: true,
127
111
  localCommandRunner: runner,
128
112
  processEnvironReader: makeEnvironReader(),
129
- cacheRepository: makeCacheRepository(),
130
113
  ownerCallMarker: null,
131
114
  subAgentOutputRootDirectory: null,
132
115
  subAgentProcessMatchPattern: null,
@@ -219,54 +202,27 @@ describe('notifySilentTmuxSessions', () => {
219
202
  expect(sendCall).toBeUndefined();
220
203
  });
221
204
 
222
- it('does not re-notify the same silent session on the next cycle within cooldown', async () => {
205
+ it('re-notifies the same silent session on the next cycle with no cooldown suppression', async () => {
223
206
  silentAssistantTranscript();
224
- const cacheRepository = makeCacheRepository();
225
207
  const firstRunner = liveSessionRunner();
226
208
 
227
- await notifySilentTmuxSessions({
228
- ...baseParams(firstRunner),
229
- cacheRepository,
230
- });
209
+ await notifySilentTmuxSessions(baseParams(firstRunner));
231
210
 
232
- const secondRunner = liveSessionRunner();
233
- await notifySilentTmuxSessions({
234
- ...baseParams(secondRunner),
235
- cacheRepository,
236
- now: new Date(NOW.getTime() + 60 * 1000),
237
- });
238
-
239
- const secondSendCall = secondRunner.runCommand.mock.calls.find(
211
+ const firstSendCall = firstRunner.runCommand.mock.calls.find(
240
212
  (call) => call[0] === 'tmux' && call[1][0] === 'send-keys',
241
213
  );
242
- expect(secondSendCall).toBeUndefined();
243
- });
244
-
245
- it('shares the cooldown across project passes when a single project-independent cache scope is used', async () => {
246
- silentAssistantTranscript();
247
- // The handler runs once per project over the same global set of tmux
248
- // sessions. When the silent-session cooldown lives in one shared cache
249
- // scope, a second per-project pass for the same session within the cooldown
250
- // window must NOT re-send, even though it is a different project pass.
251
- const sharedCacheBasePath = path.join(cacheDirectory, 'shared');
252
- const firstRunner = liveSessionRunner();
253
-
254
- await notifySilentTmuxSessions({
255
- ...baseParams(firstRunner),
256
- cacheRepository: makeCacheRepositoryAt(sharedCacheBasePath),
257
- });
214
+ expect(firstSendCall?.[1][2]).toBe(SESSION_NAME);
258
215
 
259
216
  const secondRunner = liveSessionRunner();
260
217
  await notifySilentTmuxSessions({
261
218
  ...baseParams(secondRunner),
262
- cacheRepository: makeCacheRepositoryAt(sharedCacheBasePath),
263
219
  now: new Date(NOW.getTime() + 60 * 1000),
264
220
  });
265
221
 
266
222
  const secondSendCall = secondRunner.runCommand.mock.calls.find(
267
223
  (call) => call[0] === 'tmux' && call[1][0] === 'send-keys',
268
224
  );
269
- expect(secondSendCall).toBeUndefined();
225
+ expect(secondSendCall?.[1][2]).toBe(SESSION_NAME);
270
226
  });
271
227
 
272
228
  const HUB_TASK_SESSION_NAME =
@@ -385,32 +341,4 @@ describe('notifySilentTmuxSessions', () => {
385
341
  );
386
342
  expect(sendCall?.[1][2]).toBe(HUB_TASK_SESSION_NAME);
387
343
  });
388
-
389
- it('re-sends within the cooldown window when each project pass uses its own cache scope', async () => {
390
- silentAssistantTranscript();
391
- // This reproduces the per-project cooldown defect: when each project pass
392
- // stores its cooldown under its own project-scoped cache path, the same
393
- // session is notified once per project within the cooldown window.
394
- const firstRunner = liveSessionRunner();
395
- await notifySilentTmuxSessions({
396
- ...baseParams(firstRunner),
397
- cacheRepository: makeCacheRepositoryAt(
398
- path.join(cacheDirectory, 'umino'),
399
- ),
400
- });
401
-
402
- const secondRunner = liveSessionRunner();
403
- await notifySilentTmuxSessions({
404
- ...baseParams(secondRunner),
405
- cacheRepository: makeCacheRepositoryAt(
406
- path.join(cacheDirectory, 'xmile'),
407
- ),
408
- now: new Date(NOW.getTime() + 60 * 1000),
409
- });
410
-
411
- const secondSendCall = secondRunner.runCommand.mock.calls.find(
412
- (call) => call[0] === 'tmux' && call[1][0] === 'send-keys',
413
- );
414
- expect(secondSendCall?.[1][2]).toBe(SESSION_NAME);
415
- });
416
344
  });
@@ -8,7 +8,6 @@ import {
8
8
  DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
9
9
  DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
10
10
  DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
11
- DEFAULT_NOTIFICATION_COOLDOWN_SECONDS,
12
11
  DEFAULT_NOTIFICATION_STAGGER_SECONDS,
13
12
  } from '../../../domain/usecases/NotifySilentLiveSessionsUseCase';
14
13
  import { DefaultSilentSessionMessageComposer } from '../../../domain/usecases/DefaultSilentSessionMessageComposer';
@@ -17,7 +16,6 @@ import { ProcFsProcessEnvironReader } from '../../repositories/ProcFsProcessEnvi
17
16
  import { FileSystemInteractiveLiveSessionTranscriptResolver } from '../../repositories/FileSystemInteractiveLiveSessionTranscriptResolver';
18
17
  import { FileSystemSessionOutputActivityRepository } from '../../repositories/FileSystemSessionOutputActivityRepository';
19
18
  import { TmuxSilentSessionNotificationRepository } from '../../repositories/TmuxSilentSessionNotificationRepository';
20
- import { LocalStorageCacheRepository } from '../../repositories/LocalStorageCacheRepository';
21
19
  import { NoUnansweredOwnerCallStatusProvider } from '../../repositories/NoUnansweredOwnerCallStatusProvider';
22
20
  import { TranscriptOwnerCallStatusProvider } from '../../repositories/TranscriptOwnerCallStatusProvider';
23
21
  import { ProcessListSessionSubAgentActivityRepository } from '../../repositories/ProcessListSessionSubAgentActivityRepository';
@@ -35,7 +33,6 @@ export type NotifySilentTmuxSessionsParams = {
35
33
  enabled: boolean;
36
34
  localCommandRunner: LocalCommandRunner;
37
35
  processEnvironReader?: ProcessEnvironReader;
38
- cacheRepository: Pick<LocalStorageCacheRepository, 'getLatest' | 'set'>;
39
36
  ownerCallMarker: string | null;
40
37
  subAgentOutputRootDirectory: string | null;
41
38
  subAgentProcessMatchPattern: string | null;
@@ -43,7 +40,6 @@ export type NotifySilentTmuxSessionsParams = {
43
40
  mainSilentThresholdSeconds: number;
44
41
  subAgentSilentThresholdSeconds: number;
45
42
  subAgentRunningThresholdSeconds: number;
46
- cooldownSeconds: number;
47
43
  staggerSeconds: number;
48
44
  activeHubTaskStatus: string | null;
49
45
  hubTaskStatusResolver: HubTaskStatusResolver | null;
@@ -92,7 +88,6 @@ export const notifySilentTmuxSessions = async (
92
88
  enabled,
93
89
  localCommandRunner,
94
90
  processEnvironReader,
95
- cacheRepository,
96
91
  ownerCallMarker,
97
92
  subAgentOutputRootDirectory,
98
93
  subAgentProcessMatchPattern,
@@ -100,7 +95,6 @@ export const notifySilentTmuxSessions = async (
100
95
  mainSilentThresholdSeconds,
101
96
  subAgentSilentThresholdSeconds,
102
97
  subAgentRunningThresholdSeconds,
103
- cooldownSeconds,
104
98
  staggerSeconds,
105
99
  activeHubTaskStatus,
106
100
  hubTaskStatusResolver,
@@ -132,10 +126,7 @@ export const notifySilentTmuxSessions = async (
132
126
  now,
133
127
  ),
134
128
  createOwnerCallStatusProvider(ownerCallMarker),
135
- new TmuxSilentSessionNotificationRepository(
136
- localCommandRunner,
137
- cacheRepository,
138
- ),
129
+ new TmuxSilentSessionNotificationRepository(localCommandRunner),
139
130
  messageComposer,
140
131
  new RealSleeper(),
141
132
  hubTaskStatusResolver,
@@ -144,7 +135,6 @@ export const notifySilentTmuxSessions = async (
144
135
  mainSilentThresholdSeconds,
145
136
  subAgentSilentThresholdSeconds,
146
137
  subAgentRunningThresholdSeconds,
147
- cooldownSeconds,
148
138
  staggerSeconds,
149
139
  activeHubTaskStatus,
150
140
  now,
@@ -155,6 +145,5 @@ export const DEFAULT_NOTIFY_SILENT_TMUX_SESSIONS_PARAMS = {
155
145
  mainSilentThresholdSeconds: DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
156
146
  subAgentSilentThresholdSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
157
147
  subAgentRunningThresholdSeconds: DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
158
- cooldownSeconds: DEFAULT_NOTIFICATION_COOLDOWN_SECONDS,
159
148
  staggerSeconds: DEFAULT_NOTIFICATION_STAGGER_SECONDS,
160
149
  } as const;
@@ -1,5 +1,4 @@
1
1
  import { LocalCommandRunner } from '../../domain/usecases/adapter-interfaces/LocalCommandRunner';
2
- import { LocalStorageCacheRepository } from './LocalStorageCacheRepository';
3
2
  import { TmuxSilentSessionNotificationRepository } from './TmuxSilentSessionNotificationRepository';
4
3
 
5
4
  type Mocked<T> = jest.Mocked<T> & jest.MockedObject<T>;
@@ -12,22 +11,11 @@ const createMockRunner = (): Mocked<LocalCommandRunner> => ({
12
11
  }),
13
12
  });
14
13
 
15
- const createMockCacheRepository = (): Mocked<
16
- Pick<LocalStorageCacheRepository, 'getLatest' | 'set'>
17
- > => ({
18
- getLatest: jest.fn().mockResolvedValue(null),
19
- set: jest.fn().mockResolvedValue(undefined),
20
- });
21
-
22
14
  describe('TmuxSilentSessionNotificationRepository', () => {
23
15
  describe('sendSelfCheckNotification', () => {
24
16
  it('sends the message literally then submits it with Enter', async () => {
25
17
  const runner = createMockRunner();
26
- const cache = createMockCacheRepository();
27
- const repository = new TmuxSilentSessionNotificationRepository(
28
- runner,
29
- cache,
30
- );
18
+ const repository = new TmuxSilentSessionNotificationRepository(runner);
31
19
 
32
20
  await repository.sendSelfCheckNotification(
33
21
  'https_//github_com/owner/repo/issues/9',
@@ -57,11 +45,7 @@ describe('TmuxSilentSessionNotificationRepository', () => {
57
45
  stderr: "can't find session",
58
46
  exitCode: 1,
59
47
  });
60
- const cache = createMockCacheRepository();
61
- const repository = new TmuxSilentSessionNotificationRepository(
62
- runner,
63
- cache,
64
- );
48
+ const repository = new TmuxSilentSessionNotificationRepository(runner);
65
49
 
66
50
  await expect(
67
51
  repository.sendSelfCheckNotification('missing_session', 'message'),
@@ -70,84 +54,4 @@ describe('TmuxSilentSessionNotificationRepository', () => {
70
54
  );
71
55
  });
72
56
  });
73
-
74
- describe('getLastNotifiedEpochSeconds', () => {
75
- it('returns the cached epoch seconds for a session', async () => {
76
- const runner = createMockRunner();
77
- const cache = createMockCacheRepository();
78
- cache.getLatest.mockResolvedValue({
79
- value: { epochSeconds: 1700000000 },
80
- timestamp: new Date(),
81
- });
82
- const repository = new TmuxSilentSessionNotificationRepository(
83
- runner,
84
- cache,
85
- );
86
-
87
- const result = await repository.getLastNotifiedEpochSeconds(
88
- 'https_//github_com/owner/repo/issues/9',
89
- );
90
-
91
- expect(result).toBe(1700000000);
92
- expect(cache.getLatest).toHaveBeenCalledWith(
93
- 'silent-session-notification/https___github_com_owner_repo_issues_9',
94
- );
95
- });
96
-
97
- it('returns null when no cache entry exists', async () => {
98
- const runner = createMockRunner();
99
- const cache = createMockCacheRepository();
100
- cache.getLatest.mockResolvedValue(null);
101
- const repository = new TmuxSilentSessionNotificationRepository(
102
- runner,
103
- cache,
104
- );
105
-
106
- const result = await repository.getLastNotifiedEpochSeconds(
107
- 'https_//github_com/owner/repo/issues/9',
108
- );
109
-
110
- expect(result).toBeNull();
111
- });
112
-
113
- it('returns null when the cached value has no numeric epoch', async () => {
114
- const runner = createMockRunner();
115
- const cache = createMockCacheRepository();
116
- cache.getLatest.mockResolvedValue({
117
- value: {},
118
- timestamp: new Date(),
119
- });
120
- const repository = new TmuxSilentSessionNotificationRepository(
121
- runner,
122
- cache,
123
- );
124
-
125
- const result = await repository.getLastNotifiedEpochSeconds(
126
- 'https_//github_com/owner/repo/issues/9',
127
- );
128
-
129
- expect(result).toBeNull();
130
- });
131
- });
132
-
133
- describe('setLastNotifiedEpochSeconds', () => {
134
- it('persists the epoch seconds keyed by the sanitized session name', async () => {
135
- const runner = createMockRunner();
136
- const cache = createMockCacheRepository();
137
- const repository = new TmuxSilentSessionNotificationRepository(
138
- runner,
139
- cache,
140
- );
141
-
142
- await repository.setLastNotifiedEpochSeconds(
143
- 'https_//github_com/owner/repo/issues/9',
144
- 1700000000,
145
- );
146
-
147
- expect(cache.set).toHaveBeenCalledWith(
148
- 'silent-session-notification/https___github_com_owner_repo_issues_9',
149
- { epochSeconds: 1700000000 },
150
- );
151
- });
152
- });
153
57
  });
@@ -1,49 +1,8 @@
1
1
  import { SilentSessionNotificationRepository } from '../../domain/usecases/adapter-interfaces/SilentSessionNotificationRepository';
2
2
  import { LocalCommandRunner } from '../../domain/usecases/adapter-interfaces/LocalCommandRunner';
3
- import { LocalStorageCacheRepository } from './LocalStorageCacheRepository';
4
-
5
- const CACHE_KEY_PREFIX = 'silent-session-notification';
6
-
7
- const readEpochSeconds = (value: object): number | null => {
8
- if (!('epochSeconds' in value)) {
9
- return null;
10
- }
11
- const candidate = value.epochSeconds;
12
- if (typeof candidate !== 'number') {
13
- return null;
14
- }
15
- return candidate;
16
- };
17
3
 
18
4
  export class TmuxSilentSessionNotificationRepository implements SilentSessionNotificationRepository {
19
- constructor(
20
- private readonly localCommandRunner: LocalCommandRunner,
21
- private readonly cacheRepository: Pick<
22
- LocalStorageCacheRepository,
23
- 'getLatest' | 'set'
24
- >,
25
- ) {}
26
-
27
- getLastNotifiedEpochSeconds = async (
28
- sessionName: string,
29
- ): Promise<number | null> => {
30
- const cached = await this.cacheRepository.getLatest(
31
- this.toCacheKey(sessionName),
32
- );
33
- if (cached === null) {
34
- return null;
35
- }
36
- return readEpochSeconds(cached.value);
37
- };
38
-
39
- setLastNotifiedEpochSeconds = async (
40
- sessionName: string,
41
- epochSeconds: number,
42
- ): Promise<void> => {
43
- await this.cacheRepository.set(this.toCacheKey(sessionName), {
44
- epochSeconds,
45
- });
46
- };
5
+ constructor(private readonly localCommandRunner: LocalCommandRunner) {}
47
6
 
48
7
  sendSelfCheckNotification = async (
49
8
  sessionName: string,
@@ -77,7 +36,4 @@ export class TmuxSilentSessionNotificationRepository implements SilentSessionNot
77
36
  );
78
37
  }
79
38
  };
80
-
81
- private toCacheKey = (sessionName: string): string =>
82
- `${CACHE_KEY_PREFIX}/${sessionName.replace(/\//g, '_')}`;
83
39
  }
@@ -5,7 +5,6 @@ import {
5
5
  DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
6
6
  DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
7
7
  DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
8
- DEFAULT_NOTIFICATION_COOLDOWN_SECONDS,
9
8
  DEFAULT_NOTIFICATION_STAGGER_SECONDS,
10
9
  } from './NotifySilentLiveSessionsUseCase';
11
10
  import { Issue } from '../entities/Issue';
@@ -46,7 +45,6 @@ describe('NotifySilentLiveSessionsUseCase', () => {
46
45
  mainSilentThresholdSeconds: number;
47
46
  subAgentSilentThresholdSeconds: number;
48
47
  subAgentRunningThresholdSeconds: number;
49
- cooldownSeconds: number;
50
48
  staggerSeconds: number;
51
49
  activeHubTaskStatus: string | null;
52
50
  now: Date;
@@ -54,7 +52,6 @@ describe('NotifySilentLiveSessionsUseCase', () => {
54
52
  mainSilentThresholdSeconds: DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
55
53
  subAgentSilentThresholdSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
56
54
  subAgentRunningThresholdSeconds: DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
57
- cooldownSeconds: DEFAULT_NOTIFICATION_COOLDOWN_SECONDS,
58
55
  staggerSeconds: DEFAULT_NOTIFICATION_STAGGER_SECONDS,
59
56
  activeHubTaskStatus: null,
60
57
  now,
@@ -127,8 +124,6 @@ describe('NotifySilentLiveSessionsUseCase', () => {
127
124
  .mockResolvedValue(new Set<string>()),
128
125
  };
129
126
  mockNotificationRepository = {
130
- getLastNotifiedEpochSeconds: jest.fn().mockResolvedValue(null),
131
- setLastNotifiedEpochSeconds: jest.fn().mockResolvedValue(undefined),
132
127
  sendSelfCheckNotification: jest.fn().mockResolvedValue(undefined),
133
128
  };
134
129
  mockMessageComposer = {
@@ -210,7 +205,6 @@ describe('NotifySilentLiveSessionsUseCase', () => {
210
205
  expect(DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS).toBe(600);
211
206
  expect(DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS).toBe(300);
212
207
  expect(DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS).toBe(900);
213
- expect(DEFAULT_NOTIFICATION_COOLDOWN_SECONDS).toBe(30 * 60);
214
208
  expect(DEFAULT_NOTIFICATION_STAGGER_SECONDS).toBe(25);
215
209
  });
216
210
 
@@ -379,7 +373,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
379
373
  ).not.toHaveBeenCalled();
380
374
  });
381
375
 
382
- it('does not re-notify a session within the cooldown window', async () => {
376
+ it('re-notifies a session on every consecutive cycle while it remains a valid silent target', async () => {
383
377
  setupLiveInteractiveSession('workbench');
384
378
  mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
385
379
  [
@@ -390,15 +384,44 @@ describe('NotifySilentLiveSessionsUseCase', () => {
390
384
  },
391
385
  ],
392
386
  );
393
- mockNotificationRepository.getLastNotifiedEpochSeconds.mockResolvedValue(
394
- nowEpochSeconds - DEFAULT_NOTIFICATION_COOLDOWN_SECONDS + 1,
387
+
388
+ await useCase.run(runParams());
389
+ await useCase.run(runParams());
390
+
391
+ expect(
392
+ mockNotificationRepository.sendSelfCheckNotification,
393
+ ).toHaveBeenCalledTimes(2);
394
+ expect(
395
+ mockNotificationRepository.sendSelfCheckNotification,
396
+ ).toHaveBeenNthCalledWith(1, 'workbench', MAIN_STALLED_SECTION);
397
+ expect(
398
+ mockNotificationRepository.sendSelfCheckNotification,
399
+ ).toHaveBeenNthCalledWith(2, 'workbench', MAIN_STALLED_SECTION);
400
+ });
401
+
402
+ it('notifies on every repeated cycle at the same instant without reading any cooldown state', async () => {
403
+ setupLiveInteractiveSession('workbench');
404
+ mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
405
+ [
406
+ {
407
+ sessionName: 'workbench',
408
+ lastOutputEpochSeconds:
409
+ nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
410
+ },
411
+ ],
395
412
  );
396
413
 
414
+ await useCase.run(runParams());
415
+ await useCase.run(runParams());
397
416
  await useCase.run(runParams());
398
417
 
399
418
  expect(
400
419
  mockNotificationRepository.sendSelfCheckNotification,
401
- ).not.toHaveBeenCalled();
420
+ ).toHaveBeenCalledTimes(3);
421
+ expect(Object.keys(mockNotificationRepository)).toEqual([
422
+ 'sendSelfCheckNotification',
423
+ ]);
424
+ expect(runParams()).not.toHaveProperty('cooldownSeconds');
402
425
  });
403
426
 
404
427
  it('sends to multiple sessions sequentially with a stagger delay between sends', async () => {
@@ -14,7 +14,6 @@ import { ResolveInteractiveLiveSessionsUseCase } from './ResolveInteractiveLiveS
14
14
  export const DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS = 10 * 60;
15
15
  export const DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS = 5 * 60;
16
16
  export const DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS = 15 * 60;
17
- export const DEFAULT_NOTIFICATION_COOLDOWN_SECONDS = 30 * 60;
18
17
  export const DEFAULT_NOTIFICATION_STAGGER_SECONDS = 25;
19
18
 
20
19
  const GITHUB_ISSUE_URL_PATTERN =
@@ -53,7 +52,6 @@ export class NotifySilentLiveSessionsUseCase {
53
52
  mainSilentThresholdSeconds: number;
54
53
  subAgentSilentThresholdSeconds: number;
55
54
  subAgentRunningThresholdSeconds: number;
56
- cooldownSeconds: number;
57
55
  staggerSeconds: number;
58
56
  activeHubTaskStatus: string | null;
59
57
  now: Date;
@@ -92,24 +90,8 @@ export class NotifySilentLiveSessionsUseCase {
92
90
  `Silent live session notification: ${candidates.length} candidate(s) of ${interactiveSessions.length} interactive session(s).`,
93
91
  );
94
92
 
95
- const nowEpochSeconds = Math.floor(params.now.getTime() / 1000);
96
93
  let sentCount = 0;
97
94
  for (const candidate of candidates) {
98
- const lastNotifiedEpochSeconds =
99
- await this.notificationRepository.getLastNotifiedEpochSeconds(
100
- candidate.sessionName,
101
- );
102
- if (
103
- lastNotifiedEpochSeconds !== null &&
104
- nowEpochSeconds - lastNotifiedEpochSeconds < params.cooldownSeconds
105
- ) {
106
- console.log(
107
- `Skipping ${candidate.sessionName}: notified ${
108
- nowEpochSeconds - lastNotifiedEpochSeconds
109
- } seconds ago, within cooldown ${params.cooldownSeconds} seconds.`,
110
- );
111
- continue;
112
- }
113
95
  if (
114
96
  !(await this.isHubTaskActive(
115
97
  candidate.sessionName,
@@ -125,10 +107,6 @@ export class NotifySilentLiveSessionsUseCase {
125
107
  candidate.sessionName,
126
108
  candidate.message,
127
109
  );
128
- await this.notificationRepository.setLastNotifiedEpochSeconds(
129
- candidate.sessionName,
130
- nowEpochSeconds,
131
- );
132
110
  sentCount += 1;
133
111
  console.log(`Notified ${candidate.sessionName}.`);
134
112
  }
@@ -1,9 +1,4 @@
1
1
  export interface SilentSessionNotificationRepository {
2
- getLastNotifiedEpochSeconds: (sessionName: string) => Promise<number | null>;
3
- setLastNotifiedEpochSeconds: (
4
- sessionName: string,
5
- epochSeconds: number,
6
- ) => Promise<void>;
7
2
  sendSelfCheckNotification: (
8
3
  sessionName: string,
9
4
  message: string,
@@ -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,CA+iBP;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,CAmiBP;CACH"}
@@ -1,13 +1,11 @@
1
1
  import { LocalCommandRunner } from '../../../domain/usecases/adapter-interfaces/LocalCommandRunner';
2
2
  import { ProcessEnvironReader } from '../../../domain/usecases/adapter-interfaces/ProcessEnvironReader';
3
3
  import { HubTaskStatusResolver } from '../../../domain/usecases/NotifySilentLiveSessionsUseCase';
4
- import { LocalStorageCacheRepository } from '../../repositories/LocalStorageCacheRepository';
5
4
  import { SilentSessionMessageTemplates } from '../../repositories/ConfigurableSilentSessionMessageComposer';
6
5
  export type NotifySilentTmuxSessionsParams = {
7
6
  enabled: boolean;
8
7
  localCommandRunner: LocalCommandRunner;
9
8
  processEnvironReader?: ProcessEnvironReader;
10
- cacheRepository: Pick<LocalStorageCacheRepository, 'getLatest' | 'set'>;
11
9
  ownerCallMarker: string | null;
12
10
  subAgentOutputRootDirectory: string | null;
13
11
  subAgentProcessMatchPattern: string | null;
@@ -15,7 +13,6 @@ export type NotifySilentTmuxSessionsParams = {
15
13
  mainSilentThresholdSeconds: number;
16
14
  subAgentSilentThresholdSeconds: number;
17
15
  subAgentRunningThresholdSeconds: number;
18
- cooldownSeconds: number;
19
16
  staggerSeconds: number;
20
17
  activeHubTaskStatus: string | null;
21
18
  hubTaskStatusResolver: HubTaskStatusResolver | null;
@@ -27,7 +24,6 @@ export declare const DEFAULT_NOTIFY_SILENT_TMUX_SESSIONS_PARAMS: {
27
24
  readonly mainSilentThresholdSeconds: number;
28
25
  readonly subAgentSilentThresholdSeconds: number;
29
26
  readonly subAgentRunningThresholdSeconds: number;
30
- readonly cooldownSeconds: number;
31
27
  readonly staggerSeconds: 25;
32
28
  };
33
29
  //# sourceMappingURL=notifySilentTmuxSessions.d.ts.map
@@ -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,EAMtB,MAAM,0DAA0D,CAAC;AAOlE,OAAO,EAAE,2BAA2B,EAAE,MAAM,gDAAgD,CAAC;AAQ7F,OAAO,EAEL,6BAA6B,EAC9B,MAAM,6DAA6D,CAAC;AAGrE,MAAM,MAAM,8BAA8B,GAAG;IAC3C,OAAO,EAAE,OAAO,CAAC;IACjB,kBAAkB,EAAE,kBAAkB,CAAC;IACvC,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C,eAAe,EAAE,IAAI,CAAC,2BAA2B,EAAE,WAAW,GAAG,KAAK,CAAC,CAAC;IACxE,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,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,qBAAqB,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACpD,gBAAgB,EAAE,6BAA6B,CAAC;IAChD,GAAG,EAAE,IAAI,CAAC;CACX,CAAC;AAoCF,eAAO,MAAM,wBAAwB,GACnC,QAAQ,8BAA8B,KACrC,OAAO,CAAC,IAAI,CA8Dd,CAAC;AAEF,eAAO,MAAM,0CAA0C;;;;;;CAM7C,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,EAKtB,MAAM,0DAA0D,CAAC;AAclE,OAAO,EAEL,6BAA6B,EAC9B,MAAM,6DAA6D,CAAC;AAGrE,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,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,qBAAqB,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACpD,gBAAgB,EAAE,6BAA6B,CAAC;IAChD,GAAG,EAAE,IAAI,CAAC;CACX,CAAC;AAoCF,eAAO,MAAM,wBAAwB,GACnC,QAAQ,8BAA8B,KACrC,OAAO,CAAC,IAAI,CAwDd,CAAC;AAEF,eAAO,MAAM,0CAA0C;;;;;CAK7C,CAAC"}
@@ -1,13 +1,8 @@
1
1
  import { SilentSessionNotificationRepository } from '../../domain/usecases/adapter-interfaces/SilentSessionNotificationRepository';
2
2
  import { LocalCommandRunner } from '../../domain/usecases/adapter-interfaces/LocalCommandRunner';
3
- import { LocalStorageCacheRepository } from './LocalStorageCacheRepository';
4
3
  export declare class TmuxSilentSessionNotificationRepository implements SilentSessionNotificationRepository {
5
4
  private readonly localCommandRunner;
6
- private readonly cacheRepository;
7
- constructor(localCommandRunner: LocalCommandRunner, cacheRepository: Pick<LocalStorageCacheRepository, 'getLatest' | 'set'>);
8
- getLastNotifiedEpochSeconds: (sessionName: string) => Promise<number | null>;
9
- setLastNotifiedEpochSeconds: (sessionName: string, epochSeconds: number) => Promise<void>;
5
+ constructor(localCommandRunner: LocalCommandRunner);
10
6
  sendSelfCheckNotification: (sessionName: string, message: string) => Promise<void>;
11
- private toCacheKey;
12
7
  }
13
8
  //# sourceMappingURL=TmuxSilentSessionNotificationRepository.d.ts.map