@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.
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +13 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/extensions/auto-memory/__tests__/extract-result.test.ts +42 -0
- package/dist/extensions/auto-memory/__tests__/prefetch-history.test.ts +136 -0
- package/dist/extensions/auto-memory/__tests__/prompts.test.ts +29 -0
- package/dist/extensions/auto-memory/__tests__/skip-rules.test.ts +366 -0
- package/dist/extensions/auto-memory/contract.d.ts +16 -0
- package/dist/extensions/auto-memory/contract.d.ts.map +1 -1
- package/dist/extensions/auto-memory/contract.js.map +1 -1
- package/dist/extensions/auto-memory/contract.ts +16 -0
- package/dist/extensions/auto-memory/index.ts +134 -13
- package/dist/extensions/auto-memory/prompts.ts +10 -0
- package/dist/extensions/auto-memory/skip-rules.ts +2 -0
- package/dist/extensions/bash-ext/index.ts +855 -845
- package/dist/extensions/claude-hooks-compat/index.ts +12 -7
- package/dist/extensions/coordinator/handler.test.ts +388 -123
- package/dist/extensions/coordinator/handler.ts +78 -12
- package/dist/extensions/coordinator/index.ts +267 -198
- package/dist/extensions/coordinator/types.d.ts +16 -0
- package/dist/extensions/coordinator/types.d.ts.map +1 -1
- package/dist/extensions/coordinator/types.js.map +1 -1
- package/dist/extensions/coordinator/types.ts +57 -49
- package/dist/extensions/lsp/lsp/index.ts +15 -9
- package/dist/extensions/lsp/lsp/lsp-clangd-e2e.test.ts +229 -0
- package/dist/extensions/message-bridge/index.ts +14 -11
- package/dist/extensions/session-supervisor/index.ts +14 -8
- package/dist/extensions/subagent-v2/index.ts +58 -42
- package/dist/extensions/todo-ext/index.ts +7 -3
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +9 -1
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- 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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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("
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
path.join(__dirname, "index.ts"),
|
|
161
|
-
"utf-8",
|
|
162
|
-
);
|
|
125
|
+
describe("TaskStore.clearStopped()", () => {
|
|
126
|
+
let tempDir: string;
|
|
163
127
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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("
|
|
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: "
|
|
213
|
-
title: "
|
|
214
|
-
status: "
|
|
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).
|
|
219
|
-
|
|
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
|
-
|
|
245
|
+
const prompt = store.buildPrompt();
|
|
246
|
+
expect(prompt).toContain("Recent stopped task");
|
|
222
247
|
});
|
|
223
248
|
|
|
224
|
-
it("
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
|
242
|
-
|
|
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
|
-
|
|
246
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
expect(
|
|
260
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
"
|
|
275
|
-
|
|
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
|
});
|