@dyyz1993/pi-coding-agent 0.74.45 → 0.74.47

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.
Files changed (33) hide show
  1. package/dist/core/agent-session.d.ts.map +1 -1
  2. package/dist/core/agent-session.js +13 -0
  3. package/dist/core/agent-session.js.map +1 -1
  4. package/dist/extensions/auto-memory/__tests__/extract-result.test.ts +42 -0
  5. package/dist/extensions/auto-memory/__tests__/prefetch-history.test.ts +136 -0
  6. package/dist/extensions/auto-memory/__tests__/prompts.test.ts +29 -0
  7. package/dist/extensions/auto-memory/__tests__/skip-rules.test.ts +366 -0
  8. package/dist/extensions/auto-memory/contract.d.ts +16 -0
  9. package/dist/extensions/auto-memory/contract.d.ts.map +1 -1
  10. package/dist/extensions/auto-memory/contract.js.map +1 -1
  11. package/dist/extensions/auto-memory/contract.ts +16 -0
  12. package/dist/extensions/auto-memory/index.ts +134 -13
  13. package/dist/extensions/auto-memory/prompts.ts +10 -0
  14. package/dist/extensions/auto-memory/skip-rules.ts +2 -0
  15. package/dist/extensions/bash-ext/index.ts +855 -845
  16. package/dist/extensions/claude-hooks-compat/index.ts +12 -7
  17. package/dist/extensions/coordinator/handler.test.ts +388 -123
  18. package/dist/extensions/coordinator/handler.ts +78 -12
  19. package/dist/extensions/coordinator/index.ts +267 -198
  20. package/dist/extensions/coordinator/types.d.ts +16 -0
  21. package/dist/extensions/coordinator/types.d.ts.map +1 -1
  22. package/dist/extensions/coordinator/types.js.map +1 -1
  23. package/dist/extensions/coordinator/types.ts +57 -49
  24. package/dist/extensions/lsp/lsp/index.ts +15 -9
  25. package/dist/extensions/lsp/lsp/lsp-clangd-e2e.test.ts +229 -0
  26. package/dist/extensions/message-bridge/index.ts +14 -11
  27. package/dist/extensions/session-supervisor/index.ts +14 -8
  28. package/dist/extensions/subagent-v2/index.ts +58 -42
  29. package/dist/extensions/todo-ext/index.ts +7 -3
  30. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  31. package/dist/modes/rpc/rpc-mode.js +9 -1
  32. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  33. package/package.json +1 -1
