github-issue-tower-defence-management 1.117.9 → 1.118.0
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 +14 -0
- package/README.md +7 -5
- package/bin/adapter/entry-points/console/ui-dist/assets/{index-DID_sTas.js → index-DWiG7btO.js} +24 -24
- package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +26 -12
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js +44 -11
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js.map +1 -1
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js +27 -7
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js.map +1 -1
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +4 -1
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +39 -26
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +28 -2
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.stories.tsx +2 -2
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +1 -7
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +55 -26
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +1 -8
- package/src/adapter/entry-points/console/ui-dist/assets/{index-DID_sTas.js → index-DWiG7btO.js} +24 -24
- package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +18 -8
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +8 -4
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.test.ts +81 -28
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts +91 -19
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.test.ts +91 -15
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.ts +54 -11
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +34 -0
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +6 -1
- package/src/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.ts +9 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts +8 -4
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts.map +1 -1
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts +2 -2
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts.map +1 -1
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts +5 -1
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts.map +1 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>TDPM Console</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-DWiG7btO.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-BMe1bEF4.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
@@ -121,8 +121,10 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
121
121
|
candidateDebounceStateFilePath?: string;
|
|
122
122
|
activeHubTaskStatus?: string;
|
|
123
123
|
silentMainStalledMessage?: string;
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
silentSubAgentIdleMessageHeader?: string;
|
|
125
|
+
silentSubAgentIdleMessageFooter?: string;
|
|
126
|
+
silentSubAgentLongRunningMessageHeader?: string;
|
|
127
|
+
silentSubAgentLongRunningMessageFooter?: string;
|
|
126
128
|
credentials: {
|
|
127
129
|
manager: {
|
|
128
130
|
github: {
|
|
@@ -629,13 +631,21 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
629
631
|
mergedInput.silentMainStalledMessage ??
|
|
630
632
|
process.env.TDPM_SILENT_MAIN_STALLED_MESSAGE ??
|
|
631
633
|
null,
|
|
632
|
-
|
|
633
|
-
mergedInput.
|
|
634
|
-
process.env.
|
|
634
|
+
subAgentIdleMessageHeader:
|
|
635
|
+
mergedInput.silentSubAgentIdleMessageHeader ??
|
|
636
|
+
process.env.TDPM_SILENT_SUBAGENT_IDLE_MESSAGE_HEADER ??
|
|
635
637
|
null,
|
|
636
|
-
|
|
637
|
-
mergedInput.
|
|
638
|
-
process.env.
|
|
638
|
+
subAgentIdleMessageFooter:
|
|
639
|
+
mergedInput.silentSubAgentIdleMessageFooter ??
|
|
640
|
+
process.env.TDPM_SILENT_SUBAGENT_IDLE_MESSAGE_FOOTER ??
|
|
641
|
+
null,
|
|
642
|
+
subAgentLongRunningMessageHeader:
|
|
643
|
+
mergedInput.silentSubAgentLongRunningMessageHeader ??
|
|
644
|
+
process.env.TDPM_SILENT_SUBAGENT_LONG_RUNNING_MESSAGE_HEADER ??
|
|
645
|
+
null,
|
|
646
|
+
subAgentLongRunningMessageFooter:
|
|
647
|
+
mergedInput.silentSubAgentLongRunningMessageFooter ??
|
|
648
|
+
process.env.TDPM_SILENT_SUBAGENT_LONG_RUNNING_MESSAGE_FOOTER ??
|
|
639
649
|
null,
|
|
640
650
|
},
|
|
641
651
|
now: inTmuxNow,
|
|
@@ -20,8 +20,10 @@ const CLAUDE_PID = 201;
|
|
|
20
20
|
|
|
21
21
|
const EMPTY_TEMPLATES: SilentSessionMessageTemplates = {
|
|
22
22
|
mainStalledMessage: null,
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
subAgentIdleMessageHeader: null,
|
|
24
|
+
subAgentIdleMessageFooter: null,
|
|
25
|
+
subAgentLongRunningMessageHeader: null,
|
|
26
|
+
subAgentLongRunningMessageFooter: null,
|
|
25
27
|
};
|
|
26
28
|
|
|
27
29
|
type Mocked<T> = jest.Mocked<T> & jest.MockedObject<T>;
|
|
@@ -214,8 +216,10 @@ describe('notifySilentTmuxSessions', () => {
|
|
|
214
216
|
...baseParams(runner),
|
|
215
217
|
messageTemplates: {
|
|
216
218
|
mainStalledMessage: 'CUSTOM_MAIN_TEMPLATE',
|
|
217
|
-
|
|
218
|
-
|
|
219
|
+
subAgentIdleMessageHeader: null,
|
|
220
|
+
subAgentIdleMessageFooter: null,
|
|
221
|
+
subAgentLongRunningMessageHeader: null,
|
|
222
|
+
subAgentLongRunningMessageFooter: null,
|
|
219
223
|
},
|
|
220
224
|
});
|
|
221
225
|
|
|
@@ -4,20 +4,29 @@ 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
|
+
|
|
7
12
|
const createFallback = (): Mocked<SilentSessionMessageComposer> => ({
|
|
8
13
|
composeMainStalledSection: jest.fn().mockReturnValue('FALLBACK_MAIN'),
|
|
9
14
|
composeSubAgentSection: jest.fn().mockReturnValue('FALLBACK_SUB'),
|
|
10
15
|
});
|
|
11
16
|
|
|
17
|
+
const noTemplates = {
|
|
18
|
+
mainStalledMessage: null,
|
|
19
|
+
subAgentIdleMessageHeader: null,
|
|
20
|
+
subAgentIdleMessageFooter: null,
|
|
21
|
+
subAgentLongRunningMessageHeader: null,
|
|
22
|
+
subAgentLongRunningMessageFooter: null,
|
|
23
|
+
};
|
|
24
|
+
|
|
12
25
|
describe('ConfigurableSilentSessionMessageComposer', () => {
|
|
13
26
|
it('uses the fallback main section when no main template is configured', () => {
|
|
14
27
|
const fallback = createFallback();
|
|
15
28
|
const composer = new ConfigurableSilentSessionMessageComposer(
|
|
16
|
-
|
|
17
|
-
mainStalledMessage: null,
|
|
18
|
-
subAgentMessageHeader: null,
|
|
19
|
-
subAgentMessageFooter: null,
|
|
20
|
-
},
|
|
29
|
+
noTemplates,
|
|
21
30
|
fallback,
|
|
22
31
|
);
|
|
23
32
|
expect(composer.composeMainStalledSection(600)).toBe('FALLBACK_MAIN');
|
|
@@ -28,9 +37,8 @@ describe('ConfigurableSilentSessionMessageComposer', () => {
|
|
|
28
37
|
const fallback = createFallback();
|
|
29
38
|
const composer = new ConfigurableSilentSessionMessageComposer(
|
|
30
39
|
{
|
|
40
|
+
...noTemplates,
|
|
31
41
|
mainStalledMessage: 'CUSTOM_MAIN',
|
|
32
|
-
subAgentMessageHeader: null,
|
|
33
|
-
subAgentMessageFooter: null,
|
|
34
42
|
},
|
|
35
43
|
fallback,
|
|
36
44
|
);
|
|
@@ -40,52 +48,97 @@ describe('ConfigurableSilentSessionMessageComposer', () => {
|
|
|
40
48
|
expect(fallback.composeMainStalledSection).not.toHaveBeenCalled();
|
|
41
49
|
});
|
|
42
50
|
|
|
43
|
-
it('uses the fallback sub-agent section when
|
|
51
|
+
it('uses the fallback sub-agent section when no sub-agent template is configured', () => {
|
|
44
52
|
const fallback = createFallback();
|
|
45
53
|
const composer = new ConfigurableSilentSessionMessageComposer(
|
|
46
|
-
|
|
47
|
-
mainStalledMessage: null,
|
|
48
|
-
subAgentMessageHeader: null,
|
|
49
|
-
subAgentMessageFooter: null,
|
|
50
|
-
},
|
|
54
|
+
noTemplates,
|
|
51
55
|
fallback,
|
|
52
56
|
);
|
|
53
57
|
expect(
|
|
54
|
-
composer.composeSubAgentSection(
|
|
55
|
-
{ label: 'task-a', silentSeconds: 360, runningSeconds: 1200 },
|
|
56
|
-
|
|
58
|
+
composer.composeSubAgentSection(
|
|
59
|
+
[{ label: 'task-a', silentSeconds: 360, runningSeconds: 1200 }],
|
|
60
|
+
THRESHOLDS,
|
|
61
|
+
),
|
|
57
62
|
).toBe('FALLBACK_SUB');
|
|
63
|
+
expect(fallback.composeSubAgentSection).toHaveBeenCalledWith(
|
|
64
|
+
[{ label: 'task-a', silentSeconds: 360, runningSeconds: 1200 }],
|
|
65
|
+
THRESHOLDS,
|
|
66
|
+
);
|
|
58
67
|
});
|
|
59
68
|
|
|
60
|
-
it('renders the configured
|
|
69
|
+
it('renders the configured idle header, list, and footer for an idle sub-agent', () => {
|
|
61
70
|
const fallback = createFallback();
|
|
62
71
|
const composer = new ConfigurableSilentSessionMessageComposer(
|
|
63
72
|
{
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
...noTemplates,
|
|
74
|
+
subAgentIdleMessageHeader: 'IDLE_HEADER',
|
|
75
|
+
subAgentIdleMessageFooter: 'IDLE_FOOTER',
|
|
67
76
|
},
|
|
68
77
|
fallback,
|
|
69
78
|
);
|
|
70
|
-
const section = composer.composeSubAgentSection(
|
|
71
|
-
{ label: 'task-a', silentSeconds: 360, runningSeconds:
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
const section = composer.composeSubAgentSection(
|
|
80
|
+
[{ label: 'task-a', silentSeconds: 360, runningSeconds: 60 }],
|
|
81
|
+
THRESHOLDS,
|
|
82
|
+
);
|
|
83
|
+
expect(section).toContain('IDLE_HEADER');
|
|
74
84
|
expect(section).toContain('task-a');
|
|
75
|
-
expect(section).toContain('
|
|
85
|
+
expect(section).toContain('no output for 6m');
|
|
86
|
+
expect(section).toContain('IDLE_FOOTER');
|
|
87
|
+
expect(section).toContain(SILENT_SESSION_REMINDER_SENTINEL);
|
|
88
|
+
expect(fallback.composeSubAgentSection).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('renders the configured long-running header, list, and footer for a long-running sub-agent', () => {
|
|
92
|
+
const fallback = createFallback();
|
|
93
|
+
const composer = new ConfigurableSilentSessionMessageComposer(
|
|
94
|
+
{
|
|
95
|
+
...noTemplates,
|
|
96
|
+
subAgentLongRunningMessageHeader: 'LONG_HEADER',
|
|
97
|
+
subAgentLongRunningMessageFooter: 'LONG_FOOTER',
|
|
98
|
+
},
|
|
99
|
+
fallback,
|
|
100
|
+
);
|
|
101
|
+
const section = composer.composeSubAgentSection(
|
|
102
|
+
[{ label: 'task-b', silentSeconds: 30, runningSeconds: 1200 }],
|
|
103
|
+
THRESHOLDS,
|
|
104
|
+
);
|
|
105
|
+
expect(section).toContain('LONG_HEADER');
|
|
106
|
+
expect(section).toContain('task-b');
|
|
76
107
|
expect(section).toContain('running for 20m');
|
|
77
|
-
expect(section).toContain('
|
|
108
|
+
expect(section).toContain('LONG_FOOTER');
|
|
78
109
|
expect(section).toContain(SILENT_SESSION_REMINDER_SENTINEL);
|
|
79
110
|
expect(fallback.composeSubAgentSection).not.toHaveBeenCalled();
|
|
80
111
|
});
|
|
81
112
|
|
|
113
|
+
it('emits both distinct configured sections for a sub-agent matching both conditions', () => {
|
|
114
|
+
const fallback = createFallback();
|
|
115
|
+
const composer = new ConfigurableSilentSessionMessageComposer(
|
|
116
|
+
{
|
|
117
|
+
...noTemplates,
|
|
118
|
+
subAgentIdleMessageHeader: 'IDLE_HEADER',
|
|
119
|
+
subAgentLongRunningMessageHeader: 'LONG_HEADER',
|
|
120
|
+
},
|
|
121
|
+
fallback,
|
|
122
|
+
);
|
|
123
|
+
const section = composer.composeSubAgentSection(
|
|
124
|
+
[{ label: 'task-both', silentSeconds: 360, runningSeconds: 1200 }],
|
|
125
|
+
THRESHOLDS,
|
|
126
|
+
);
|
|
127
|
+
const [idleSection, longRunningSection] = section
|
|
128
|
+
.replace(`${SILENT_SESSION_REMINDER_SENTINEL} `, '')
|
|
129
|
+
.split('\n\n');
|
|
130
|
+
expect(idleSection).toContain('IDLE_HEADER');
|
|
131
|
+
expect(idleSection).toContain('no output for 6m');
|
|
132
|
+
expect(longRunningSection).toContain('LONG_HEADER');
|
|
133
|
+
expect(longRunningSection).toContain('running for 20m');
|
|
134
|
+
});
|
|
135
|
+
|
|
82
136
|
it('does not double-prepend the sentinel when the template already carries it', () => {
|
|
83
137
|
const fallback = createFallback();
|
|
84
138
|
const composer = new ConfigurableSilentSessionMessageComposer(
|
|
85
139
|
{
|
|
140
|
+
...noTemplates,
|
|
86
141
|
mainStalledMessage: `${SILENT_SESSION_REMINDER_SENTINEL} CUSTOM_MAIN`,
|
|
87
|
-
subAgentMessageHeader: null,
|
|
88
|
-
subAgentMessageFooter: null,
|
|
89
142
|
},
|
|
90
143
|
fallback,
|
|
91
144
|
);
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { SubAgentActivity } from '../../domain/entities/LiveSessionActivitySnapshot';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
SilentSessionMessageComposer,
|
|
4
|
+
SubAgentStallThresholds,
|
|
5
|
+
} from '../../domain/usecases/adapter-interfaces/SilentSessionMessageComposer';
|
|
3
6
|
import { SILENT_SESSION_REMINDER_SENTINEL } from '../../domain/usecases/silentSessionReminderSentinel';
|
|
4
7
|
|
|
5
8
|
const withReminderSentinel = (message: string): string =>
|
|
@@ -9,8 +12,10 @@ const withReminderSentinel = (message: string): string =>
|
|
|
9
12
|
|
|
10
13
|
export type SilentSessionMessageTemplates = {
|
|
11
14
|
mainStalledMessage: string | null;
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
subAgentIdleMessageHeader: string | null;
|
|
16
|
+
subAgentIdleMessageFooter: string | null;
|
|
17
|
+
subAgentLongRunningMessageHeader: string | null;
|
|
18
|
+
subAgentLongRunningMessageFooter: string | null;
|
|
14
19
|
};
|
|
15
20
|
|
|
16
21
|
const formatMinutes = (seconds: number): string => {
|
|
@@ -31,27 +36,94 @@ export class ConfigurableSilentSessionMessageComposer implements SilentSessionMe
|
|
|
31
36
|
return withReminderSentinel(this.templates.mainStalledMessage);
|
|
32
37
|
};
|
|
33
38
|
|
|
34
|
-
composeSubAgentSection = (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
composeSubAgentSection = (
|
|
40
|
+
subAgents: SubAgentActivity[],
|
|
41
|
+
thresholds: SubAgentStallThresholds,
|
|
42
|
+
): string => {
|
|
43
|
+
const hasIdleTemplate =
|
|
44
|
+
this.templates.subAgentIdleMessageHeader !== null ||
|
|
45
|
+
this.templates.subAgentIdleMessageFooter !== null;
|
|
46
|
+
const hasLongRunningTemplate =
|
|
47
|
+
this.templates.subAgentLongRunningMessageHeader !== null ||
|
|
48
|
+
this.templates.subAgentLongRunningMessageFooter !== null;
|
|
49
|
+
if (!hasIdleTemplate && !hasLongRunningTemplate) {
|
|
50
|
+
return this.fallback.composeSubAgentSection(subAgents, thresholds);
|
|
40
51
|
}
|
|
41
|
-
|
|
52
|
+
|
|
53
|
+
const idleSubAgents = subAgents.filter(
|
|
54
|
+
(subAgent) =>
|
|
55
|
+
subAgent.silentSeconds >= thresholds.subAgentSilentThresholdSeconds,
|
|
56
|
+
);
|
|
57
|
+
const longRunningSubAgents = subAgents.filter(
|
|
42
58
|
(subAgent) =>
|
|
43
|
-
|
|
44
|
-
subAgent.silentSeconds,
|
|
45
|
-
)}, running for ${formatMinutes(subAgent.runningSeconds)}`,
|
|
59
|
+
subAgent.runningSeconds >= thresholds.subAgentRunningThresholdSeconds,
|
|
46
60
|
);
|
|
61
|
+
|
|
47
62
|
const sections: string[] = [];
|
|
48
|
-
if (
|
|
49
|
-
sections.push(
|
|
63
|
+
if (idleSubAgents.length > 0 && hasIdleTemplate) {
|
|
64
|
+
sections.push(
|
|
65
|
+
this.composeIdleSection(
|
|
66
|
+
idleSubAgents,
|
|
67
|
+
this.templates.subAgentIdleMessageHeader,
|
|
68
|
+
this.templates.subAgentIdleMessageFooter,
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
} else if (idleSubAgents.length > 0) {
|
|
72
|
+
sections.push(this.composeIdleSection(idleSubAgents, null, null));
|
|
73
|
+
}
|
|
74
|
+
if (longRunningSubAgents.length > 0 && hasLongRunningTemplate) {
|
|
75
|
+
sections.push(
|
|
76
|
+
this.composeLongRunningSection(
|
|
77
|
+
longRunningSubAgents,
|
|
78
|
+
this.templates.subAgentLongRunningMessageHeader,
|
|
79
|
+
this.templates.subAgentLongRunningMessageFooter,
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
} else if (longRunningSubAgents.length > 0) {
|
|
83
|
+
sections.push(
|
|
84
|
+
this.composeLongRunningSection(longRunningSubAgents, null, null),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
return withReminderSentinel(sections.join('\n\n'));
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
private composeIdleSection = (
|
|
91
|
+
idleSubAgents: SubAgentActivity[],
|
|
92
|
+
header: string | null,
|
|
93
|
+
footer: string | null,
|
|
94
|
+
): string => {
|
|
95
|
+
const lines = idleSubAgents.map(
|
|
96
|
+
(subAgent) =>
|
|
97
|
+
`- ${subAgent.label}: no output for ${formatMinutes(subAgent.silentSeconds)}`,
|
|
98
|
+
);
|
|
99
|
+
const parts: string[] = [];
|
|
100
|
+
if (header !== null) {
|
|
101
|
+
parts.push(header);
|
|
102
|
+
}
|
|
103
|
+
parts.push(...lines);
|
|
104
|
+
if (footer !== null) {
|
|
105
|
+
parts.push(footer);
|
|
106
|
+
}
|
|
107
|
+
return parts.join('\n');
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
private composeLongRunningSection = (
|
|
111
|
+
longRunningSubAgents: SubAgentActivity[],
|
|
112
|
+
header: string | null,
|
|
113
|
+
footer: string | null,
|
|
114
|
+
): string => {
|
|
115
|
+
const lines = longRunningSubAgents.map(
|
|
116
|
+
(subAgent) =>
|
|
117
|
+
`- ${subAgent.label}: running for ${formatMinutes(subAgent.runningSeconds)}`,
|
|
118
|
+
);
|
|
119
|
+
const parts: string[] = [];
|
|
120
|
+
if (header !== null) {
|
|
121
|
+
parts.push(header);
|
|
50
122
|
}
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
123
|
+
parts.push(...lines);
|
|
124
|
+
if (footer !== null) {
|
|
125
|
+
parts.push(footer);
|
|
54
126
|
}
|
|
55
|
-
return
|
|
127
|
+
return parts.join('\n');
|
|
56
128
|
};
|
|
57
129
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
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
|
+
|
|
4
9
|
describe('DefaultSilentSessionMessageComposer', () => {
|
|
5
10
|
const composer = new DefaultSilentSessionMessageComposer();
|
|
6
11
|
|
|
@@ -10,9 +15,10 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
10
15
|
});
|
|
11
16
|
|
|
12
17
|
it('embeds the reminder sentinel in the sub-agent section', () => {
|
|
13
|
-
const section = composer.composeSubAgentSection(
|
|
14
|
-
{ label: 'sub-process-1', silentSeconds: 360, runningSeconds: 1200 },
|
|
15
|
-
|
|
18
|
+
const section = composer.composeSubAgentSection(
|
|
19
|
+
[{ label: 'sub-process-1', silentSeconds: 360, runningSeconds: 1200 }],
|
|
20
|
+
THRESHOLDS,
|
|
21
|
+
);
|
|
16
22
|
expect(section).toContain(SILENT_SESSION_REMINDER_SENTINEL);
|
|
17
23
|
});
|
|
18
24
|
|
|
@@ -99,23 +105,93 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
99
105
|
expect(section).not.toContain('""');
|
|
100
106
|
});
|
|
101
107
|
|
|
102
|
-
it('
|
|
103
|
-
const section = composer.composeSubAgentSection(
|
|
104
|
-
{ label: 'sub-process-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
expect(section).toContain('sub-process-
|
|
108
|
-
expect(section).toContain('
|
|
108
|
+
it('emits a distinct idle message for a sub-agent that is only output-idle', () => {
|
|
109
|
+
const section = composer.composeSubAgentSection(
|
|
110
|
+
[{ label: 'sub-process-idle', silentSeconds: 360, runningSeconds: 60 }],
|
|
111
|
+
THRESHOLDS,
|
|
112
|
+
);
|
|
113
|
+
expect(section).toContain('sub-process-idle');
|
|
114
|
+
expect(section).toContain('no output for 6m');
|
|
115
|
+
expect(section).toContain('may be stalled');
|
|
116
|
+
expect(section).toContain('restart, hand off, or replace it');
|
|
117
|
+
expect(section).toContain('waiting on an external dependency');
|
|
118
|
+
expect(section).not.toContain('infinite loop');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('emits a distinct long-running message for a sub-agent that has only run too long', () => {
|
|
122
|
+
const section = composer.composeSubAgentSection(
|
|
123
|
+
[
|
|
124
|
+
{
|
|
125
|
+
label: 'sub-process-long',
|
|
126
|
+
silentSeconds: 30,
|
|
127
|
+
runningSeconds: 1200,
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
THRESHOLDS,
|
|
131
|
+
);
|
|
132
|
+
expect(section).toContain('sub-process-long');
|
|
109
133
|
expect(section).toContain('running for 20m');
|
|
110
|
-
expect(section).toContain('
|
|
111
|
-
expect(section).toContain('
|
|
134
|
+
expect(section).toContain('infinite loop');
|
|
135
|
+
expect(section).toContain('too large');
|
|
136
|
+
expect(section).toContain('not making forward progress');
|
|
137
|
+
expect(section).toContain(
|
|
138
|
+
'do not dismiss it merely because it produced output recently',
|
|
139
|
+
);
|
|
140
|
+
expect(section).toContain(
|
|
141
|
+
'break the task down, restart, hand off, or replace',
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('does not foreground the short idle time in the long-running message', () => {
|
|
146
|
+
const section = composer.composeSubAgentSection(
|
|
147
|
+
[
|
|
148
|
+
{
|
|
149
|
+
label: 'sub-process-long',
|
|
150
|
+
silentSeconds: 30,
|
|
151
|
+
runningSeconds: 1200,
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
THRESHOLDS,
|
|
155
|
+
);
|
|
156
|
+
expect(section).not.toContain('no output for');
|
|
157
|
+
expect(section).not.toContain('silent for');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('emits both distinct messages for a sub-agent matching both conditions, kept separate', () => {
|
|
161
|
+
const section = composer.composeSubAgentSection(
|
|
162
|
+
[{ label: 'sub-process-both', silentSeconds: 360, runningSeconds: 1200 }],
|
|
163
|
+
THRESHOLDS,
|
|
164
|
+
);
|
|
165
|
+
expect(section).toContain('no output for 6m');
|
|
166
|
+
expect(section).toContain('may be stalled');
|
|
167
|
+
expect(section).toContain('running for 20m');
|
|
168
|
+
expect(section).toContain('infinite loop');
|
|
169
|
+
const sentinelOccurrences =
|
|
170
|
+
section.split(SILENT_SESSION_REMINDER_SENTINEL).length - 1;
|
|
171
|
+
expect(sentinelOccurrences).toBe(2);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('groups each sub-agent under the condition it matched', () => {
|
|
175
|
+
const section = composer.composeSubAgentSection(
|
|
176
|
+
[
|
|
177
|
+
{ label: 'idle-only', silentSeconds: 360, runningSeconds: 60 },
|
|
178
|
+
{ label: 'long-only', silentSeconds: 10, runningSeconds: 960 },
|
|
179
|
+
],
|
|
180
|
+
THRESHOLDS,
|
|
181
|
+
);
|
|
182
|
+
const [idleSection, longRunningSection] = section.split('\n\n');
|
|
183
|
+
expect(idleSection).toContain('idle-only');
|
|
184
|
+
expect(idleSection).not.toContain('long-only');
|
|
185
|
+
expect(longRunningSection).toContain('long-only');
|
|
186
|
+
expect(longRunningSection).not.toContain('idle-only');
|
|
112
187
|
});
|
|
113
188
|
|
|
114
189
|
it('does not contain any host-specific or internal identifiers', () => {
|
|
115
190
|
const mainSection = composer.composeMainStalledSection(600);
|
|
116
|
-
const subSection = composer.composeSubAgentSection(
|
|
117
|
-
{ label: 'sub-process-1', silentSeconds: 360, runningSeconds: 1200 },
|
|
118
|
-
|
|
191
|
+
const subSection = composer.composeSubAgentSection(
|
|
192
|
+
[{ label: 'sub-process-1', silentSeconds: 360, runningSeconds: 1200 }],
|
|
193
|
+
THRESHOLDS,
|
|
194
|
+
);
|
|
119
195
|
const combined = `${mainSection}\n${subSection}`.toLowerCase();
|
|
120
196
|
expect(combined).not.toContain('claude');
|
|
121
197
|
expect(combined).not.toContain('take ownership');
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { SubAgentActivity } from '../entities/LiveSessionActivitySnapshot';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
SilentSessionMessageComposer,
|
|
4
|
+
SubAgentStallThresholds,
|
|
5
|
+
} from './adapter-interfaces/SilentSessionMessageComposer';
|
|
3
6
|
import { SILENT_SESSION_REMINDER_SENTINEL } from './silentSessionReminderSentinel';
|
|
4
7
|
|
|
5
8
|
const formatMinutes = (seconds: number): string => {
|
|
@@ -7,6 +10,38 @@ const formatMinutes = (seconds: number): string => {
|
|
|
7
10
|
return `${minutes}m`;
|
|
8
11
|
};
|
|
9
12
|
|
|
13
|
+
const composeIdleSubAgentSection = (
|
|
14
|
+
idleSubAgents: SubAgentActivity[],
|
|
15
|
+
): string => {
|
|
16
|
+
const lines = idleSubAgents.map(
|
|
17
|
+
(subAgent) =>
|
|
18
|
+
`- ${subAgent.label}: no output for ${formatMinutes(
|
|
19
|
+
subAgent.silentSeconds,
|
|
20
|
+
)}`,
|
|
21
|
+
);
|
|
22
|
+
return [
|
|
23
|
+
`${SILENT_SESSION_REMINDER_SENTINEL} The following sub-process(es) have produced no output for several minutes and may be stalled:`,
|
|
24
|
+
...lines,
|
|
25
|
+
'Check each one. If it is stuck, take action (restart, hand off, or replace it). If it is legitimately waiting on an external dependency (continuous integration, an external API, or another process), let it continue.',
|
|
26
|
+
].join('\n');
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const composeLongRunningSubAgentSection = (
|
|
30
|
+
longRunningSubAgents: SubAgentActivity[],
|
|
31
|
+
): string => {
|
|
32
|
+
const lines = longRunningSubAgents.map(
|
|
33
|
+
(subAgent) =>
|
|
34
|
+
`- ${subAgent.label}: running for ${formatMinutes(
|
|
35
|
+
subAgent.runningSeconds,
|
|
36
|
+
)}`,
|
|
37
|
+
);
|
|
38
|
+
return [
|
|
39
|
+
`${SILENT_SESSION_REMINDER_SENTINEL} The following sub-process(es) have been running longer than a task should normally take, which may mean an infinite loop, a task that is too large, or being stuck in an incorrect approach that is not making forward progress:`,
|
|
40
|
+
...lines,
|
|
41
|
+
'Verify each one is genuinely advancing toward completion; do not dismiss it merely because it produced output recently. If it is not progressing, intervene: break the task down, restart, hand off, or replace it.',
|
|
42
|
+
].join('\n');
|
|
43
|
+
};
|
|
44
|
+
|
|
10
45
|
const composeOwnerCallFormatGuidance = (
|
|
11
46
|
ownerCallMarker: string | null,
|
|
12
47
|
): string => {
|
|
@@ -39,17 +74,25 @@ export class DefaultSilentSessionMessageComposer implements SilentSessionMessage
|
|
|
39
74
|
return composeMainStalledMessage(mainSilentSeconds, this.ownerCallMarker);
|
|
40
75
|
};
|
|
41
76
|
|
|
42
|
-
composeSubAgentSection = (
|
|
43
|
-
|
|
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(
|
|
44
86
|
(subAgent) =>
|
|
45
|
-
|
|
46
|
-
subAgent.silentSeconds,
|
|
47
|
-
)}, running for ${formatMinutes(subAgent.runningSeconds)}`,
|
|
87
|
+
subAgent.runningSeconds >= thresholds.subAgentRunningThresholdSeconds,
|
|
48
88
|
);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
89
|
+
const sections: string[] = [];
|
|
90
|
+
if (idleSubAgents.length > 0) {
|
|
91
|
+
sections.push(composeIdleSubAgentSection(idleSubAgents));
|
|
92
|
+
}
|
|
93
|
+
if (longRunningSubAgents.length > 0) {
|
|
94
|
+
sections.push(composeLongRunningSubAgentSection(longRunningSubAgents));
|
|
95
|
+
}
|
|
96
|
+
return sections.join('\n\n');
|
|
54
97
|
};
|
|
55
98
|
}
|
|
@@ -446,12 +446,46 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
446
446
|
|
|
447
447
|
expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalledWith(
|
|
448
448
|
subAgents,
|
|
449
|
+
{
|
|
450
|
+
subAgentSilentThresholdSeconds:
|
|
451
|
+
DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
452
|
+
subAgentRunningThresholdSeconds:
|
|
453
|
+
DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
|
|
454
|
+
},
|
|
449
455
|
);
|
|
450
456
|
expect(
|
|
451
457
|
mockNotificationRepository.sendSelfCheckNotification,
|
|
452
458
|
).toHaveBeenCalledWith(GITHUB_SESSION, SUBAGENT_SECTION);
|
|
453
459
|
});
|
|
454
460
|
|
|
461
|
+
it('passes a configured running threshold through to the sub-agent section composer', async () => {
|
|
462
|
+
setupLiveInteractiveSession(GITHUB_SESSION);
|
|
463
|
+
const subAgents: SubAgentActivity[] = [
|
|
464
|
+
{
|
|
465
|
+
label: 'sub-process-1',
|
|
466
|
+
silentSeconds: 10,
|
|
467
|
+
runningSeconds: 600,
|
|
468
|
+
},
|
|
469
|
+
];
|
|
470
|
+
mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName.mockResolvedValue(
|
|
471
|
+
new Map([[GITHUB_SESSION, subAgents]]),
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
await useCase.run({
|
|
475
|
+
...runParams(),
|
|
476
|
+
subAgentRunningThresholdSeconds: 600,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalledWith(
|
|
480
|
+
subAgents,
|
|
481
|
+
{
|
|
482
|
+
subAgentSilentThresholdSeconds:
|
|
483
|
+
DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
484
|
+
subAgentRunningThresholdSeconds: 600,
|
|
485
|
+
},
|
|
486
|
+
);
|
|
487
|
+
});
|
|
488
|
+
|
|
455
489
|
it('excludes an owner-handover spawn from selection so no notification is sent', async () => {
|
|
456
490
|
mockSnapshotProvider.getSnapshot.mockResolvedValue({
|
|
457
491
|
sessions: [{ sessionName: 'aw-host', panePids: [100] }],
|
|
@@ -277,7 +277,12 @@ export class NotifySilentLiveSessionsUseCase {
|
|
|
277
277
|
);
|
|
278
278
|
if (stalledSubAgents.length > 0) {
|
|
279
279
|
sections.push(
|
|
280
|
-
this.messageComposer.composeSubAgentSection(stalledSubAgents
|
|
280
|
+
this.messageComposer.composeSubAgentSection(stalledSubAgents, {
|
|
281
|
+
subAgentSilentThresholdSeconds:
|
|
282
|
+
thresholds.subAgentSilentThresholdSeconds,
|
|
283
|
+
subAgentRunningThresholdSeconds:
|
|
284
|
+
thresholds.subAgentRunningThresholdSeconds,
|
|
285
|
+
}),
|
|
281
286
|
);
|
|
282
287
|
}
|
|
283
288
|
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { SubAgentActivity } from '../../entities/LiveSessionActivitySnapshot';
|
|
2
2
|
|
|
3
|
+
export type SubAgentStallThresholds = {
|
|
4
|
+
subAgentSilentThresholdSeconds: number;
|
|
5
|
+
subAgentRunningThresholdSeconds: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
3
8
|
export interface SilentSessionMessageComposer {
|
|
4
9
|
composeMainStalledSection: (mainSilentSeconds: number) => string;
|
|
5
|
-
composeSubAgentSection: (
|
|
10
|
+
composeSubAgentSection: (
|
|
11
|
+
subAgents: SubAgentActivity[],
|
|
12
|
+
thresholds: SubAgentStallThresholds,
|
|
13
|
+
) => string;
|
|
6
14
|
}
|