@dyyz1993/pi-coding-agent 0.74.46 → 0.74.48

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 (49) hide show
  1. package/dist/core/agent-session.d.ts.map +1 -1
  2. package/dist/core/agent-session.js +16 -0
  3. package/dist/core/agent-session.js.map +1 -1
  4. package/dist/core/session-manager.d.ts +28 -1
  5. package/dist/core/session-manager.d.ts.map +1 -1
  6. package/dist/core/session-manager.js +89 -10
  7. package/dist/core/session-manager.js.map +1 -1
  8. package/dist/extensions/ask-tools/index.ts +45 -0
  9. package/dist/extensions/auto-memory/__tests__/extract-result.test.ts +42 -0
  10. package/dist/extensions/auto-memory/__tests__/prefetch-history.test.ts +136 -0
  11. package/dist/extensions/auto-memory/__tests__/prompts.test.ts +29 -0
  12. package/dist/extensions/auto-memory/__tests__/skip-rules.test.ts +366 -0
  13. package/dist/extensions/auto-memory/contract.d.ts +16 -0
  14. package/dist/extensions/auto-memory/contract.d.ts.map +1 -1
  15. package/dist/extensions/auto-memory/contract.js.map +1 -1
  16. package/dist/extensions/auto-memory/contract.ts +16 -0
  17. package/dist/extensions/auto-memory/index.ts +134 -13
  18. package/dist/extensions/auto-memory/prompts.ts +10 -0
  19. package/dist/extensions/auto-memory/skip-rules.ts +2 -0
  20. package/dist/extensions/auto-session-title/index.ts +2 -0
  21. package/dist/extensions/bash-ext/index.ts +855 -845
  22. package/dist/extensions/claude-hooks-compat/index.ts +12 -7
  23. package/dist/extensions/compaction-manager/index.ts +68 -7
  24. package/dist/extensions/coordinator/handler.test.ts +388 -123
  25. package/dist/extensions/coordinator/handler.ts +78 -12
  26. package/dist/extensions/coordinator/index.ts +306 -198
  27. package/dist/extensions/coordinator/types.d.ts +16 -0
  28. package/dist/extensions/coordinator/types.d.ts.map +1 -1
  29. package/dist/extensions/coordinator/types.js.map +1 -1
  30. package/dist/extensions/coordinator/types.ts +57 -49
  31. package/dist/extensions/hooks-engine/index.ts +3 -0
  32. package/dist/extensions/lsp/lsp/client/smart-file-tracker.ts +302 -0
  33. package/dist/extensions/lsp/lsp/index.ts +15 -9
  34. package/dist/extensions/lsp/lsp/lsp-clangd-e2e.test.ts +229 -0
  35. package/dist/extensions/lsp/lsp/utils/project-scanner.ts +101 -12
  36. package/dist/extensions/message-bridge/index.ts +14 -11
  37. package/dist/extensions/output-guard/index.ts +39 -0
  38. package/dist/extensions/preview/index.ts +23 -0
  39. package/dist/extensions/session-supervisor/index.ts +14 -8
  40. package/dist/extensions/subagent-v2/extract-parent-todos.test.ts +146 -0
  41. package/dist/extensions/subagent-v2/index.ts +430 -57
  42. package/dist/extensions/todo-ext/index.ts +62 -3
  43. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  44. package/dist/modes/interactive/interactive-mode.js +6 -0
  45. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  46. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  47. package/dist/modes/rpc/rpc-mode.js +10 -0
  48. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  49. package/package.json +1 -1
@@ -5,26 +5,339 @@ import type { Message } from "@dyyz1993/pi-ai";
5
5
  import { StringEnum } from "@dyyz1993/pi-ai";
6
6
  import {
7
7
  type AgentScope,
8
+ type ChannelContract,
8
9
  type ExtensionAPI,
9
10
  RpcClient,
10
11
  createTypedChannel,
11
12
  discoverAgents,
13
+ getMarkdownTheme,
14
+ withFileMutationQueue,
12
15
  } from "@dyyz1993/pi-coding-agent";
13
- import { Text } from "@dyyz1993/pi-tui";
16
+ import type { Theme, ThemeColor } from "@dyyz1993/pi-coding-agent";
17
+ import { type Component, Container, Markdown, Spacer, Text } from "@dyyz1993/pi-tui";
14
18
  import { Type } from "typebox";
