@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
|
@@ -9,8 +9,10 @@ export interface ProcessManagerApi {
|
|
|
9
9
|
delegate_status(sessionId: string): Promise<{ status: SessionStatus }>;
|
|
10
10
|
delegate_list(): Promise<Array<{ sessionId: string; status: SessionStatus; projectPath: string }>>;
|
|
11
11
|
delegate_stop(sessionId: string): Promise<boolean>;
|
|
12
|
-
delegate_fork(sessionId: string, task: string, title?: string): Promise<{ sessionId: string; status: "started" | "already_running" }>;
|
|
12
|
+
delegate_fork(sessionId: string, task: string, title?: string, projectPath?: string): Promise<{ sessionId: string; status: "started" | "already_running" }>;
|
|
13
13
|
delegate_compact_status(sessionId: string): Promise<{ isCompacting: boolean; contextUsage: { tokens: number | null; contextWindow: number; percent: number | null } }>;
|
|
14
|
+
delegate_remove(sessionId: string): Promise<boolean>;
|
|
15
|
+
delegate_clear_stopped(): Promise<number>;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export class TaskStore {
|
|
@@ -43,6 +45,9 @@ export class TaskStore {
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
add(task: DelegatedTask): void {
|
|
48
|
+
if (!task.sessionId) {
|
|
49
|
+
throw new Error("[coordinator] cannot add task with empty sessionId");
|
|
50
|
+
}
|
|
46
51
|
this.tasks.set(task.sessionId, task);
|
|
47
52
|
this.save();
|
|
48
53
|
}
|
|
@@ -67,15 +72,35 @@ export class TaskStore {
|
|
|
67
72
|
return Array.from(this.tasks.values());
|
|
68
73
|
}
|
|
69
74
|
|
|
75
|
+
clearStopped(): number {
|
|
76
|
+
let removed = 0;
|
|
77
|
+
for (const [id, task] of this.tasks) {
|
|
78
|
+
if (task.status === "stopped" || task.status === "completed") {
|
|
79
|
+
this.tasks.delete(id);
|
|
80
|
+
removed++;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (removed > 0) this.save();
|
|
84
|
+
return removed;
|
|
85
|
+
}
|
|
86
|
+
|
|
70
87
|
buildPrompt(): string {
|
|
71
|
-
const
|
|
88
|
+
const FINISHED_MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
const tasks = this.list().filter((t) => {
|
|
91
|
+
if ((t.status === "stopped" || t.status === "completed") && t.completedAt && now - t.completedAt > FINISHED_MAX_AGE_MS) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
});
|
|
72
96
|
if (tasks.length === 0) return "";
|
|
73
97
|
|
|
74
98
|
const lines = ["## Delegated Tasks", ""];
|
|
75
99
|
for (const t of tasks) {
|
|
76
100
|
const status = t.status === "completed" ? "DONE" : t.status === "stopped" ? "STOPPED" : t.status.toUpperCase();
|
|
77
|
-
const compactTag = (t as
|
|
78
|
-
const
|
|
101
|
+
const compactTag = (t as Record<string, unknown>).isCompacting ? " COMPACTING" : "";
|
|
102
|
+
const ctxUsage = (t as Record<string, unknown>).contextUsage as { percent: number | null } | undefined;
|
|
103
|
+
const ctxTag = ctxUsage?.percent != null ? ` ctx:${Math.round(ctxUsage.percent)}%` : "";
|
|
79
104
|
const elapsed = t.completedAt
|
|
80
105
|
? `${((t.completedAt - t.dispatchedAt) / 1000).toFixed(1)}s`
|
|
81
106
|
: `${((Date.now() - t.dispatchedAt) / 1000).toFixed(0)}s elapsed`;
|
|
@@ -96,9 +121,19 @@ export function createCoordinatorHandler(
|
|
|
96
121
|
getStore: () => TaskStore,
|
|
97
122
|
): void {
|
|
98
123
|
channel.handle("session_delegate", async (params: unknown) => {
|
|
99
|
-
const { task, title } = params as { task: string; title?: string };
|
|
100
|
-
const projectPath = process.cwd();
|
|
101
|
-
|
|
124
|
+
const { task, title, projectPath: rawProjectPath } = params as { task: string; title?: string; projectPath?: string };
|
|
125
|
+
const projectPath = rawProjectPath || process.cwd();
|
|
126
|
+
|
|
127
|
+
let result: { sessionId: string; status: "started" | "already_running" };
|
|
128
|
+
try {
|
|
129
|
+
result = await pm.delegate(task, projectPath);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
return { __error: err instanceof Error ? err.message : String(err) };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!result.sessionId) {
|
|
135
|
+
return { __error: "[coordinator] delegate failed: no sessionId returned" };
|
|
136
|
+
}
|
|
102
137
|
|
|
103
138
|
getStore().add({
|
|
104
139
|
sessionId: result.sessionId,
|
|
@@ -126,7 +161,7 @@ export function createCoordinatorHandler(
|
|
|
126
161
|
const store = getStore();
|
|
127
162
|
const task = store.get(targetSessionId);
|
|
128
163
|
if (task && task.status === "stopped") {
|
|
129
|
-
store.update(targetSessionId, { status: "idle" });
|
|
164
|
+
store.update(targetSessionId, { status: "idle", completedAt: undefined });
|
|
130
165
|
}
|
|
131
166
|
}
|
|
132
167
|
|
|
@@ -160,20 +195,51 @@ export function createCoordinatorHandler(
|
|
|
160
195
|
const { sessionId } = params as { sessionId: string };
|
|
161
196
|
const ok = await pm.delegate_stop(sessionId);
|
|
162
197
|
if (ok) {
|
|
163
|
-
|
|
198
|
+
const store = getStore();
|
|
199
|
+
store.update(sessionId, { status: "stopped", completedAt: Date.now() });
|
|
164
200
|
channel.emit("task_stopped", { sessionId });
|
|
165
201
|
}
|
|
166
202
|
return { ok };
|
|
167
203
|
});
|
|
168
204
|
|
|
205
|
+
channel.handle("session_delegate_remove", async (params: unknown) => {
|
|
206
|
+
const { sessionId } = params as { sessionId: string };
|
|
207
|
+
const store = getStore();
|
|
208
|
+
const task = store.get(sessionId);
|
|
209
|
+
if (!task) {
|
|
210
|
+
return { ok: false };
|
|
211
|
+
}
|
|
212
|
+
await pm.delegate_stop(sessionId).catch(() => {});
|
|
213
|
+
store.remove(sessionId);
|
|
214
|
+
return { ok: true };
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
channel.handle("session_delegate_clear_stopped", async () => {
|
|
218
|
+
const store = getStore();
|
|
219
|
+
const removed = store.clearStopped();
|
|
220
|
+
return { removed };
|
|
221
|
+
});
|
|
222
|
+
|
|
169
223
|
channel.handle("session_delegate_fork", async (params: unknown) => {
|
|
170
|
-
const { sessionId, task, title } = params as { sessionId: string; task: string; title?: string };
|
|
171
|
-
const
|
|
224
|
+
const { sessionId, task, title, projectPath: rawProjectPath } = params as { sessionId: string; task: string; title?: string; projectPath?: string };
|
|
225
|
+
const projectPath = rawProjectPath || process.cwd();
|
|
226
|
+
|
|
227
|
+
let result: { sessionId: string; status: "started" | "already_running" };
|
|
228
|
+
try {
|
|
229
|
+
result = await pm.delegate_fork(sessionId, task, title, projectPath);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
return { __error: err instanceof Error ? err.message : String(err) };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!result.sessionId) {
|
|
235
|
+
return { __error: "[coordinator] fork failed: no sessionId returned" };
|
|
236
|
+
}
|
|
237
|
+
|
|
172
238
|
getStore().add({
|
|
173
239
|
sessionId: result.sessionId,
|
|
174
240
|
title: title || task.slice(0, 60),
|
|
175
241
|
task,
|
|
176
|
-
projectPath
|
|
242
|
+
projectPath,
|
|
177
243
|
dispatchedAt: Date.now(),
|
|
178
244
|
status: "idle",
|
|
179
245
|
});
|
|
@@ -1,158 +1,178 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
createTypedChannel,
|
|
3
|
+
type ExtensionAPI,
|
|
4
4
|
} from "@dyyz1993/pi-coding-agent";
|
|
5
5
|
import { Type } from "typebox";
|
|
6
6
|
import { COORDINATOR_CHANNEL_NAME, type CoordinatorChannelContract, type SessionStatus } from "./types.js";
|
|
7
7
|
import { createCoordinatorHandler, TaskStore, type ProcessManagerApi } from "./handler.js";
|
|
8
8
|
|
|
9
9
|
const DelegateParams = Type.Object({
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
task: Type.String({ description: "Task description to delegate to the background session" }),
|
|
11
|
+
title: Type.Optional(Type.String({ description: "Short title for this delegated task" })),
|
|
12
|
+
projectPath: Type.Optional(Type.String({ description: "Project directory to run the delegated session in. Defaults to the current working directory." })),
|
|
12
13
|
});
|
|
13
14
|
|
|
14
15
|
const DelegateSendParams = Type.Object({
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
targetSessionId: Type.String({ description: "Session ID to send the message to" }),
|
|
17
|
+
message: Type.String({ description: "Message content to send" }),
|
|
17
18
|
});
|
|
18
19
|
|
|
19
20
|
const DelegateStatusParams = Type.Object({
|
|
20
|
-
|
|
21
|
+
sessionId: Type.String({ description: "Session ID to check status for" }),
|
|
21
22
|
});
|
|
22
23
|
|
|
23
24
|
const DelegateStopParams = Type.Object({
|
|
24
|
-
|
|
25
|
+
sessionId: Type.String({ description: "Session ID to stop" }),
|
|
25
26
|
});
|
|
26
27
|
|
|
27
28
|
const DelegateForkParams = Type.Object({
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
sessionId: Type.String({ description: "Source session ID to fork from" }),
|
|
30
|
+
task: Type.String({ description: "Task description for the forked session" }),
|
|
31
|
+
title: Type.Optional(Type.String({ description: "Short title for the forked task" })),
|
|
32
|
+
projectPath: Type.Optional(Type.String({ description: "Project directory to run the forked session in. Defaults to the current working directory." })),
|
|
31
33
|
});
|
|
32
34
|
|
|
33
35
|
export default function coordinatorExtension(pi: ExtensionAPI) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
36
|
+
const rawChannel = pi.registerChannel(COORDINATOR_CHANNEL_NAME);
|
|
37
|
+
|
|
38
|
+
const { server: serverChannel, client } = createTypedChannel<CoordinatorChannelContract>(rawChannel);
|
|
39
|
+
|
|
40
|
+
let currentSessionId = "";
|
|
41
|
+
let store: TaskStore | null = null;
|
|
42
|
+
|
|
43
|
+
pi.on("session_start", (_event, ctx) => {
|
|
44
|
+
currentSessionId = ctx.sessionManager.getSessionId();
|
|
45
|
+
store = new TaskStore(ctx.sessionManager.getSessionDir());
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const serverProxy: ProcessManagerApi = {
|
|
49
|
+
async delegate(task, projectPath) {
|
|
50
|
+
return client.call("session_delegate", { task, projectPath }) as Promise<{ sessionId: string; status: "started" | "already_running" }>;
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
async delegate_send(fromSessionId, toSessionId, message) {
|
|
54
|
+
return client.call("session_delegate_send", {
|
|
55
|
+
targetSessionId: toSessionId,
|
|
56
|
+
message,
|
|
57
|
+
}) as Promise<{ delivered: boolean; targetStatus: "active" | "started" | "not_found" }>;
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
async delegate_status(sessionId) {
|
|
61
|
+
try {
|
|
62
|
+
const result = await client.call("session_delegate_status", { sessionId }) as { task: { status: string } | null };
|
|
63
|
+
return result.task ? { status: result.task.status as SessionStatus } : { status: "stopped" as const };
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.debug("[coordinator] delegate_status failed:", err instanceof Error ? err.message : err);
|
|
66
|
+
return { status: "stopped" as const };
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
async delegate_list() {
|
|
71
|
+
try {
|
|
72
|
+
const result = await client.call("session_delegate_list", {}) as { tasks: Array<{ sessionId: string; status: SessionStatus; projectPath: string }> };
|
|
73
|
+
return result.tasks;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.debug("[coordinator] delegate_list failed:", err instanceof Error ? err.message : err);
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
async delegate_stop(sessionId) {
|
|
81
|
+
try {
|
|
82
|
+
const result = await client.call("session_delegate_stop", { sessionId }) as { ok: boolean };
|
|
83
|
+
return result.ok;
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.debug("[coordinator] delegate_stop failed:", err instanceof Error ? err.message : err);
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
async delegate_fork(sessionId, task, title, projectPath) {
|
|
91
|
+
return client.call("session_delegate_fork", { sessionId, task, title, projectPath }) as Promise<{ sessionId: string; status: "started" | "already_running" }>;
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
async delegate_compact_status(sessionId: string) {
|
|
95
|
+
try {
|
|
96
|
+
const result = await client.call("session_delegate_status", { sessionId }) as { isCompacting?: boolean; contextUsage?: { tokens: number | null; contextWindow: number; percent: number | null } };
|
|
97
|
+
return {
|
|
98
|
+
isCompacting: result.isCompacting ?? false,
|
|
99
|
+
contextUsage: result.contextUsage ?? { tokens: null as number | null, contextWindow: 0, percent: null as number | null },
|
|
100
|
+
};
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.debug("[coordinator] delegate_compact_status failed:", err instanceof Error ? err.message : err);
|
|
103
|
+
return { isCompacting: false, contextUsage: { tokens: null as number | null, contextWindow: 0, percent: null as number | null } };
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
async delegate_remove(sessionId: string) {
|
|
108
|
+
try {
|
|
109
|
+
const result = await client.call("session_delegate_remove", { sessionId }) as { ok: boolean };
|
|
110
|
+
return result.ok;
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.debug("[coordinator] delegate_remove failed:", err instanceof Error ? err.message : err);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
async delegate_clear_stopped() {
|
|
118
|
+
try {
|
|
119
|
+
const result = await client.call("session_delegate_clear_stopped", {}) as { removed: number };
|
|
120
|
+
return result.removed;
|
|
121
|
+
} catch (err) {
|
|
122
|
+
console.debug("[coordinator] delegate_clear_stopped failed:", err instanceof Error ? err.message : err);
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
createCoordinatorHandler(
|
|
129
|
+
serverChannel,
|
|
130
|
+
serverProxy,
|
|
131
|
+
() => currentSessionId,
|
|
132
|
+
() => store ?? new TaskStore("/tmp/coordinator-fallback"),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
pi.on("context", (event, _ctx) => {
|
|
136
|
+
if (!store) return;
|
|
137
|
+
const prompt = store.buildPrompt();
|
|
138
|
+
if (prompt) {
|
|
139
|
+
event.messages.push({
|
|
140
|
+
role: "user",
|
|
141
|
+
content: [{ type: "text", text: prompt }],
|
|
142
|
+
timestamp: Date.now(),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
});
|
|
124
146
|
|
|
125
147
|
pi.registerTool({
|
|
126
148
|
name: "session_delegate",
|
|
127
149
|
label: "Session Delegate",
|
|
128
150
|
description: [
|
|
129
151
|
"Delegate a task to a background pi session.",
|
|
152
|
+
"Optionally specify a projectPath to run the session in a specific project directory.",
|
|
130
153
|
"Returns a sessionId for communication via session_delegate_send.",
|
|
131
154
|
"The delegated session can message back using its own coordinator channel.",
|
|
132
155
|
"The delegate session is automatically restarted if inactive when receiving messages.",
|
|
133
156
|
].join(" "),
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
};
|
|
154
|
-
},
|
|
155
|
-
});
|
|
157
|
+
parameters: DelegateParams,
|
|
158
|
+
async execute(toolCallId, params, _signal, _onUpdate, ctx) {
|
|
159
|
+
const sid = currentSessionId || ctx.sessionManager.getSessionId();
|
|
160
|
+
const projectPath = params.projectPath || ctx.cwd;
|
|
161
|
+
const result = await serverProxy.delegate(params.task, projectPath);
|
|
162
|
+
|
|
163
|
+
if (!result.sessionId) {
|
|
164
|
+
return {
|
|
165
|
+
content: [{ type: "text" as const, text: `Failed to delegate task: no sessionId returned.` }],
|
|
166
|
+
details: { error: "no sessionId" },
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
content: [{ type: "text" as const, text: `Delegated task to session ${result.sessionId} (status: ${result.status}, cwd: ${projectPath}). Use session_delegate_send to communicate.` }],
|
|
172
|
+
details: { ...result, dispatchedBy: sid, projectPath },
|
|
173
|
+
};
|
|
174
|
+
},
|
|
175
|
+
});
|
|
156
176
|
|
|
157
177
|
pi.registerTool({
|
|
158
178
|
name: "session_delegate_send",
|
|
@@ -183,79 +203,128 @@ export default function coordinatorExtension(pi: ExtensionAPI) {
|
|
|
183
203
|
},
|
|
184
204
|
});
|
|
185
205
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
206
|
+
pi.registerTool({
|
|
207
|
+
name: "session_delegate_status",
|
|
208
|
+
label: "Session Delegate Status",
|
|
209
|
+
description: "Check the status of a delegated task session.",
|
|
210
|
+
parameters: DelegateStatusParams,
|
|
211
|
+
async execute(toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
212
|
+
const task = store?.get(params.sessionId);
|
|
213
|
+
if (task) {
|
|
214
|
+
const status = task.status === "completed" ? "DONE" : task.status.toUpperCase();
|
|
215
|
+
return {
|
|
216
|
+
content: [{ type: "text" as const, text: `Task "${task.title}" (${params.sessionId}): ${status}` }],
|
|
217
|
+
details: { task },
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const remote = await serverProxy.delegate_status(params.sessionId);
|
|
221
|
+
return {
|
|
222
|
+
content: [{ type: "text" as const, text: `Session ${params.sessionId} status: ${remote.status}` }],
|
|
223
|
+
details: { task: null },
|
|
224
|
+
};
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
pi.registerTool({
|
|
229
|
+
name: "session_delegate_stop",
|
|
230
|
+
label: "Session Delegate Stop",
|
|
231
|
+
description: "Stop a delegated task session.",
|
|
232
|
+
parameters: DelegateStopParams,
|
|
233
|
+
async execute(toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
234
|
+
const ok = await serverProxy.delegate_stop(params.sessionId);
|
|
235
|
+
return {
|
|
236
|
+
content: [{ type: "text" as const, text: ok ? `Session ${params.sessionId} stopped.` : `Session ${params.sessionId} not found or already stopped.` }],
|
|
237
|
+
details: { ok },
|
|
238
|
+
};
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
pi.registerTool({
|
|
243
|
+
name: "session_delegate_fork",
|
|
244
|
+
label: "Session Delegate Fork",
|
|
245
|
+
description: [
|
|
246
|
+
"Fork an existing session and delegate a new task to the forked session.",
|
|
247
|
+
"The forked session starts with a copy of the source session's conversation history.",
|
|
248
|
+
"Optionally specify a projectPath to run the forked session in a specific project directory.",
|
|
249
|
+
"The original session continues running unchanged.",
|
|
250
|
+
].join(" "),
|
|
251
|
+
parameters: DelegateForkParams,
|
|
252
|
+
async execute(toolCallId, params, _signal, _onUpdate, ctx) {
|
|
253
|
+
const sid = currentSessionId || ctx.sessionManager.getSessionId();
|
|
254
|
+
const projectPath = params.projectPath || ctx.cwd;
|
|
255
|
+
const result = await serverProxy.delegate_fork(params.sessionId, params.task, params.title, projectPath);
|
|
256
|
+
return {
|
|
257
|
+
content: [{ type: "text" as const, text: `Forked session ${params.sessionId} → ${result.sessionId} (status: ${result.status}, cwd: ${projectPath}). Task: ${params.task}` }],
|
|
258
|
+
details: { ...result, forkedFrom: params.sessionId, dispatchedBy: sid, projectPath },
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
pi.registerTool({
|
|
264
|
+
name: "session_delegate_remove",
|
|
265
|
+
label: "Session Delegate Remove",
|
|
266
|
+
description: [
|
|
267
|
+
"Remove a delegated task from the task list.",
|
|
268
|
+
"Stops the session if still running, then removes the task entry.",
|
|
269
|
+
"Use this to clean up completed, stopped, or zombie tasks.",
|
|
270
|
+
].join(" "),
|
|
271
|
+
parameters: DelegateStatusParams,
|
|
272
|
+
async execute(toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
273
|
+
const ok = await serverProxy.delegate_remove(params.sessionId);
|
|
274
|
+
return {
|
|
275
|
+
content: [{ type: "text" as const, text: ok ? `Task ${params.sessionId} removed.` : `Task ${params.sessionId} not found.` }],
|
|
276
|
+
details: { ok },
|
|
277
|
+
};
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
pi.registerTool({
|
|
282
|
+
name: "session_delegate_clear_stopped",
|
|
283
|
+
label: "Session Delegate Clear Stopped",
|
|
284
|
+
description: [
|
|
285
|
+
"Remove all stopped and completed tasks from the task list.",
|
|
286
|
+
"Use this to clean up accumulated zombie tasks.",
|
|
287
|
+
].join(" "),
|
|
288
|
+
parameters: Type.Object({}),
|
|
289
|
+
async execute(toolCallId, _params, _signal, _onUpdate, _ctx) {
|
|
290
|
+
const removed = await serverProxy.delegate_clear_stopped();
|
|
291
|
+
return {
|
|
292
|
+
content: [{ type: "text" as const, text: `Cleared ${removed} stopped/completed task(s).` }],
|
|
293
|
+
details: { removed },
|
|
294
|
+
};
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
client.on("message_received", (data: unknown) => {
|
|
299
|
+
const d = data as { fromSessionId: string; message: string };
|
|
300
|
+
// Skip messages from sessions that have been stopped
|
|
301
|
+
const task = store?.get(d.fromSessionId);
|
|
302
|
+
if (task?.status === "stopped") return;
|
|
303
|
+
|
|
304
|
+
// Detect completion signals from delegated sessions
|
|
305
|
+
if (store && task) {
|
|
306
|
+
const lowerMsg = d.message.toLowerCase();
|
|
307
|
+
const isCompletion = lowerMsg.includes("[completed]") || lowerMsg.includes("[done]") || lowerMsg.includes("task completed");
|
|
308
|
+
if (isCompletion) {
|
|
309
|
+
store.update(d.fromSessionId, { status: "completed", completedAt: Date.now(), result: d.message });
|
|
310
|
+
} else if (task.status !== "completed") {
|
|
311
|
+
store.update(d.fromSessionId, { status: "streaming" });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
pi.sendUserMessage(
|
|
317
|
+
`[Coordinator] Message from session ${d.fromSessionId}:\n${d.message}`,
|
|
318
|
+
{ deliverAs: "followUp" },
|
|
319
|
+
);
|
|
320
|
+
} catch (err) {
|
|
321
|
+
// Silently ignore stale-ctx errors: the extension runtime may have been
|
|
322
|
+
// invalidated by a concurrent session replacement or reload. The new
|
|
323
|
+
// runtime's handler will take over.
|
|
324
|
+
if (err instanceof Error && err.message.includes("stale")) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
throw err;
|
|
328
|
+
}
|
|
329
|
+
});
|
|
261
330
|
}
|