@@ -123,13 +123,18 @@ export default function (pi: ExtensionAPI) {
123
123
  if (isAsync && hookEventName === "PreToolUse") {
124
124
  const runner = getCallLLM(pi);
125
125
  runHandler(handler, stdinData, ctx, runner).then((output) => {
126
- const result = interpretHookOutput(output);
127
- if (handler.asyncRewake && output.exitCode === 2 && result.reason) {
128
- pi.sendMessage({
129
- customType: "hook_async_block",
130
- content: result.reason,
131
- display: true,
132
- });
126
+ try {
127
+ const result = interpretHookOutput(output);
128
+ if (handler.asyncRewake && output.exitCode === 2 && result.reason) {
129
+ pi.sendMessage({
130
+ customType: "hook_async_block",
131
+ content: result.reason,
132
+ display: true,
133
+ });
134
+ }
135
+ } catch (e) {
136
+ const msg = e instanceof Error ? e.message : String(e);
137
+ if (!/stale/i.test(msg)) console.debug("[hooks-compat] async handler failed:", msg);
133
138
  }
134
139
  });
135
140
  continue;
@@ -33,7 +33,7 @@ describe("TaskStore.buildPrompt()", () => {
33
33
  const store = new TaskStore(tempDir);
34
34
  const task = makeTask({ title: "Re-activatable task", sessionId: "sess-stopped-1" });
35
35
  store.add(task);
36
- store.update("sess-stopped-1", { status: "stopped" });
36
+ store.update("sess-stopped-1", { status: "stopped", completedAt: Date.now() });
37
37
 
38
38
  const prompt = store.buildPrompt();
39
39
 
@@ -91,187 +91,452 @@ describe("TaskStore.buildPrompt()", () => {
91
91
  expect(store.buildPrompt()).toBe("");
92
92
  });
93
93
 
94
- it("BUG: store.remove() is never called by any channel handler or tool", () => {
95
- tempDir = path.join(os.tmpdir(), `coordinator-test-${Date.now()}`);
96
- fs.mkdirSync(tempDir, { recursive: true });
97
-
94
+ it("session_delegate_remove handler calls store.remove() to clean up tasks", () => {
98
95
  const handlerSource = fs.readFileSync(
99
96
  path.join(__dirname, "handler.ts"),
100
97
  "utf-8",
101
98
  );
102
99
 
103
- const indexSource = fs.readFileSync(
104
- path.join(__dirname, "index.ts"),
105
- "utf-8",
106
- );
107
-
108
- expect(
109
- handlerSource.includes("remove("),
110
- "TaskStore.remove() method exists in handler.ts",
111
- ).toBe(true);
112
-
113
- const handlerBodies: string[] = [];
114
- const handlePattern = /channel\.handle\("([^"]+)",\s*async\s*\([^)]*\)\s*=>\s*\{/g;
115
- let match;
116
- while ((match = handlePattern.exec(handlerSource)) !== null) {
117
- const startIdx = match.index + match[0].length;
118
- let braceCount = 1;
119
- let endIdx = startIdx;
120
- while (braceCount > 0 && endIdx < handlerSource.length) {
121
- if (handlerSource[endIdx] === "{") braceCount++;
122
- else if (handlerSource[endIdx] === "}") braceCount--;
123
- endIdx++;
124
- }
125
- handlerBodies.push(handlerSource.slice(startIdx, endIdx));
126
- }
127
-
128
- for (const body of handlerBodies) {
129
- expect(
130
- body.includes("remove("),
131
- "No channel.handle() body should call remove()",
132
- ).toBe(false);
133
- }
134
-
135
- expect(
136
- indexSource.includes(".remove("),
137
- "BUG: index.ts never calls store.remove() - no tool/command exposes removal",
138
- ).toBe(false);
100
+ const removeHandlerStart = handlerSource.indexOf('channel.handle("session_delegate_remove"');
101
+ expect(removeHandlerStart).toBeGreaterThan(-1);
102
+ const handlerBlock = handlerSource.slice(removeHandlerStart);
103
+ expect(handlerBlock).toContain("store.remove(sessionId)");
139
104
  });
140
105
 
141
- it("session_delegate_stop only sets status=stopped, does not remove task", () => {
106
+ it("session_delegate_stop sets status=stopped with completedAt timestamp", () => {
142
107
  tempDir = path.join(os.tmpdir(), `coordinator-test-${Date.now()}`);
143
108
  fs.mkdirSync(tempDir, { recursive: true });
144
109
 
145
110
  const store = new TaskStore(tempDir);
146
111
  store.add(makeTask({ sessionId: "sess-stop-test", title: "Accumulated task" }));
147
- store.update("sess-stop-test", { status: "stopped" });
112
+ store.update("sess-stop-test", { status: "stopped", completedAt: Date.now() });
148
113
 
149
114
  const task = store.get("sess-stop-test");
150
115
  expect(task).toBeDefined();
151
116
  expect(task?.status).toBe("stopped");
117
+ expect(task?.completedAt).toBeDefined();
152
118
 
153
119
  const prompt = store.buildPrompt();
154
120
  expect(prompt).toContain("Accumulated task");
155
121
  expect(prompt).toContain("STOPPED");
156
122
  });
123
+ });
157
124
 
158
- it("BUG: no tool or command exposes remove functionality to LLM or user", () => {
159
- const indexSource = fs.readFileSync(
160
- path.join(__dirname, "index.ts"),
161
- "utf-8",
162
- );
125
+ describe("TaskStore.clearStopped()", () => {
126
+ let tempDir: string;
163
127
 
164
- const registeredTools: string[] = [];
165
- const toolPattern = /pi\.registerTool\(\{[^}]*name:\s*"([^"]+)"/g;
166
- let match;
167
- while ((match = toolPattern.exec(indexSource)) !== null) {
168
- registeredTools.push(match[1]);
128
+ afterEach(() => {
129
+ if (tempDir && fs.existsSync(tempDir)) {
130
+ fs.rmSync(tempDir, { recursive: true, force: true });
169
131
  }
132
+ });
170
133
 
171
- const registeredCommands: string[] = [];
172
- const cmdPattern = /pi\.registerCommand\(\s*"([^"]+)"/g;
173
- while ((match = cmdPattern.exec(indexSource)) !== null) {
174
- registeredCommands.push(match[1]);
175
- }
134
+ it("removes all stopped and completed tasks", () => {
135
+ tempDir = path.join(os.tmpdir(), `coordinator-test-${Date.now()}`);
136
+ fs.mkdirSync(tempDir, { recursive: true });
137
+
138
+ const store = new TaskStore(tempDir);
139
+ store.add(makeTask({ sessionId: "sess-idle", title: "Active task", status: "idle" }));
140
+ store.add(makeTask({ sessionId: "sess-stopped", title: "Stopped task", status: "stopped" }));
141
+ store.add(makeTask({ sessionId: "sess-completed", title: "Completed task", status: "completed" }));
142
+ store.add(makeTask({ sessionId: "sess-streaming", title: "Streaming task", status: "streaming" }));
143
+
144
+ const removed = store.clearStopped();
145
+
146
+ expect(removed).toBe(2);
147
+ expect(store.list().length).toBe(2);
148
+ expect(store.list().map((t) => t.sessionId)).toEqual(
149
+ expect.arrayContaining(["sess-idle", "sess-streaming"]),
150
+ );
151
+ });
176
152
 
177
- const removalToolNames = [
178
- "session_delegate_remove",
179
- "session_delegate_cleanup",
180
- "session_delegate_forget",
181
- "session_delegate_delete",
182
- ];
183
-
184
- for (const name of removalToolNames) {
185
- expect(
186
- registeredTools.includes(name),
187
- `BUG: No tool named "${name}" is registered`,
188
- ).toBe(false);
153
+ it("returns 0 when no tasks are stopped or completed", () => {
154
+ tempDir = path.join(os.tmpdir(), `coordinator-test-${Date.now()}`);
155
+ fs.mkdirSync(tempDir, { recursive: true });
156
+
157
+ const store = new TaskStore(tempDir);
158
+ store.add(makeTask({ sessionId: "sess-idle", status: "idle" }));
159
+ store.add(makeTask({ sessionId: "sess-streaming", status: "streaming" }));
160
+
161
+ expect(store.clearStopped()).toBe(0);
162
+ expect(store.list().length).toBe(2);
163
+ });
164
+
165
+ it("persists removal to disk", () => {
166
+ tempDir = path.join(os.tmpdir(), `coordinator-test-${Date.now()}`);
167
+ fs.mkdirSync(tempDir, { recursive: true });
168
+
169
+ const store1 = new TaskStore(tempDir);
170
+ store1.add(makeTask({ sessionId: "sess-a", status: "stopped" }));
171
+ store1.add(makeTask({ sessionId: "sess-b", status: "idle" }));
172
+ store1.clearStopped();
173
+
174
+ const store2 = new TaskStore(tempDir);
175
+ expect(store2.list().length).toBe(1);
176
+ expect(store2.list()[0].sessionId).toBe("sess-b");
177
+ });
178
+ });
179
+
180
+ describe("TaskStore.add() sessionId guard", () => {
181
+ let tempDir: string;
182
+
183
+ afterEach(() => {
184
+ if (tempDir && fs.existsSync(tempDir)) {
185
+ fs.rmSync(tempDir, { recursive: true, force: true });
189
186
  }
187
+ });
190
188
 
191
- const removalCmdPatterns = [
192
- "coordinator-cleanup",
193
- "coordinator-remove",
194
- "coordinator-forget",
195
- "coordinator-clear",
196
- ];
197
-
198
- for (const name of removalCmdPatterns) {
199
- expect(
200
- registeredCommands.includes(name),
201
- `BUG: No command named "${name}" is registered`,
202
- ).toBe(false);
189
+ it("throws on empty sessionId", () => {
190
+ tempDir = path.join(os.tmpdir(), `coordinator-test-${Date.now()}`);
191
+ fs.mkdirSync(tempDir, { recursive: true });
192
+
193
+ const store = new TaskStore(tempDir);
194
+ expect(() => store.add(makeTask({ sessionId: "" }))).toThrow("cannot add task with empty sessionId");
195
+ });
196
+
197
+ it("throws on undefined sessionId when cast", () => {
198
+ tempDir = path.join(os.tmpdir(), `coordinator-test-${Date.now()}`);
199
+ fs.mkdirSync(tempDir, { recursive: true });
200
+
201
+ const store = new TaskStore(tempDir);
202
+ expect(() => store.add(makeTask({ sessionId: undefined as unknown as string }))).toThrow("cannot add task with empty sessionId");
203
+ });
204
+ });
205
+
206
+ describe("TaskStore.buildPrompt() stale task filtering", () => {
207
+ let tempDir: string;
208
+
209
+ afterEach(() => {
210
+ if (tempDir && fs.existsSync(tempDir)) {
211
+ fs.rmSync(tempDir, { recursive: true, force: true });
203
212
  }
204
213
  });
205
214
 
206
- it("BUG: no automatic cleanup of tasks whose sessions no longer exist", () => {
215
+ it("hides stopped tasks older than 5 minutes", () => {
207
216
  tempDir = path.join(os.tmpdir(), `coordinator-test-${Date.now()}`);
208
217
  fs.mkdirSync(tempDir, { recursive: true });
209
218
 
210
219
  const store = new TaskStore(tempDir);
220
+ const oldTime = Date.now() - 10 * 60 * 1000; // 10 minutes ago
221
+
211
222
  store.add(makeTask({
212
- sessionId: "ghost-session-999",
213
- title: "Ghost task",
214
- status: "idle",
223
+ sessionId: "sess-old-stopped",
224
+ title: "Old stopped task",
225
+ status: "stopped",
226
+ completedAt: oldTime,
215
227
  }));
216
228
 
217
229
  const prompt = store.buildPrompt();
218
- expect(prompt).toContain("ghost-session-999");
219
- expect(prompt).toContain("Ghost task");
230
+ expect(prompt).toBe("");
231
+ });
232
+
233
+ it("keeps recently stopped tasks", () => {
234
+ tempDir = path.join(os.tmpdir(), `coordinator-test-${Date.now()}`);
235
+ fs.mkdirSync(tempDir, { recursive: true });
236
+
237
+ const store = new TaskStore(tempDir);
238
+ store.add(makeTask({
239
+ sessionId: "sess-recent-stopped",
240
+ title: "Recent stopped task",
241
+ status: "stopped",
242
+ completedAt: Date.now() - 1000, // 1 second ago
243
+ }));
220
244
 
221
- expect(store.list().length).toBe(1);
245
+ const prompt = store.buildPrompt();
246
+ expect(prompt).toContain("Recent stopped task");
222
247
  });
223
248
 
224
- it("BUG: no age-based or context-pressure-based cleanup mechanism", () => {
249
+ it("keeps old stopped tasks without completedAt (no timestamp to check)", () => {
225
250
  tempDir = path.join(os.tmpdir(), `coordinator-test-${Date.now()}`);
226
251
  fs.mkdirSync(tempDir, { recursive: true });
227
252
 
228
253
  const store = new TaskStore(tempDir);
229
- const oldTimestamp = Date.now() - 7 * 24 * 60 * 60 * 1000;
230
-
231
- for (let i = 0; i < 20; i++) {
232
- store.add(makeTask({
233
- sessionId: `sess-old-${i}`,
234
- title: `Old task ${i}`,
235
- dispatchedAt: oldTimestamp + i * 1000,
236
- status: "completed",
237
- completedAt: oldTimestamp + i * 1000 + 5000,
238
- }));
239
- }
254
+ store.add(makeTask({
255
+ sessionId: "sess-no-completedat",
256
+ title: "No completedAt",
257
+ status: "stopped",
258
+ }));
259
+
260
+ const prompt = store.buildPrompt();
261
+ expect(prompt).toContain("No completedAt");
262
+ });
263
+
264
+ it("keeps idle/streaming tasks regardless of age", () => {
265
+ tempDir = path.join(os.tmpdir(), `coordinator-test-${Date.now()}`);
266
+ fs.mkdirSync(tempDir, { recursive: true });
240
267
 
241
- const allTasks = store.list();
242
- expect(allTasks.length).toBe(20);
268
+ const store = new TaskStore(tempDir);
269
+ const oldTime = Date.now() - 10 * 60 * 1000;
270
+
271
+ store.add(makeTask({
272
+ sessionId: "sess-old-idle",
273
+ title: "Old idle task",
274
+ status: "idle",
275
+ dispatchedAt: oldTime,
276
+ }));
243
277
 
244
278
  const prompt = store.buildPrompt();
245
- for (let i = 0; i < 20; i++) {
246
- expect(prompt).toContain(`Old task ${i}`);
247
- }
279
+ expect(prompt).toContain("Old idle task");
280
+ });
281
+ });
248
282
 
283
+ describe("session_delegate_remove and session_delegate_clear_stopped handlers exist", () => {
284
+ it("session_delegate_remove handler is registered in handler.ts", () => {
249
285
  const handlerSource = fs.readFileSync(
250
286
  path.join(__dirname, "handler.ts"),
251
287
  "utf-8",
252
288
  );
289
+ expect(handlerSource).toContain('channel.handle("session_delegate_remove"');
290
+ });
253
291
 
254
- const hasMaxAgeLogic = handlerSource.includes("maxAge") ||
255
- handlerSource.includes("ttl") ||
256
- handlerSource.includes("expiry") ||
257
- handlerSource.includes("staleThreshold") ||
258
- handlerSource.includes("cleanupInterval");
259
- expect(
260
- hasMaxAgeLogic,
261
- "BUG: No age-based cleanup logic (maxAge/ttl/expiry) in handler.ts",
262
- ).toBe(false);
292
+ it("session_delegate_clear_stopped handler is registered in handler.ts", () => {
293
+ const handlerSource = fs.readFileSync(
294
+ path.join(__dirname, "handler.ts"),
295
+ "utf-8",
296
+ );
297
+ expect(handlerSource).toContain('channel.handle("session_delegate_clear_stopped"');
298
+ });
263
299
 
300
+ it("session_delegate_remove and session_delegate_clear_stopped tools are registered in index.ts", () => {
264
301
  const indexSource = fs.readFileSync(
265
302
  path.join(__dirname, "index.ts"),
266
303
  "utf-8",
267
304
  );
268
- const hasContextEviction = indexSource.includes("contextPressure") ||
269
- indexSource.includes("evict") ||
270
- indexSource.includes("tokenBudget") ||
271
- indexSource.includes("maxContextTasks");
272
- expect(
273
- hasContextEviction,
274
- "BUG: No context-pressure-based eviction logic in index.ts",
275
- ).toBe(false);
305
+ expect(indexSource).toContain('name: "session_delegate_remove"');
306
+ expect(indexSource).toContain('name: "session_delegate_clear_stopped"');
307
+ });
308
+
309
+ it("delegate_remove and delegate_clear_stopped are in ProcessManagerApi", () => {
310
+ const handlerSource = fs.readFileSync(
311
+ path.join(__dirname, "handler.ts"),
312
+ "utf-8",
313
+ );
314
+ expect(handlerSource).toContain("delegate_remove(sessionId: string): Promise<boolean>");
315
+ expect(handlerSource).toContain("delegate_clear_stopped(): Promise<number>");
316
+ });
317
+ });
318
+
319
+ // ── Regression tests for bugs found during audit ──
320
+
321
+ describe("buildPrompt() filters completed tasks older than 5 minutes (Bug 2 fix)", () => {
322
+ let tempDir: string;
323
+
324
+ afterEach(() => {
325
+ if (tempDir && fs.existsSync(tempDir)) {
326
+ fs.rmSync(tempDir, { recursive: true, force: true });
327
+ }
328
+ });
329
+
330
+ it("hides completed tasks older than 5 minutes", () => {
331
+ tempDir = path.join(os.tmpdir(), `coordinator-test-${Date.now()}`);
332
+ fs.mkdirSync(tempDir, { recursive: true });
333
+
334
+ const store = new TaskStore(tempDir);
335
+ store.add(makeTask({
336
+ sessionId: "sess-old-completed",
337
+ title: "Old completed task",
338
+ status: "completed",
339
+ completedAt: Date.now() - 10 * 60 * 1000, // 10 minutes ago
340
+ }));
341
+
342
+ expect(store.buildPrompt()).toBe("");
343
+ });
344
+
345
+ it("keeps recently completed tasks (within 5 minutes)", () => {
346
+ tempDir = path.join(os.tmpdir(), `coordinator-test-${Date.now()}`);
347
+ fs.mkdirSync(tempDir, { recursive: true });
348
+
349
+ const store = new TaskStore(tempDir);
350
+ store.add(makeTask({
351
+ sessionId: "sess-recent-completed",
352
+ title: "Recent completed task",
353
+ status: "completed",
354
+ completedAt: Date.now() - 30 * 1000, // 30 seconds ago
355
+ }));
356
+
357
+ const prompt = store.buildPrompt();
358
+ expect(prompt).toContain("Recent completed task");
359
+ expect(prompt).toContain("DONE");
360
+ });
361
+
362
+ it("mixed: old completed hidden, recent completed visible, idle always visible", () => {
363
+ tempDir = path.join(os.tmpdir(), `coordinator-test-${Date.now()}`);
364
+ fs.mkdirSync(tempDir, { recursive: true });
365
+
366
+ const store = new TaskStore(tempDir);
367
+ store.add(makeTask({
368
+ sessionId: "sess-old-completed",
369
+ title: "Old Completed",
370
+ status: "completed",
371
+ completedAt: Date.now() - 10 * 60 * 1000,
372
+ }));
373
+ store.add(makeTask({
374
+ sessionId: "sess-recent-completed",
375
+ title: "Recent Completed",
376
+ status: "completed",
377
+ completedAt: Date.now() - 10 * 1000,
378
+ }));
379
+ store.add(makeTask({
380
+ sessionId: "sess-idle",
381
+ title: "Active Task",
382
+ status: "idle",
383
+ }));
384
+
385
+ const prompt = store.buildPrompt();
386
+ expect(prompt).not.toContain("Old Completed");
387
+ expect(prompt).toContain("Recent Completed");
388
+ expect(prompt).toContain("Active Task");
389
+ });
390
+ });
391
+
392
+ describe("re-activating a stopped task clears completedAt (Bug 5 fix)", () => {
393
+ let tempDir: string;
394
+
395
+ afterEach(() => {
396
+ if (tempDir && fs.existsSync(tempDir)) {
397
+ fs.rmSync(tempDir, { recursive: true, force: true });
398
+ }
399
+ });
400
+
401
+ it("completedAt is cleared when re-activating from stopped to idle", () => {
402
+ tempDir = path.join(os.tmpdir(), `coordinator-test-${Date.now()}`);
403
+ fs.mkdirSync(tempDir, { recursive: true });
404
+
405
+ const store = new TaskStore(tempDir);
406
+ store.add(makeTask({ sessionId: "sess-reactivate", title: "Reactivated task" }));
407
+ store.update("sess-reactivate", { status: "stopped", completedAt: Date.now() });
408
+
409
+ // Verify completedAt is set
410
+ expect(store.get("sess-reactivate")?.completedAt).toBeDefined();
411
+
412
+ // Re-activate (simulating session_delegate_send handler)
413
+ store.update("sess-reactivate", { status: "idle", completedAt: undefined });
414
+
415
+ const task = store.get("sess-reactivate");
416
+ expect(task?.status).toBe("idle");
417
+ expect(task?.completedAt).toBeUndefined();
418
+ });
419
+
420
+ it("buildPrompt shows correct elapsed time after re-activation", () => {
421
+ tempDir = path.join(os.tmpdir(), `coordinator-test-${Date.now()}`);
422
+ fs.mkdirSync(tempDir, { recursive: true });
423
+
424
+ const store = new TaskStore(tempDir);
425
+ const dispatchedAt = Date.now() - 60 * 1000; // 1 minute ago
426
+ store.add(makeTask({
427
+ sessionId: "sess-timecheck",
428
+ title: "Time check task",
429
+ dispatchedAt,
430
+ }));
431
+ store.update("sess-timecheck", { status: "stopped", completedAt: Date.now() });
432
+
433
+ // Before re-activate: shows frozen duration (completedAt - dispatchedAt)
434
+ let prompt = store.buildPrompt();
435
+ expect(prompt).toContain("Time check task");
436
+
437
+ // Re-activate
438
+ store.update("sess-timecheck", { status: "idle", completedAt: undefined });
439
+
440
+ // After re-activate: shows "elapsed" (live counter)
441
+ prompt = store.buildPrompt();
442
+ expect(prompt).toContain("Time check task");
443
+ expect(prompt).toContain("elapsed");
444
+ expect(prompt).not.toContain("STOPPED");
445
+ });
446
+ });
447
+
448
+ describe("no double store operations in tool handlers (Bug 3/4/6 fix)", () => {
449
+ it("session_delegate tool handler does NOT call store.add (handler.ts does it)", () => {
450
+ const indexSource = fs.readFileSync(
451
+ path.join(__dirname, "index.ts"),
452
+ "utf-8",
453
+ );
454
+
455
+ // Find the session_delegate tool execute block
456
+ const delegateToolStart = indexSource.indexOf('name: "session_delegate"');
457
+ expect(delegateToolStart).toBeGreaterThan(-1);
458
+
459
+ // Find the session_delegate_fork tool start to bound the search
460
+ const forkToolStart = indexSource.indexOf('name: "session_delegate_fork"');
461
+ expect(forkToolStart).toBeGreaterThan(-1);
462
+
463
+ const delegateToolBlock = indexSource.slice(delegateToolStart, forkToolStart);
464
+ expect(delegateToolBlock).not.toContain("store.add(");
465
+ });
466
+
467
+ it("session_delegate_stop tool handler does NOT call store.update (handler.ts does it)", () => {
468
+ const indexSource = fs.readFileSync(
469
+ path.join(__dirname, "index.ts"),
470
+ "utf-8",
471
+ );
472
+
473
+ const stopToolStart = indexSource.indexOf('name: "session_delegate_stop"');
474
+ expect(stopToolStart).toBeGreaterThan(-1);
475
+
476
+ const nextToolStart = indexSource.indexOf('name: "session_delegate_fork"');
477
+ const stopToolBlock = indexSource.slice(stopToolStart, nextToolStart);
478
+ expect(stopToolBlock).not.toContain("store.update(");
479
+ });
480
+
481
+ it("session_delegate_remove tool handler does NOT call store.remove (handler.ts does it)", () => {
482
+ const indexSource = fs.readFileSync(
483
+ path.join(__dirname, "index.ts"),
484
+ "utf-8",
485
+ );
486
+
487
+ const removeToolStart = indexSource.indexOf('name: "session_delegate_remove"');
488
+ expect(removeToolStart).toBeGreaterThan(-1);
489
+
490
+ const nextToolStart = indexSource.indexOf('name: "session_delegate_clear_stopped"');
491
+ const removeToolBlock = indexSource.slice(removeToolStart, nextToolStart);
492
+ expect(removeToolBlock).not.toContain("store.remove(");
493
+ });
494
+
495
+ it("session_delegate_clear_stopped tool handler does NOT call store.clearStopped (handler.ts does it)", () => {
496
+ const indexSource = fs.readFileSync(
497
+ path.join(__dirname, "index.ts"),
498
+ "utf-8",
499
+ );
500
+
501
+ const clearToolStart = indexSource.indexOf('name: "session_delegate_clear_stopped"');
502
+ expect(clearToolStart).toBeGreaterThan(-1);
503
+
504
+ const messageReceivedStart = indexSource.indexOf('client.on("message_received"');
505
+ const clearToolBlock = indexSource.slice(clearToolStart, messageReceivedStart);
506
+ expect(clearToolBlock).not.toContain("store.clearStopped(");
507
+ });
508
+
509
+ it("session_delegate_fork tool handler does NOT call store.add (handler.ts does it)", () => {
510
+ const indexSource = fs.readFileSync(
511
+ path.join(__dirname, "index.ts"),
512
+ "utf-8",
513
+ );
514
+
515
+ const forkToolStart = indexSource.indexOf('name: "session_delegate_fork"');
516
+ expect(forkToolStart).toBeGreaterThan(-1);
517
+
518
+ const removeToolStart = indexSource.indexOf('name: "session_delegate_remove"');
519
+ const forkToolBlock = indexSource.slice(forkToolStart, removeToolStart);
520
+ expect(forkToolBlock).not.toContain("store.add(");
521
+ });
522
+ });
523
+
524
+ describe("message_received handler tracks task status (Bug 7 fix)", () => {
525
+ it("index.ts message_received handler detects completion signals and updates status", () => {
526
+ const indexSource = fs.readFileSync(
527
+ path.join(__dirname, "index.ts"),
528
+ "utf-8",
529
+ );
530
+
531
+ const msgReceivedStart = indexSource.indexOf('client.on("message_received"');
532
+ expect(msgReceivedStart).toBeGreaterThan(-1);
533
+
534
+ const msgReceivedBlock = indexSource.slice(msgReceivedStart);
535
+ // Should detect [completed], [done], or "task completed" signals
536
+ expect(msgReceivedBlock).toContain("[completed]");
537
+ expect(msgReceivedBlock).toContain("isCompletion");
538
+ expect(msgReceivedBlock).toContain("status: \"completed\"");
539
+ // Should also update streaming status for regular messages
540
+ expect(msgReceivedBlock).toContain("status: \"streaming\"");
276
541
  });
277
542
  });