github-issue-tower-defence-management 1.112.1 → 1.112.3
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 +5 -8
- package/bin/adapter/entry-points/console/ui-dist/assets/index-BNPHMdzv.css +1 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +2 -9
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js +0 -6
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js.map +1 -1
- package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js +26 -1
- package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js.map +1 -1
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js +0 -9
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js.map +1 -1
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +3 -13
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/console/ui/src/index.css +2 -4
- package/src/adapter/entry-points/console/ui-dist/assets/index-BNPHMdzv.css +1 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +0 -5
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +2 -42
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.test.ts +0 -44
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts +0 -8
- package/src/adapter/repositories/GoogleSpreadsheetRepository.integration.test.ts +4 -4
- package/src/adapter/repositories/GraphqlProjectRepository.test.ts +4 -2
- package/src/adapter/repositories/KySlackRepository.test.ts +5 -4
- package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.test.ts +90 -0
- package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.ts +29 -1
- package/src/adapter/repositories/issue/ApiV3IssueRepository.test.ts +6 -2
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.test.ts +1 -17
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.ts +0 -11
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +2 -62
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +7 -20
- package/src/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.ts +0 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts +0 -2
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts.map +1 -1
- package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts.map +1 -1
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts +0 -1
- 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 +0 -1
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-BaVsPpWs.css +0 -1
- package/src/adapter/entry-points/console/ui-dist/assets/index-BaVsPpWs.css +0 -1
- /package/bin/adapter/entry-points/console/ui-dist/assets/{index-ITb3VBcs.js → index-BMjm7L0w.js} +0 -0
- /package/src/adapter/entry-points/console/ui-dist/assets/{index-ITb3VBcs.js → index-BMjm7L0w.js} +0 -0
|
@@ -128,7 +128,6 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
128
128
|
silentNotificationCooldownSeconds?: number;
|
|
129
129
|
silentNotificationStaggerSeconds?: number;
|
|
130
130
|
silentMainStalledMessage?: string;
|
|
131
|
-
silentOwnerReNotificationMessage?: string;
|
|
132
131
|
silentSubAgentMessageHeader?: string;
|
|
133
132
|
silentSubAgentMessageFooter?: string;
|
|
134
133
|
credentials: {
|
|
@@ -634,10 +633,6 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
634
633
|
mergedInput.silentMainStalledMessage ??
|
|
635
634
|
process.env.TDPM_SILENT_MAIN_STALLED_MESSAGE ??
|
|
636
635
|
null,
|
|
637
|
-
ownerReNotificationMessage:
|
|
638
|
-
mergedInput.silentOwnerReNotificationMessage ??
|
|
639
|
-
process.env.TDPM_SILENT_OWNER_RE_NOTIFICATION_MESSAGE ??
|
|
640
|
-
null,
|
|
641
636
|
subAgentMessageHeader:
|
|
642
637
|
mergedInput.silentSubAgentMessageHeader ??
|
|
643
638
|
process.env.TDPM_SILENT_SUBAGENT_MESSAGE_HEADER ??
|
|
@@ -21,7 +21,6 @@ const CLAUDE_PID = 201;
|
|
|
21
21
|
|
|
22
22
|
const EMPTY_TEMPLATES: SilentSessionMessageTemplates = {
|
|
23
23
|
mainStalledMessage: null,
|
|
24
|
-
ownerReNotificationMessage: null,
|
|
25
24
|
subAgentMessageHeader: null,
|
|
26
25
|
subAgentMessageFooter: null,
|
|
27
26
|
};
|
|
@@ -169,7 +168,6 @@ describe('notifySilentTmuxSessions', () => {
|
|
|
169
168
|
...baseParams(runner),
|
|
170
169
|
messageTemplates: {
|
|
171
170
|
mainStalledMessage: 'CUSTOM_MAIN_TEMPLATE',
|
|
172
|
-
ownerReNotificationMessage: null,
|
|
173
171
|
subAgentMessageHeader: null,
|
|
174
172
|
subAgentMessageFooter: null,
|
|
175
173
|
},
|
|
@@ -183,42 +181,7 @@ describe('notifySilentTmuxSessions', () => {
|
|
|
183
181
|
);
|
|
184
182
|
});
|
|
185
183
|
|
|
186
|
-
it('
|
|
187
|
-
const recentOwnerCall = new Date(
|
|
188
|
-
(NOW_EPOCH_SECONDS - 5 * 60) * 1000,
|
|
189
|
-
).toISOString();
|
|
190
|
-
writeTranscript([
|
|
191
|
-
{
|
|
192
|
-
type: 'user',
|
|
193
|
-
timestamp: '2026-06-25T23:00:00.000Z',
|
|
194
|
-
message: { role: 'user', content: 'go ahead' },
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
type: 'assistant',
|
|
198
|
-
timestamp: recentOwnerCall,
|
|
199
|
-
message: {
|
|
200
|
-
role: 'assistant',
|
|
201
|
-
stop_reason: 'end_turn',
|
|
202
|
-
content: [
|
|
203
|
-
{ type: 'text', text: 'waiting <<OWNER_CALL>> please decide' },
|
|
204
|
-
],
|
|
205
|
-
},
|
|
206
|
-
},
|
|
207
|
-
]);
|
|
208
|
-
const runner = liveSessionRunner();
|
|
209
|
-
|
|
210
|
-
await notifySilentTmuxSessions({
|
|
211
|
-
...baseParams(runner),
|
|
212
|
-
ownerCallMarker: '<<OWNER_CALL>>',
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
const sendCall = runner.runCommand.mock.calls.find(
|
|
216
|
-
(call) => call[0] === 'tmux' && call[1][0] === 'send-keys',
|
|
217
|
-
);
|
|
218
|
-
expect(sendCall).toBeUndefined();
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it('re-notifies the owner when an owner call has been unanswered past the threshold', async () => {
|
|
184
|
+
it('suppresses the notification whenever an owner call is unanswered, regardless of how long the session has been silent', async () => {
|
|
222
185
|
const stalePendingOwnerCall = new Date(
|
|
223
186
|
(NOW_EPOCH_SECONDS - 11 * 60) * 1000,
|
|
224
187
|
).toISOString();
|
|
@@ -250,10 +213,7 @@ describe('notifySilentTmuxSessions', () => {
|
|
|
250
213
|
const sendCall = runner.runCommand.mock.calls.find(
|
|
251
214
|
(call) => call[0] === 'tmux' && call[1][0] === 'send-keys',
|
|
252
215
|
);
|
|
253
|
-
expect(sendCall
|
|
254
|
-
expect(sendCall?.[1][4]).toContain(SILENT_SESSION_REMINDER_SENTINEL);
|
|
255
|
-
expect(sendCall?.[1][4]).toContain('Re-raise your pending call-to-user');
|
|
256
|
-
expect(sendCall?.[1][4]).not.toContain('You have produced no output for');
|
|
216
|
+
expect(sendCall).toBeUndefined();
|
|
257
217
|
});
|
|
258
218
|
|
|
259
219
|
it('does not re-notify the same silent session on the next cycle within cooldown', async () => {
|
|
@@ -6,9 +6,6 @@ type Mocked<T> = jest.Mocked<T> & jest.MockedObject<T>;
|
|
|
6
6
|
|
|
7
7
|
const createFallback = (): Mocked<SilentSessionMessageComposer> => ({
|
|
8
8
|
composeMainStalledSection: jest.fn().mockReturnValue('FALLBACK_MAIN'),
|
|
9
|
-
composeOwnerReNotificationSection: jest
|
|
10
|
-
.fn()
|
|
11
|
-
.mockReturnValue('FALLBACK_OWNER'),
|
|
12
9
|
composeSubAgentSection: jest.fn().mockReturnValue('FALLBACK_SUB'),
|
|
13
10
|
});
|
|
14
11
|
|
|
@@ -18,7 +15,6 @@ describe('ConfigurableSilentSessionMessageComposer', () => {
|
|
|
18
15
|
const composer = new ConfigurableSilentSessionMessageComposer(
|
|
19
16
|
{
|
|
20
17
|
mainStalledMessage: null,
|
|
21
|
-
ownerReNotificationMessage: null,
|
|
22
18
|
subAgentMessageHeader: null,
|
|
23
19
|
subAgentMessageFooter: null,
|
|
24
20
|
},
|
|
@@ -33,7 +29,6 @@ describe('ConfigurableSilentSessionMessageComposer', () => {
|
|
|
33
29
|
const composer = new ConfigurableSilentSessionMessageComposer(
|
|
34
30
|
{
|
|
35
31
|
mainStalledMessage: 'CUSTOM_MAIN',
|
|
36
|
-
ownerReNotificationMessage: null,
|
|
37
32
|
subAgentMessageHeader: null,
|
|
38
33
|
subAgentMessageFooter: null,
|
|
39
34
|
},
|
|
@@ -50,7 +45,6 @@ describe('ConfigurableSilentSessionMessageComposer', () => {
|
|
|
50
45
|
const composer = new ConfigurableSilentSessionMessageComposer(
|
|
51
46
|
{
|
|
52
47
|
mainStalledMessage: null,
|
|
53
|
-
ownerReNotificationMessage: null,
|
|
54
48
|
subAgentMessageHeader: null,
|
|
55
49
|
subAgentMessageFooter: null,
|
|
56
50
|
},
|
|
@@ -68,7 +62,6 @@ describe('ConfigurableSilentSessionMessageComposer', () => {
|
|
|
68
62
|
const composer = new ConfigurableSilentSessionMessageComposer(
|
|
69
63
|
{
|
|
70
64
|
mainStalledMessage: null,
|
|
71
|
-
ownerReNotificationMessage: null,
|
|
72
65
|
subAgentMessageHeader: 'HEADER',
|
|
73
66
|
subAgentMessageFooter: 'FOOTER',
|
|
74
67
|
},
|
|
@@ -86,48 +79,11 @@ describe('ConfigurableSilentSessionMessageComposer', () => {
|
|
|
86
79
|
expect(fallback.composeSubAgentSection).not.toHaveBeenCalled();
|
|
87
80
|
});
|
|
88
81
|
|
|
89
|
-
it('uses the fallback owner-re-notification section when no template is configured', () => {
|
|
90
|
-
const fallback = createFallback();
|
|
91
|
-
const composer = new ConfigurableSilentSessionMessageComposer(
|
|
92
|
-
{
|
|
93
|
-
mainStalledMessage: null,
|
|
94
|
-
ownerReNotificationMessage: null,
|
|
95
|
-
subAgentMessageHeader: null,
|
|
96
|
-
subAgentMessageFooter: null,
|
|
97
|
-
},
|
|
98
|
-
fallback,
|
|
99
|
-
);
|
|
100
|
-
expect(composer.composeOwnerReNotificationSection(600)).toBe(
|
|
101
|
-
'FALLBACK_OWNER',
|
|
102
|
-
);
|
|
103
|
-
expect(fallback.composeOwnerReNotificationSection).toHaveBeenCalledWith(
|
|
104
|
-
600,
|
|
105
|
-
);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('uses the configured owner-re-notification template when provided', () => {
|
|
109
|
-
const fallback = createFallback();
|
|
110
|
-
const composer = new ConfigurableSilentSessionMessageComposer(
|
|
111
|
-
{
|
|
112
|
-
mainStalledMessage: null,
|
|
113
|
-
ownerReNotificationMessage: 'CUSTOM_OWNER',
|
|
114
|
-
subAgentMessageHeader: null,
|
|
115
|
-
subAgentMessageFooter: null,
|
|
116
|
-
},
|
|
117
|
-
fallback,
|
|
118
|
-
);
|
|
119
|
-
const section = composer.composeOwnerReNotificationSection(600);
|
|
120
|
-
expect(section).toContain('CUSTOM_OWNER');
|
|
121
|
-
expect(section).toContain(SILENT_SESSION_REMINDER_SENTINEL);
|
|
122
|
-
expect(fallback.composeOwnerReNotificationSection).not.toHaveBeenCalled();
|
|
123
|
-
});
|
|
124
|
-
|
|
125
82
|
it('does not double-prepend the sentinel when the template already carries it', () => {
|
|
126
83
|
const fallback = createFallback();
|
|
127
84
|
const composer = new ConfigurableSilentSessionMessageComposer(
|
|
128
85
|
{
|
|
129
86
|
mainStalledMessage: `${SILENT_SESSION_REMINDER_SENTINEL} CUSTOM_MAIN`,
|
|
130
|
-
ownerReNotificationMessage: null,
|
|
131
87
|
subAgentMessageHeader: null,
|
|
132
88
|
subAgentMessageFooter: null,
|
|
133
89
|
},
|
|
@@ -9,7 +9,6 @@ const withReminderSentinel = (message: string): string =>
|
|
|
9
9
|
|
|
10
10
|
export type SilentSessionMessageTemplates = {
|
|
11
11
|
mainStalledMessage: string | null;
|
|
12
|
-
ownerReNotificationMessage: string | null;
|
|
13
12
|
subAgentMessageHeader: string | null;
|
|
14
13
|
subAgentMessageFooter: string | null;
|
|
15
14
|
};
|
|
@@ -32,13 +31,6 @@ export class ConfigurableSilentSessionMessageComposer implements SilentSessionMe
|
|
|
32
31
|
return withReminderSentinel(this.templates.mainStalledMessage);
|
|
33
32
|
};
|
|
34
33
|
|
|
35
|
-
composeOwnerReNotificationSection = (waitingSeconds: number): string => {
|
|
36
|
-
if (this.templates.ownerReNotificationMessage === null) {
|
|
37
|
-
return this.fallback.composeOwnerReNotificationSection(waitingSeconds);
|
|
38
|
-
}
|
|
39
|
-
return withReminderSentinel(this.templates.ownerReNotificationMessage);
|
|
40
|
-
};
|
|
41
|
-
|
|
42
34
|
composeSubAgentSection = (subAgents: SubAgentActivity[]): string => {
|
|
43
35
|
if (
|
|
44
36
|
this.templates.subAgentMessageHeader === null &&
|
|
@@ -6,11 +6,11 @@ dotenv.config();
|
|
|
6
6
|
|
|
7
7
|
const GOOGLE_SERVICE_ACCOUNT_KEY = process.env.GOOGLE_SERVICE_ACCOUNT_KEY;
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const describeWhenCredentials = GOOGLE_SERVICE_ACCOUNT_KEY
|
|
10
|
+
? describe
|
|
11
|
+
: describe.skip;
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
describeWhenCredentials('GoogleSpreadsheetRepository integration tests', () => {
|
|
14
14
|
jest.setTimeout(60 * 1000);
|
|
15
15
|
jest.retryTimes(3, { logErrorsBeforeRetry: true });
|
|
16
16
|
|
|
@@ -28,10 +28,12 @@ describe('convertToFieldOptionColor', () => {
|
|
|
28
28
|
});
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
const token = process.env.GH_TOKEN;
|
|
32
|
+
const describeWhenCredentials = token ? describe : describe.skip;
|
|
33
|
+
|
|
34
|
+
describeWhenCredentials('GraphqlProjectRepository', () => {
|
|
32
35
|
const localStorageRepository = new LocalStorageRepository();
|
|
33
36
|
let repository: GraphqlProjectRepository;
|
|
34
|
-
const token = process.env.GH_TOKEN;
|
|
35
37
|
const login = 'HiromiShikata';
|
|
36
38
|
const projectUrl = `https://github.com/users/HiromiShikata/projects/49`;
|
|
37
39
|
const projectNumber = 49;
|
|
@@ -12,16 +12,17 @@ const TEST_USER_NAME = 'shikata.hiromi_test2';
|
|
|
12
12
|
const TEST_IMAGE_URL = 'https://i.imgur.com/Zi3qToQ.jpeg';
|
|
13
13
|
const TEST_IMAGE_PATH = './tmp/test/fixtures/test-image.png';
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
throw new Error('SLACK_USER_TOKEN is required');
|
|
17
|
-
}
|
|
15
|
+
const describeWhenCredentials = SLACK_USER_TOKEN ? describe : describe.skip;
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
describeWhenCredentials('KySlackRepository Integration Tests', () => {
|
|
20
18
|
jest.setTimeout(60 * 1000);
|
|
21
19
|
jest.retryTimes(3, { logErrorsBeforeRetry: true });
|
|
22
20
|
let slackRepository: KySlackRepository;
|
|
23
21
|
|
|
24
22
|
beforeAll(() => {
|
|
23
|
+
if (!SLACK_USER_TOKEN) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
25
26
|
slackRepository = new KySlackRepository(SLACK_USER_TOKEN);
|
|
26
27
|
});
|
|
27
28
|
beforeEach(async () => {
|
|
@@ -52,9 +52,42 @@ describe('TranscriptOwnerCallStatusProvider', () => {
|
|
|
52
52
|
const ownerReply = (timestamp: string): object => ({
|
|
53
53
|
type: 'user',
|
|
54
54
|
timestamp,
|
|
55
|
+
origin: { kind: 'human' },
|
|
56
|
+
promptSource: 'typed',
|
|
55
57
|
message: { role: 'user', content: 'go ahead' },
|
|
56
58
|
});
|
|
57
59
|
|
|
60
|
+
const ownerReplyQueuedNoOrigin = (timestamp: string): object => ({
|
|
61
|
+
type: 'user',
|
|
62
|
+
timestamp,
|
|
63
|
+
promptSource: 'queued',
|
|
64
|
+
message: { role: 'user', content: 'go ahead' },
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const taskNotification = (timestamp: string): object => ({
|
|
68
|
+
type: 'user',
|
|
69
|
+
timestamp,
|
|
70
|
+
origin: { kind: 'task-notification' },
|
|
71
|
+
promptSource: 'system',
|
|
72
|
+
message: {
|
|
73
|
+
role: 'user',
|
|
74
|
+
content:
|
|
75
|
+
'<task-notification>\n<task-id>abc123</task-id>\nA sub-agent finished.\n</task-notification>',
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const peerAgentMessage = (timestamp: string): object => ({
|
|
80
|
+
type: 'user',
|
|
81
|
+
timestamp,
|
|
82
|
+
origin: { kind: 'peer' },
|
|
83
|
+
promptSource: 'system',
|
|
84
|
+
isMeta: true,
|
|
85
|
+
message: {
|
|
86
|
+
role: 'user',
|
|
87
|
+
content: 'Another Claude session sent a message: please continue.',
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
58
91
|
const toolResult = (timestamp: string): object => ({
|
|
59
92
|
type: 'user',
|
|
60
93
|
timestamp,
|
|
@@ -188,6 +221,63 @@ describe('TranscriptOwnerCallStatusProvider', () => {
|
|
|
188
221
|
expect(result.has(sessionName)).toBe(false);
|
|
189
222
|
});
|
|
190
223
|
|
|
224
|
+
it('does not count a system-injected task-notification user entry as an owner reply', async () => {
|
|
225
|
+
const transcriptPath = writeTranscript('workbench.jsonl', [
|
|
226
|
+
assistantWithMarker('2026-06-27T10:00:00.000Z'),
|
|
227
|
+
taskNotification('2026-06-27T10:10:00.000Z'),
|
|
228
|
+
]);
|
|
229
|
+
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
230
|
+
|
|
231
|
+
const result = await provider.listSessionNamesWithUnansweredOwnerCall(
|
|
232
|
+
new Map([[sessionName, transcriptPath]]),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
expect(result.has(sessionName)).toBe(true);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('does not count a cross-session peer agent message as an owner reply', async () => {
|
|
239
|
+
const transcriptPath = writeTranscript('workbench.jsonl', [
|
|
240
|
+
assistantWithMarker('2026-06-27T10:00:00.000Z'),
|
|
241
|
+
peerAgentMessage('2026-06-27T10:10:00.000Z'),
|
|
242
|
+
]);
|
|
243
|
+
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
244
|
+
|
|
245
|
+
const result = await provider.listSessionNamesWithUnansweredOwnerCall(
|
|
246
|
+
new Map([[sessionName, transcriptPath]]),
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
expect(result.has(sessionName)).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('counts a genuine human reply that has promptSource queued but no origin field', async () => {
|
|
253
|
+
const transcriptPath = writeTranscript('workbench.jsonl', [
|
|
254
|
+
assistantWithMarker('2026-06-27T10:00:00.000Z'),
|
|
255
|
+
ownerReplyQueuedNoOrigin('2026-06-27T10:10:00.000Z'),
|
|
256
|
+
]);
|
|
257
|
+
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
258
|
+
|
|
259
|
+
const result = await provider.listSessionNamesWithUnansweredOwnerCall(
|
|
260
|
+
new Map([[sessionName, transcriptPath]]),
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
expect(result.has(sessionName)).toBe(false);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('counts a genuine human-origin reply that arrives after a task-notification', async () => {
|
|
267
|
+
const transcriptPath = writeTranscript('workbench.jsonl', [
|
|
268
|
+
assistantWithMarker('2026-06-27T10:00:00.000Z'),
|
|
269
|
+
taskNotification('2026-06-27T10:10:00.000Z'),
|
|
270
|
+
ownerReply('2026-06-27T10:20:00.000Z'),
|
|
271
|
+
]);
|
|
272
|
+
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
273
|
+
|
|
274
|
+
const result = await provider.listSessionNamesWithUnansweredOwnerCall(
|
|
275
|
+
new Map([[sessionName, transcriptPath]]),
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
expect(result.has(sessionName)).toBe(false);
|
|
279
|
+
});
|
|
280
|
+
|
|
191
281
|
it('does not report a session that never raised an owner call', async () => {
|
|
192
282
|
const transcriptPath = writeTranscript('workbench.jsonl', [
|
|
193
283
|
assistantPlain('2026-06-27T10:00:00.000Z'),
|
|
@@ -40,6 +40,30 @@ const extractText = (content: unknown): string => {
|
|
|
40
40
|
return texts.join('\n');
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
+
// A transcript user entry is only a genuine owner reply when it was actually
|
|
44
|
+
// typed (or queued) by the human owner. Claude Code records the provenance of
|
|
45
|
+
// each user entry on the top-level `origin` and `promptSource` fields. A
|
|
46
|
+
// genuine human prompt is identified by EITHER `origin.kind === 'human'` OR a
|
|
47
|
+
// `promptSource` of `typed`/`queued`; the second condition is required because
|
|
48
|
+
// older-format transcripts record genuine human replies with a `promptSource`
|
|
49
|
+
// of `queued`/`typed` but no `origin` field, and dropping those would leave a
|
|
50
|
+
// real owner reply uncounted and the session waiting forever. Every
|
|
51
|
+
// system-injected user entry, by contrast, uses a `promptSource` of `system`
|
|
52
|
+
// (sub-agent `task-notification` notices, cross-session `peer` messages) or
|
|
53
|
+
// `sdk` (spawn prompts), or has neither field (tool results, skill/meta
|
|
54
|
+
// entries) — none of which match. Only a genuine human entry may clear an
|
|
55
|
+
// outstanding call-to-user; otherwise a system-injected entry would be
|
|
56
|
+
// miscounted as the owner answering and a genuinely waiting session would stop
|
|
57
|
+
// being suppressed.
|
|
58
|
+
const isGenuineHumanEntry = (parsed: Record<string, unknown>): boolean => {
|
|
59
|
+
const origin = parsed.origin;
|
|
60
|
+
if (isRecord(origin) && readString(origin, 'kind') === 'human') {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
const promptSource = readString(parsed, 'promptSource');
|
|
64
|
+
return promptSource === 'typed' || promptSource === 'queued';
|
|
65
|
+
};
|
|
66
|
+
|
|
43
67
|
const hasOwnerTextReply = (content: unknown): boolean => {
|
|
44
68
|
if (typeof content === 'string') {
|
|
45
69
|
if (content.length === 0) {
|
|
@@ -122,7 +146,11 @@ export class TranscriptOwnerCallStatusProvider implements OwnerCallStatusProvide
|
|
|
122
146
|
) {
|
|
123
147
|
lastOwnerCallEpochMs = epochMs;
|
|
124
148
|
}
|
|
125
|
-
if (
|
|
149
|
+
if (
|
|
150
|
+
type === 'user' &&
|
|
151
|
+
isGenuineHumanEntry(parsed) &&
|
|
152
|
+
hasOwnerTextReply(messageContent)
|
|
153
|
+
) {
|
|
126
154
|
lastOwnerReplyEpochMs = epochMs;
|
|
127
155
|
}
|
|
128
156
|
}
|
|
@@ -3,11 +3,15 @@ import { LocalStorageRepository } from '../LocalStorageRepository';
|
|
|
3
3
|
import dotenv from 'dotenv';
|
|
4
4
|
|
|
5
5
|
dotenv.config();
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
const githubToken = process.env.GH_TOKEN;
|
|
8
|
+
const describeWhenCredentials = githubToken ? describe : describe.skip;
|
|
9
|
+
|
|
10
|
+
describeWhenCredentials('ApiV3IssueRepository', () => {
|
|
7
11
|
const localStorageRepository = new LocalStorageRepository();
|
|
8
12
|
const repository = new ApiV3IssueRepository(
|
|
9
13
|
localStorageRepository,
|
|
10
|
-
|
|
14
|
+
githubToken,
|
|
11
15
|
);
|
|
12
16
|
test('searchIssue', async () => {
|
|
13
17
|
const result = await repository.searchIssue({
|
|
@@ -9,20 +9,6 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
9
9
|
expect(section).toContain(SILENT_SESSION_REMINDER_SENTINEL);
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
it('embeds the reminder sentinel in the owner-re-notification section', () => {
|
|
13
|
-
const section = composer.composeOwnerReNotificationSection(1800);
|
|
14
|
-
expect(section).toContain(SILENT_SESSION_REMINDER_SENTINEL);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('instructs the agent to re-raise its pending call-to-user without treating the reminder as an owner reply', () => {
|
|
18
|
-
const section = composer.composeOwnerReNotificationSection(1800);
|
|
19
|
-
expect(section).toContain('waiting on the owner for 30 minutes');
|
|
20
|
-
expect(section).toContain('NOT the owner replying');
|
|
21
|
-
expect(section).toContain('Re-raise your pending call-to-user');
|
|
22
|
-
expect(section).toContain('you are not stalled or idle');
|
|
23
|
-
expect(section.toLowerCase()).not.toContain('no output for');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
12
|
it('embeds the reminder sentinel in the sub-agent section', () => {
|
|
27
13
|
const section = composer.composeSubAgentSection([
|
|
28
14
|
{ label: 'sub-process-1', silentSeconds: 360, runningSeconds: 1200 },
|
|
@@ -67,12 +53,10 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
67
53
|
|
|
68
54
|
it('does not contain any host-specific or internal identifiers', () => {
|
|
69
55
|
const mainSection = composer.composeMainStalledSection(600);
|
|
70
|
-
const ownerSection = composer.composeOwnerReNotificationSection(1800);
|
|
71
56
|
const subSection = composer.composeSubAgentSection([
|
|
72
57
|
{ label: 'sub-process-1', silentSeconds: 360, runningSeconds: 1200 },
|
|
73
58
|
]);
|
|
74
|
-
const combined =
|
|
75
|
-
`${mainSection}\n${ownerSection}\n${subSection}`.toLowerCase();
|
|
59
|
+
const combined = `${mainSection}\n${subSection}`.toLowerCase();
|
|
76
60
|
expect(combined).not.toContain('claude');
|
|
77
61
|
expect(combined).not.toContain('take ownership');
|
|
78
62
|
expect(combined).not.toContain('/home/');
|
|
@@ -19,22 +19,11 @@ const composeMainStalledMessage = (mainSilentSeconds: number): string => {
|
|
|
19
19
|
].join('\n');
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
const composeOwnerReNotificationMessage = (waitingSeconds: number): string => {
|
|
23
|
-
const minutes = Math.floor(waitingSeconds / 60);
|
|
24
|
-
return [
|
|
25
|
-
`${SILENT_SESSION_REMINDER_SENTINEL} This is an automated monitor reminder, NOT the owner replying. Do NOT treat this message as an owner answer to your outstanding call-to-user; your pending question is still unanswered. You have been waiting on the owner for ${minutes} minutes and the owner has not yet replied. The owner is only notified when a fresh call-to-user is raised, so a single past call-to-user that has gone quiet will never reach them. Re-raise your pending call-to-user to the owner now: re-emit it so a new call-to-user marker fires and the owner is notified again. Keep your question and proposed options unchanged unless you have new information. You are correctly waiting on the owner — you are not stalled or idle — so do not abandon the wait and do not invent an owner decision yourself; simply re-surface the same request so the owner sees it.`,
|
|
26
|
-
].join('\n');
|
|
27
|
-
};
|
|
28
|
-
|
|
29
22
|
export class DefaultSilentSessionMessageComposer implements SilentSessionMessageComposer {
|
|
30
23
|
composeMainStalledSection = (mainSilentSeconds: number): string => {
|
|
31
24
|
return composeMainStalledMessage(mainSilentSeconds);
|
|
32
25
|
};
|
|
33
26
|
|
|
34
|
-
composeOwnerReNotificationSection = (waitingSeconds: number): string => {
|
|
35
|
-
return composeOwnerReNotificationMessage(waitingSeconds);
|
|
36
|
-
};
|
|
37
|
-
|
|
38
27
|
composeSubAgentSection = (subAgents: SubAgentActivity[]): string => {
|
|
39
28
|
const lines = subAgents.map(
|
|
40
29
|
(subAgent) =>
|
|
@@ -21,7 +21,6 @@ import { InteractiveLiveSession } from '../entities/InteractiveLiveSession';
|
|
|
21
21
|
type Mocked<T> = jest.Mocked<T> & jest.MockedObject<T>;
|
|
22
22
|
|
|
23
23
|
const MAIN_STALLED_SECTION = 'MAIN_STALLED_SECTION';
|
|
24
|
-
const OWNER_RENOTIFICATION_SECTION = 'OWNER_RENOTIFICATION_SECTION';
|
|
25
24
|
const SUBAGENT_SECTION = 'SUBAGENT_SECTION';
|
|
26
25
|
|
|
27
26
|
describe('NotifySilentLiveSessionsUseCase', () => {
|
|
@@ -125,9 +124,6 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
125
124
|
composeMainStalledSection: jest
|
|
126
125
|
.fn()
|
|
127
126
|
.mockReturnValue(MAIN_STALLED_SECTION),
|
|
128
|
-
composeOwnerReNotificationSection: jest
|
|
129
|
-
.fn()
|
|
130
|
-
.mockReturnValue(OWNER_RENOTIFICATION_SECTION),
|
|
131
127
|
composeSubAgentSection: jest.fn().mockReturnValue(SUBAGENT_SECTION),
|
|
132
128
|
};
|
|
133
129
|
mockSleeper = {
|
|
@@ -201,7 +197,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
201
197
|
).toHaveBeenCalledWith(['workbench']);
|
|
202
198
|
});
|
|
203
199
|
|
|
204
|
-
it('
|
|
200
|
+
it('suppresses the stalled section and sends nothing when an owner call is pending past the threshold', async () => {
|
|
205
201
|
setupLiveInteractiveSession('workbench');
|
|
206
202
|
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
207
203
|
[
|
|
@@ -221,65 +217,12 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
221
217
|
expect(
|
|
222
218
|
mockMessageComposer.composeMainStalledSection,
|
|
223
219
|
).not.toHaveBeenCalled();
|
|
224
|
-
expect(
|
|
225
|
-
mockMessageComposer.composeOwnerReNotificationSection,
|
|
226
|
-
).toHaveBeenCalledWith(DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS);
|
|
227
|
-
expect(
|
|
228
|
-
mockNotificationRepository.sendSelfCheckNotification,
|
|
229
|
-
).toHaveBeenCalledWith('workbench', OWNER_RENOTIFICATION_SECTION);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it('does not send the owner-re-notification section when the owner-call wait is within the threshold', async () => {
|
|
233
|
-
setupLiveInteractiveSession('workbench');
|
|
234
|
-
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
235
|
-
[
|
|
236
|
-
{
|
|
237
|
-
sessionName: 'workbench',
|
|
238
|
-
lastOutputEpochSeconds:
|
|
239
|
-
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS + 1,
|
|
240
|
-
},
|
|
241
|
-
],
|
|
242
|
-
);
|
|
243
|
-
mockOwnerCallStatusProvider.listSessionNamesWithUnansweredOwnerCall.mockResolvedValue(
|
|
244
|
-
new Set(['workbench']),
|
|
245
|
-
);
|
|
246
|
-
|
|
247
|
-
await useCase.run(runParams());
|
|
248
|
-
|
|
249
|
-
expect(
|
|
250
|
-
mockMessageComposer.composeOwnerReNotificationSection,
|
|
251
|
-
).not.toHaveBeenCalled();
|
|
252
|
-
expect(
|
|
253
|
-
mockNotificationRepository.sendSelfCheckNotification,
|
|
254
|
-
).not.toHaveBeenCalled();
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it('does not re-notify the owner within the cooldown window while still waiting on the owner', async () => {
|
|
258
|
-
setupLiveInteractiveSession('workbench');
|
|
259
|
-
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
260
|
-
[
|
|
261
|
-
{
|
|
262
|
-
sessionName: 'workbench',
|
|
263
|
-
lastOutputEpochSeconds:
|
|
264
|
-
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
265
|
-
},
|
|
266
|
-
],
|
|
267
|
-
);
|
|
268
|
-
mockOwnerCallStatusProvider.listSessionNamesWithUnansweredOwnerCall.mockResolvedValue(
|
|
269
|
-
new Set(['workbench']),
|
|
270
|
-
);
|
|
271
|
-
mockNotificationRepository.getLastNotifiedEpochSeconds.mockResolvedValue(
|
|
272
|
-
nowEpochSeconds - DEFAULT_NOTIFICATION_COOLDOWN_SECONDS + 1,
|
|
273
|
-
);
|
|
274
|
-
|
|
275
|
-
await useCase.run(runParams());
|
|
276
|
-
|
|
277
220
|
expect(
|
|
278
221
|
mockNotificationRepository.sendSelfCheckNotification,
|
|
279
222
|
).not.toHaveBeenCalled();
|
|
280
223
|
});
|
|
281
224
|
|
|
282
|
-
it('
|
|
225
|
+
it('sends the main stalled section when the session is silent past the threshold and not waiting on the owner', async () => {
|
|
283
226
|
setupLiveInteractiveSession('workbench');
|
|
284
227
|
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
285
228
|
[
|
|
@@ -296,9 +239,6 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
296
239
|
|
|
297
240
|
await useCase.run(runParams());
|
|
298
241
|
|
|
299
|
-
expect(
|
|
300
|
-
mockMessageComposer.composeOwnerReNotificationSection,
|
|
301
|
-
).not.toHaveBeenCalled();
|
|
302
242
|
expect(mockMessageComposer.composeMainStalledSection).toHaveBeenCalledWith(
|
|
303
243
|
DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
304
244
|
);
|
|
@@ -172,27 +172,14 @@ export class NotifySilentLiveSessionsUseCase {
|
|
|
172
172
|
|
|
173
173
|
if (
|
|
174
174
|
snapshot.mainSilentSeconds !== null &&
|
|
175
|
-
snapshot.mainSilentSeconds >= thresholds.mainSilentThresholdSeconds
|
|
175
|
+
snapshot.mainSilentSeconds >= thresholds.mainSilentThresholdSeconds &&
|
|
176
|
+
!snapshot.hasUnansweredOwnerCall
|
|
176
177
|
) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
// run() limits this to roughly once per cooldown window, and it stops
|
|
183
|
-
// automatically once the owner replies (hasUnansweredOwnerCall clears).
|
|
184
|
-
sections.push(
|
|
185
|
-
this.messageComposer.composeOwnerReNotificationSection(
|
|
186
|
-
snapshot.mainSilentSeconds,
|
|
187
|
-
),
|
|
188
|
-
);
|
|
189
|
-
} else {
|
|
190
|
-
sections.push(
|
|
191
|
-
this.messageComposer.composeMainStalledSection(
|
|
192
|
-
snapshot.mainSilentSeconds,
|
|
193
|
-
),
|
|
194
|
-
);
|
|
195
|
-
}
|
|
178
|
+
sections.push(
|
|
179
|
+
this.messageComposer.composeMainStalledSection(
|
|
180
|
+
snapshot.mainSilentSeconds,
|
|
181
|
+
),
|
|
182
|
+
);
|
|
196
183
|
}
|
|
197
184
|
|
|
198
185
|
const stalledSubAgents = snapshot.subAgents.filter(
|
|
@@ -2,6 +2,5 @@ import { SubAgentActivity } from '../../entities/LiveSessionActivitySnapshot';
|
|
|
2
2
|
|
|
3
3
|
export interface SilentSessionMessageComposer {
|
|
4
4
|
composeMainStalledSection: (mainSilentSeconds: number) => string;
|
|
5
|
-
composeOwnerReNotificationSection: (waitingSeconds: number) => string;
|
|
6
5
|
composeSubAgentSection: (subAgents: SubAgentActivity[]) => string;
|
|
7
6
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HandleScheduledEventUseCaseHandler.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts"],"names":[],"mappings":"AAkCA,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AA0D3D,qBAAa,kCAAkC;IAC7C,MAAM,GACJ,gBAAgB,MAAM,EACtB,UAAU,OAAO,KAChB,OAAO,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,EAAE,KAAK,EAAE,CAAC;QAChB,SAAS,EAAE,OAAO,CAAC;QACnB,eAAe,EAAE,IAAI,EAAE,CAAC;KACzB,GAAG,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"HandleScheduledEventUseCaseHandler.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts"],"names":[],"mappings":"AAkCA,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AA0D3D,qBAAa,kCAAkC;IAC7C,MAAM,GACJ,gBAAgB,MAAM,EACtB,UAAU,OAAO,KAChB,OAAO,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,EAAE,KAAK,EAAE,CAAC;QAChB,SAAS,EAAE,OAAO,CAAC;QACnB,eAAe,EAAE,IAAI,EAAE,CAAC;KACzB,GAAG,IAAI,CAAC,CAyiBP;CACH"}
|
|
@@ -2,7 +2,6 @@ import { SubAgentActivity } from '../../domain/entities/LiveSessionActivitySnaps
|
|
|
2
2
|
import { SilentSessionMessageComposer } from '../../domain/usecases/adapter-interfaces/SilentSessionMessageComposer';
|
|
3
3
|
export type SilentSessionMessageTemplates = {
|
|
4
4
|
mainStalledMessage: string | null;
|
|
5
|
-
ownerReNotificationMessage: string | null;
|
|
6
5
|
subAgentMessageHeader: string | null;
|
|
7
6
|
subAgentMessageFooter: string | null;
|
|
8
7
|
};
|
|
@@ -11,7 +10,6 @@ export declare class ConfigurableSilentSessionMessageComposer implements SilentS
|
|
|
11
10
|
private readonly fallback;
|
|
12
11
|
constructor(templates: SilentSessionMessageTemplates, fallback: SilentSessionMessageComposer);
|
|
13
12
|
composeMainStalledSection: (mainSilentSeconds: number) => string;
|
|
14
|
-
composeOwnerReNotificationSection: (waitingSeconds: number) => string;
|
|
15
13
|
composeSubAgentSection: (subAgents: SubAgentActivity[]) => string;
|
|
16
14
|
}
|
|
17
15
|
//# sourceMappingURL=ConfigurableSilentSessionMessageComposer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConfigurableSilentSessionMessageComposer.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mDAAmD,CAAC;AACrF,OAAO,EAAE,4BAA4B,EAAE,MAAM,uEAAuE,CAAC;AAQrH,MAAM,MAAM,6BAA6B,GAAG;IAC1C,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,
|
|
1
|
+
{"version":3,"file":"ConfigurableSilentSessionMessageComposer.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mDAAmD,CAAC;AACrF,OAAO,EAAE,4BAA4B,EAAE,MAAM,uEAAuE,CAAC;AAQrH,MAAM,MAAM,6BAA6B,GAAG;IAC1C,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC,CAAC;AAOF,qBAAa,wCAAyC,YAAW,4BAA4B;IAEzF,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBADR,SAAS,EAAE,6BAA6B,EACxC,QAAQ,EAAE,4BAA4B;IAGzD,yBAAyB,GAAI,mBAAmB,MAAM,KAAG,MAAM,CAK7D;IAEF,sBAAsB,GAAI,WAAW,gBAAgB,EAAE,KAAG,MAAM,CAsB9D;CACH"}
|