github-issue-tower-defence-management 1.122.3 → 1.122.4
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.
- package/CHANGELOG.md +8 -0
- package/README.md +3 -3
- package/bin/adapter/entry-points/console/consoleDoneStore.js +19 -5
- package/bin/adapter/entry-points/console/consoleDoneStore.js.map +1 -1
- package/bin/adapter/entry-points/handlers/consoleListsWriter.js +2 -0
- package/bin/adapter/entry-points/handlers/consoleListsWriter.js.map +1 -1
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +1 -1
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -1
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js +6 -7
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js.map +1 -1
- package/bin/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.js +60 -11
- package/bin/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.js.map +1 -1
- package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js +1 -0
- package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js.map +1 -1
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js +64 -7
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js.map +1 -1
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js +5 -7
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js.map +1 -1
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +51 -16
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/console/consoleDoneStore.test.ts +38 -0
- package/src/adapter/entry-points/console/consoleDoneStore.ts +29 -4
- package/src/adapter/entry-points/handlers/consoleListsWriter.test.ts +72 -0
- package/src/adapter/entry-points/handlers/consoleListsWriter.ts +3 -0
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +1 -0
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.test.ts +60 -37
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts +7 -18
- package/src/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.test.ts +223 -0
- package/src/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.ts +94 -10
- package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.test.ts +12 -2
- package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.ts +1 -0
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.test.ts +196 -4
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts +91 -7
- package/src/domain/entities/LiveSessionActivitySnapshot.ts +1 -0
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.test.ts +112 -51
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.ts +8 -17
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +250 -18
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +72 -22
- package/src/domain/usecases/adapter-interfaces/SilentSessionCandidateStateRepository.ts +8 -0
- package/src/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.ts +4 -7
- package/types/adapter/entry-points/console/consoleDoneStore.d.ts +2 -0
- package/types/adapter/entry-points/console/consoleDoneStore.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/consoleListsWriter.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -1
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts +2 -3
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts.map +1 -1
- package/types/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.d.ts +13 -2
- package/types/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.d.ts.map +1 -1
- package/types/adapter/repositories/ProcessListSessionSubAgentActivityRepository.d.ts.map +1 -1
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts +5 -1
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts.map +1 -1
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts +1 -0
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts.map +1 -1
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts +2 -3
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts.map +1 -1
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +2 -1
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/SilentSessionCandidateStateRepository.d.ts +8 -0
- package/types/domain/usecases/adapter-interfaces/SilentSessionCandidateStateRepository.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts +4 -4
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts.map +1 -1
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { DefaultSilentSessionMessageComposer } from './DefaultSilentSessionMessageComposer';
|
|
2
2
|
import { SILENT_SESSION_REMINDER_SENTINEL } from './silentSessionReminderSentinel';
|
|
3
3
|
|
|
4
|
-
const THRESHOLDS = {
|
|
5
|
-
subAgentSilentThresholdSeconds: 300,
|
|
6
|
-
subAgentRunningThresholdSeconds: 900,
|
|
7
|
-
};
|
|
8
|
-
|
|
9
4
|
describe('DefaultSilentSessionMessageComposer', () => {
|
|
10
5
|
const composer = new DefaultSilentSessionMessageComposer();
|
|
11
6
|
|
|
@@ -15,10 +10,16 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
15
10
|
});
|
|
16
11
|
|
|
17
12
|
it('embeds the reminder sentinel in the sub-agent section', () => {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
const subAgent = {
|
|
14
|
+
label: 'sub-process-1',
|
|
15
|
+
silentSeconds: 360,
|
|
16
|
+
runningSeconds: 1200,
|
|
17
|
+
waitingOnExternalProcess: false,
|
|
18
|
+
};
|
|
19
|
+
const section = composer.composeSubAgentSection({
|
|
20
|
+
idleSubAgents: [subAgent],
|
|
21
|
+
longRunningSubAgents: [subAgent],
|
|
22
|
+
});
|
|
22
23
|
expect(section).toContain(SILENT_SESSION_REMINDER_SENTINEL);
|
|
23
24
|
});
|
|
24
25
|
|
|
@@ -116,10 +117,17 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
116
117
|
});
|
|
117
118
|
|
|
118
119
|
it('emits a distinct idle message for a sub-agent that is only output-idle', () => {
|
|
119
|
-
const section = composer.composeSubAgentSection(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
const section = composer.composeSubAgentSection({
|
|
121
|
+
idleSubAgents: [
|
|
122
|
+
{
|
|
123
|
+
label: 'sub-process-idle',
|
|
124
|
+
silentSeconds: 360,
|
|
125
|
+
runningSeconds: 60,
|
|
126
|
+
waitingOnExternalProcess: false,
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
longRunningSubAgents: [],
|
|
130
|
+
});
|
|
123
131
|
expect(section).toContain('sub-process-idle');
|
|
124
132
|
expect(section).toContain('no output for 6m');
|
|
125
133
|
expect(section).toContain('restart, hand off, or replace it');
|
|
@@ -128,10 +136,17 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
128
136
|
});
|
|
129
137
|
|
|
130
138
|
it('presents the system-detected idle duration as the authoritative signal in the idle message', () => {
|
|
131
|
-
const section = composer.composeSubAgentSection(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
139
|
+
const section = composer.composeSubAgentSection({
|
|
140
|
+
idleSubAgents: [
|
|
141
|
+
{
|
|
142
|
+
label: 'sub-process-idle',
|
|
143
|
+
silentSeconds: 360,
|
|
144
|
+
runningSeconds: 60,
|
|
145
|
+
waitingOnExternalProcess: false,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
longRunningSubAgents: [],
|
|
149
|
+
});
|
|
135
150
|
expect(section).toContain(
|
|
136
151
|
"The system has already detected, from each sub-process's last tool activity, that it has produced no output for about the minutes shown below.",
|
|
137
152
|
);
|
|
@@ -142,10 +157,17 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
142
157
|
});
|
|
143
158
|
|
|
144
159
|
it('forbids speculation and dismissal without evidence in the idle message', () => {
|
|
145
|
-
const section = composer.composeSubAgentSection(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
160
|
+
const section = composer.composeSubAgentSection({
|
|
161
|
+
idleSubAgents: [
|
|
162
|
+
{
|
|
163
|
+
label: 'sub-process-idle',
|
|
164
|
+
silentSeconds: 360,
|
|
165
|
+
runningSeconds: 60,
|
|
166
|
+
waitingOnExternalProcess: false,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
longRunningSubAgents: [],
|
|
170
|
+
});
|
|
149
171
|
expect(section).toContain(
|
|
150
172
|
'Around five minutes of silence is a real warning of a possible hang',
|
|
151
173
|
);
|
|
@@ -155,10 +177,17 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
155
177
|
});
|
|
156
178
|
|
|
157
179
|
it('requires a concrete cause-check covering recent activity and external-dependency waiting in the idle message', () => {
|
|
158
|
-
const section = composer.composeSubAgentSection(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
180
|
+
const section = composer.composeSubAgentSection({
|
|
181
|
+
idleSubAgents: [
|
|
182
|
+
{
|
|
183
|
+
label: 'sub-process-idle',
|
|
184
|
+
silentSeconds: 360,
|
|
185
|
+
runningSeconds: 60,
|
|
186
|
+
waitingOnExternalProcess: false,
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
longRunningSubAgents: [],
|
|
190
|
+
});
|
|
162
191
|
expect(section).toContain('determine the CAUSE by a concrete check');
|
|
163
192
|
expect(section).toContain(
|
|
164
193
|
'a very recent push or commit, or output from any nested sub-processes this sub-process itself started',
|
|
@@ -175,26 +204,34 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
175
204
|
});
|
|
176
205
|
|
|
177
206
|
it('requires logging the investigation result even though owner notification is not required', () => {
|
|
178
|
-
const section = composer.composeSubAgentSection(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
207
|
+
const section = composer.composeSubAgentSection({
|
|
208
|
+
idleSubAgents: [
|
|
209
|
+
{
|
|
210
|
+
label: 'sub-process-idle',
|
|
211
|
+
silentSeconds: 360,
|
|
212
|
+
runningSeconds: 60,
|
|
213
|
+
waitingOnExternalProcess: false,
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
longRunningSubAgents: [],
|
|
217
|
+
});
|
|
182
218
|
expect(section).toContain(
|
|
183
219
|
'Owner notification is not required, but you MUST output your investigation result in this session so it remains as a log.',
|
|
184
220
|
);
|
|
185
221
|
});
|
|
186
222
|
|
|
187
223
|
it('emits a distinct long-running message for a sub-agent that has only run too long', () => {
|
|
188
|
-
const section = composer.composeSubAgentSection(
|
|
189
|
-
[
|
|
224
|
+
const section = composer.composeSubAgentSection({
|
|
225
|
+
idleSubAgents: [],
|
|
226
|
+
longRunningSubAgents: [
|
|
190
227
|
{
|
|
191
228
|
label: 'sub-process-long',
|
|
192
229
|
silentSeconds: 30,
|
|
193
230
|
runningSeconds: 1200,
|
|
231
|
+
waitingOnExternalProcess: false,
|
|
194
232
|
},
|
|
195
233
|
],
|
|
196
|
-
|
|
197
|
-
);
|
|
234
|
+
});
|
|
198
235
|
expect(section).toContain('sub-process-long');
|
|
199
236
|
expect(section).toContain('running for 20m');
|
|
200
237
|
expect(section).toContain('infinite loop');
|
|
@@ -209,25 +246,32 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
209
246
|
});
|
|
210
247
|
|
|
211
248
|
it('does not foreground the short idle time in the long-running message', () => {
|
|
212
|
-
const section = composer.composeSubAgentSection(
|
|
213
|
-
[
|
|
249
|
+
const section = composer.composeSubAgentSection({
|
|
250
|
+
idleSubAgents: [],
|
|
251
|
+
longRunningSubAgents: [
|
|
214
252
|
{
|
|
215
253
|
label: 'sub-process-long',
|
|
216
254
|
silentSeconds: 30,
|
|
217
255
|
runningSeconds: 1200,
|
|
256
|
+
waitingOnExternalProcess: false,
|
|
218
257
|
},
|
|
219
258
|
],
|
|
220
|
-
|
|
221
|
-
);
|
|
259
|
+
});
|
|
222
260
|
expect(section).not.toContain('no output for');
|
|
223
261
|
expect(section).not.toContain('silent for');
|
|
224
262
|
});
|
|
225
263
|
|
|
226
264
|
it('emits both distinct messages for a sub-agent matching both conditions, kept separate', () => {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
265
|
+
const subAgent = {
|
|
266
|
+
label: 'sub-process-both',
|
|
267
|
+
silentSeconds: 360,
|
|
268
|
+
runningSeconds: 1200,
|
|
269
|
+
waitingOnExternalProcess: false,
|
|
270
|
+
};
|
|
271
|
+
const section = composer.composeSubAgentSection({
|
|
272
|
+
idleSubAgents: [subAgent],
|
|
273
|
+
longRunningSubAgents: [subAgent],
|
|
274
|
+
});
|
|
231
275
|
expect(section).toContain('no output for 6m');
|
|
232
276
|
expect(section).toContain('determine the CAUSE by a concrete check');
|
|
233
277
|
expect(section).toContain('running for 20m');
|
|
@@ -238,13 +282,24 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
238
282
|
});
|
|
239
283
|
|
|
240
284
|
it('groups each sub-agent under the condition it matched', () => {
|
|
241
|
-
const section = composer.composeSubAgentSection(
|
|
242
|
-
[
|
|
243
|
-
{
|
|
244
|
-
|
|
285
|
+
const section = composer.composeSubAgentSection({
|
|
286
|
+
idleSubAgents: [
|
|
287
|
+
{
|
|
288
|
+
label: 'idle-only',
|
|
289
|
+
silentSeconds: 360,
|
|
290
|
+
runningSeconds: 60,
|
|
291
|
+
waitingOnExternalProcess: false,
|
|
292
|
+
},
|
|
245
293
|
],
|
|
246
|
-
|
|
247
|
-
|
|
294
|
+
longRunningSubAgents: [
|
|
295
|
+
{
|
|
296
|
+
label: 'long-only',
|
|
297
|
+
silentSeconds: 10,
|
|
298
|
+
runningSeconds: 960,
|
|
299
|
+
waitingOnExternalProcess: false,
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
});
|
|
248
303
|
const [idleSection, longRunningSection] = section.split('\n\n');
|
|
249
304
|
expect(idleSection).toContain('idle-only');
|
|
250
305
|
expect(idleSection).not.toContain('long-only');
|
|
@@ -254,10 +309,16 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
254
309
|
|
|
255
310
|
it('does not contain any host-specific or internal identifiers', () => {
|
|
256
311
|
const mainSection = composer.composeMainStalledSection(600);
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
312
|
+
const subAgent = {
|
|
313
|
+
label: 'sub-process-1',
|
|
314
|
+
silentSeconds: 360,
|
|
315
|
+
runningSeconds: 1200,
|
|
316
|
+
waitingOnExternalProcess: false,
|
|
317
|
+
};
|
|
318
|
+
const subSection = composer.composeSubAgentSection({
|
|
319
|
+
idleSubAgents: [subAgent],
|
|
320
|
+
longRunningSubAgents: [subAgent],
|
|
321
|
+
});
|
|
261
322
|
const combined = `${mainSection}\n${subSection}`.toLowerCase();
|
|
262
323
|
expect(combined).not.toContain('claude');
|
|
263
324
|
expect(combined).not.toContain('take ownership');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SubAgentActivity } from '../entities/LiveSessionActivitySnapshot';
|
|
2
2
|
import {
|
|
3
3
|
SilentSessionMessageComposer,
|
|
4
|
-
|
|
4
|
+
SubAgentStallSections,
|
|
5
5
|
} from './adapter-interfaces/SilentSessionMessageComposer';
|
|
6
6
|
import { SILENT_SESSION_REMINDER_SENTINEL } from './silentSessionReminderSentinel';
|
|
7
7
|
|
|
@@ -74,24 +74,15 @@ export class DefaultSilentSessionMessageComposer implements SilentSessionMessage
|
|
|
74
74
|
return composeMainStalledMessage(mainSilentSeconds, this.ownerCallMarker);
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
-
composeSubAgentSection = (
|
|
78
|
-
subAgents: SubAgentActivity[],
|
|
79
|
-
thresholds: SubAgentStallThresholds,
|
|
80
|
-
): string => {
|
|
81
|
-
const idleSubAgents = subAgents.filter(
|
|
82
|
-
(subAgent) =>
|
|
83
|
-
subAgent.silentSeconds >= thresholds.subAgentSilentThresholdSeconds,
|
|
84
|
-
);
|
|
85
|
-
const longRunningSubAgents = subAgents.filter(
|
|
86
|
-
(subAgent) =>
|
|
87
|
-
subAgent.runningSeconds >= thresholds.subAgentRunningThresholdSeconds,
|
|
88
|
-
);
|
|
77
|
+
composeSubAgentSection = (stallSections: SubAgentStallSections): string => {
|
|
89
78
|
const sections: string[] = [];
|
|
90
|
-
if (idleSubAgents.length > 0) {
|
|
91
|
-
sections.push(composeIdleSubAgentSection(idleSubAgents));
|
|
79
|
+
if (stallSections.idleSubAgents.length > 0) {
|
|
80
|
+
sections.push(composeIdleSubAgentSection(stallSections.idleSubAgents));
|
|
92
81
|
}
|
|
93
|
-
if (longRunningSubAgents.length > 0) {
|
|
94
|
-
sections.push(
|
|
82
|
+
if (stallSections.longRunningSubAgents.length > 0) {
|
|
83
|
+
sections.push(
|
|
84
|
+
composeLongRunningSubAgentSection(stallSections.longRunningSubAgents),
|
|
85
|
+
);
|
|
95
86
|
}
|
|
96
87
|
return sections.join('\n\n');
|
|
97
88
|
};
|
|
@@ -159,6 +159,12 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
159
159
|
.fn()
|
|
160
160
|
.mockResolvedValue(everyNameRecentSet()),
|
|
161
161
|
saveCandidateSessionNames: jest.fn().mockResolvedValue(undefined),
|
|
162
|
+
loadAnnouncedRunningSubAgentLabels: jest
|
|
163
|
+
.fn()
|
|
164
|
+
.mockResolvedValue(new Set<string>()),
|
|
165
|
+
saveAnnouncedRunningSubAgentLabels: jest
|
|
166
|
+
.fn()
|
|
167
|
+
.mockResolvedValue(undefined),
|
|
162
168
|
};
|
|
163
169
|
mockMessageComposer = {
|
|
164
170
|
composeMainStalledSection: jest
|
|
@@ -448,6 +454,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
448
454
|
label: 'sub-process-1',
|
|
449
455
|
silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
450
456
|
runningSeconds: 60,
|
|
457
|
+
waitingOnExternalProcess: false,
|
|
451
458
|
},
|
|
452
459
|
];
|
|
453
460
|
mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName.mockResolvedValue(
|
|
@@ -456,27 +463,23 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
456
463
|
|
|
457
464
|
await useCase.run(runParams());
|
|
458
465
|
|
|
459
|
-
expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalledWith(
|
|
460
|
-
subAgents,
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
464
|
-
subAgentRunningThresholdSeconds:
|
|
465
|
-
DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
|
|
466
|
-
},
|
|
467
|
-
);
|
|
466
|
+
expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalledWith({
|
|
467
|
+
idleSubAgents: subAgents,
|
|
468
|
+
longRunningSubAgents: [],
|
|
469
|
+
});
|
|
468
470
|
expect(
|
|
469
471
|
mockNotificationRepository.sendSelfCheckNotification,
|
|
470
472
|
).toHaveBeenCalledWith(GITHUB_SESSION, SUBAGENT_SECTION);
|
|
471
473
|
});
|
|
472
474
|
|
|
473
|
-
it('
|
|
475
|
+
it('applies a configured running threshold when selecting long-running sub-agents', async () => {
|
|
474
476
|
setupLiveInteractiveSession(GITHUB_SESSION);
|
|
475
477
|
const subAgents: SubAgentActivity[] = [
|
|
476
478
|
{
|
|
477
479
|
label: 'sub-process-1',
|
|
478
480
|
silentSeconds: 10,
|
|
479
481
|
runningSeconds: 600,
|
|
482
|
+
waitingOnExternalProcess: false,
|
|
480
483
|
},
|
|
481
484
|
];
|
|
482
485
|
mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName.mockResolvedValue(
|
|
@@ -488,14 +491,10 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
488
491
|
subAgentRunningThresholdSeconds: 600,
|
|
489
492
|
});
|
|
490
493
|
|
|
491
|
-
expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalledWith(
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
496
|
-
subAgentRunningThresholdSeconds: 600,
|
|
497
|
-
},
|
|
498
|
-
);
|
|
494
|
+
expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalledWith({
|
|
495
|
+
idleSubAgents: [],
|
|
496
|
+
longRunningSubAgents: subAgents,
|
|
497
|
+
});
|
|
499
498
|
});
|
|
500
499
|
|
|
501
500
|
it('excludes an owner-handover spawn from selection so no notification is sent', async () => {
|
|
@@ -656,6 +655,239 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
656
655
|
});
|
|
657
656
|
});
|
|
658
657
|
|
|
658
|
+
describe('sub-agent state-based reminder judgment', () => {
|
|
659
|
+
const hungSubAgent = (label: string): SubAgentActivity => ({
|
|
660
|
+
label,
|
|
661
|
+
silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
662
|
+
runningSeconds: 60,
|
|
663
|
+
waitingOnExternalProcess: false,
|
|
664
|
+
});
|
|
665
|
+
const waitingSubAgent = (label: string): SubAgentActivity => ({
|
|
666
|
+
label,
|
|
667
|
+
silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
668
|
+
runningSeconds: 60,
|
|
669
|
+
waitingOnExternalProcess: true,
|
|
670
|
+
});
|
|
671
|
+
const longRunningSubAgent = (
|
|
672
|
+
label: string,
|
|
673
|
+
waitingOnExternalProcess: boolean,
|
|
674
|
+
): SubAgentActivity => ({
|
|
675
|
+
label,
|
|
676
|
+
silentSeconds: 30,
|
|
677
|
+
runningSeconds: DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
|
|
678
|
+
waitingOnExternalProcess,
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
const setupSubAgents = (subAgents: SubAgentActivity[]): void => {
|
|
682
|
+
setupLiveInteractiveSession(GITHUB_SESSION);
|
|
683
|
+
mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName.mockResolvedValue(
|
|
684
|
+
new Map([[GITHUB_SESSION, subAgents]]),
|
|
685
|
+
);
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
it('never selects a waiting sub-agent as a silent-reminder candidate', async () => {
|
|
689
|
+
setupSubAgents([waitingSubAgent('sub-process-1')]);
|
|
690
|
+
|
|
691
|
+
await useCase.run(runParams());
|
|
692
|
+
|
|
693
|
+
expect(mockMessageComposer.composeSubAgentSection).not.toHaveBeenCalled();
|
|
694
|
+
expect(
|
|
695
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
696
|
+
).not.toHaveBeenCalled();
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
it('notifies a hung sub-agent whose in-flight tool has no live process', async () => {
|
|
700
|
+
setupSubAgents([hungSubAgent('sub-process-1')]);
|
|
701
|
+
|
|
702
|
+
await useCase.run(runParams());
|
|
703
|
+
|
|
704
|
+
expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalledWith({
|
|
705
|
+
idleSubAgents: [hungSubAgent('sub-process-1')],
|
|
706
|
+
longRunningSubAgents: [],
|
|
707
|
+
});
|
|
708
|
+
expect(
|
|
709
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
710
|
+
).toHaveBeenCalledWith(GITHUB_SESSION, SUBAGENT_SECTION);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it('keeps notifying a hung sub-agent on every cycle while the condition holds', async () => {
|
|
714
|
+
setupSubAgents([hungSubAgent('sub-process-1')]);
|
|
715
|
+
|
|
716
|
+
await useCase.run(runParams());
|
|
717
|
+
await useCase.run(runParams());
|
|
718
|
+
|
|
719
|
+
expect(
|
|
720
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
721
|
+
).toHaveBeenCalledTimes(2);
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
it('announces a running-threshold crossing for a waiting sub-agent without any idle section', async () => {
|
|
725
|
+
setupSubAgents([
|
|
726
|
+
{
|
|
727
|
+
label: 'sub-process-1',
|
|
728
|
+
silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
729
|
+
runningSeconds: DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
|
|
730
|
+
waitingOnExternalProcess: true,
|
|
731
|
+
},
|
|
732
|
+
]);
|
|
733
|
+
|
|
734
|
+
await useCase.run(runParams());
|
|
735
|
+
|
|
736
|
+
expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalledWith({
|
|
737
|
+
idleSubAgents: [],
|
|
738
|
+
longRunningSubAgents: [
|
|
739
|
+
{
|
|
740
|
+
label: 'sub-process-1',
|
|
741
|
+
silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
742
|
+
runningSeconds: DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
|
|
743
|
+
waitingOnExternalProcess: true,
|
|
744
|
+
},
|
|
745
|
+
],
|
|
746
|
+
});
|
|
747
|
+
expect(
|
|
748
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
749
|
+
).toHaveBeenCalledWith(GITHUB_SESSION, SUBAGENT_SECTION);
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
it('records the announced running label only after the notification is sent', async () => {
|
|
753
|
+
setupSubAgents([longRunningSubAgent('sub-process-1', false)]);
|
|
754
|
+
|
|
755
|
+
await useCase.run(runParams());
|
|
756
|
+
|
|
757
|
+
expect(
|
|
758
|
+
mockCandidateStateRepository.saveAnnouncedRunningSubAgentLabels,
|
|
759
|
+
).toHaveBeenCalledWith({
|
|
760
|
+
sessionName: GITHUB_SESSION,
|
|
761
|
+
labels: ['sub-process-1'],
|
|
762
|
+
now,
|
|
763
|
+
});
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
it('does not record an announced running label when the first-cycle debounce defers the send', async () => {
|
|
767
|
+
setupSubAgents([longRunningSubAgent('sub-process-1', false)]);
|
|
768
|
+
mockCandidateStateRepository.loadRecentCandidateSessionNames.mockResolvedValue(
|
|
769
|
+
new Set<string>(),
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
await useCase.run(runParams());
|
|
773
|
+
|
|
774
|
+
expect(
|
|
775
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
776
|
+
).not.toHaveBeenCalled();
|
|
777
|
+
expect(
|
|
778
|
+
mockCandidateStateRepository.saveAnnouncedRunningSubAgentLabels,
|
|
779
|
+
).not.toHaveBeenCalled();
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
it('does not repeat the running announcement while the same label stays over the threshold', async () => {
|
|
783
|
+
setupSubAgents([longRunningSubAgent('sub-process-1', false)]);
|
|
784
|
+
mockCandidateStateRepository.loadAnnouncedRunningSubAgentLabels.mockResolvedValue(
|
|
785
|
+
new Set(['sub-process-1']),
|
|
786
|
+
);
|
|
787
|
+
|
|
788
|
+
await useCase.run(runParams());
|
|
789
|
+
|
|
790
|
+
expect(mockMessageComposer.composeSubAgentSection).not.toHaveBeenCalled();
|
|
791
|
+
expect(
|
|
792
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
793
|
+
).not.toHaveBeenCalled();
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
it('announces only the new label when another announced label is still over the threshold', async () => {
|
|
797
|
+
setupSubAgents([
|
|
798
|
+
longRunningSubAgent('sub-process-1', false),
|
|
799
|
+
longRunningSubAgent('sub-process-2', false),
|
|
800
|
+
]);
|
|
801
|
+
mockCandidateStateRepository.loadAnnouncedRunningSubAgentLabels.mockResolvedValue(
|
|
802
|
+
new Set(['sub-process-1']),
|
|
803
|
+
);
|
|
804
|
+
|
|
805
|
+
await useCase.run(runParams());
|
|
806
|
+
|
|
807
|
+
expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalledWith({
|
|
808
|
+
idleSubAgents: [],
|
|
809
|
+
longRunningSubAgents: [longRunningSubAgent('sub-process-2', false)],
|
|
810
|
+
});
|
|
811
|
+
expect(
|
|
812
|
+
mockCandidateStateRepository.saveAnnouncedRunningSubAgentLabels,
|
|
813
|
+
).toHaveBeenCalledWith({
|
|
814
|
+
sessionName: GITHUB_SESSION,
|
|
815
|
+
labels: ['sub-process-1', 'sub-process-2'],
|
|
816
|
+
now,
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it('clears the announced record when the label disappears from the sub-agent set', async () => {
|
|
821
|
+
setupSubAgents([]);
|
|
822
|
+
mockCandidateStateRepository.loadAnnouncedRunningSubAgentLabels.mockResolvedValue(
|
|
823
|
+
new Set(['sub-process-1']),
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
await useCase.run(runParams());
|
|
827
|
+
|
|
828
|
+
expect(
|
|
829
|
+
mockCandidateStateRepository.saveAnnouncedRunningSubAgentLabels,
|
|
830
|
+
).toHaveBeenCalledWith({
|
|
831
|
+
sessionName: GITHUB_SESSION,
|
|
832
|
+
labels: [],
|
|
833
|
+
now,
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
it('announces exactly once per crossing across use-case instances sharing the persisted state', async () => {
|
|
838
|
+
setupSubAgents([longRunningSubAgent('sub-process-1', false)]);
|
|
839
|
+
const announcedLabelsBySessionName = new Map<string, string[]>();
|
|
840
|
+
mockCandidateStateRepository.loadAnnouncedRunningSubAgentLabels.mockImplementation(
|
|
841
|
+
async ({ sessionName }) =>
|
|
842
|
+
new Set(announcedLabelsBySessionName.get(sessionName) ?? []),
|
|
843
|
+
);
|
|
844
|
+
mockCandidateStateRepository.saveAnnouncedRunningSubAgentLabels.mockImplementation(
|
|
845
|
+
async ({ sessionName, labels }) => {
|
|
846
|
+
announcedLabelsBySessionName.set(sessionName, labels);
|
|
847
|
+
},
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
await useCase.run(runParams());
|
|
851
|
+
expect(
|
|
852
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
853
|
+
).toHaveBeenCalledTimes(1);
|
|
854
|
+
|
|
855
|
+
const secondUseCase = new NotifySilentLiveSessionsUseCase(
|
|
856
|
+
mockSnapshotProvider,
|
|
857
|
+
mockTranscriptResolver,
|
|
858
|
+
mockSessionOutputActivityRepository,
|
|
859
|
+
mockSubAgentActivityRepository,
|
|
860
|
+
mockOwnerCallStatusProvider,
|
|
861
|
+
mockNotificationRepository,
|
|
862
|
+
mockCandidateStateRepository,
|
|
863
|
+
mockMessageComposer,
|
|
864
|
+
mockSleeper,
|
|
865
|
+
mockHubTaskStatusResolver,
|
|
866
|
+
mockHubTaskStatusCacheRepository,
|
|
867
|
+
);
|
|
868
|
+
await secondUseCase.run(
|
|
869
|
+
runParams({ now: new Date(now.getTime() + 60 * 1000) }),
|
|
870
|
+
);
|
|
871
|
+
|
|
872
|
+
expect(
|
|
873
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
874
|
+
).toHaveBeenCalledTimes(1);
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
it('does not touch the announced-label record for a main-only reminder', async () => {
|
|
878
|
+
setupSilentMainSession(GITHUB_SESSION);
|
|
879
|
+
|
|
880
|
+
await useCase.run(runParams());
|
|
881
|
+
|
|
882
|
+
expect(
|
|
883
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
884
|
+
).toHaveBeenCalledWith(GITHUB_SESSION, MAIN_STALLED_SECTION);
|
|
885
|
+
expect(
|
|
886
|
+
mockCandidateStateRepository.saveAnnouncedRunningSubAgentLabels,
|
|
887
|
+
).not.toHaveBeenCalled();
|
|
888
|
+
});
|
|
889
|
+
});
|
|
890
|
+
|
|
659
891
|
it('sends to multiple sessions sequentially with a stagger delay between sends', async () => {
|
|
660
892
|
mockSnapshotProvider.getSnapshot.mockResolvedValue(
|
|
661
893
|
snapshotWithSessions([GITHUB_SESSION_ALPHA, GITHUB_SESSION_BRAVO]),
|