15
- import {
16
- accumulateUsage,
17
- cleanupTempFiles,
18
- type SingleResult,
19
- type SubagentEventPayload,
20
- type UsageStats,
21
- formatUsageStats,
22
- getFinalOutput,
23
- makeUsage,
24
- renderSingleResult,
25
- writePromptToTempFile,
26
- } from "../subagent-shared/index.js";
27
- import type { SubagentChannelContract } from "../subagent-shared/contract.js";
19
+
20
+ // ── Inlined from subagent-shared/contract.ts ──
21
+
22
+ const SUBAGENT_CHANNEL_NAME = "subagent";
23
+
24
+ interface SubagentEventPayload {
25
+ event: unknown;
26
+ sessionId: string;
27
+ taskId?: string;
28
+ [key: string]: unknown;
29
+ }
30
+
31
+ interface SubagentStartPayload {
32
+ event: {
33
+ type: "subagent_start";
34
+ toolCallId: string;
35
+ description: string;
36
+ instruction: string;
37
+ };
38
+ sessionId: string;
39
+ }
40
+
41
+ interface SubagentChannelContract extends ChannelContract {
42
+ methods?: Record<string, never>;
43
+ events: {
44
+ event: SubagentEventPayload;
45
+ subagent_start: SubagentStartPayload;
46
+ };
47
+ }
48
+
49
+ // ── Inlined from subagent-shared/types.ts ──
50
+
51
+ interface UsageStats {
52
+ input: number;
53
+ output: number;
54
+ cacheRead: number;
55
+ cacheWrite: number;
56
+ cost: number;
57
+ contextTokens: number;
58
+ turns: number;
59
+ }
60
+
61
+ interface SingleResult {
62
+ agent: string;
63
+ agentSource: "user" | "project" | "unknown" | "builtin" | "plugin" | "flag" | "policy";
64
+ task: string;
65
+ exitCode: number;
66
+ messages: Message[];
67
+ stderr: string;
68
+ usage: UsageStats;
69
+ model?: string;
70
+ stopReason?: string;
71
+ errorMessage?: string;
72
+ step?: number;
73
+ sessionPath?: string;
74
+ }
75
+
76
+ type DisplayItem =
77
+ | { type: "text"; text: string }
78
+ | { type: "toolCall"; name: string; args: Record<string, unknown> };
79
+
80
+ interface SubagentDetailsBase {
81
+ agentScope: string;
82
+ projectAgentsDir: string | null;
83
+ }
84
+
85
+ // ── Inlined from subagent-shared/utils.ts ──
86
+
87
+ function formatTokens(count: number): string {
88
+ if (count < 1000) return count.toString();
89
+ if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
90
+ if (count < 1000000) return `${Math.round(count / 1000)}k`;
91
+ return `${(count / 1000000).toFixed(1)}M`;
92
+ }
93
+
94
+ function formatUsageStats(
95
+ usage: {
96
+ input: number;
97
+ output: number;
98
+ cacheRead: number;
99
+ cacheWrite: number;
100
+ cost: number;
101
+ contextTokens?: number;
102
+ turns?: number;
103
+ },
104
+ model?: string,
105
+ ): string {
106
+ const parts: string[] = [];
107
+ if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
108
+ if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
109
+ if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
110
+ if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
111
+ if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
112
+ if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
113
+ if (usage.contextTokens && usage.contextTokens > 0) parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
114
+ if (model) parts.push(model);
115
+ return parts.join(" ");
116
+ }
117
+
118
+ function getFinalOutput(messages: Message[]): string {
119
+ for (let i = messages.length - 1; i >= 0; i--) {
120
+ const msg = messages[i];
121
+ if (msg.role === "assistant") {
122
+ for (const part of msg.content) {
123
+ if (part.type === "text") return part.text;
124
+ }
125
+ }
126
+ }
127
+ return "";
128
+ }
129
+
130
+ function getDisplayItems(messages: Message[]): DisplayItem[] {
131
+ const items: DisplayItem[] = [];
132
+ for (const msg of messages) {
133
+ if (msg.role === "assistant") {
134
+ for (const part of msg.content) {
135
+ if (part.type === "text") items.push({ type: "text", text: part.text });
136
+ else if (part.type === "toolCall") items.push({ type: "toolCall", name: part.name, args: part.arguments });
137
+ }
138
+ }
139
+ }
140
+ return items;
141
+ }
142
+
143
+ async function writePromptToTempFile(
144
+ agentName: string,
145
+ prompt: string,
146
+ tmpPrefix = "pi-subagent-",
147
+ ): Promise<{ dir: string; filePath: string }> {
148
+ const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), tmpPrefix));
149
+ const safeName = agentName.replace(/[^\w.-]+/g, "_");
150
+ const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
151
+ await withFileMutationQueue(filePath, async () => {
152
+ await fs.promises.writeFile(filePath, prompt, { encoding: "utf-8", mode: 0o600 });
153
+ });
154
+ return { dir: tmpDir, filePath };
155
+ }
156
+
157
+ function cleanupTempFiles(filePath: string | null, dir: string | null, logPrefix = "subagent"): void {
158
+ if (filePath)
159
+ try {
160
+ fs.unlinkSync(filePath);
161
+ } catch (err) {
162
+ console.debug(`[${logPrefix}] temp file cleanup failed:`, err instanceof Error ? err.message : err);
163
+ }
164
+ if (dir)
165
+ try {
166
+ fs.rmdirSync(dir);
167
+ } catch (err) {
168
+ console.debug(`[${logPrefix}] temp dir cleanup failed:`, err instanceof Error ? err.message : err);
169
+ }
170
+ }
171
+
172
+ function makeUsage(): UsageStats {
173
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
174
+ }
175
+
176
+ function accumulateUsage(result: { usage: UsageStats; model?: string; stopReason?: string; errorMessage?: string }, msg: Message): void {
177
+ if (msg.role !== "assistant") return;
178
+ result.usage.turns++;
179
+ const usage = msg.usage;
180
+ if (usage) {
181
+ result.usage.input += usage.input || 0;
182
+ result.usage.output += usage.output || 0;
183
+ result.usage.cacheRead += usage.cacheRead || 0;
184
+ result.usage.cacheWrite += usage.cacheWrite || 0;
185
+ result.usage.cost += usage.cost?.total || 0;
186
+ result.usage.contextTokens = usage.totalTokens || 0;
187
+ }
188
+ if (!result.model && msg.model) result.model = msg.model;
189
+ if (msg.stopReason) result.stopReason = msg.stopReason;
190
+ if (msg.errorMessage) result.errorMessage = msg.errorMessage;
191
+ }
192
+
193
+ // ── Inlined from subagent-shared/render.ts ──
194
+
195
+ const COLLAPSED_ITEM_COUNT = 10;
196
+
197
+ function formatToolCall(
198
+ toolName: string,
199
+ args: Record<string, unknown>,
200
+ themeFg: (color: ThemeColor, text: string) => string,
201
+ ): string {
202
+ const shortenPath = (p: string) => {
203
+ const home = os.homedir();
204
+ return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
205
+ };
206
+
207
+ switch (toolName) {
208
+ case "bash": {
209
+ const command = (args.command as string) || "...";
210
+ const preview = command.length > 60 ? `${command.slice(0, 60)}...` : command;
211
+ return themeFg("muted", "$ ") + themeFg("toolOutput", preview);
212
+ }
213
+ case "read": {
214
+ const rawPath = (args.file_path || args.path || "...") as string;
215
+ const filePath = shortenPath(rawPath);
216
+ const offset = args.offset as number | undefined;
217
+ const limit = args.limit as number | undefined;
218
+ let text = themeFg("accent", filePath);
219
+ if (offset !== undefined || limit !== undefined) {
220
+ const startLine = offset ?? 1;
221
+ const endLine = limit !== undefined ? startLine + limit - 1 : "";
222
+ text += themeFg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
223
+ }
224
+ return themeFg("muted", "read ") + text;
225
+ }
226
+ case "write": {
227
+ const rawPath = (args.file_path || args.path || "...") as string;
228
+ const filePath = shortenPath(rawPath);
229
+ const content = (args.content || "") as string;
230
+ const lines = content.split("\n").length;
231
+ let text = themeFg("muted", "write ") + themeFg("accent", filePath);
232
+ if (lines > 1) text += themeFg("dim", ` (${lines} lines)`);
233
+ return text;
234
+ }
235
+ case "edit": {
236
+ const rawPath = (args.file_path || args.path || "...") as string;
237
+ return themeFg("muted", "edit ") + themeFg("accent", shortenPath(rawPath));
238
+ }
239
+ case "ls": {
240
+ const rawPath = (args.path || ".") as string;
241
+ return themeFg("muted", "ls ") + themeFg("accent", shortenPath(rawPath));
242
+ }
243
+ case "find": {
244
+ const pattern = (args.pattern || "*") as string;
245
+ const rawPath = (args.path || ".") as string;
246
+ return themeFg("muted", "find ") + themeFg("accent", pattern) + themeFg("dim", ` in ${shortenPath(rawPath)}`);
247
+ }
248
+ case "grep": {
249
+ const pattern = (args.pattern || "") as string;
250
+ const rawPath = (args.path || ".") as string;
251
+ return (
252
+ themeFg("muted", "grep ") +
253
+ themeFg("accent", `/${pattern}/`) +
254
+ themeFg("dim", ` in ${shortenPath(rawPath)}`)
255
+ );
256
+ }
257
+ default: {
258
+ const argsStr = JSON.stringify(args);
259
+ const preview = argsStr.length > 50 ? `${argsStr.slice(0, 50)}...` : argsStr;
260
+ return themeFg("accent", toolName) + themeFg("dim", ` ${preview}`);
261
+ }
262
+ }
263
+ }
264
+
265
+ function renderDisplayItemsInner(
266
+ items: DisplayItem[],
267
+ expanded: boolean,
268
+ theme: Theme,
269
+ limit?: number,
270
+ ): string {
271
+ const fg = (c: string, t: string) => theme.fg(c as ThemeColor, t);
272
+ const toShow = limit ? items.slice(-limit) : items;
273
+ const skipped = limit && items.length > limit ? items.length - limit : 0;
274
+ let text = "";
275
+ if (skipped > 0) text += theme.fg("muted", `... ${skipped} earlier items\n`);
276
+ for (const item of toShow) {
277
+ if (item.type === "text") {
278
+ const preview = expanded ? item.text : item.text.split("\n").slice(0, 3).join("\n");
279
+ text += `${theme.fg("toolOutput", preview)}\n`;
280
+ } else {
281
+ text += `${theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, fg)}\n`;
282
+ }
283
+ }
284
+ return text.trimEnd();
285
+ }
286
+
287
+ function renderSingleResult(r: SingleResult, expanded: boolean, theme: Theme): Component {
288
+ const mdTheme = getMarkdownTheme();
289
+ const fg = (c: string, t: string) => theme.fg(c as ThemeColor, t);
290
+ const isError =
291
+ r.exitCode !== 0 || r.stopReason === "error" || r.stopReason === "aborted" || r.stopReason === "timeout";
292
+ const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
293
+ const displayItems = getDisplayItems(r.messages);
294
+ const finalOutput = getFinalOutput(r.messages);
295
+
296
+ if (expanded) {
297
+ const container = new Container();
298
+ let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
299
+ if (isError && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
300
+ container.addChild(new Text(header, 0, 0));
301
+ if (isError && r.errorMessage) container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
302
+ container.addChild(new Spacer(1));
303
+ container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0));
304
+ container.addChild(new Text(theme.fg("dim", r.task), 0, 0));
305
+ container.addChild(new Spacer(1));
306
+ container.addChild(new Text(theme.fg("muted", "─── Output ───"), 0, 0));
307
+ if (displayItems.length === 0 && !finalOutput) {
308
+ container.addChild(new Text(theme.fg("muted", "(no output)"), 0, 0));
309
+ } else {
310
+ for (const item of displayItems) {
311
+ if (item.type === "toolCall")
312
+ container.addChild(new Text(theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, fg), 0, 0));
313
+ }
314
+ if (finalOutput) {
315
+ container.addChild(new Spacer(1));
316
+ container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
317
+ }
318
+ }
319
+ const usageStr = formatUsageStats(r.usage, r.model);
320
+ if (usageStr) {
321
+ container.addChild(new Spacer(1));
322
+ container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
323
+ }
324
+ return container;
325
+ }
326
+
327
+ let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
328
+ if (isError && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
329
+ if (isError && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
330
+ else if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`;
331
+ else {
332
+ text += `\n${renderDisplayItemsInner(displayItems, expanded, theme, COLLAPSED_ITEM_COUNT)}`;
333
+ if (displayItems.length > COLLAPSED_ITEM_COUNT) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
334
+ }
335
+ const usageStr = formatUsageStats(r.usage, r.model);
336
+ if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
337
+ return new Text(text, 0, 0);
338
+ }
339
+
340
+ // ── subagent-v2 logic ──
28
341
 
29
342
  const STEER_GRACE_MS = 30_000;
30
343
 
@@ -167,6 +480,9 @@ export default function (pi: ExtensionAPI) {
167
480
  const timeoutMs = Math.max((params.timeout ?? 300) * 1000, STEER_GRACE_MS + 10_000);
168
481
  const background = params.background ?? false;
169
482
 
483
+ // Extract parent todo list from session history (for read-only reference)
484
+ const parentTodos = extractParentTodos(ctx.sessionManager.getBranch());
485
+
170
486
  const details: SubagentDetails = {
171
487
  agentScope,
172
488
  projectAgentsDir: discovery.projectAgentsDir,
@@ -241,10 +557,17 @@ export default function (pi: ExtensionAPI) {
241
557
  }
242
558
  };
243
559
 
244
- const client = new RpcClient({
560
+ // Pass subagent role and parent todos to child process via env vars
561
+ const env: Record<string, string> = { PI_SUBAGENT: "true" };
562
+ if (parentTodos.length > 0) {
563
+ env.PI_PARENT_TODOS = JSON.stringify(parentTodos);
564
+ }
565
+
566
+ const client = new RpcClient({
245
567
  cwd: params.cwd ?? ctx.cwd,
246
568
  provider: ctx.model?.provider || undefined,
247
569
  model: agent.model,
570
+ env,
248
571
  args: extraArgs,
249
572
  });
250
573
 
@@ -279,29 +602,37 @@ export default function (pi: ExtensionAPI) {
279
602
  cleanupTempFiles(tmpPromptPath, tmpPromptDir, "subagent-v2");
280
603
  backgroundTasks.delete(taskId);
281
604
 
282
- const finalText = getFinalOutput(currentResult.messages) || "(no output)";
283
- pi.appendEntry("subagent", {
284
- toolCallId,
285
- sessionId,
286
- sessionPath,
287
- description: params.agent,
288
- instruction: params.task,
289
- startedAt,
290
- completedAt: Date.now(),
291
- exitCode: currentResult.exitCode,
292
- finalText,
293
- });
294
-
295
- const isCrash = currentResult.exitCode !== 0;
296
- const summary = finalText.slice(0, 200);
297
- const msg = isCrash
298
- ? `子任务中断:${params.agent} — ${currentResult.errorMessage || summary}`
299
- : `子任务完成:${params.agent} — ${summary}`;
300
605
  try {
301
- pi.sendUserMessage(msg, { deliverAs: "followUp" });
606
+ const finalText = getFinalOutput(currentResult.messages) || "(no output)";
607
+ pi.appendEntry("subagent", {
608
+ toolCallId,
609
+ sessionId,
610
+ sessionPath,
611
+ description: params.agent,
612
+ instruction: params.task,
613
+ startedAt,
614
+ completedAt: Date.now(),
615
+ exitCode: currentResult.exitCode,
616
+ finalText,
617
+ });
618
+
619
+ const isCrash = currentResult.exitCode !== 0;
620
+ const summary = finalText.slice(0, 200);
621
+ const msg = isCrash
622
+ ? `子任务中断:${params.agent} — ${currentResult.errorMessage || summary}`
623
+ : `子任务完成:${params.agent} — ${summary}`;
624
+ try {
625
+ pi.sendUserMessage(msg, { deliverAs: "followUp" });
626
+ } catch (err) {
627
+ const eMsg = err instanceof Error ? err.message : String(err);
628
+ if (/stale/i.test(eMsg)) return;
629
+ console.debug("[subagent-v2] followUp delivery failed:", eMsg);
630
+ pi.sendUserMessage(msg);
631
+ }
302
632
  } catch (err) {
303
- console.debug("[subagent-v2] followUp delivery failed:", err instanceof Error ? err.message : err);
304
- pi.sendUserMessage(msg);
633
+ const eMsg = err instanceof Error ? err.message : String(err);
634
+ if (/stale/i.test(eMsg)) return;
635
+ throw err;
305
636
  }
306
637
  }
307
638
  };
@@ -499,29 +830,37 @@ export default function (pi: ExtensionAPI) {
499
830
  await client.stop();
500
831
  backgroundTasks.delete(taskId);
501
832
 
502
- const finalText = getFinalOutput(currentResult.messages) || "(no output)";
503
- pi.appendEntry("subagent", {
504
- toolCallId,
505
- sessionId,
506
- sessionPath: sPath,
507
- description: "(resumed)",
508
- instruction: params.instruction ?? "(resume)",
509
- startedAt,
510
- completedAt: Date.now(),
511
- exitCode: currentResult.exitCode,
512
- finalText,
513
- });
514
-
515
- const isCrash = currentResult.exitCode !== 0;
516
- const summary = finalText.slice(0, 200);
517
- const msg = isCrash
518
- ? `子任务中断:(resumed) — ${currentResult.errorMessage || summary}`
519
- : `子任务完成:(resumed) — ${summary}`;
520
833
  try {
521
- pi.sendUserMessage(msg, { deliverAs: "followUp" });
834
+ const finalText = getFinalOutput(currentResult.messages) || "(no output)";
835
+ pi.appendEntry("subagent", {
836
+ toolCallId,
837
+ sessionId,
838
+ sessionPath: sPath,
839
+ description: "(resumed)",
840
+ instruction: params.instruction ?? "(resume)",
841
+ startedAt,
842
+ completedAt: Date.now(),
843
+ exitCode: currentResult.exitCode,
844
+ finalText,
845
+ });
846
+
847
+ const isCrash = currentResult.exitCode !== 0;
848
+ const summary = finalText.slice(0, 200);
849
+ const msg = isCrash
850
+ ? `子任务中断:(resumed) — ${currentResult.errorMessage || summary}`
851
+ : `子任务完成:(resumed) — ${summary}`;
852
+ try {
853
+ pi.sendUserMessage(msg, { deliverAs: "followUp" });
854
+ } catch (err) {
855
+ const eMsg = err instanceof Error ? err.message : String(err);
856
+ if (/stale/i.test(eMsg)) return;
857
+ console.debug("[subagent-v2] resumed followUp delivery failed:", eMsg);
858
+ pi.sendUserMessage(msg);
859
+ }
522
860
  } catch (err) {
523
- console.debug("[subagent-v2] resumed followUp delivery failed:", err instanceof Error ? err.message : err);
524
- pi.sendUserMessage(msg);
861
+ const eMsg = err instanceof Error ? err.message : String(err);
862
+ if (/stale/i.test(eMsg)) return;
863
+ throw err;
525
864
  }
526
865
  }
527
866
  };
@@ -597,3 +936,37 @@ export default function (pi: ExtensionAPI) {
597
936
  },
598
937
  });
599
938
  }
939
+
940
+ /**
941
+ * Extract the parent session's todo list from session history entries.
942
+ * Scans custom "todo" entries and tool result messages for the latest list.
943
+ * Returns active (not deleted, not done) todos for read-only reference.
944
+ */
945
+ export function extractParentTodos(branch: unknown[]): Array<{ id: number; text: string; priority?: string; done: boolean }> {
946
+ let todos: Array<{ id: number; text: string; done: boolean; deleted?: boolean; priority?: string }> = [];
947
+ let nextId = 1;
948
+
949
+ for (const entry of branch) {
950
+ const e = entry as Record<string, unknown>;
951
+ if (e.type === "custom" && (e as Record<string, unknown>).customType === "todo") {
952
+ const data = (e as Record<string, unknown>).data as Record<string, unknown> | undefined;
953
+ if (data?.todos) {
954
+ todos = data.todos as typeof todos;
955
+ nextId = (data.nextId as number) ?? nextId;
956
+ }
957
+ continue;
958
+ }
959
+ if (e.type !== "message") continue;
960
+ const msg = (e as Record<string, unknown>).message as Record<string, unknown> | undefined;
961
+ if (!msg || msg.role !== "toolResult" || (msg as Record<string, unknown>).toolName !== "todo") continue;
962
+ const details = (msg as Record<string, unknown>).details as Record<string, unknown> | undefined;
963
+ if (details?.todos) {
964
+ todos = details.todos as typeof todos;
965
+ nextId = (details.nextId as number) ?? nextId;
966
+ }
967
+ }
968
+
969
+ return todos
970
+ .filter((t) => !t.deleted && !t.done)
971
+ .map((t) => ({ id: t.id, text: t.text, priority: t.priority, done: t.done }));
972
+ }
@@ -79,14 +79,51 @@ function updateWidget(ctx: ExtensionContext | undefined, todos: Todo[]): void {
79
79
  }
80
80
 
81
81
  export default function (pi: ExtensionAPI) {
82
+ // ── Sub-agent mode: inject parent's todos as read-only, no todo tool ──
83
+ if (process.env.PI_SUBAGENT === "true") {
84
+ let parentTodos: TodoItem[] = [];
85
+ try {
86
+ const raw = process.env.PI_PARENT_TODOS;
87
+ if (raw) parentTodos = JSON.parse(raw) as TodoItem[];
88
+ } catch {
89
+ // ignore parse errors
90
+ }
91
+ const active = parentTodos.filter((t) => !t.deleted && !t.done);
92
+ if (active.length > 0) {
93
+ const lines = active.map((t) => {
94
+ const pri = t.priority === "high" ? " [!]" : t.priority === "low" ? " [?]" : "";
95
+ return ` #${t.id}${pri}: ${t.text}`;
96
+ });
97
+ const header = `[Parent session's tasks — read-only]\nThese are the parent session's active tasks for reference. Do not modify them.\n${lines.join("\n")}`;
98
+ pi.on("context", (_event, _ctx) => {
99
+ return {
100
+ messages: [
101
+ ...(_event as any).messages,
102
+ {
103
+ role: "user" as const,
104
+ content: [{ type: "text" as const, text: header }],
105
+ timestamp: Date.now(),
106
+ },
107
+ ],
108
+ };
109
+ });
110
+ }
111
+ return; // Skip all tool/command/channel registration
112
+ }
113
+
114
+ // ── Normal mode ──
82
115
  let todos: Todo[] = [];
