github-issue-tower-defence-management 1.122.4 → 1.122.6
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 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +14 -2
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +3 -1
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -1
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js +6 -0
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js.map +1 -1
- package/bin/adapter/repositories/NoUnansweredOwnerCallStatusProvider.js +2 -2
- package/bin/adapter/repositories/NoUnansweredOwnerCallStatusProvider.js.map +1 -1
- package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js +14 -11
- package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js.map +1 -1
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js +18 -5
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js.map +1 -1
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +15 -5
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +11 -0
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +47 -8
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +5 -0
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.test.ts +44 -2
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts +16 -0
- package/src/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.test.ts +1 -1
- package/src/adapter/repositories/NoUnansweredOwnerCallStatusProvider.test.ts +7 -6
- package/src/adapter/repositories/NoUnansweredOwnerCallStatusProvider.ts +3 -3
- package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.test.ts +99 -49
- package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.ts +24 -14
- package/src/domain/entities/LiveSessionActivitySnapshot.ts +1 -1
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.test.ts +117 -9
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.ts +31 -5
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +106 -10
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +24 -6
- package/src/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.ts +2 -2
- package/src/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.ts +4 -0
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts +2 -0
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -1
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts +2 -0
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts.map +1 -1
- package/types/adapter/repositories/NoUnansweredOwnerCallStatusProvider.d.ts +1 -1
- package/types/adapter/repositories/NoUnansweredOwnerCallStatusProvider.d.ts.map +1 -1
- package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts +2 -2
- package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts.map +1 -1
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts +1 -1
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts.map +1 -1
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts +1 -0
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts.map +1 -1
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +2 -0
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.d.ts +1 -1
- package/types/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts +1 -0
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts.map +1 -1
|
@@ -129,13 +129,48 @@ describe('TranscriptOwnerCallStatusProvider', () => {
|
|
|
129
129
|
]);
|
|
130
130
|
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
131
131
|
|
|
132
|
-
const result =
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
const result =
|
|
133
|
+
await provider.listUnansweredOwnerCallEpochSecondsBySessionName(
|
|
134
|
+
new Map([[sessionName, transcriptPath]]),
|
|
135
|
+
);
|
|
135
136
|
|
|
136
137
|
expect(result.has(sessionName)).toBe(true);
|
|
137
138
|
});
|
|
138
139
|
|
|
140
|
+
it('exposes the epoch seconds of the unanswered owner call so its age can be computed', async () => {
|
|
141
|
+
const transcriptPath = writeTranscript('workbench.jsonl', [
|
|
142
|
+
ownerReply('2026-06-27T10:00:00.000Z'),
|
|
143
|
+
assistantWithMarker('2026-06-27T10:05:00.000Z'),
|
|
144
|
+
]);
|
|
145
|
+
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
146
|
+
|
|
147
|
+
const result =
|
|
148
|
+
await provider.listUnansweredOwnerCallEpochSecondsBySessionName(
|
|
149
|
+
new Map([[sessionName, transcriptPath]]),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect(result.get(sessionName)).toBe(
|
|
153
|
+
Math.floor(Date.parse('2026-06-27T10:05:00.000Z') / 1000),
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('exposes the epoch seconds of the latest owner call when several calls are unanswered', async () => {
|
|
158
|
+
const transcriptPath = writeTranscript('workbench.jsonl', [
|
|
159
|
+
assistantWithMarker('2026-06-27T09:00:00.000Z'),
|
|
160
|
+
assistantWithMarker('2026-06-27T10:05:00.000Z'),
|
|
161
|
+
]);
|
|
162
|
+
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
163
|
+
|
|
164
|
+
const result =
|
|
165
|
+
await provider.listUnansweredOwnerCallEpochSecondsBySessionName(
|
|
166
|
+
new Map([[sessionName, transcriptPath]]),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect(result.get(sessionName)).toBe(
|
|
170
|
+
Math.floor(Date.parse('2026-06-27T10:05:00.000Z') / 1000),
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
139
174
|
it('does not report a session as waiting when a later owner reply followed the call', async () => {
|
|
140
175
|
const transcriptPath = writeTranscript('workbench.jsonl', [
|
|
141
176
|
assistantWithMarker('2026-06-27T10:00:00.000Z'),
|
|
@@ -143,9 +178,10 @@ describe('TranscriptOwnerCallStatusProvider', () => {
|
|
|
143
178
|
]);
|
|
144
179
|
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
145
180
|
|
|
146
|
-
const result =
|
|
147
|
-
|
|
148
|
-
|
|
181
|
+
const result =
|
|
182
|
+
await provider.listUnansweredOwnerCallEpochSecondsBySessionName(
|
|
183
|
+
new Map([[sessionName, transcriptPath]]),
|
|
184
|
+
);
|
|
149
185
|
|
|
150
186
|
expect(result.has(sessionName)).toBe(false);
|
|
151
187
|
});
|
|
@@ -157,9 +193,10 @@ describe('TranscriptOwnerCallStatusProvider', () => {
|
|
|
157
193
|
]);
|
|
158
194
|
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
159
195
|
|
|
160
|
-
const result =
|
|
161
|
-
|
|
162
|
-
|
|
196
|
+
const result =
|
|
197
|
+
await provider.listUnansweredOwnerCallEpochSecondsBySessionName(
|
|
198
|
+
new Map([[sessionName, transcriptPath]]),
|
|
199
|
+
);
|
|
163
200
|
|
|
164
201
|
expect(result.has(sessionName)).toBe(false);
|
|
165
202
|
});
|
|
@@ -171,9 +208,10 @@ describe('TranscriptOwnerCallStatusProvider', () => {
|
|
|
171
208
|
]);
|
|
172
209
|
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
173
210
|
|
|
174
|
-
const result =
|
|
175
|
-
|
|
176
|
-
|
|
211
|
+
const result =
|
|
212
|
+
await provider.listUnansweredOwnerCallEpochSecondsBySessionName(
|
|
213
|
+
new Map([[sessionName, transcriptPath]]),
|
|
214
|
+
);
|
|
177
215
|
|
|
178
216
|
expect(result.has(sessionName)).toBe(true);
|
|
179
217
|
});
|
|
@@ -185,9 +223,10 @@ describe('TranscriptOwnerCallStatusProvider', () => {
|
|
|
185
223
|
]);
|
|
186
224
|
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
187
225
|
|
|
188
|
-
const result =
|
|
189
|
-
|
|
190
|
-
|
|
226
|
+
const result =
|
|
227
|
+
await provider.listUnansweredOwnerCallEpochSecondsBySessionName(
|
|
228
|
+
new Map([[sessionName, transcriptPath]]),
|
|
229
|
+
);
|
|
191
230
|
|
|
192
231
|
expect(result.has(sessionName)).toBe(true);
|
|
193
232
|
});
|
|
@@ -199,9 +238,10 @@ describe('TranscriptOwnerCallStatusProvider', () => {
|
|
|
199
238
|
]);
|
|
200
239
|
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
201
240
|
|
|
202
|
-
const result =
|
|
203
|
-
|
|
204
|
-
|
|
241
|
+
const result =
|
|
242
|
+
await provider.listUnansweredOwnerCallEpochSecondsBySessionName(
|
|
243
|
+
new Map([[sessionName, transcriptPath]]),
|
|
244
|
+
);
|
|
205
245
|
|
|
206
246
|
expect(result.has(sessionName)).toBe(true);
|
|
207
247
|
});
|
|
@@ -214,9 +254,10 @@ describe('TranscriptOwnerCallStatusProvider', () => {
|
|
|
214
254
|
]);
|
|
215
255
|
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
216
256
|
|
|
217
|
-
const result =
|
|
218
|
-
|
|
219
|
-
|
|
257
|
+
const result =
|
|
258
|
+
await provider.listUnansweredOwnerCallEpochSecondsBySessionName(
|
|
259
|
+
new Map([[sessionName, transcriptPath]]),
|
|
260
|
+
);
|
|
220
261
|
|
|
221
262
|
expect(result.has(sessionName)).toBe(false);
|
|
222
263
|
});
|
|
@@ -228,9 +269,10 @@ describe('TranscriptOwnerCallStatusProvider', () => {
|
|
|
228
269
|
]);
|
|
229
270
|
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
230
271
|
|
|
231
|
-
const result =
|
|
232
|
-
|
|
233
|
-
|
|
272
|
+
const result =
|
|
273
|
+
await provider.listUnansweredOwnerCallEpochSecondsBySessionName(
|
|
274
|
+
new Map([[sessionName, transcriptPath]]),
|
|
275
|
+
);
|
|
234
276
|
|
|
235
277
|
expect(result.has(sessionName)).toBe(true);
|
|
236
278
|
});
|
|
@@ -242,9 +284,10 @@ describe('TranscriptOwnerCallStatusProvider', () => {
|
|
|
242
284
|
]);
|
|
243
285
|
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
244
286
|
|
|
245
|
-
const result =
|
|
246
|
-
|
|
247
|
-
|
|
287
|
+
const result =
|
|
288
|
+
await provider.listUnansweredOwnerCallEpochSecondsBySessionName(
|
|
289
|
+
new Map([[sessionName, transcriptPath]]),
|
|
290
|
+
);
|
|
248
291
|
|
|
249
292
|
expect(result.has(sessionName)).toBe(true);
|
|
250
293
|
});
|
|
@@ -256,9 +299,10 @@ describe('TranscriptOwnerCallStatusProvider', () => {
|
|
|
256
299
|
]);
|
|
257
300
|
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
258
301
|
|
|
259
|
-
const result =
|
|
260
|
-
|
|
261
|
-
|
|
302
|
+
const result =
|
|
303
|
+
await provider.listUnansweredOwnerCallEpochSecondsBySessionName(
|
|
304
|
+
new Map([[sessionName, transcriptPath]]),
|
|
305
|
+
);
|
|
262
306
|
|
|
263
307
|
expect(result.has(sessionName)).toBe(false);
|
|
264
308
|
});
|
|
@@ -271,9 +315,10 @@ describe('TranscriptOwnerCallStatusProvider', () => {
|
|
|
271
315
|
]);
|
|
272
316
|
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
273
317
|
|
|
274
|
-
const result =
|
|
275
|
-
|
|
276
|
-
|
|
318
|
+
const result =
|
|
319
|
+
await provider.listUnansweredOwnerCallEpochSecondsBySessionName(
|
|
320
|
+
new Map([[sessionName, transcriptPath]]),
|
|
321
|
+
);
|
|
277
322
|
|
|
278
323
|
expect(result.has(sessionName)).toBe(false);
|
|
279
324
|
});
|
|
@@ -285,23 +330,25 @@ describe('TranscriptOwnerCallStatusProvider', () => {
|
|
|
285
330
|
]);
|
|
286
331
|
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
287
332
|
|
|
288
|
-
const result =
|
|
289
|
-
|
|
290
|
-
|
|
333
|
+
const result =
|
|
334
|
+
await provider.listUnansweredOwnerCallEpochSecondsBySessionName(
|
|
335
|
+
new Map([[sessionName, transcriptPath]]),
|
|
336
|
+
);
|
|
291
337
|
|
|
292
338
|
expect(result.has(sessionName)).toBe(false);
|
|
293
339
|
});
|
|
294
340
|
|
|
295
|
-
it('returns an empty
|
|
341
|
+
it('returns an empty map when the marker is null', async () => {
|
|
296
342
|
const transcriptPath = writeTranscript('workbench.jsonl', [
|
|
297
343
|
ownerReply('2026-06-27T10:00:00.000Z'),
|
|
298
344
|
assistantWithMarker('2026-06-27T10:05:00.000Z'),
|
|
299
345
|
]);
|
|
300
346
|
const provider = new TranscriptOwnerCallStatusProvider(null);
|
|
301
347
|
|
|
302
|
-
const result =
|
|
303
|
-
|
|
304
|
-
|
|
348
|
+
const result =
|
|
349
|
+
await provider.listUnansweredOwnerCallEpochSecondsBySessionName(
|
|
350
|
+
new Map([[sessionName, transcriptPath]]),
|
|
351
|
+
);
|
|
305
352
|
|
|
306
353
|
expect(result.size).toBe(0);
|
|
307
354
|
});
|
|
@@ -309,9 +356,10 @@ describe('TranscriptOwnerCallStatusProvider', () => {
|
|
|
309
356
|
it('ignores a session whose transcript file is missing', async () => {
|
|
310
357
|
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
311
358
|
|
|
312
|
-
const result =
|
|
313
|
-
|
|
314
|
-
|
|
359
|
+
const result =
|
|
360
|
+
await provider.listUnansweredOwnerCallEpochSecondsBySessionName(
|
|
361
|
+
new Map([[sessionName, path.join(rootDirectory, 'missing.jsonl')]]),
|
|
362
|
+
);
|
|
315
363
|
|
|
316
364
|
expect(result.size).toBe(0);
|
|
317
365
|
});
|
|
@@ -328,9 +376,10 @@ describe('TranscriptOwnerCallStatusProvider', () => {
|
|
|
328
376
|
);
|
|
329
377
|
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
330
378
|
|
|
331
|
-
const result =
|
|
332
|
-
|
|
333
|
-
|
|
379
|
+
const result =
|
|
380
|
+
await provider.listUnansweredOwnerCallEpochSecondsBySessionName(
|
|
381
|
+
new Map([[sessionName, filePath]]),
|
|
382
|
+
);
|
|
334
383
|
|
|
335
384
|
expect(result.has(sessionName)).toBe(true);
|
|
336
385
|
});
|
|
@@ -348,9 +397,10 @@ describe('TranscriptOwnerCallStatusProvider', () => {
|
|
|
348
397
|
]);
|
|
349
398
|
const provider = new TranscriptOwnerCallStatusProvider('<call-to-user>');
|
|
350
399
|
|
|
351
|
-
const result =
|
|
352
|
-
|
|
353
|
-
|
|
400
|
+
const result =
|
|
401
|
+
await provider.listUnansweredOwnerCallEpochSecondsBySessionName(
|
|
402
|
+
new Map([[sessionName, transcriptPath]]),
|
|
403
|
+
);
|
|
354
404
|
|
|
355
405
|
expect(result.has(sessionName)).toBe(true);
|
|
356
406
|
});
|
|
@@ -91,31 +91,41 @@ const hasOwnerTextReply = (content: unknown): boolean => {
|
|
|
91
91
|
export class TranscriptOwnerCallStatusProvider implements OwnerCallStatusProvider {
|
|
92
92
|
constructor(private readonly ownerCallMarker: string | null) {}
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
listUnansweredOwnerCallEpochSecondsBySessionName = async (
|
|
95
95
|
transcriptPathBySessionName: Map<string, string>,
|
|
96
|
-
): Promise<
|
|
97
|
-
const
|
|
96
|
+
): Promise<Map<string, number>> => {
|
|
97
|
+
const unansweredOwnerCallEpochSecondsBySessionName = new Map<
|
|
98
|
+
string,
|
|
99
|
+
number
|
|
100
|
+
>();
|
|
98
101
|
if (this.ownerCallMarker === null || this.ownerCallMarker.length === 0) {
|
|
99
|
-
return
|
|
102
|
+
return unansweredOwnerCallEpochSecondsBySessionName;
|
|
100
103
|
}
|
|
101
104
|
const marker = this.ownerCallMarker;
|
|
102
105
|
for (const [sessionName, transcriptPath] of transcriptPathBySessionName) {
|
|
103
|
-
|
|
104
|
-
|
|
106
|
+
const unansweredOwnerCallEpochMs = this.findUnansweredOwnerCallEpochMs(
|
|
107
|
+
transcriptPath,
|
|
108
|
+
marker,
|
|
109
|
+
);
|
|
110
|
+
if (unansweredOwnerCallEpochMs !== null) {
|
|
111
|
+
unansweredOwnerCallEpochSecondsBySessionName.set(
|
|
112
|
+
sessionName,
|
|
113
|
+
Math.floor(unansweredOwnerCallEpochMs / 1000),
|
|
114
|
+
);
|
|
105
115
|
}
|
|
106
116
|
}
|
|
107
|
-
return
|
|
117
|
+
return unansweredOwnerCallEpochSecondsBySessionName;
|
|
108
118
|
};
|
|
109
119
|
|
|
110
|
-
private
|
|
120
|
+
private findUnansweredOwnerCallEpochMs = (
|
|
111
121
|
transcriptPath: string,
|
|
112
122
|
marker: string,
|
|
113
|
-
):
|
|
123
|
+
): number | null => {
|
|
114
124
|
let content: string;
|
|
115
125
|
try {
|
|
116
126
|
content = fs.readFileSync(transcriptPath, 'utf8');
|
|
117
127
|
} catch {
|
|
118
|
-
return
|
|
128
|
+
return null;
|
|
119
129
|
}
|
|
120
130
|
let lastOwnerCallEpochMs: number | null = null;
|
|
121
131
|
let lastOwnerReplyEpochMs: number | null = null;
|
|
@@ -155,11 +165,11 @@ export class TranscriptOwnerCallStatusProvider implements OwnerCallStatusProvide
|
|
|
155
165
|
}
|
|
156
166
|
}
|
|
157
167
|
if (lastOwnerCallEpochMs === null) {
|
|
158
|
-
return
|
|
168
|
+
return null;
|
|
159
169
|
}
|
|
160
|
-
return
|
|
161
|
-
lastOwnerReplyEpochMs === null ||
|
|
170
|
+
return lastOwnerReplyEpochMs === null ||
|
|
162
171
|
lastOwnerCallEpochMs > lastOwnerReplyEpochMs
|
|
163
|
-
|
|
172
|
+
? lastOwnerCallEpochMs
|
|
173
|
+
: null;
|
|
164
174
|
};
|
|
165
175
|
}
|
|
@@ -30,7 +30,7 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
30
30
|
expect(section).toContain(
|
|
31
31
|
'Always work proactively and stay ahead of the work',
|
|
32
32
|
);
|
|
33
|
-
expect(section).toContain('
|
|
33
|
+
expect(section).toContain('rather than waiting passively');
|
|
34
34
|
expect(section).toContain('Self-check now:');
|
|
35
35
|
expect(section).toContain('1.');
|
|
36
36
|
expect(section).toContain('2.');
|
|
@@ -50,7 +50,7 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
50
50
|
const section = composer.composeMainStalledSection(600);
|
|
51
51
|
expect(section).toContain('🔴');
|
|
52
52
|
expect(section).toContain(
|
|
53
|
-
'The content between the markers
|
|
53
|
+
'The content between the markers needs to begin with the 🔴 emoji immediately, with no space after it.',
|
|
54
54
|
);
|
|
55
55
|
expect(section).toContain(
|
|
56
56
|
'complete matching pair — opening marker, content, then closing marker — on a single line with no newline inside the tag.',
|
|
@@ -63,17 +63,17 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
63
63
|
it('requires the owner-call message to be fully self-contained and forbids telling the owner to scroll back', () => {
|
|
64
64
|
const section = composer.composeMainStalledSection(600);
|
|
65
65
|
expect(section).toContain(
|
|
66
|
-
'Make the owner-call message fully self-contained: the owner
|
|
66
|
+
'Make the owner-call message fully self-contained: the owner should be able to understand the whole situation — what happened, what you are asking, and any decision needed — from this single latest owner-call message alone, without reading or scrolling back to earlier messages.',
|
|
67
67
|
);
|
|
68
68
|
expect(section).toContain(
|
|
69
|
-
'
|
|
69
|
+
'Please avoid telling the owner to scroll up, go back, or read previous or above messages; if context is needed, restate it inside the owner-call message itself.',
|
|
70
70
|
);
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
it('instructs the agent to fire the owner-call when an owner request has been completed or answered', () => {
|
|
74
74
|
const section = composer.composeMainStalledSection(600);
|
|
75
75
|
expect(section).toContain(
|
|
76
|
-
'If you have COMPLETED or ANSWERED a request from the owner in this session,
|
|
76
|
+
'If you have COMPLETED or ANSWERED a request from the owner in this session, please fire the owner-call to report the RESULT to the owner',
|
|
77
77
|
);
|
|
78
78
|
expect(section).toContain(
|
|
79
79
|
"completing or answering an owner's requested action is itself a reason to fire the owner-call",
|
|
@@ -83,7 +83,7 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
83
83
|
it('explains that completing an owner request without an owner-call leaves the owner unnotified', () => {
|
|
84
84
|
const section = composer.composeMainStalledSection(600);
|
|
85
85
|
expect(section).toContain(
|
|
86
|
-
'Completing or answering an owner request WITHOUT firing the owner-call means the owner is
|
|
86
|
+
'Completing or answering an owner request WITHOUT firing the owner-call means the owner is not notified',
|
|
87
87
|
);
|
|
88
88
|
expect(section).toContain(
|
|
89
89
|
"the owner's app only surfaces this session when the owner-call fires",
|
|
@@ -116,6 +116,81 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
116
116
|
expect(section).not.toContain('""');
|
|
117
117
|
});
|
|
118
118
|
|
|
119
|
+
it('embeds the reminder sentinel in the stale-owner-call main-stalled section', () => {
|
|
120
|
+
const section = composer.composeMainStalledWithStaleOwnerCallSection(
|
|
121
|
+
600,
|
|
122
|
+
3600,
|
|
123
|
+
);
|
|
124
|
+
expect(section).toContain(SILENT_SESSION_REMINDER_SENTINEL);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('renders the stale-owner-call section with the silent and owner-call-age minutes substituted', () => {
|
|
128
|
+
const section = composer.composeMainStalledWithStaleOwnerCallSection(
|
|
129
|
+
659,
|
|
130
|
+
3659,
|
|
131
|
+
);
|
|
132
|
+
expect(section).toContain(
|
|
133
|
+
'You have produced no output for 10 minutes, and the owner call you raised 60 minutes ago is still unanswered.',
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('directs the agent to re-raise its pending ask as a fresh fully self-contained owner call', () => {
|
|
138
|
+
const section = composer.composeMainStalledWithStaleOwnerCallSection(
|
|
139
|
+
600,
|
|
140
|
+
3600,
|
|
141
|
+
);
|
|
142
|
+
expect(section).toContain(
|
|
143
|
+
'If you are still blocked on the owner, re-raise your pending ask NOW as a fresh owner call.',
|
|
144
|
+
);
|
|
145
|
+
expect(section).toContain(
|
|
146
|
+
'Make it fully self-contained: restate the whole situation — what happened, what you are asking, and any decision needed — inside the new owner call itself',
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('directs the agent to continue autonomously when it is no longer blocked', () => {
|
|
151
|
+
const section = composer.composeMainStalledWithStaleOwnerCallSection(
|
|
152
|
+
600,
|
|
153
|
+
3600,
|
|
154
|
+
);
|
|
155
|
+
expect(section).toContain(
|
|
156
|
+
'If you are no longer blocked — the answer became unnecessary, you can proceed safely, or the information arrived another way — resume immediately and drive all remaining tasks to completion.',
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('states that the stale-owner-call reminder itself does not notify the owner', () => {
|
|
161
|
+
const section = composer.composeMainStalledWithStaleOwnerCallSection(
|
|
162
|
+
600,
|
|
163
|
+
3600,
|
|
164
|
+
);
|
|
165
|
+
expect(section).toContain(
|
|
166
|
+
'This reminder does not notify the owner; only a fresh owner call from you surfaces this session to the owner.',
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('interpolates the configured owner-call marker into the stale-owner-call format guidance', () => {
|
|
171
|
+
const markedComposer = new DefaultSilentSessionMessageComposer(
|
|
172
|
+
'<<OWNER_CALL>>',
|
|
173
|
+
);
|
|
174
|
+
const section = markedComposer.composeMainStalledWithStaleOwnerCallSection(
|
|
175
|
+
600,
|
|
176
|
+
3600,
|
|
177
|
+
);
|
|
178
|
+
expect(section).toContain(
|
|
179
|
+
'the configured owner-call marker tag "<<OWNER_CALL>>" as a complete matching pair',
|
|
180
|
+
);
|
|
181
|
+
expect(section).toContain('🔴');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('composes the stale-owner-call section distinctly from the plain main-stalled section', () => {
|
|
185
|
+
const plainSection = composer.composeMainStalledSection(600);
|
|
186
|
+
const staleSection = composer.composeMainStalledWithStaleOwnerCallSection(
|
|
187
|
+
600,
|
|
188
|
+
3600,
|
|
189
|
+
);
|
|
190
|
+
expect(staleSection).not.toBe(plainSection);
|
|
191
|
+
expect(plainSection).not.toContain('is still unanswered');
|
|
192
|
+
});
|
|
193
|
+
|
|
119
194
|
it('emits a distinct idle message for a sub-agent that is only output-idle', () => {
|
|
120
195
|
const section = composer.composeSubAgentSection({
|
|
121
196
|
idleSubAgents: [
|
|
@@ -151,7 +226,7 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
151
226
|
"The system has already detected, from each sub-process's last tool activity, that it has produced no output for about the minutes shown below.",
|
|
152
227
|
);
|
|
153
228
|
expect(section).toContain(
|
|
154
|
-
'
|
|
229
|
+
'Please treat that figure as the authoritative system signal; there is no need to spend effort re-deriving whether the sub-process is alive.',
|
|
155
230
|
);
|
|
156
231
|
expect(section).toContain('no output for 6m');
|
|
157
232
|
});
|
|
@@ -172,7 +247,7 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
172
247
|
'Around five minutes of silence is a real warning of a possible hang',
|
|
173
248
|
);
|
|
174
249
|
expect(section).toContain(
|
|
175
|
-
'
|
|
250
|
+
'please base your assessment on concrete evidence rather than speculation ("probably still working"), and please keep the warning in view until evidence resolves it.',
|
|
176
251
|
);
|
|
177
252
|
});
|
|
178
253
|
|
|
@@ -216,7 +291,7 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
216
291
|
longRunningSubAgents: [],
|
|
217
292
|
});
|
|
218
293
|
expect(section).toContain(
|
|
219
|
-
'Owner notification is not required, but
|
|
294
|
+
'Owner notification is not required, but please output your investigation result in this session so it remains as a log.',
|
|
220
295
|
);
|
|
221
296
|
});
|
|
222
297
|
|
|
@@ -307,6 +382,39 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
307
382
|
expect(longRunningSection).not.toContain('idle-only');
|
|
308
383
|
});
|
|
309
384
|
|
|
385
|
+
it('composes default texts free of prohibition-styled vocabulary that trips model safety classifiers', () => {
|
|
386
|
+
const flaggedPatterns = [
|
|
387
|
+
/unacceptable/i,
|
|
388
|
+
/\bnever\b/i,
|
|
389
|
+
/\b[Dd]o NOT\b/,
|
|
390
|
+
/\b[Yy]ou MUST\b/,
|
|
391
|
+
];
|
|
392
|
+
const markedComposer = new DefaultSilentSessionMessageComposer(
|
|
393
|
+
'<<OWNER_CALL>>',
|
|
394
|
+
);
|
|
395
|
+
const subAgent = {
|
|
396
|
+
label: 'sub-process-1',
|
|
397
|
+
silentSeconds: 360,
|
|
398
|
+
runningSeconds: 1200,
|
|
399
|
+
waitingOnExternalProcess: false,
|
|
400
|
+
};
|
|
401
|
+
const composedDefaultTexts = [
|
|
402
|
+
composer.composeMainStalledSection(600),
|
|
403
|
+
markedComposer.composeMainStalledSection(600),
|
|
404
|
+
composer.composeMainStalledWithStaleOwnerCallSection(600, 3600),
|
|
405
|
+
markedComposer.composeMainStalledWithStaleOwnerCallSection(600, 3600),
|
|
406
|
+
composer.composeSubAgentSection({
|
|
407
|
+
idleSubAgents: [subAgent],
|
|
408
|
+
longRunningSubAgents: [subAgent],
|
|
409
|
+
}),
|
|
410
|
+
];
|
|
411
|
+
for (const text of composedDefaultTexts) {
|
|
412
|
+
for (const pattern of flaggedPatterns) {
|
|
413
|
+
expect(text).not.toMatch(pattern);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
310
418
|
it('does not contain any host-specific or internal identifiers', () => {
|
|
311
419
|
const mainSection = composer.composeMainStalledSection(600);
|
|
312
420
|
const subAgent = {
|
|
@@ -20,9 +20,9 @@ const composeIdleSubAgentSection = (
|
|
|
20
20
|
)}`,
|
|
21
21
|
);
|
|
22
22
|
return [
|
|
23
|
-
`${SILENT_SESSION_REMINDER_SENTINEL} The system has already detected, from each sub-process's last tool activity, that it has produced no output for about the minutes shown below.
|
|
23
|
+
`${SILENT_SESSION_REMINDER_SENTINEL} The system has already detected, from each sub-process's last tool activity, that it has produced no output for about the minutes shown below. Please treat that figure as the authoritative system signal; there is no need to spend effort re-deriving whether the sub-process is alive. Around five minutes of silence is a real warning of a possible hang: please base your assessment on concrete evidence rather than speculation ("probably still working"), and please keep the warning in view until evidence resolves it.`,
|
|
24
24
|
...lines,
|
|
25
|
-
'For each one, determine the CAUSE by a concrete check and report the result. Verify specifically: (a) whether there is genuinely no recent activity anywhere — including a very recent push or commit, or output from any nested sub-processes this sub-process itself started; and (b) whether it is legitimately blocked waiting on an external dependency (a continuous-integration run, an external API, or another process). Waiting on continuous integration is plausible, but confirm it by investigation before concluding — do not assume. Based on the check: if it is genuinely stuck, take action (restart, hand off, or replace it); if it is legitimately waiting, state that conclusion together with the concrete evidence you found. Owner notification is not required, but
|
|
25
|
+
'For each one, determine the CAUSE by a concrete check and report the result. Verify specifically: (a) whether there is genuinely no recent activity anywhere — including a very recent push or commit, or output from any nested sub-processes this sub-process itself started; and (b) whether it is legitimately blocked waiting on an external dependency (a continuous-integration run, an external API, or another process). Waiting on continuous integration is plausible, but confirm it by investigation before concluding — do not assume. Based on the check: if it is genuinely stuck, take action (restart, hand off, or replace it); if it is legitimately waiting, state that conclusion together with the concrete evidence you found. Owner notification is not required, but please output your investigation result in this session so it remains as a log.',
|
|
26
26
|
].join('\n');
|
|
27
27
|
};
|
|
28
28
|
|
|
@@ -49,7 +49,7 @@ export const composeOwnerCallFormatGuidance = (
|
|
|
49
49
|
ownerCallMarker !== null && ownerCallMarker.length > 0
|
|
50
50
|
? `Emit the owner-call as the configured owner-call marker tag "${ownerCallMarker}" as a complete matching pair — opening marker, content, then closing marker — on a single line with no newline inside the tag.`
|
|
51
51
|
: `Emit the owner-call as the configured owner-call marker tag as a complete matching pair — opening marker, content, then closing marker — on a single line with no newline inside the tag.`;
|
|
52
|
-
return `${markerInstruction} The content between the markers
|
|
52
|
+
return `${markerInstruction} The content between the markers needs to begin with the 🔴 emoji immediately, with no space after it. The owner's app only surfaces the notification together with its content when the exact, well-formed tag with the leading 🔴 is present; a malformed tag (a broken or missing closing marker, or a missing leading 🔴) results in only a red indicator with no readable content. Make the owner-call message fully self-contained: the owner should be able to understand the whole situation — what happened, what you are asking, and any decision needed — from this single latest owner-call message alone, without reading or scrolling back to earlier messages. Please avoid telling the owner to scroll up, go back, or read previous or above messages; if context is needed, restate it inside the owner-call message itself.`;
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
const composeMainStalledMessage = (
|
|
@@ -58,15 +58,30 @@ const composeMainStalledMessage = (
|
|
|
58
58
|
): string => {
|
|
59
59
|
const minutes = Math.floor(mainSilentSeconds / 60);
|
|
60
60
|
return [
|
|
61
|
-
`${SILENT_SESSION_REMINDER_SENTINEL} You have produced no output for ${minutes} minutes. Idle waiting wastes this live session
|
|
61
|
+
`${SILENT_SESSION_REMINDER_SENTINEL} You have produced no output for ${minutes} minutes. Idle waiting wastes this live session, so please keep making progress. Always work proactively and stay ahead of the work: anticipate the next steps and, at every point, choose the fastest path so the whole task finishes as early as possible, rather than waiting passively. Finish every task in the shortest possible time — but "fastest" means correct and incident-free, not merely quick: a fast but wrong result is worthless and causes incidents, so be fast without breaking things. Use parallel execution and your whole team of sub-agents to minimize total wall-clock time. Your goal is to drive every task to completion and have the owner confirm that all tasks are done. Please leave this session open rather than closing it on your own — it is closed only after the owner has verified completion and asks you to close it. Self-check now:`,
|
|
62
62
|
`1. Every request from the owner is registered as a session task and your task list is kept current (mark tasks completed when done); verify nothing is missing or stale.`,
|
|
63
63
|
`2. Your plan is the fastest correct path: parallelize independent work across sub-agents, delegate, and remove needless serialization. Choose the fastest safe method, not the easiest.`,
|
|
64
64
|
`3. A monitor is in place that detects when any sub-agent produces no output for 5 minutes.`,
|
|
65
|
-
`4. If you are blocked on an owner decision — or once you believe all tasks are complete — do not wait passively: the owner is not notified of passive waiting and will not reply unless you raise a new call-to-user, so raise one now (to get the decision, or to ask the owner to verify completion). If you have COMPLETED or ANSWERED a request from the owner in this session,
|
|
65
|
+
`4. If you are blocked on an owner decision — or once you believe all tasks are complete — do not wait passively: the owner is not notified of passive waiting and will not reply unless you raise a new call-to-user, so raise one now (to get the decision, or to ask the owner to verify completion). If you have COMPLETED or ANSWERED a request from the owner in this session, please fire the owner-call to report the RESULT to the owner: completing or answering an owner's requested action is itself a reason to fire the owner-call. Completing or answering an owner request WITHOUT firing the owner-call means the owner is not notified — the owner's app only surfaces this session when the owner-call fires — so the task silently stalls. If this self-check reminder keeps arriving, it is likely because an owner request was completed or answered without firing the owner-call; fire the owner-call now to report the result to the owner. ${composeOwnerCallFormatGuidance(ownerCallMarker)} If no owner input is needed yet, resume immediately and drive all remaining tasks to completion.`,
|
|
66
66
|
`Also, in your next output, report an estimate of how many minutes you expect to need to finish all remaining tasks.`,
|
|
67
67
|
].join('\n');
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
+
const composeMainStalledWithStaleOwnerCallMessage = (
|
|
71
|
+
mainSilentSeconds: number,
|
|
72
|
+
unansweredOwnerCallAgeSeconds: number,
|
|
73
|
+
ownerCallMarker: string | null,
|
|
74
|
+
): string => {
|
|
75
|
+
const silentMinutes = Math.floor(mainSilentSeconds / 60);
|
|
76
|
+
const ownerCallAgeMinutes = Math.floor(unansweredOwnerCallAgeSeconds / 60);
|
|
77
|
+
return [
|
|
78
|
+
`${SILENT_SESSION_REMINDER_SENTINEL} You have produced no output for ${silentMinutes} minutes, and the owner call you raised ${ownerCallAgeMinutes} minutes ago is still unanswered. An owner call outstanding this long may have been missed, may have scrolled out of the owner's view, or may no longer reflect your current situation, so do not keep waiting on it passively. Act now, choosing one of the following:`,
|
|
79
|
+
`1. If you are still blocked on the owner, re-raise your pending ask NOW as a fresh owner call. Make it fully self-contained: restate the whole situation — what happened, what you are asking, and any decision needed — inside the new owner call itself, so the owner understands everything from that single latest message without scrolling back. ${composeOwnerCallFormatGuidance(ownerCallMarker)}`,
|
|
80
|
+
`2. If you are no longer blocked — the answer became unnecessary, you can proceed safely, or the information arrived another way — resume immediately and drive all remaining tasks to completion.`,
|
|
81
|
+
`This reminder does not notify the owner; only a fresh owner call from you surfaces this session to the owner. Also, in your next output, report an estimate of how many minutes you expect to need to finish all remaining tasks.`,
|
|
82
|
+
].join('\n');
|
|
83
|
+
};
|
|
84
|
+
|
|
70
85
|
export class DefaultSilentSessionMessageComposer implements SilentSessionMessageComposer {
|
|
71
86
|
constructor(private readonly ownerCallMarker: string | null = null) {}
|
|
72
87
|
|
|
@@ -74,6 +89,17 @@ export class DefaultSilentSessionMessageComposer implements SilentSessionMessage
|
|
|
74
89
|
return composeMainStalledMessage(mainSilentSeconds, this.ownerCallMarker);
|
|
75
90
|
};
|
|
76
91
|
|
|
92
|
+
composeMainStalledWithStaleOwnerCallSection = (
|
|
93
|
+
mainSilentSeconds: number,
|
|
94
|
+
unansweredOwnerCallAgeSeconds: number,
|
|
95
|
+
): string => {
|
|
96
|
+
return composeMainStalledWithStaleOwnerCallMessage(
|
|
97
|
+
mainSilentSeconds,
|
|
98
|
+
unansweredOwnerCallAgeSeconds,
|
|
99
|
+
this.ownerCallMarker,
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
77
103
|
composeSubAgentSection = (stallSections: SubAgentStallSections): string => {
|
|
78
104
|
const sections: string[] = [];
|
|
79
105
|
if (stallSections.idleSubAgents.length > 0) {
|