github-issue-tower-defence-management 1.122.2 → 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 +15 -0
- package/README.md +3 -3
- package/bin/adapter/entry-points/cli/index.js +9 -0
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- 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/console/consoleOperationApi.js +78 -39
- package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleProjectResolver.js +8 -1
- package/bin/adapter/entry-points/console/consoleProjectResolver.js.map +1 -1
- package/bin/adapter/entry-points/console/webServer.js +5 -1
- package/bin/adapter/entry-points/console/webServer.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/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +23 -4
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.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/cli/index.test.ts +3 -1
- package/src/adapter/entry-points/cli/index.ts +11 -0
- 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/console/consoleOperationApi.test.ts +190 -11
- package/src/adapter/entry-points/console/consoleOperationApi.ts +97 -56
- package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +13 -0
- package/src/adapter/entry-points/console/consoleProjectResolver.ts +11 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +3 -0
- package/src/adapter/entry-points/console/webServer.test.ts +4 -0
- package/src/adapter/entry-points/console/webServer.ts +9 -1
- 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/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +86 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +34 -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/IssueRepository.ts +1 -0
- 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/cli/index.d.ts.map +1 -1
- 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/console/consoleOperationApi.d.ts +2 -0
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +2 -1
- package/types/adapter/entry-points/console/consoleProjectResolver.d.ts.map +1 -1
- package/types/adapter/entry-points/console/webServer.d.ts +2 -1
- package/types/adapter/entry-points/console/webServer.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/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +2 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.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/IssueRepository.d.ts +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.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
|
@@ -4,6 +4,15 @@ import path from 'path';
|
|
|
4
4
|
import { mock } from 'jest-mock-extended';
|
|
5
5
|
import { Issue } from '../../../domain/entities/Issue';
|
|
6
6
|
import { FieldOption, Project } from '../../../domain/entities/Project';
|
|
7
|
+
import {
|
|
8
|
+
buildConsoleDataResponse,
|
|
9
|
+
parseConsoleDataRoute,
|
|
10
|
+
} from '../console/consoleDataDelivery';
|
|
11
|
+
import {
|
|
12
|
+
CONSOLE_DONE_TAB_NAMES,
|
|
13
|
+
readDoneProjectItemIds,
|
|
14
|
+
recordDoneProjectItemId,
|
|
15
|
+
} from '../console/consoleDoneStore';
|
|
7
16
|
import {
|
|
8
17
|
formatConsoleGeneratedAt,
|
|
9
18
|
writeConsoleLists,
|
|
@@ -226,6 +235,69 @@ describe('writeConsoleLists', () => {
|
|
|
226
235
|
});
|
|
227
236
|
expect(fs.readdirSync(outDir)).toHaveLength(0);
|
|
228
237
|
});
|
|
238
|
+
|
|
239
|
+
const regenerateUnread = (): void => {
|
|
240
|
+
writeConsoleLists({
|
|
241
|
+
consoleDataOutputDir: outDir,
|
|
242
|
+
pjcode: 'demo',
|
|
243
|
+
assigneeLogin: ASSIGNEE,
|
|
244
|
+
project,
|
|
245
|
+
issues: [makeIssue({ status: 'Unread', itemId: 'item-1' })],
|
|
246
|
+
generatedAt: '2026-06-14T07:22:33Z',
|
|
247
|
+
});
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const unreadServedItemCount = (): number => {
|
|
251
|
+
const route = parseConsoleDataRoute('projects/demo/unread/list.json');
|
|
252
|
+
if (route === null) {
|
|
253
|
+
throw new Error('route should not be null');
|
|
254
|
+
}
|
|
255
|
+
const response = buildConsoleDataResponse(outDir, route);
|
|
256
|
+
const body: unknown = JSON.parse(response.body);
|
|
257
|
+
const items: unknown = isRecord(body) ? body.items : undefined;
|
|
258
|
+
return isUnknownArray(items) ? items.length : 0;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
it('resets the done file of every tab to an empty record on regeneration', () => {
|
|
262
|
+
regenerateUnread();
|
|
263
|
+
|
|
264
|
+
for (const tab of CONSOLE_DONE_TAB_NAMES) {
|
|
265
|
+
const doneFile = path.join(outDir, 'demo', tab, '.done.json');
|
|
266
|
+
expect(fs.existsSync(doneFile)).toBe(true);
|
|
267
|
+
const raw: unknown = JSON.parse(fs.readFileSync(doneFile, 'utf8'));
|
|
268
|
+
expect(raw).toEqual({ projectItemIds: [] });
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('clears a pre-existing accumulated done file on regeneration', () => {
|
|
273
|
+
for (const id of ['PVTI_1', 'PVTI_2', 'PVTI_3']) {
|
|
274
|
+
recordDoneProjectItemId(outDir, 'demo', 'unread', id);
|
|
275
|
+
}
|
|
276
|
+
expect(readDoneProjectItemIds(outDir, 'demo', 'unread')).toHaveLength(3);
|
|
277
|
+
|
|
278
|
+
regenerateUnread();
|
|
279
|
+
|
|
280
|
+
expect(readDoneProjectItemIds(outDir, 'demo', 'unread')).toEqual([]);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('serves the full list after regeneration despite a prior accumulated done record', () => {
|
|
284
|
+
recordDoneProjectItemId(outDir, 'demo', 'unread', 'item-1');
|
|
285
|
+
|
|
286
|
+
regenerateUnread();
|
|
287
|
+
|
|
288
|
+
expect(unreadServedItemCount()).toBe(1);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('re-hides an item recorded as done until the next regeneration bounds it to one cycle', () => {
|
|
292
|
+
regenerateUnread();
|
|
293
|
+
expect(unreadServedItemCount()).toBe(1);
|
|
294
|
+
|
|
295
|
+
recordDoneProjectItemId(outDir, 'demo', 'unread', 'item-1');
|
|
296
|
+
expect(unreadServedItemCount()).toBe(0);
|
|
297
|
+
|
|
298
|
+
regenerateUnread();
|
|
299
|
+
expect(unreadServedItemCount()).toBe(1);
|
|
300
|
+
});
|
|
229
301
|
});
|
|
230
302
|
|
|
231
303
|
describe('formatConsoleGeneratedAt', () => {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
ConsoleTabName,
|
|
8
8
|
GenerateConsoleListsUseCase,
|
|
9
9
|
} from '../../../domain/usecases/console/GenerateConsoleListsUseCase';
|
|
10
|
+
import { resetDoneProjectItemIdsAcrossTabs } from '../console/consoleDoneStore';
|
|
10
11
|
|
|
11
12
|
export type ConsoleListsWriterParams = {
|
|
12
13
|
consoleDataOutputDir: string | null | undefined;
|
|
@@ -61,4 +62,6 @@ export const writeConsoleLists = (params: ConsoleListsWriterParams): void => {
|
|
|
61
62
|
lists[tab],
|
|
62
63
|
);
|
|
63
64
|
}
|
|
65
|
+
|
|
66
|
+
resetDoneProjectItemIdsAcrossTabs(consoleDataOutputDir, pjcode);
|
|
64
67
|
};
|
|
@@ -4,11 +4,6 @@ import { SILENT_SESSION_REMINDER_SENTINEL } from '../../domain/usecases/silentSe
|
|
|
4
4
|
|
|
5
5
|
type Mocked<T> = jest.Mocked<T> & jest.MockedObject<T>;
|
|
6
6
|
|
|
7
|
-
const THRESHOLDS = {
|
|
8
|
-
subAgentSilentThresholdSeconds: 300,
|
|
9
|
-
subAgentRunningThresholdSeconds: 900,
|
|
10
|
-
};
|
|
11
|
-
|
|
12
7
|
const createFallback = (): Mocked<SilentSessionMessageComposer> => ({
|
|
13
8
|
composeMainStalledSection: jest.fn().mockReturnValue('FALLBACK_MAIN'),
|
|
14
9
|
composeSubAgentSection: jest.fn().mockReturnValue('FALLBACK_SUB'),
|
|
@@ -78,16 +73,22 @@ describe('ConfigurableSilentSessionMessageComposer', () => {
|
|
|
78
73
|
noTemplates,
|
|
79
74
|
fallback,
|
|
80
75
|
);
|
|
76
|
+
const subAgent = {
|
|
77
|
+
label: 'task-a',
|
|
78
|
+
silentSeconds: 360,
|
|
79
|
+
runningSeconds: 1200,
|
|
80
|
+
waitingOnExternalProcess: false,
|
|
81
|
+
};
|
|
81
82
|
expect(
|
|
82
|
-
composer.composeSubAgentSection(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
),
|
|
83
|
+
composer.composeSubAgentSection({
|
|
84
|
+
idleSubAgents: [subAgent],
|
|
85
|
+
longRunningSubAgents: [subAgent],
|
|
86
|
+
}),
|
|
86
87
|
).toBe('FALLBACK_SUB');
|
|
87
|
-
expect(fallback.composeSubAgentSection).toHaveBeenCalledWith(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
);
|
|
88
|
+
expect(fallback.composeSubAgentSection).toHaveBeenCalledWith({
|
|
89
|
+
idleSubAgents: [subAgent],
|
|
90
|
+
longRunningSubAgents: [subAgent],
|
|
91
|
+
});
|
|
91
92
|
});
|
|
92
93
|
|
|
93
94
|
it('renders the configured idle header, list, and footer for an idle sub-agent', () => {
|
|
@@ -100,10 +101,17 @@ describe('ConfigurableSilentSessionMessageComposer', () => {
|
|
|
100
101
|
},
|
|
101
102
|
fallback,
|
|
102
103
|
);
|
|
103
|
-
const section = composer.composeSubAgentSection(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
const section = composer.composeSubAgentSection({
|
|
105
|
+
idleSubAgents: [
|
|
106
|
+
{
|
|
107
|
+
label: 'task-a',
|
|
108
|
+
silentSeconds: 360,
|
|
109
|
+
runningSeconds: 60,
|
|
110
|
+
waitingOnExternalProcess: false,
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
longRunningSubAgents: [],
|
|
114
|
+
});
|
|
107
115
|
expect(section).toContain('IDLE_HEADER');
|
|
108
116
|
expect(section).toContain('task-a');
|
|
109
117
|
expect(section).toContain('no output for 6m');
|
|
@@ -122,10 +130,17 @@ describe('ConfigurableSilentSessionMessageComposer', () => {
|
|
|
122
130
|
},
|
|
123
131
|
fallback,
|
|
124
132
|
);
|
|
125
|
-
const section = composer.composeSubAgentSection(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
133
|
+
const section = composer.composeSubAgentSection({
|
|
134
|
+
idleSubAgents: [],
|
|
135
|
+
longRunningSubAgents: [
|
|
136
|
+
{
|
|
137
|
+
label: 'task-b',
|
|
138
|
+
silentSeconds: 30,
|
|
139
|
+
runningSeconds: 1200,
|
|
140
|
+
waitingOnExternalProcess: false,
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
});
|
|
129
144
|
expect(section).toContain('LONG_HEADER');
|
|
130
145
|
expect(section).toContain('task-b');
|
|
131
146
|
expect(section).toContain('running for 20m');
|
|
@@ -144,10 +159,16 @@ describe('ConfigurableSilentSessionMessageComposer', () => {
|
|
|
144
159
|
},
|
|
145
160
|
fallback,
|
|
146
161
|
);
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
162
|
+
const subAgent = {
|
|
163
|
+
label: 'task-both',
|
|
164
|
+
silentSeconds: 360,
|
|
165
|
+
runningSeconds: 1200,
|
|
166
|
+
waitingOnExternalProcess: false,
|
|
167
|
+
};
|
|
168
|
+
const section = composer.composeSubAgentSection({
|
|
169
|
+
idleSubAgents: [subAgent],
|
|
170
|
+
longRunningSubAgents: [subAgent],
|
|
171
|
+
});
|
|
151
172
|
const [idleSection, longRunningSection] = section
|
|
152
173
|
.replace(`${SILENT_SESSION_REMINDER_SENTINEL} `, '')
|
|
153
174
|
.split('\n\n');
|
|
@@ -167,19 +188,21 @@ describe('ConfigurableSilentSessionMessageComposer', () => {
|
|
|
167
188
|
},
|
|
168
189
|
fallback,
|
|
169
190
|
);
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
191
|
+
const idleSubAgent = {
|
|
192
|
+
label: 'task-idle',
|
|
193
|
+
silentSeconds: 360,
|
|
194
|
+
runningSeconds: 60,
|
|
195
|
+
waitingOnExternalProcess: false,
|
|
196
|
+
};
|
|
197
|
+
const section = composer.composeSubAgentSection({
|
|
198
|
+
idleSubAgents: [idleSubAgent],
|
|
199
|
+
longRunningSubAgents: [],
|
|
200
|
+
});
|
|
174
201
|
expect(section).toContain('BUILTIN_IDLE');
|
|
175
|
-
expect(fallback.composeSubAgentSection).toHaveBeenCalledWith(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
THRESHOLDS.subAgentSilentThresholdSeconds,
|
|
180
|
-
subAgentRunningThresholdSeconds: Number.POSITIVE_INFINITY,
|
|
181
|
-
},
|
|
182
|
-
);
|
|
202
|
+
expect(fallback.composeSubAgentSection).toHaveBeenCalledWith({
|
|
203
|
+
idleSubAgents: [idleSubAgent],
|
|
204
|
+
longRunningSubAgents: [],
|
|
205
|
+
});
|
|
183
206
|
});
|
|
184
207
|
|
|
185
208
|
it('does not double-prepend the sentinel when the template already carries it', () => {
|
|
@@ -2,7 +2,7 @@ import { SubAgentActivity } from '../../domain/entities/LiveSessionActivitySnaps
|
|
|
2
2
|
import { composeOwnerCallFormatGuidance } from '../../domain/usecases/DefaultSilentSessionMessageComposer';
|
|
3
3
|
import {
|
|
4
4
|
SilentSessionMessageComposer,
|
|
5
|
-
|
|
5
|
+
SubAgentStallSections,
|
|
6
6
|
} from '../../domain/usecases/adapter-interfaces/SilentSessionMessageComposer';
|
|
7
7
|
import { SILENT_SESSION_REMINDER_SENTINEL } from '../../domain/usecases/silentSessionReminderSentinel';
|
|
8
8
|
|
|
@@ -40,10 +40,7 @@ export class ConfigurableSilentSessionMessageComposer implements SilentSessionMe
|
|
|
40
40
|
);
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
composeSubAgentSection = (
|
|
44
|
-
subAgents: SubAgentActivity[],
|
|
45
|
-
thresholds: SubAgentStallThresholds,
|
|
46
|
-
): string => {
|
|
43
|
+
composeSubAgentSection = (stallSections: SubAgentStallSections): string => {
|
|
47
44
|
const hasIdleTemplate =
|
|
48
45
|
this.templates.subAgentIdleMessageHeader !== null ||
|
|
49
46
|
this.templates.subAgentIdleMessageFooter !== null;
|
|
@@ -51,17 +48,10 @@ export class ConfigurableSilentSessionMessageComposer implements SilentSessionMe
|
|
|
51
48
|
this.templates.subAgentLongRunningMessageHeader !== null ||
|
|
52
49
|
this.templates.subAgentLongRunningMessageFooter !== null;
|
|
53
50
|
if (!hasIdleTemplate && !hasLongRunningTemplate) {
|
|
54
|
-
return this.fallback.composeSubAgentSection(
|
|
51
|
+
return this.fallback.composeSubAgentSection(stallSections);
|
|
55
52
|
}
|
|
56
53
|
|
|
57
|
-
const idleSubAgents =
|
|
58
|
-
(subAgent) =>
|
|
59
|
-
subAgent.silentSeconds >= thresholds.subAgentSilentThresholdSeconds,
|
|
60
|
-
);
|
|
61
|
-
const longRunningSubAgents = subAgents.filter(
|
|
62
|
-
(subAgent) =>
|
|
63
|
-
subAgent.runningSeconds >= thresholds.subAgentRunningThresholdSeconds,
|
|
64
|
-
);
|
|
54
|
+
const { idleSubAgents, longRunningSubAgents } = stallSections;
|
|
65
55
|
|
|
66
56
|
const sections: string[] = [];
|
|
67
57
|
if (idleSubAgents.length > 0 && hasIdleTemplate) {
|
|
@@ -74,10 +64,9 @@ export class ConfigurableSilentSessionMessageComposer implements SilentSessionMe
|
|
|
74
64
|
);
|
|
75
65
|
} else if (idleSubAgents.length > 0) {
|
|
76
66
|
sections.push(
|
|
77
|
-
this.fallback.composeSubAgentSection(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
subAgentRunningThresholdSeconds: Number.POSITIVE_INFINITY,
|
|
67
|
+
this.fallback.composeSubAgentSection({
|
|
68
|
+
idleSubAgents,
|
|
69
|
+
longRunningSubAgents: [],
|
|
81
70
|
}),
|
|
82
71
|
);
|
|
83
72
|
}
|
|
@@ -3,6 +3,7 @@ import * as os from 'os';
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import {
|
|
5
5
|
DEFAULT_STATE_RETENTION_WINDOW_SECONDS,
|
|
6
|
+
ANNOUNCED_RUNNING_RETENTION_WINDOW_SECONDS,
|
|
6
7
|
FileSystemSilentSessionCandidateStateRepository,
|
|
7
8
|
} from './FileSystemSilentSessionCandidateStateRepository';
|
|
8
9
|
|
|
@@ -141,6 +142,7 @@ describe('FileSystemSilentSessionCandidateStateRepository', () => {
|
|
|
141
142
|
recordedEpochSeconds: Math.floor(secondSaveAt.getTime() / 1000),
|
|
142
143
|
},
|
|
143
144
|
],
|
|
145
|
+
announcedRunningSubAgents: [],
|
|
144
146
|
});
|
|
145
147
|
});
|
|
146
148
|
|
|
@@ -198,4 +200,225 @@ describe('FileSystemSilentSessionCandidateStateRepository', () => {
|
|
|
198
200
|
it('exposes the default retention window as a named constant', () => {
|
|
199
201
|
expect(DEFAULT_STATE_RETENTION_WINDOW_SECONDS).toBe(60 * 60);
|
|
200
202
|
});
|
|
203
|
+
|
|
204
|
+
describe('announced running sub-agent labels', () => {
|
|
205
|
+
it('round-trips saved announced labels so the next cycle reads them back', async () => {
|
|
206
|
+
const repository = new FileSystemSilentSessionCandidateStateRepository(
|
|
207
|
+
stateFilePath,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
await repository.saveAnnouncedRunningSubAgentLabels({
|
|
211
|
+
sessionName: sessionAlpha,
|
|
212
|
+
labels: ['sub-process-1', 'sub-process-2'],
|
|
213
|
+
now: new Date('2026-06-26T00:00:00Z'),
|
|
214
|
+
});
|
|
215
|
+
const loaded = await repository.loadAnnouncedRunningSubAgentLabels({
|
|
216
|
+
sessionName: sessionAlpha,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(loaded).toEqual(new Set(['sub-process-1', 'sub-process-2']));
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('returns an empty set when no announcement has been recorded for the session', async () => {
|
|
223
|
+
const repository = new FileSystemSilentSessionCandidateStateRepository(
|
|
224
|
+
stateFilePath,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const loaded = await repository.loadAnnouncedRunningSubAgentLabels({
|
|
228
|
+
sessionName: sessionAlpha,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
expect(loaded).toEqual(new Set<string>());
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('replaces the previous announced labels of the same session', async () => {
|
|
235
|
+
const repository = new FileSystemSilentSessionCandidateStateRepository(
|
|
236
|
+
stateFilePath,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
await repository.saveAnnouncedRunningSubAgentLabels({
|
|
240
|
+
sessionName: sessionAlpha,
|
|
241
|
+
labels: ['sub-process-1'],
|
|
242
|
+
now: new Date('2026-06-26T00:00:00Z'),
|
|
243
|
+
});
|
|
244
|
+
await repository.saveAnnouncedRunningSubAgentLabels({
|
|
245
|
+
sessionName: sessionAlpha,
|
|
246
|
+
labels: ['sub-process-2'],
|
|
247
|
+
now: new Date('2026-06-26T00:01:00Z'),
|
|
248
|
+
});
|
|
249
|
+
const loaded = await repository.loadAnnouncedRunningSubAgentLabels({
|
|
250
|
+
sessionName: sessionAlpha,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(loaded).toEqual(new Set(['sub-process-2']));
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('removes the stored entry when saving an empty label set', async () => {
|
|
257
|
+
const repository = new FileSystemSilentSessionCandidateStateRepository(
|
|
258
|
+
stateFilePath,
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
await repository.saveAnnouncedRunningSubAgentLabels({
|
|
262
|
+
sessionName: sessionAlpha,
|
|
263
|
+
labels: ['sub-process-1'],
|
|
264
|
+
now: new Date('2026-06-26T00:00:00Z'),
|
|
265
|
+
});
|
|
266
|
+
await repository.saveAnnouncedRunningSubAgentLabels({
|
|
267
|
+
sessionName: sessionAlpha,
|
|
268
|
+
labels: [],
|
|
269
|
+
now: new Date('2026-06-26T00:01:00Z'),
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
expect(
|
|
273
|
+
await repository.loadAnnouncedRunningSubAgentLabels({
|
|
274
|
+
sessionName: sessionAlpha,
|
|
275
|
+
}),
|
|
276
|
+
).toEqual(new Set<string>());
|
|
277
|
+
const persisted: unknown = JSON.parse(
|
|
278
|
+
fs.readFileSync(stateFilePath, 'utf8'),
|
|
279
|
+
);
|
|
280
|
+
expect(persisted).toEqual({
|
|
281
|
+
candidates: [],
|
|
282
|
+
announcedRunningSubAgents: [],
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('keeps the announced labels of another session when saving', async () => {
|
|
287
|
+
const repository = new FileSystemSilentSessionCandidateStateRepository(
|
|
288
|
+
stateFilePath,
|
|
289
|
+
);
|
|
290
|
+
const at = new Date('2026-06-26T00:00:00Z');
|
|
291
|
+
|
|
292
|
+
await repository.saveAnnouncedRunningSubAgentLabels({
|
|
293
|
+
sessionName: sessionAlpha,
|
|
294
|
+
labels: ['sub-process-1'],
|
|
295
|
+
now: at,
|
|
296
|
+
});
|
|
297
|
+
await repository.saveAnnouncedRunningSubAgentLabels({
|
|
298
|
+
sessionName: sessionBravo,
|
|
299
|
+
labels: ['sub-process-2'],
|
|
300
|
+
now: at,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
expect(
|
|
304
|
+
await repository.loadAnnouncedRunningSubAgentLabels({
|
|
305
|
+
sessionName: sessionAlpha,
|
|
306
|
+
}),
|
|
307
|
+
).toEqual(new Set(['sub-process-1']));
|
|
308
|
+
expect(
|
|
309
|
+
await repository.loadAnnouncedRunningSubAgentLabels({
|
|
310
|
+
sessionName: sessionBravo,
|
|
311
|
+
}),
|
|
312
|
+
).toEqual(new Set(['sub-process-2']));
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('drops an announced entry that has aged beyond the retention window on the next save', async () => {
|
|
316
|
+
const repository = new FileSystemSilentSessionCandidateStateRepository(
|
|
317
|
+
stateFilePath,
|
|
318
|
+
);
|
|
319
|
+
const firstSaveAt = new Date('2026-06-26T00:00:00Z');
|
|
320
|
+
const secondSaveAt = new Date(
|
|
321
|
+
firstSaveAt.getTime() +
|
|
322
|
+
(ANNOUNCED_RUNNING_RETENTION_WINDOW_SECONDS + 1) * 1000,
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
await repository.saveAnnouncedRunningSubAgentLabels({
|
|
326
|
+
sessionName: sessionAlpha,
|
|
327
|
+
labels: ['sub-process-1'],
|
|
328
|
+
now: firstSaveAt,
|
|
329
|
+
});
|
|
330
|
+
await repository.saveAnnouncedRunningSubAgentLabels({
|
|
331
|
+
sessionName: sessionBravo,
|
|
332
|
+
labels: ['sub-process-2'],
|
|
333
|
+
now: secondSaveAt,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
expect(
|
|
337
|
+
await repository.loadAnnouncedRunningSubAgentLabels({
|
|
338
|
+
sessionName: sessionAlpha,
|
|
339
|
+
}),
|
|
340
|
+
).toEqual(new Set<string>());
|
|
341
|
+
expect(
|
|
342
|
+
await repository.loadAnnouncedRunningSubAgentLabels({
|
|
343
|
+
sessionName: sessionBravo,
|
|
344
|
+
}),
|
|
345
|
+
).toEqual(new Set(['sub-process-2']));
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('preserves the candidate set when saving announced labels and preserves announced labels when saving candidates', async () => {
|
|
349
|
+
const repository = new FileSystemSilentSessionCandidateStateRepository(
|
|
350
|
+
stateFilePath,
|
|
351
|
+
);
|
|
352
|
+
const at = new Date('2026-06-26T00:00:00Z');
|
|
353
|
+
|
|
354
|
+
await repository.saveCandidateSessionNames({
|
|
355
|
+
sessionNames: [sessionAlpha],
|
|
356
|
+
now: at,
|
|
357
|
+
});
|
|
358
|
+
await repository.saveAnnouncedRunningSubAgentLabels({
|
|
359
|
+
sessionName: sessionBravo,
|
|
360
|
+
labels: ['sub-process-1'],
|
|
361
|
+
now: at,
|
|
362
|
+
});
|
|
363
|
+
await repository.saveCandidateSessionNames({
|
|
364
|
+
sessionNames: [sessionAlpha],
|
|
365
|
+
now: new Date('2026-06-26T00:01:00Z'),
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const loadedCandidates = await repository.loadRecentCandidateSessionNames(
|
|
369
|
+
{
|
|
370
|
+
now: new Date('2026-06-26T00:02:00Z'),
|
|
371
|
+
recencyWindowSeconds: 15 * 60,
|
|
372
|
+
},
|
|
373
|
+
);
|
|
374
|
+
expect(loadedCandidates).toEqual(new Set([sessionAlpha]));
|
|
375
|
+
expect(
|
|
376
|
+
await repository.loadAnnouncedRunningSubAgentLabels({
|
|
377
|
+
sessionName: sessionBravo,
|
|
378
|
+
}),
|
|
379
|
+
).toEqual(new Set(['sub-process-1']));
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('treats a corrupt state file as no recorded announcements', async () => {
|
|
383
|
+
fs.writeFileSync(stateFilePath, 'not valid json');
|
|
384
|
+
const repository = new FileSystemSilentSessionCandidateStateRepository(
|
|
385
|
+
stateFilePath,
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const loaded = await repository.loadAnnouncedRunningSubAgentLabels({
|
|
389
|
+
sessionName: sessionAlpha,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
expect(loaded).toEqual(new Set<string>());
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('ignores a stored announced entry whose labels are malformed', async () => {
|
|
396
|
+
fs.writeFileSync(
|
|
397
|
+
stateFilePath,
|
|
398
|
+
JSON.stringify({
|
|
399
|
+
candidates: [],
|
|
400
|
+
announcedRunningSubAgents: [
|
|
401
|
+
{
|
|
402
|
+
sessionName: sessionAlpha,
|
|
403
|
+
labels: ['sub-process-1', 42],
|
|
404
|
+
recordedEpochSeconds: 1782000000,
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
}),
|
|
408
|
+
);
|
|
409
|
+
const repository = new FileSystemSilentSessionCandidateStateRepository(
|
|
410
|
+
stateFilePath,
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
const loaded = await repository.loadAnnouncedRunningSubAgentLabels({
|
|
414
|
+
sessionName: sessionAlpha,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
expect(loaded).toEqual(new Set<string>());
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('exposes the announced-label retention window as a named constant', () => {
|
|
421
|
+
expect(ANNOUNCED_RUNNING_RETENTION_WINDOW_SECONDS).toBe(24 * 60 * 60);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
201
424
|
});
|