83
116
  let nextId = 1;
84
117
  let channel: ServerChannel<TodoChannelContract> | null = null;
85
118
 
86
119
  pi.on("session_start", async (_event, ctx) => {
87
- const rawChannel = pi.registerChannel(TODO_CHANNEL_NAME);
88
- const typed = createTypedChannel<TodoChannelContract>(rawChannel);
89
- channel = typed.server;
120
+ try {
121
+ const rawChannel = pi.registerChannel(TODO_CHANNEL_NAME);
122
+ const typed = createTypedChannel<TodoChannelContract>(rawChannel);
123
+ channel = typed.server;
124
+ } catch {
125
+ // registerChannel only available in RPC mode — skip in interactive mode
126
+ }
90
127
 
91
128
  todos = [];
92
129
  nextId = 1;
@@ -399,6 +436,28 @@ IMPORTANT: For creating a plan with multiple steps, use a SINGLE add call with n
399
436
  },
400
437
  });
401
438
 
439
+ // Inject active todo list into context so the LLM is aware of ongoing tasks
440
+ pi.on("context", (_event, _ctx) => {
441
+ const active = todos.filter((t) => !t.deleted && !t.done);
442
+ if (active.length === 0) return;
443
+
444
+ const lines = active.map((t) => {
445
+ const pri = t.priority === "high" ? " [!]" : t.priority === "low" ? " [?]" : "";
446
+ return ` #${t.id}${pri}: ${t.text}`;
447
+ });
448
+ const text = `[Todo list — ${active.length} active task(s)]\n${lines.join("\n")}`;
449
+ return {
450
+ messages: [
451
+ ...(_event as any).messages,
452
+ {
453
+ role: "user" as const,
454
+ content: [{ type: "text" as const, text }],
455
+ timestamp: Date.now(),
456
+ },
457
+ ],
458
+ };
459
+ });
460
+
402
461
  pi.registerCommand("todos", {
403
462
  description: "Show all todos on the current branch",
404
463
  handler: async (_args, ctx) => {