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
|
@@ -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
|
});
|
|
@@ -8,10 +8,17 @@ type StoredCandidateEntry = {
|
|
|
8
8
|
recordedEpochSeconds: number;
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
type StoredAnnouncedRunningEntry = {
|
|
12
|
+
sessionName: string;
|
|
13
|
+
labels: string[];
|
|
14
|
+
recordedEpochSeconds: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
11
17
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
12
18
|
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13
19
|
|
|
14
20
|
export const DEFAULT_STATE_RETENTION_WINDOW_SECONDS = 60 * 60;
|
|
21
|
+
export const ANNOUNCED_RUNNING_RETENTION_WINDOW_SECONDS = 24 * 60 * 60;
|
|
15
22
|
|
|
16
23
|
const defaultStateFilePath = (): string => {
|
|
17
24
|
const base = process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), '.cache');
|
|
@@ -31,7 +38,7 @@ export class FileSystemSilentSessionCandidateStateRepository implements SilentSe
|
|
|
31
38
|
const nowEpochSeconds = Math.floor(params.now.getTime() / 1000);
|
|
32
39
|
const oldestAllowedEpochSeconds =
|
|
33
40
|
nowEpochSeconds - params.recencyWindowSeconds;
|
|
34
|
-
const entries = this.
|
|
41
|
+
const entries = this.readCandidateEntries();
|
|
35
42
|
const recentSessionNames = new Set<string>();
|
|
36
43
|
for (const entry of entries) {
|
|
37
44
|
if (entry.recordedEpochSeconds >= oldestAllowedEpochSeconds) {
|
|
@@ -50,7 +57,7 @@ export class FileSystemSilentSessionCandidateStateRepository implements SilentSe
|
|
|
50
57
|
recordedEpochSeconds - this.retentionWindowSeconds;
|
|
51
58
|
const currentSessionNames = new Set(params.sessionNames);
|
|
52
59
|
const mergedBySessionName = new Map<string, StoredCandidateEntry>();
|
|
53
|
-
for (const entry of this.
|
|
60
|
+
for (const entry of this.readCandidateEntries()) {
|
|
54
61
|
if (
|
|
55
62
|
entry.recordedEpochSeconds >= oldestRetainedEpochSeconds &&
|
|
56
63
|
!currentSessionNames.has(entry.sessionName)
|
|
@@ -64,26 +71,65 @@ export class FileSystemSilentSessionCandidateStateRepository implements SilentSe
|
|
|
64
71
|
recordedEpochSeconds,
|
|
65
72
|
});
|
|
66
73
|
}
|
|
67
|
-
this.
|
|
74
|
+
this.writeState(
|
|
75
|
+
Array.from(mergedBySessionName.values()),
|
|
76
|
+
this.readAnnouncedRunningEntries(),
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
loadAnnouncedRunningSubAgentLabels = async (params: {
|
|
81
|
+
sessionName: string;
|
|
82
|
+
}): Promise<Set<string>> => {
|
|
83
|
+
const entry = this.readAnnouncedRunningEntries().find(
|
|
84
|
+
(candidate) => candidate.sessionName === params.sessionName,
|
|
85
|
+
);
|
|
86
|
+
return new Set(entry?.labels ?? []);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
saveAnnouncedRunningSubAgentLabels = async (params: {
|
|
90
|
+
sessionName: string;
|
|
91
|
+
labels: string[];
|
|
92
|
+
now: Date;
|
|
93
|
+
}): Promise<void> => {
|
|
94
|
+
const recordedEpochSeconds = Math.floor(params.now.getTime() / 1000);
|
|
95
|
+
const oldestRetainedEpochSeconds =
|
|
96
|
+
recordedEpochSeconds - ANNOUNCED_RUNNING_RETENTION_WINDOW_SECONDS;
|
|
97
|
+
const retainedEntries = this.readAnnouncedRunningEntries().filter(
|
|
98
|
+
(entry) =>
|
|
99
|
+
entry.sessionName !== params.sessionName &&
|
|
100
|
+
entry.recordedEpochSeconds >= oldestRetainedEpochSeconds,
|
|
101
|
+
);
|
|
102
|
+
if (params.labels.length > 0) {
|
|
103
|
+
retainedEntries.push({
|
|
104
|
+
sessionName: params.sessionName,
|
|
105
|
+
labels: params.labels,
|
|
106
|
+
recordedEpochSeconds,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
this.writeState(this.readCandidateEntries(), retainedEntries);
|
|
68
110
|
};
|
|
69
111
|
|
|
70
|
-
private
|
|
112
|
+
private readState = (): Record<string, unknown> => {
|
|
71
113
|
let raw: string;
|
|
72
114
|
try {
|
|
73
115
|
raw = fs.readFileSync(this.stateFilePath, 'utf8');
|
|
74
116
|
} catch {
|
|
75
|
-
return
|
|
117
|
+
return {};
|
|
76
118
|
}
|
|
77
119
|
let parsed: unknown;
|
|
78
120
|
try {
|
|
79
121
|
parsed = JSON.parse(raw);
|
|
80
122
|
} catch {
|
|
81
|
-
return
|
|
123
|
+
return {};
|
|
82
124
|
}
|
|
83
125
|
if (!isRecord(parsed)) {
|
|
84
|
-
return
|
|
126
|
+
return {};
|
|
85
127
|
}
|
|
86
|
-
|
|
128
|
+
return parsed;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
private readCandidateEntries = (): StoredCandidateEntry[] => {
|
|
132
|
+
const storedEntries = this.readState().candidates;
|
|
87
133
|
if (!Array.isArray(storedEntries)) {
|
|
88
134
|
return [];
|
|
89
135
|
}
|
|
@@ -105,11 +151,49 @@ export class FileSystemSilentSessionCandidateStateRepository implements SilentSe
|
|
|
105
151
|
return entries;
|
|
106
152
|
};
|
|
107
153
|
|
|
108
|
-
private
|
|
154
|
+
private readAnnouncedRunningEntries = (): StoredAnnouncedRunningEntry[] => {
|
|
155
|
+
const storedEntries = this.readState().announcedRunningSubAgents;
|
|
156
|
+
if (!Array.isArray(storedEntries)) {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
const entries: StoredAnnouncedRunningEntry[] = [];
|
|
160
|
+
for (const storedEntry of storedEntries) {
|
|
161
|
+
if (!isRecord(storedEntry)) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const sessionName = storedEntry.sessionName;
|
|
165
|
+
const recordedEpochSeconds = storedEntry.recordedEpochSeconds;
|
|
166
|
+
const storedLabels = storedEntry.labels;
|
|
167
|
+
if (
|
|
168
|
+
typeof sessionName !== 'string' ||
|
|
169
|
+
typeof recordedEpochSeconds !== 'number' ||
|
|
170
|
+
!Number.isFinite(recordedEpochSeconds) ||
|
|
171
|
+
!Array.isArray(storedLabels)
|
|
172
|
+
) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const labels = storedLabels.filter(
|
|
176
|
+
(label): label is string => typeof label === 'string',
|
|
177
|
+
);
|
|
178
|
+
if (labels.length !== storedLabels.length) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
entries.push({ sessionName, labels, recordedEpochSeconds });
|
|
182
|
+
}
|
|
183
|
+
return entries;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
private writeState = (
|
|
187
|
+
candidates: StoredCandidateEntry[],
|
|
188
|
+
announcedRunningSubAgents: StoredAnnouncedRunningEntry[],
|
|
189
|
+
): void => {
|
|
109
190
|
const directory = path.dirname(this.stateFilePath);
|
|
110
191
|
fs.mkdirSync(directory, { recursive: true });
|
|
111
192
|
const temporaryPath = `${this.stateFilePath}.${process.pid}.tmp`;
|
|
112
|
-
fs.writeFileSync(
|
|
193
|
+
fs.writeFileSync(
|
|
194
|
+
temporaryPath,
|
|
195
|
+
JSON.stringify({ candidates, announcedRunningSubAgents }),
|
|
196
|
+
);
|
|
113
197
|
fs.renameSync(temporaryPath, this.stateFilePath);
|
|
114
198
|
};
|
|
115
199
|
}
|
|
@@ -54,7 +54,12 @@ describe('ProcessListSessionSubAgentActivityRepository', () => {
|
|
|
54
54
|
);
|
|
55
55
|
const result = await repository.listSubAgentActivitiesBySessionName(['s1']);
|
|
56
56
|
expect(result.get('s1')).toEqual([
|
|
57
|
-
{
|
|
57
|
+
{
|
|
58
|
+
label: 'task-a',
|
|
59
|
+
silentSeconds: 180,
|
|
60
|
+
runningSeconds: 1200,
|
|
61
|
+
waitingOnExternalProcess: false,
|
|
62
|
+
},
|
|
58
63
|
]);
|
|
59
64
|
});
|
|
60
65
|
|
|
@@ -78,7 +83,12 @@ describe('ProcessListSessionSubAgentActivityRepository', () => {
|
|
|
78
83
|
);
|
|
79
84
|
const result = await repository.listSubAgentActivitiesBySessionName(['s1']);
|
|
80
85
|
expect(result.get('s1')).toEqual([
|
|
81
|
-
{
|
|
86
|
+
{
|
|
87
|
+
label: 's1',
|
|
88
|
+
silentSeconds: 0,
|
|
89
|
+
runningSeconds: 30,
|
|
90
|
+
waitingOnExternalProcess: false,
|
|
91
|
+
},
|
|
82
92
|
]);
|
|
83
93
|
});
|
|
84
94
|
|
|
@@ -43,6 +43,7 @@ export class ProcessListSessionSubAgentActivityRepository implements SessionSubA
|
|
|
43
43
|
label,
|
|
44
44
|
silentSeconds: this.silentSecondsResolver.resolveSilentSeconds(label),
|
|
45
45
|
runningSeconds: process.elapsedSeconds,
|
|
46
|
+
waitingOnExternalProcess: false,
|
|
46
47
|
};
|
|
47
48
|
const existing = result.get(sessionName);
|
|
48
49
|
if (existing === undefined) {
|