@ryan_nookpi/pi-extension-subagent 0.3.0 → 0.3.2
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/README.md +22 -0
- package/claude-sdk-runner.ts +1 -1
- package/commands.ts +46 -52
- package/diagnostics.ts +145 -0
- package/index.ts +41 -3
- package/lifecycle.ts +13 -2
- package/package.json +99 -97
- package/registration-manifest.ts +23 -0
- package/run-utils.ts +1 -1
- package/runner.ts +185 -33
- package/tool-execute.ts +4 -2
package/README.md
CHANGED
|
@@ -238,6 +238,28 @@ Confirm the environment used to start pi has valid Anthropic authentication, suc
|
|
|
238
238
|
|
|
239
239
|
That is intentional. Hidden `>` runs are human-only UI jobs. Inspect them with `/sub:peek`, `<>runId`, or `/sub:open`.
|
|
240
240
|
|
|
241
|
+
### Debug runner termination
|
|
242
|
+
|
|
243
|
+
The extension records process lifecycle diagnostics as `subagent-runner-diagnostic` custom entries in the parent session JSONL. They include run/batch IDs, parent and child PID/PGID, abort reason, internal kill cause, `exit`/`close` code and signal, settle reason, shutdown reason, and process-error stacks. Custom entries are not sent to the LLM and remain hidden unless an entry renderer is registered, as documented by pi's [`appendEntry` API](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md#piappendentrycustomtype-data).
|
|
244
|
+
|
|
245
|
+
Run `/session` to find the current session file, set `SESSION_FILE` to that path, then extract only the diagnostic entries:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
jq -c 'select(.type == "custom" and .customType == "subagent-runner-diagnostic") | {timestamp, data}' "$SESSION_FILE"
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
For batch failures, compare entries by `batchId` and `runId`, then follow each run from `spawn` through `kill_intent`, `exit`, `close`, and `settled`. Share only the selected diagnostic lines—not the full session file, which may contain prompts or tool output.
|
|
252
|
+
|
|
253
|
+
To report a reproducible extension bug, create `subagent-issue.md` with the pi and extension versions, OS and Node version, launch mode/command, reproduction steps, expected and actual behavior, and the sanitized diagnostic lines. Then submit it with:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
gh issue create --repo Jonghakseo/pi-extension \
|
|
257
|
+
--title "subagent: unexpected child termination" \
|
|
258
|
+
--body-file subagent-issue.md
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Without GitHub CLI, use the repository's [new issue page](https://github.com/Jonghakseo/pi-extension/issues/new). Remove secrets and sensitive paths before attaching diagnostics.
|
|
262
|
+
|
|
241
263
|
## Security and trust boundary
|
|
242
264
|
|
|
243
265
|
- Claude SDK execution uses `permissionMode: "bypassPermissions"` with `allowDangerouslySkipPermissions`; Claude CLI execution uses `--dangerously-skip-permissions`.
|
package/claude-sdk-runner.ts
CHANGED
package/commands.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
SUBVIEW_OVERLAY_MAX_HEIGHT,
|
|
27
27
|
SUBVIEW_OVERLAY_WIDTH,
|
|
28
28
|
} from "./constants.js";
|
|
29
|
+
import { createRunDiagnosticSink } from "./diagnostics.js";
|
|
29
30
|
import {
|
|
30
31
|
buildSubagentDisplayTaskFallback,
|
|
31
32
|
createDisplayTaskRefreshToken,
|
|
@@ -42,6 +43,7 @@ import {
|
|
|
42
43
|
} from "./group-pending.js";
|
|
43
44
|
import { enqueueSubagentInvocation } from "./invocation-queue.js";
|
|
44
45
|
import { appendDisplayTaskUpdate, getSessionFileSize } from "./persisted-session.js";
|
|
46
|
+
import { SUBAGENT_COMMANDS, type SubagentCommandName } from "./registration-manifest.js";
|
|
45
47
|
import { readSessionReplayItems, SubagentSessionReplayOverlay } from "./replay.js";
|
|
46
48
|
import { invokeWithAutoRetry, MAX_SUBAGENT_AUTO_RETRIES } from "./retry.js";
|
|
47
49
|
import { getLatestRun, removeRun, trimCommandRunHistory } from "./run-utils.js";
|
|
@@ -890,7 +892,18 @@ function finalizeHumanOnlyCompletion(
|
|
|
890
892
|
store.globalLiveRuns.delete(runId);
|
|
891
893
|
}
|
|
892
894
|
|
|
893
|
-
export
|
|
895
|
+
export type SubagentCommandDefinition = Parameters<ExtensionAPI["registerCommand"]>[1];
|
|
896
|
+
|
|
897
|
+
export interface SubagentRegistrations {
|
|
898
|
+
commands: ReadonlyMap<SubagentCommandName, SubagentCommandDefinition>;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentRegistrations {
|
|
902
|
+
const commandDefinitions = new Map<SubagentCommandName, SubagentCommandDefinition>();
|
|
903
|
+
const defineCommand = (name: SubagentCommandName, definition: SubagentCommandDefinition): void => {
|
|
904
|
+
commandDefinitions.set(name, definition);
|
|
905
|
+
};
|
|
906
|
+
|
|
894
907
|
pi.registerTool({
|
|
895
908
|
name: "list-agents",
|
|
896
909
|
label: "List Agents",
|
|
@@ -958,8 +971,7 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): void {
|
|
|
958
971
|
});
|
|
959
972
|
|
|
960
973
|
const subCommand = {
|
|
961
|
-
description:
|
|
962
|
-
"Run a subagent in a dedicated sub-session: /sub:isolate <agent|alias> <task>, /sub:isolate <runId> <task>, /sub:isolate <task> (uses configured defaultAgent)",
|
|
974
|
+
description: SUBAGENT_COMMANDS["sub:isolate"],
|
|
963
975
|
getArgumentCompletions: (argumentPrefix: string) => {
|
|
964
976
|
const trimmedStart = argumentPrefix.trimStart();
|
|
965
977
|
if (trimmedStart.includes(" ")) return null;
|
|
@@ -1423,6 +1435,7 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): void {
|
|
|
1423
1435
|
resumeSessionId: runState.claudeSessionId,
|
|
1424
1436
|
sidecarSessionFile: runState.sessionFile,
|
|
1425
1437
|
persistedSessionBaseOffset: runState.persistedSessionBaseOffset,
|
|
1438
|
+
onDiagnostic: createRunDiagnosticSink(pi, runState),
|
|
1426
1439
|
},
|
|
1427
1440
|
),
|
|
1428
1441
|
);
|
|
@@ -1574,10 +1587,10 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): void {
|
|
|
1574
1587
|
},
|
|
1575
1588
|
};
|
|
1576
1589
|
|
|
1577
|
-
|
|
1590
|
+
defineCommand("sub:isolate", subCommand);
|
|
1578
1591
|
|
|
1579
|
-
|
|
1580
|
-
description: "
|
|
1592
|
+
defineCommand("sub:main", {
|
|
1593
|
+
description: SUBAGENT_COMMANDS["sub:main"],
|
|
1581
1594
|
getArgumentCompletions: subCommand.getArgumentCompletions,
|
|
1582
1595
|
handler: async (args, ctx) => {
|
|
1583
1596
|
captureSwitchSession(store, ctx);
|
|
@@ -1586,8 +1599,8 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): void {
|
|
|
1586
1599
|
},
|
|
1587
1600
|
});
|
|
1588
1601
|
|
|
1589
|
-
|
|
1590
|
-
description:
|
|
1602
|
+
defineCommand("subagents", {
|
|
1603
|
+
description: SUBAGENT_COMMANDS.subagents,
|
|
1591
1604
|
handler: async (_args, ctx) => {
|
|
1592
1605
|
captureSwitchSession(store, ctx);
|
|
1593
1606
|
const starterPack = await offerStarterPackIfEmpty(ctx);
|
|
@@ -1619,8 +1632,8 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): void {
|
|
|
1619
1632
|
},
|
|
1620
1633
|
});
|
|
1621
1634
|
|
|
1622
|
-
|
|
1623
|
-
description: "
|
|
1635
|
+
defineCommand("sub:peek", {
|
|
1636
|
+
description: SUBAGENT_COMMANDS["sub:peek"],
|
|
1624
1637
|
getArgumentCompletions: (argumentPrefix) => {
|
|
1625
1638
|
const trimmedStart = argumentPrefix.trimStart();
|
|
1626
1639
|
if (trimmedStart.includes(" ")) return null;
|
|
@@ -1643,8 +1656,8 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): void {
|
|
|
1643
1656
|
},
|
|
1644
1657
|
});
|
|
1645
1658
|
|
|
1646
|
-
|
|
1647
|
-
description: "
|
|
1659
|
+
defineCommand("sub:open", {
|
|
1660
|
+
description: SUBAGENT_COMMANDS["sub:open"],
|
|
1648
1661
|
getArgumentCompletions: (argumentPrefix) => {
|
|
1649
1662
|
const trimmedStart = argumentPrefix.trimStart();
|
|
1650
1663
|
if (trimmedStart.includes(" ")) return null;
|
|
@@ -1735,8 +1748,8 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): void {
|
|
|
1735
1748
|
},
|
|
1736
1749
|
});
|
|
1737
1750
|
|
|
1738
|
-
|
|
1739
|
-
description: "
|
|
1751
|
+
defineCommand("sub:history", {
|
|
1752
|
+
description: SUBAGENT_COMMANDS["sub:history"],
|
|
1740
1753
|
handler: async (_args, ctx) => {
|
|
1741
1754
|
captureSwitchSession(store, ctx);
|
|
1742
1755
|
|
|
@@ -1787,8 +1800,8 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): void {
|
|
|
1787
1800
|
},
|
|
1788
1801
|
});
|
|
1789
1802
|
|
|
1790
|
-
|
|
1791
|
-
description: "
|
|
1803
|
+
defineCommand("sub:rm", {
|
|
1804
|
+
description: SUBAGENT_COMMANDS["sub:rm"],
|
|
1792
1805
|
handler: async (args, ctx) => {
|
|
1793
1806
|
captureSwitchSession(store, ctx);
|
|
1794
1807
|
const raw = (args ?? "").trim();
|
|
@@ -1871,8 +1884,8 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): void {
|
|
|
1871
1884
|
ctx.ui.notify(`Cleared ${removed} finished subagent job(s).`, "info");
|
|
1872
1885
|
};
|
|
1873
1886
|
|
|
1874
|
-
|
|
1875
|
-
description: "
|
|
1887
|
+
defineCommand("sub:clear", {
|
|
1888
|
+
description: SUBAGENT_COMMANDS["sub:clear"],
|
|
1876
1889
|
handler: async (args, ctx) => {
|
|
1877
1890
|
await handleSubClear(args, ctx);
|
|
1878
1891
|
},
|
|
@@ -1896,7 +1909,7 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): void {
|
|
|
1896
1909
|
if (!controller) return false;
|
|
1897
1910
|
run.lastLine = "Aborting by user...";
|
|
1898
1911
|
run.lastOutput = run.lastLine;
|
|
1899
|
-
controller.abort();
|
|
1912
|
+
controller.abort({ source: "sub_abort_command", runId: run.id });
|
|
1900
1913
|
return true;
|
|
1901
1914
|
};
|
|
1902
1915
|
|
|
@@ -1947,23 +1960,16 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): void {
|
|
|
1947
1960
|
ctx.ui.notify("Usage: /sub:abort [runId|all]", "info");
|
|
1948
1961
|
};
|
|
1949
1962
|
|
|
1950
|
-
|
|
1951
|
-
description: "
|
|
1963
|
+
defineCommand("sub:abort", {
|
|
1964
|
+
description: SUBAGENT_COMMANDS["sub:abort"],
|
|
1952
1965
|
handler: async (args, ctx) => {
|
|
1953
1966
|
captureSwitchSession(store, ctx);
|
|
1954
1967
|
await handleSubAbort(args, ctx);
|
|
1955
1968
|
},
|
|
1956
1969
|
});
|
|
1957
1970
|
|
|
1958
|
-
//
|
|
1959
|
-
//
|
|
1960
|
-
// so the hidden subagent prefix is documented via footer/status hints instead.
|
|
1961
|
-
pi.registerShortcut(">>" as any, {
|
|
1962
|
-
description: "Run subagent task",
|
|
1963
|
-
handler: async () => {
|
|
1964
|
-
// Documentation-only entry.
|
|
1965
|
-
},
|
|
1966
|
-
});
|
|
1971
|
+
// Text-prefix shortcuts are registered synchronously in index.ts for
|
|
1972
|
+
// /hotkeys visibility. Their actual routing remains in the input handlers below.
|
|
1967
1973
|
|
|
1968
1974
|
pi.on("input", async (event, ctx) => {
|
|
1969
1975
|
if (event.source === "extension") {
|
|
@@ -2073,13 +2079,6 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): void {
|
|
|
2073
2079
|
});
|
|
2074
2080
|
|
|
2075
2081
|
// #<runId> shortcut: resume a subagent run (e.g. #42 keep going)
|
|
2076
|
-
pi.registerShortcut("#<runId>" as any, {
|
|
2077
|
-
description: "Resume subagent run: #<runId> <task>",
|
|
2078
|
-
handler: async () => {
|
|
2079
|
-
// Documentation-only entry.
|
|
2080
|
-
},
|
|
2081
|
-
});
|
|
2082
|
-
|
|
2083
2082
|
pi.on("input", async (event, ctx) => {
|
|
2084
2083
|
if (event.source === "extension") {
|
|
2085
2084
|
return { action: "continue" as const };
|
|
@@ -2126,20 +2125,6 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): void {
|
|
|
2126
2125
|
});
|
|
2127
2126
|
|
|
2128
2127
|
// << shortcut: abort running jobs or clear finished jobs
|
|
2129
|
-
pi.registerShortcut("<<" as any, {
|
|
2130
|
-
description: "Abort or clear subagent runs",
|
|
2131
|
-
handler: async () => {
|
|
2132
|
-
// Documentation-only entry.
|
|
2133
|
-
},
|
|
2134
|
-
});
|
|
2135
|
-
|
|
2136
|
-
pi.registerShortcut("<<<" as any, {
|
|
2137
|
-
description: "Clear finished subagent jobs (= /sub:clear). <<< all to clear all",
|
|
2138
|
-
handler: async () => {
|
|
2139
|
-
// Documentation-only entry.
|
|
2140
|
-
},
|
|
2141
|
-
});
|
|
2142
|
-
|
|
2143
2128
|
pi.on("input", async (event, ctx) => {
|
|
2144
2129
|
if (event.source === "extension") {
|
|
2145
2130
|
return { action: "continue" as const };
|
|
@@ -2204,7 +2189,7 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): void {
|
|
|
2204
2189
|
if (run.status === "running" && shortcutController) {
|
|
2205
2190
|
run.lastLine = "Aborting by user...";
|
|
2206
2191
|
run.lastOutput = run.lastLine;
|
|
2207
|
-
shortcutController.abort();
|
|
2192
|
+
shortcutController.abort({ source: "sub_abort_shortcut", runId: id });
|
|
2208
2193
|
aborted++;
|
|
2209
2194
|
} else if (run.status !== "running") {
|
|
2210
2195
|
const result = removeRun(store, id, {
|
|
@@ -2226,6 +2211,15 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): void {
|
|
|
2226
2211
|
ctx.ui.notify(parts.join(", ") || "Nothing to do.", parts.length ? (aborted ? "warning" : "info") : "info");
|
|
2227
2212
|
return { action: "handled" as const };
|
|
2228
2213
|
});
|
|
2214
|
+
|
|
2215
|
+
const missingCommands = (Object.keys(SUBAGENT_COMMANDS) as SubagentCommandName[]).filter(
|
|
2216
|
+
(name) => !commandDefinitions.has(name),
|
|
2217
|
+
);
|
|
2218
|
+
if (missingCommands.length > 0) {
|
|
2219
|
+
throw new Error(`Missing subagent command definitions: ${missingCommands.join(", ")}`);
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
return { commands: commandDefinitions };
|
|
2229
2223
|
}
|
|
2230
2224
|
|
|
2231
2225
|
// ── onTerminalInput hack: auto-redirect <>7 to /sub:peek 7 ───────────
|
package/diagnostics.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import * as childProcess from "node:child_process";
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { AgentRuntime } from "./agents.js";
|
|
4
|
+
import type { CommandRunState } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export const RUNNER_DIAGNOSTIC_CUSTOM_TYPE = "subagent-runner-diagnostic";
|
|
7
|
+
export const RUNNER_DIAGNOSTIC_SCHEMA_VERSION = 1;
|
|
8
|
+
|
|
9
|
+
export type RunnerDiagnosticEventName =
|
|
10
|
+
| "spawn"
|
|
11
|
+
| "abort_received"
|
|
12
|
+
| "kill_intent"
|
|
13
|
+
| "kill_result"
|
|
14
|
+
| "process_error"
|
|
15
|
+
| "exit"
|
|
16
|
+
| "close"
|
|
17
|
+
| "settled"
|
|
18
|
+
| "session_shutdown";
|
|
19
|
+
|
|
20
|
+
export interface RunnerDiagnosticEvent {
|
|
21
|
+
event: RunnerDiagnosticEventName;
|
|
22
|
+
runtime?: AgentRuntime;
|
|
23
|
+
parentPid?: number;
|
|
24
|
+
parentProcessGroupId?: number;
|
|
25
|
+
childPid?: number;
|
|
26
|
+
childParentPid?: number;
|
|
27
|
+
childProcessGroupId?: number;
|
|
28
|
+
processGroupMode?: "inherited";
|
|
29
|
+
code?: number | null;
|
|
30
|
+
signal?: NodeJS.Signals | null;
|
|
31
|
+
cause?: string;
|
|
32
|
+
abortReason?: unknown;
|
|
33
|
+
killSent?: boolean;
|
|
34
|
+
settleReason?: string;
|
|
35
|
+
stopReason?: string;
|
|
36
|
+
activeRunIds?: number[];
|
|
37
|
+
sessionShutdownReason?: string;
|
|
38
|
+
error?: DiagnosticError;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type RunnerDiagnosticSink = (event: RunnerDiagnosticEvent) => void;
|
|
42
|
+
|
|
43
|
+
interface DiagnosticError {
|
|
44
|
+
name?: string;
|
|
45
|
+
message: string;
|
|
46
|
+
stack?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface ProcessIdentity {
|
|
50
|
+
pid: number;
|
|
51
|
+
parentPid?: number;
|
|
52
|
+
processGroupId?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function asFiniteInteger(value: string | undefined): number | undefined {
|
|
56
|
+
if (!value) return undefined;
|
|
57
|
+
const parsed = Number(value);
|
|
58
|
+
return Number.isInteger(parsed) ? parsed : undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Best-effort POSIX process identity lookup. Diagnostics must never affect execution. */
|
|
62
|
+
export function readProcessIdentity(pid: number | undefined): ProcessIdentity | undefined {
|
|
63
|
+
if (!pid || !Number.isInteger(pid) || pid <= 0) return undefined;
|
|
64
|
+
if (process.platform === "win32") return { pid };
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const result = childProcess.spawnSync("ps", ["-o", "pid=,ppid=,pgid=", "-p", String(pid)], {
|
|
68
|
+
encoding: "utf8",
|
|
69
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
70
|
+
});
|
|
71
|
+
const fields = result.stdout?.trim().split(/\s+/);
|
|
72
|
+
if (!fields || fields.length < 3) return { pid };
|
|
73
|
+
return {
|
|
74
|
+
pid: asFiniteInteger(fields[0]) ?? pid,
|
|
75
|
+
parentPid: asFiniteInteger(fields[1]),
|
|
76
|
+
processGroupId: asFiniteInteger(fields[2]),
|
|
77
|
+
};
|
|
78
|
+
} catch {
|
|
79
|
+
return { pid };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function serializeDiagnosticValue(value: unknown): unknown {
|
|
84
|
+
if (value instanceof Error) {
|
|
85
|
+
return {
|
|
86
|
+
name: value.name,
|
|
87
|
+
message: value.message,
|
|
88
|
+
stack: value.stack,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (value === undefined || value === null) return value;
|
|
92
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
return JSON.parse(JSON.stringify(value));
|
|
96
|
+
} catch {
|
|
97
|
+
return String(value);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function toDiagnosticError(error: unknown): DiagnosticError {
|
|
102
|
+
if (error instanceof Error) {
|
|
103
|
+
return { name: error.name, message: error.message, stack: error.stack };
|
|
104
|
+
}
|
|
105
|
+
return { message: String(error) };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Emit best-effort diagnostics without allowing logging failures to affect a run. */
|
|
109
|
+
export function emitRunnerDiagnostic(sink: RunnerDiagnosticSink | undefined, event: RunnerDiagnosticEvent): void {
|
|
110
|
+
if (!sink) return;
|
|
111
|
+
try {
|
|
112
|
+
sink(event);
|
|
113
|
+
} catch {
|
|
114
|
+
// Diagnostics must never alter subagent execution or completion delivery.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function appendRunnerDiagnostic(pi: Pick<ExtensionAPI, "appendEntry">, data: Record<string, unknown>): void {
|
|
119
|
+
try {
|
|
120
|
+
pi.appendEntry(RUNNER_DIAGNOSTIC_CUSTOM_TYPE, {
|
|
121
|
+
schemaVersion: RUNNER_DIAGNOSTIC_SCHEMA_VERSION,
|
|
122
|
+
recordedAt: new Date().toISOString(),
|
|
123
|
+
...data,
|
|
124
|
+
});
|
|
125
|
+
} catch {
|
|
126
|
+
// Session teardown may invalidate the old extension runtime.
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function createRunDiagnosticSink(
|
|
131
|
+
pi: Pick<ExtensionAPI, "appendEntry">,
|
|
132
|
+
runState: CommandRunState,
|
|
133
|
+
): RunnerDiagnosticSink {
|
|
134
|
+
return (event) => {
|
|
135
|
+
appendRunnerDiagnostic(pi, {
|
|
136
|
+
runId: runState.id,
|
|
137
|
+
agent: runState.agent,
|
|
138
|
+
batchId: runState.batchId,
|
|
139
|
+
pipelineId: runState.pipelineId,
|
|
140
|
+
pipelineStepIndex: runState.pipelineStepIndex,
|
|
141
|
+
...event,
|
|
142
|
+
abortReason: serializeDiagnosticValue(event.abortReason),
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
}
|
package/index.ts
CHANGED
|
@@ -29,10 +29,12 @@
|
|
|
29
29
|
|
|
30
30
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
31
31
|
import { HANG_CHECK_INTERVAL_MS } from "./constants.js";
|
|
32
|
+
import { SUBAGENT_COMMANDS, SUBAGENT_SHORTCUTS, type SubagentCommandName } from "./registration-manifest.js";
|
|
32
33
|
|
|
33
34
|
interface SubagentCore {
|
|
34
35
|
store: import("./store.js").SubagentStore;
|
|
35
36
|
commands: typeof import("./commands.js");
|
|
37
|
+
registrations: import("./commands.js").SubagentRegistrations;
|
|
36
38
|
escalation: typeof import("./escalation.js");
|
|
37
39
|
lifecycle: typeof import("./lifecycle.js");
|
|
38
40
|
}
|
|
@@ -52,10 +54,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
52
54
|
import("./store.js"),
|
|
53
55
|
]);
|
|
54
56
|
const store = storeMod.createStore();
|
|
55
|
-
commands.registerAll(pi, store);
|
|
56
|
-
core = { store, commands, escalation, lifecycle };
|
|
57
|
+
const registrations = commands.registerAll(pi, store);
|
|
58
|
+
core = { store, commands, registrations, escalation, lifecycle };
|
|
57
59
|
return core;
|
|
58
|
-
})()
|
|
60
|
+
})().catch((error) => {
|
|
61
|
+
corePromise = null;
|
|
62
|
+
throw error;
|
|
63
|
+
});
|
|
59
64
|
return corePromise;
|
|
60
65
|
};
|
|
61
66
|
|
|
@@ -69,6 +74,39 @@ export default function (pi: ExtensionAPI) {
|
|
|
69
74
|
return chain;
|
|
70
75
|
};
|
|
71
76
|
|
|
77
|
+
const getCommandDefinition = async (name: SubagentCommandName) => {
|
|
78
|
+
const loaded = await loadCore();
|
|
79
|
+
await chain;
|
|
80
|
+
const definition = loaded.registrations.commands.get(name);
|
|
81
|
+
if (!definition) throw new Error(`Subagent command definition not found: ${name}`);
|
|
82
|
+
return definition;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Register lightweight proxies synchronously so Pi's initial autocomplete
|
|
86
|
+
// and command registry contain every subagent command before lazy core load.
|
|
87
|
+
for (const [name, description] of Object.entries(SUBAGENT_COMMANDS) as Array<[SubagentCommandName, string]>) {
|
|
88
|
+
pi.registerCommand(name, {
|
|
89
|
+
description,
|
|
90
|
+
getArgumentCompletions: async (prefix) => {
|
|
91
|
+
const definition = await getCommandDefinition(name);
|
|
92
|
+
return definition.getArgumentCompletions?.(prefix) ?? null;
|
|
93
|
+
},
|
|
94
|
+
handler: async (args, ctx) => {
|
|
95
|
+
const definition = await getCommandDefinition(name);
|
|
96
|
+
return definition.handler(args, ctx);
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// These entries document text-prefix shortcuts in /hotkeys. Actual routing
|
|
102
|
+
// remains in commands.ts input handlers after the lazy core is loaded.
|
|
103
|
+
for (const [shortcut, description] of Object.entries(SUBAGENT_SHORTCUTS)) {
|
|
104
|
+
pi.registerShortcut(shortcut as never, {
|
|
105
|
+
description,
|
|
106
|
+
handler: async () => {},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
72
110
|
// Start loading in the background without blocking extension boot.
|
|
73
111
|
// setTimeout keeps the load out of the boot path's microtask drains.
|
|
74
112
|
setTimeout(() => {
|
package/lifecycle.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { HANG_TIMEOUT_MS } from "./constants.js";
|
|
10
|
+
import { appendRunnerDiagnostic } from "./diagnostics.js";
|
|
10
11
|
import { getSessionFileMtimeMs, readPersistedSessionSnapshot } from "./persisted-session.js";
|
|
11
12
|
import { getLastNonEmptyLine } from "./runner.js";
|
|
12
13
|
import type { SubagentStore } from "./store.js";
|
|
@@ -59,6 +60,16 @@ export function shutdownSubagentRuns(store: SubagentStore, pi: ExtensionAPI, rea
|
|
|
59
60
|
for (const [runId, run] of store.commandRuns) activeRuns.set(runId, run);
|
|
60
61
|
for (const [runId, entry] of store.globalLiveRuns) activeRuns.set(runId, entry.runState);
|
|
61
62
|
|
|
63
|
+
const activeRunIds = Array.from(activeRuns)
|
|
64
|
+
.filter(([, run]) => run.status === "running")
|
|
65
|
+
.map(([runId]) => runId);
|
|
66
|
+
appendRunnerDiagnostic(pi, {
|
|
67
|
+
event: "session_shutdown",
|
|
68
|
+
parentPid: process.pid,
|
|
69
|
+
sessionShutdownReason: reason,
|
|
70
|
+
activeRunIds,
|
|
71
|
+
});
|
|
72
|
+
|
|
62
73
|
for (const [runId, run] of activeRuns) {
|
|
63
74
|
if (run.status !== "running") continue;
|
|
64
75
|
const message = `Aborted because the parent pi session ${reason} is shutting down.`;
|
|
@@ -70,7 +81,7 @@ export function shutdownSubagentRuns(store: SubagentStore, pi: ExtensionAPI, rea
|
|
|
70
81
|
run.removed = true;
|
|
71
82
|
|
|
72
83
|
const controller = run.abortController ?? store.globalLiveRuns.get(runId)?.abortController;
|
|
73
|
-
controller?.abort();
|
|
84
|
+
controller?.abort({ source: "session_shutdown", reason, runId, activeRunIds });
|
|
74
85
|
run.abortController = undefined;
|
|
75
86
|
|
|
76
87
|
if (run.deliveryMode === "humanOnly") continue;
|
|
@@ -141,7 +152,7 @@ export function checkForHungRuns(store: SubagentStore, _pi: ExtensionAPI): void
|
|
|
141
152
|
run.autoAbortReason = reason;
|
|
142
153
|
|
|
143
154
|
if (controller) {
|
|
144
|
-
controller.abort();
|
|
155
|
+
controller.abort({ source: "hang_timeout", runId, idleMs, reason });
|
|
145
156
|
}
|
|
146
157
|
}
|
|
147
158
|
|
package/package.json
CHANGED
|
@@ -1,98 +1,100 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
2
|
+
"name": "@ryan_nookpi/pi-extension-subagent",
|
|
3
|
+
"version": "0.3.2",
|
|
4
|
+
"description": "Asynchronous subagent delegation for pi with run, batch, chain, and continuation workflows.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/Jonghakseo/pi-extension.git",
|
|
9
|
+
"directory": "packages/subagent"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Jonghakseo/pi-extension/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/Jonghakseo/pi-extension/tree/main/packages/subagent#readme",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"pi-package",
|
|
18
|
+
"subagent",
|
|
19
|
+
"delegation"
|
|
20
|
+
],
|
|
21
|
+
"files": [
|
|
22
|
+
"above-widget.ts",
|
|
23
|
+
"agents.ts",
|
|
24
|
+
"claude-args.ts",
|
|
25
|
+
"claude-sdk-runner.ts",
|
|
26
|
+
"claude-sdk-stream-adapter.ts",
|
|
27
|
+
"claude-sidecar-writer.ts",
|
|
28
|
+
"claude-stream-parser.ts",
|
|
29
|
+
"cli.ts",
|
|
30
|
+
"commands.ts",
|
|
31
|
+
"config.ts",
|
|
32
|
+
"constants.ts",
|
|
33
|
+
"context-limits.ts",
|
|
34
|
+
"diagnostics.ts",
|
|
35
|
+
"display-task.ts",
|
|
36
|
+
"escalation.ts",
|
|
37
|
+
"format.ts",
|
|
38
|
+
"group-pending.ts",
|
|
39
|
+
"index.ts",
|
|
40
|
+
"lifecycle.ts",
|
|
41
|
+
"invocation-queue.ts",
|
|
42
|
+
"live-preview.ts",
|
|
43
|
+
"persisted-session.ts",
|
|
44
|
+
"replay.ts",
|
|
45
|
+
"registration-manifest.ts",
|
|
46
|
+
"retry.ts",
|
|
47
|
+
"run-utils.ts",
|
|
48
|
+
"runner.ts",
|
|
49
|
+
"session.ts",
|
|
50
|
+
"starter-pack.ts",
|
|
51
|
+
"store.ts",
|
|
52
|
+
"tool-execute.ts",
|
|
53
|
+
"tool-render.ts",
|
|
54
|
+
"types.ts",
|
|
55
|
+
"widget.ts",
|
|
56
|
+
"utils/agent-utils.ts",
|
|
57
|
+
"utils/short-label.ts",
|
|
58
|
+
"utils/string-utils.ts",
|
|
59
|
+
"utils/format-utils.ts",
|
|
60
|
+
"utils/time-utils.ts",
|
|
61
|
+
"seeds",
|
|
62
|
+
"README.md"
|
|
63
|
+
],
|
|
64
|
+
"pi": {
|
|
65
|
+
"extensions": [
|
|
66
|
+
"./index.ts"
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.98",
|
|
71
|
+
"yaml": "^2.9.0"
|
|
72
|
+
},
|
|
73
|
+
"peerDependencies": {
|
|
74
|
+
"@earendil-works/pi-ai": "*",
|
|
75
|
+
"@earendil-works/pi-agent-core": "*",
|
|
76
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
77
|
+
"@earendil-works/pi-tui": "*",
|
|
78
|
+
"typebox": "*"
|
|
79
|
+
},
|
|
80
|
+
"peerDependenciesMeta": {
|
|
81
|
+
"@earendil-works/pi-ai": {
|
|
82
|
+
"optional": true
|
|
83
|
+
},
|
|
84
|
+
"@earendil-works/pi-agent-core": {
|
|
85
|
+
"optional": true
|
|
86
|
+
},
|
|
87
|
+
"@earendil-works/pi-coding-agent": {
|
|
88
|
+
"optional": true
|
|
89
|
+
},
|
|
90
|
+
"@earendil-works/pi-tui": {
|
|
91
|
+
"optional": true
|
|
92
|
+
},
|
|
93
|
+
"typebox": {
|
|
94
|
+
"optional": true
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"publishConfig": {
|
|
98
|
+
"access": "public"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const SUBAGENT_COMMANDS = {
|
|
2
|
+
"sub:isolate":
|
|
3
|
+
"Run a subagent in a dedicated sub-session: /sub:isolate <agent|alias> <task>, /sub:isolate <runId> <task>, /sub:isolate <task> (uses configured defaultAgent)",
|
|
4
|
+
"sub:main": "Run a subagent with main-session context inheritance: /sub:main <agent|alias> <task>",
|
|
5
|
+
subagents: "List available subagents and offer the starter pack when none are configured",
|
|
6
|
+
"sub:peek": "Show the latest response from a subagent in an overlay: /sub:peek [runId]",
|
|
7
|
+
"sub:open": "Open a subagent session replay overlay: /sub:open [runId]",
|
|
8
|
+
"sub:history": "Show all subagent run history (including removed) in an overlay: /sub:history",
|
|
9
|
+
"sub:rm": "Remove one /sub job entry (aborts it if running): /sub:rm [runId]",
|
|
10
|
+
"sub:clear": "Clear /sub job widget entries. /sub:clear (finished only) or /sub:clear all",
|
|
11
|
+
"sub:abort": "Abort running subagent job(s). /sub:abort [runId|all]",
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
export type SubagentCommandName = keyof typeof SUBAGENT_COMMANDS;
|
|
15
|
+
|
|
16
|
+
export const SUBAGENT_SHORTCUTS = {
|
|
17
|
+
">>": "Run subagent task",
|
|
18
|
+
"#<runId>": "Resume subagent run: #<runId> <task>",
|
|
19
|
+
"<<": "Abort or clear subagent runs",
|
|
20
|
+
"<<<": "Clear finished subagent jobs (= /sub:clear). <<< all to clear all",
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
export type SubagentShortcutName = keyof typeof SUBAGENT_SHORTCUTS;
|
package/run-utils.ts
CHANGED
|
@@ -92,7 +92,7 @@ export function removeRun(store: SubagentStore, runId: number, options: RemoveRu
|
|
|
92
92
|
const reason = options.reason ?? "Aborting by remove...";
|
|
93
93
|
run.lastLine = reason;
|
|
94
94
|
run.lastOutput = reason;
|
|
95
|
-
controller.abort();
|
|
95
|
+
controller.abort({ source: "remove_run", runId, reason, removalReason: options.removalReason });
|
|
96
96
|
aborted = true;
|
|
97
97
|
}
|
|
98
98
|
|
package/runner.ts
CHANGED
|
@@ -17,6 +17,13 @@ import {
|
|
|
17
17
|
} from "./claude-stream-parser.js";
|
|
18
18
|
import { resolveClaudeRuntimeMode } from "./config.js";
|
|
19
19
|
import { CONTEXT_GUARD_SIGNATURE, resolveContextGuardCeiling, shouldTripContextGuard } from "./context-limits.js";
|
|
20
|
+
import {
|
|
21
|
+
emitRunnerDiagnostic,
|
|
22
|
+
type RunnerDiagnosticSink,
|
|
23
|
+
readProcessIdentity,
|
|
24
|
+
serializeDiagnosticValue,
|
|
25
|
+
toDiagnosticError,
|
|
26
|
+
} from "./diagnostics.js";
|
|
20
27
|
import { formatToolCallPlain } from "./format.js";
|
|
21
28
|
import {
|
|
22
29
|
extractActivityPreviewFromTextDelta,
|
|
@@ -39,6 +46,7 @@ export interface RunSingleAgentSessionConfig {
|
|
|
39
46
|
resumeSessionId?: string;
|
|
40
47
|
sidecarSessionFile?: string;
|
|
41
48
|
persistedSessionBaseOffset?: number;
|
|
49
|
+
onDiagnostic?: RunnerDiagnosticSink;
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
function isUuidLikeSessionId(value: string | undefined): boolean {
|
|
@@ -199,6 +207,7 @@ export async function runSingleAgent(
|
|
|
199
207
|
const resumeSessionId = normalizedSessionConfig.resumeSessionId;
|
|
200
208
|
const sidecarSessionFile = normalizedSessionConfig.sidecarSessionFile ?? sessionFile;
|
|
201
209
|
const persistedSessionBaseOffset = normalizedSessionConfig.persistedSessionBaseOffset ?? 0;
|
|
210
|
+
const onDiagnostic = normalizedSessionConfig.onDiagnostic;
|
|
202
211
|
const agent = agents.find((a) => a.name === agentName);
|
|
203
212
|
|
|
204
213
|
if (!agent) {
|
|
@@ -240,6 +249,7 @@ export async function runSingleAgent(
|
|
|
240
249
|
makeDetails,
|
|
241
250
|
resumeSessionId,
|
|
242
251
|
sidecarSessionFile,
|
|
252
|
+
onDiagnostic,
|
|
243
253
|
);
|
|
244
254
|
}
|
|
245
255
|
|
|
@@ -254,6 +264,7 @@ export async function runSingleAgent(
|
|
|
254
264
|
makeDetails,
|
|
255
265
|
sessionFile,
|
|
256
266
|
persistedSessionBaseOffset,
|
|
267
|
+
onDiagnostic,
|
|
257
268
|
);
|
|
258
269
|
}
|
|
259
270
|
|
|
@@ -267,6 +278,7 @@ async function runClaudeAgent(
|
|
|
267
278
|
makeDetails: (results: SingleResult[]) => SubagentDetails,
|
|
268
279
|
resumeSessionId?: string,
|
|
269
280
|
sidecarSessionFile?: string,
|
|
281
|
+
onDiagnostic?: RunnerDiagnosticSink,
|
|
270
282
|
): Promise<SingleResult> {
|
|
271
283
|
try {
|
|
272
284
|
validateClaudeRuntimeModel(agent.model);
|
|
@@ -315,6 +327,10 @@ async function runClaudeAgent(
|
|
|
315
327
|
let completionMarkerWritten = false;
|
|
316
328
|
let wasAborted = false;
|
|
317
329
|
|
|
330
|
+
const diagnose = (event: Parameters<RunnerDiagnosticSink>[0]) => {
|
|
331
|
+
emitRunnerDiagnostic(onDiagnostic, { runtime: "claude", ...event });
|
|
332
|
+
};
|
|
333
|
+
|
|
318
334
|
const emitUpdate = () => {
|
|
319
335
|
if (!onUpdate) return;
|
|
320
336
|
const text = streamState.liveText ?? getFinalOutput(streamState.messages) ?? "(running...)";
|
|
@@ -362,9 +378,57 @@ async function runClaudeAgent(
|
|
|
362
378
|
let exitFallbackTimer: ReturnType<typeof setTimeout> | undefined;
|
|
363
379
|
let resultLingerTimer: ReturnType<typeof setTimeout> | undefined;
|
|
364
380
|
let lastExitCode = 0;
|
|
381
|
+
let lastProcessSignal: NodeJS.Signals | null = null;
|
|
365
382
|
let settleReason = "unknown";
|
|
366
383
|
let unparsedStdoutCount = 0;
|
|
367
384
|
const unparsedStdoutTail: string[] = [];
|
|
385
|
+
const parentIdentity = onDiagnostic ? readProcessIdentity(process.pid) : undefined;
|
|
386
|
+
const childIdentity = onDiagnostic ? readProcessIdentity(proc.pid) : undefined;
|
|
387
|
+
|
|
388
|
+
diagnose({
|
|
389
|
+
event: "spawn",
|
|
390
|
+
parentPid: process.pid,
|
|
391
|
+
parentProcessGroupId: parentIdentity?.processGroupId,
|
|
392
|
+
childPid: proc.pid,
|
|
393
|
+
childParentPid: childIdentity?.parentPid,
|
|
394
|
+
childProcessGroupId: childIdentity?.processGroupId,
|
|
395
|
+
processGroupMode: "inherited",
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const terminateProcess = (terminationSignal: NodeJS.Signals, cause: string): boolean => {
|
|
399
|
+
diagnose({
|
|
400
|
+
event: "kill_intent",
|
|
401
|
+
childPid: proc.pid,
|
|
402
|
+
signal: terminationSignal,
|
|
403
|
+
cause,
|
|
404
|
+
stopReason: streamState.stopReason,
|
|
405
|
+
});
|
|
406
|
+
try {
|
|
407
|
+
const killSent = proc.kill(terminationSignal);
|
|
408
|
+
diagnose({
|
|
409
|
+
event: "kill_result",
|
|
410
|
+
childPid: proc.pid,
|
|
411
|
+
signal: terminationSignal,
|
|
412
|
+
cause,
|
|
413
|
+
killSent,
|
|
414
|
+
});
|
|
415
|
+
return killSent;
|
|
416
|
+
} catch (error) {
|
|
417
|
+
diagnose({
|
|
418
|
+
event: "process_error",
|
|
419
|
+
childPid: proc.pid,
|
|
420
|
+
cause: `kill:${cause}`,
|
|
421
|
+
error: toDiagnosticError(error),
|
|
422
|
+
});
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const scheduleKillEscalation = (cause: string) => {
|
|
428
|
+
setTimeout(() => {
|
|
429
|
+
if (!procExited && proc.exitCode === null) terminateProcess("SIGKILL", `${cause}:grace_timeout`);
|
|
430
|
+
}, 5000);
|
|
431
|
+
};
|
|
368
432
|
|
|
369
433
|
const processLine = (line: string) => {
|
|
370
434
|
if (!line.trim()) return;
|
|
@@ -440,6 +504,14 @@ async function runClaudeAgent(
|
|
|
440
504
|
stderrBuf += `[runner] ${tail}\n`;
|
|
441
505
|
}
|
|
442
506
|
}
|
|
507
|
+
diagnose({
|
|
508
|
+
event: "settled",
|
|
509
|
+
childPid: proc.pid,
|
|
510
|
+
code: finalCode,
|
|
511
|
+
signal: lastProcessSignal,
|
|
512
|
+
settleReason,
|
|
513
|
+
stopReason: wasAborted ? "aborted" : streamState.stopReason,
|
|
514
|
+
});
|
|
443
515
|
resolve(finalCode);
|
|
444
516
|
};
|
|
445
517
|
|
|
@@ -452,10 +524,8 @@ async function runClaudeAgent(
|
|
|
452
524
|
if (resultLingerTimer) clearTimeout(resultLingerTimer);
|
|
453
525
|
resultLingerTimer = setTimeout(() => {
|
|
454
526
|
if (settled || procExited) return;
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
if (!procExited && proc.exitCode === null) proc.kill("SIGKILL");
|
|
458
|
-
}, 5000);
|
|
527
|
+
terminateProcess("SIGTERM", "result_linger_timeout");
|
|
528
|
+
scheduleKillEscalation("result_linger_timeout");
|
|
459
529
|
settleReason = "result_linger_timeout";
|
|
460
530
|
resolveOnce(0);
|
|
461
531
|
}, 3000);
|
|
@@ -472,9 +542,11 @@ async function runClaudeAgent(
|
|
|
472
542
|
stderrBuf += data.toString();
|
|
473
543
|
});
|
|
474
544
|
|
|
475
|
-
proc.on("exit", (code) => {
|
|
545
|
+
proc.on("exit", (code, terminationSignal) => {
|
|
476
546
|
procExited = true;
|
|
477
547
|
lastExitCode = code ?? 0;
|
|
548
|
+
lastProcessSignal = terminationSignal;
|
|
549
|
+
diagnose({ event: "exit", childPid: proc.pid, code, signal: terminationSignal });
|
|
478
550
|
if (streamState.resultReceived) {
|
|
479
551
|
settleReason = "exit_after_result";
|
|
480
552
|
resolveOnce(0);
|
|
@@ -486,8 +558,10 @@ async function runClaudeAgent(
|
|
|
486
558
|
}, 1500);
|
|
487
559
|
});
|
|
488
560
|
|
|
489
|
-
proc.on("close", (code) => {
|
|
561
|
+
proc.on("close", (code, terminationSignal) => {
|
|
490
562
|
procExited = true;
|
|
563
|
+
lastProcessSignal = terminationSignal ?? lastProcessSignal;
|
|
564
|
+
diagnose({ event: "close", childPid: proc.pid, code, signal: terminationSignal });
|
|
491
565
|
if (!settled) {
|
|
492
566
|
settleReason = "close";
|
|
493
567
|
resolveOnce(streamState.resultReceived ? 0 : (code ?? lastExitCode ?? 0));
|
|
@@ -497,6 +571,12 @@ async function runClaudeAgent(
|
|
|
497
571
|
proc.on("error", (error) => {
|
|
498
572
|
procExited = true;
|
|
499
573
|
stderrBuf += `[runner] process error: ${error?.message || String(error)}\n`;
|
|
574
|
+
diagnose({
|
|
575
|
+
event: "process_error",
|
|
576
|
+
childPid: proc.pid,
|
|
577
|
+
cause: "child_process_event",
|
|
578
|
+
error: toDiagnosticError(error),
|
|
579
|
+
});
|
|
500
580
|
settleReason = "process_error";
|
|
501
581
|
resolveOnce(1);
|
|
502
582
|
});
|
|
@@ -505,10 +585,13 @@ async function runClaudeAgent(
|
|
|
505
585
|
const killProc = () => {
|
|
506
586
|
wasAborted = true;
|
|
507
587
|
settleReason = "aborted_by_signal";
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
588
|
+
diagnose({
|
|
589
|
+
event: "abort_received",
|
|
590
|
+
childPid: proc.pid,
|
|
591
|
+
abortReason: serializeDiagnosticValue(signal.reason),
|
|
592
|
+
});
|
|
593
|
+
terminateProcess("SIGTERM", "abort_signal");
|
|
594
|
+
scheduleKillEscalation("abort_signal");
|
|
512
595
|
};
|
|
513
596
|
if (signal.aborted) killProc();
|
|
514
597
|
else signal.addEventListener("abort", killProc, { once: true });
|
|
@@ -549,6 +632,7 @@ async function runPiAgent(
|
|
|
549
632
|
makeDetails: (results: SingleResult[]) => SubagentDetails,
|
|
550
633
|
sessionFile?: string,
|
|
551
634
|
persistedSessionBaseOffset = 0,
|
|
635
|
+
onDiagnostic?: RunnerDiagnosticSink,
|
|
552
636
|
): Promise<SingleResult> {
|
|
553
637
|
const args: string[] = ["--mode", "json", "-p"];
|
|
554
638
|
if (sessionFile) args.push("--session", sessionFile);
|
|
@@ -578,6 +662,10 @@ async function runPiAgent(
|
|
|
578
662
|
let contextGuardTripped = false;
|
|
579
663
|
let peakContextTokens = 0;
|
|
580
664
|
|
|
665
|
+
const diagnose = (event: Parameters<RunnerDiagnosticSink>[0]) => {
|
|
666
|
+
emitRunnerDiagnostic(onDiagnostic, { runtime: "pi", ...event });
|
|
667
|
+
};
|
|
668
|
+
|
|
581
669
|
const emitUpdate = () => {
|
|
582
670
|
if (onUpdate) {
|
|
583
671
|
onUpdate({
|
|
@@ -617,11 +705,59 @@ async function runPiAgent(
|
|
|
617
705
|
let terminalMessageFallbackTimer: ReturnType<typeof setTimeout> | undefined;
|
|
618
706
|
let sessionPollTimer: ReturnType<typeof setInterval> | undefined;
|
|
619
707
|
let lastExitCode = 0;
|
|
708
|
+
let lastProcessSignal: NodeJS.Signals | null = null;
|
|
620
709
|
let lastEventAt = Date.now();
|
|
621
710
|
let sawAgentEnd = false;
|
|
622
711
|
let settleReason = "unknown";
|
|
623
712
|
let unparsedStdoutCount = 0;
|
|
624
713
|
const unparsedStdoutTail: string[] = [];
|
|
714
|
+
const parentIdentity = onDiagnostic ? readProcessIdentity(process.pid) : undefined;
|
|
715
|
+
const childIdentity = onDiagnostic ? readProcessIdentity(proc.pid) : undefined;
|
|
716
|
+
|
|
717
|
+
diagnose({
|
|
718
|
+
event: "spawn",
|
|
719
|
+
parentPid: process.pid,
|
|
720
|
+
parentProcessGroupId: parentIdentity?.processGroupId,
|
|
721
|
+
childPid: proc.pid,
|
|
722
|
+
childParentPid: childIdentity?.parentPid,
|
|
723
|
+
childProcessGroupId: childIdentity?.processGroupId,
|
|
724
|
+
processGroupMode: "inherited",
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
const terminateProcess = (terminationSignal: NodeJS.Signals, cause: string): boolean => {
|
|
728
|
+
diagnose({
|
|
729
|
+
event: "kill_intent",
|
|
730
|
+
childPid: proc.pid,
|
|
731
|
+
signal: terminationSignal,
|
|
732
|
+
cause,
|
|
733
|
+
stopReason: currentResult.stopReason,
|
|
734
|
+
});
|
|
735
|
+
try {
|
|
736
|
+
const killSent = proc.kill(terminationSignal);
|
|
737
|
+
diagnose({
|
|
738
|
+
event: "kill_result",
|
|
739
|
+
childPid: proc.pid,
|
|
740
|
+
signal: terminationSignal,
|
|
741
|
+
cause,
|
|
742
|
+
killSent,
|
|
743
|
+
});
|
|
744
|
+
return killSent;
|
|
745
|
+
} catch (error) {
|
|
746
|
+
diagnose({
|
|
747
|
+
event: "process_error",
|
|
748
|
+
childPid: proc.pid,
|
|
749
|
+
cause: `kill:${cause}`,
|
|
750
|
+
error: toDiagnosticError(error),
|
|
751
|
+
});
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
const scheduleKillEscalation = (cause: string) => {
|
|
757
|
+
setTimeout(() => {
|
|
758
|
+
if (!procExited && proc.exitCode === null) terminateProcess("SIGKILL", `${cause}:grace_timeout`);
|
|
759
|
+
}, 5000);
|
|
760
|
+
};
|
|
625
761
|
|
|
626
762
|
const syncFromPersistedSession = (allowResolve: boolean): boolean => {
|
|
627
763
|
if (!sessionFile) return false;
|
|
@@ -640,11 +776,12 @@ async function runPiAgent(
|
|
|
640
776
|
snapshot.completionMarker?.exitCode ??
|
|
641
777
|
(snapshot.terminalStopReason === "error" || snapshot.terminalStopReason === "aborted" ? 1 : 0);
|
|
642
778
|
writeCompletionMarkerOnce(forcedCode);
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
779
|
+
const fallbackReason = snapshot.completionMarker
|
|
780
|
+
? "session_done_marker_fallback"
|
|
781
|
+
: "session_terminal_message_fallback";
|
|
782
|
+
terminateProcess("SIGTERM", fallbackReason);
|
|
783
|
+
scheduleKillEscalation(fallbackReason);
|
|
784
|
+
settleReason = fallbackReason;
|
|
648
785
|
resolveOnce(forcedCode);
|
|
649
786
|
return true;
|
|
650
787
|
};
|
|
@@ -776,10 +913,8 @@ async function runPiAgent(
|
|
|
776
913
|
currentResult.errorMessage =
|
|
777
914
|
`${CONTEXT_GUARD_SIGNATURE} stopped at ${peakContextTokens} tokens ` +
|
|
778
915
|
`(ceiling ${contextGuardCeiling}) to preserve partial findings before provider context overflow.`;
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
if (!procExited && proc.exitCode === null) proc.kill("SIGKILL");
|
|
782
|
-
}, 5000);
|
|
916
|
+
terminateProcess("SIGTERM", "context_guard");
|
|
917
|
+
scheduleKillEscalation("context_guard");
|
|
783
918
|
settleReason = "context_guard";
|
|
784
919
|
resolveOnce(1);
|
|
785
920
|
return;
|
|
@@ -858,6 +993,14 @@ async function runPiAgent(
|
|
|
858
993
|
);
|
|
859
994
|
}
|
|
860
995
|
}
|
|
996
|
+
diagnose({
|
|
997
|
+
event: "settled",
|
|
998
|
+
childPid: proc.pid,
|
|
999
|
+
code: finalCode,
|
|
1000
|
+
signal: lastProcessSignal,
|
|
1001
|
+
settleReason,
|
|
1002
|
+
stopReason: wasAborted ? "aborted" : currentResult.stopReason,
|
|
1003
|
+
});
|
|
861
1004
|
resolve(finalCode);
|
|
862
1005
|
};
|
|
863
1006
|
|
|
@@ -870,10 +1013,8 @@ async function runPiAgent(
|
|
|
870
1013
|
|
|
871
1014
|
const forcedCode = currentResult.stopReason === "error" || currentResult.stopReason === "aborted" ? 1 : 0;
|
|
872
1015
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
if (!procExited && proc.exitCode === null) proc.kill("SIGKILL");
|
|
876
|
-
}, 5000);
|
|
1016
|
+
terminateProcess("SIGTERM", "terminal_message_fallback_timeout");
|
|
1017
|
+
scheduleKillEscalation("terminal_message_fallback_timeout");
|
|
877
1018
|
|
|
878
1019
|
settleReason = "terminal_message_fallback_timeout";
|
|
879
1020
|
resolveOnce(forcedCode);
|
|
@@ -894,10 +1035,8 @@ async function runPiAgent(
|
|
|
894
1035
|
|
|
895
1036
|
const forcedCode = currentResult.stopReason === "error" || currentResult.stopReason === "aborted" ? 1 : 0;
|
|
896
1037
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
if (!procExited && proc.exitCode === null) proc.kill("SIGKILL");
|
|
900
|
-
}, 5000);
|
|
1038
|
+
terminateProcess("SIGTERM", "agent_end_fallback_timeout");
|
|
1039
|
+
scheduleKillEscalation("agent_end_fallback_timeout");
|
|
901
1040
|
|
|
902
1041
|
settleReason = "agent_end_fallback_timeout";
|
|
903
1042
|
resolveOnce(forcedCode);
|
|
@@ -921,9 +1060,11 @@ async function runPiAgent(
|
|
|
921
1060
|
currentResult.stderr += data.toString();
|
|
922
1061
|
});
|
|
923
1062
|
|
|
924
|
-
proc.on("exit", (code) => {
|
|
1063
|
+
proc.on("exit", (code, terminationSignal) => {
|
|
925
1064
|
procExited = true;
|
|
926
1065
|
lastExitCode = code ?? 0;
|
|
1066
|
+
lastProcessSignal = terminationSignal;
|
|
1067
|
+
diagnose({ event: "exit", childPid: proc.pid, code, signal: terminationSignal });
|
|
927
1068
|
// In rare cases stdout/stderr pipes may stay open after process exit.
|
|
928
1069
|
// Use a short fallback so runs cannot stay "running" forever.
|
|
929
1070
|
exitFallbackTimer = setTimeout(() => {
|
|
@@ -932,8 +1073,10 @@ async function runPiAgent(
|
|
|
932
1073
|
}, 1500);
|
|
933
1074
|
});
|
|
934
1075
|
|
|
935
|
-
proc.on("close", (code) => {
|
|
1076
|
+
proc.on("close", (code, terminationSignal) => {
|
|
936
1077
|
procExited = true;
|
|
1078
|
+
lastProcessSignal = terminationSignal ?? lastProcessSignal;
|
|
1079
|
+
diagnose({ event: "close", childPid: proc.pid, code, signal: terminationSignal });
|
|
937
1080
|
settleReason = "close";
|
|
938
1081
|
resolveOnce(code ?? lastExitCode ?? 0);
|
|
939
1082
|
});
|
|
@@ -941,6 +1084,12 @@ async function runPiAgent(
|
|
|
941
1084
|
proc.on("error", (error) => {
|
|
942
1085
|
procExited = true;
|
|
943
1086
|
appendStderrDiagnostic(currentResult, `process error: ${error?.message || String(error)}`);
|
|
1087
|
+
diagnose({
|
|
1088
|
+
event: "process_error",
|
|
1089
|
+
childPid: proc.pid,
|
|
1090
|
+
cause: "child_process_event",
|
|
1091
|
+
error: toDiagnosticError(error),
|
|
1092
|
+
});
|
|
944
1093
|
settleReason = "process_error";
|
|
945
1094
|
resolveOnce(1);
|
|
946
1095
|
});
|
|
@@ -949,10 +1098,13 @@ async function runPiAgent(
|
|
|
949
1098
|
const killProc = () => {
|
|
950
1099
|
wasAborted = true;
|
|
951
1100
|
settleReason = "aborted_by_signal";
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1101
|
+
diagnose({
|
|
1102
|
+
event: "abort_received",
|
|
1103
|
+
childPid: proc.pid,
|
|
1104
|
+
abortReason: serializeDiagnosticValue(signal.reason),
|
|
1105
|
+
});
|
|
1106
|
+
terminateProcess("SIGTERM", "abort_signal");
|
|
1107
|
+
scheduleKillEscalation("abort_signal");
|
|
956
1108
|
};
|
|
957
1109
|
if (signal.aborted) killProc();
|
|
958
1110
|
else signal.addEventListener("abort", killProc, { once: true });
|
package/tool-execute.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
SUBAGENT_STRONG_WAIT_MESSAGE,
|
|
23
23
|
} from "./constants.js";
|
|
24
24
|
import { isContextOverflowText } from "./context-limits.js";
|
|
25
|
+
import { createRunDiagnosticSink } from "./diagnostics.js";
|
|
25
26
|
import {
|
|
26
27
|
buildSubagentDisplayTaskFallback,
|
|
27
28
|
createDisplayTaskRefreshToken,
|
|
@@ -956,7 +957,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
956
957
|
|
|
957
958
|
run.lastLine = "Aborting by subagent tool...";
|
|
958
959
|
run.lastOutput = run.lastLine;
|
|
959
|
-
abortCtrl.abort();
|
|
960
|
+
abortCtrl.abort({ source: "subagent_tool_abort", runId });
|
|
960
961
|
aborting.push(runId);
|
|
961
962
|
}
|
|
962
963
|
|
|
@@ -1080,7 +1081,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1080
1081
|
clearParentAbortListener(runId);
|
|
1081
1082
|
if (!signal || shouldRunAsync) return;
|
|
1082
1083
|
|
|
1083
|
-
const abortFromParent = () => abortController.abort();
|
|
1084
|
+
const abortFromParent = () => abortController.abort({ source: "parent_tool_abort", runId });
|
|
1084
1085
|
if (signal.aborted) {
|
|
1085
1086
|
abortFromParent();
|
|
1086
1087
|
return;
|
|
@@ -1201,6 +1202,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1201
1202
|
resumeSessionId: runState.claudeSessionId,
|
|
1202
1203
|
sidecarSessionFile: runState.sessionFile,
|
|
1203
1204
|
persistedSessionBaseOffset: runState.persistedSessionBaseOffset,
|
|
1205
|
+
onDiagnostic: createRunDiagnosticSink(pi, runState),
|
|
1204
1206
|
},
|
|
1205
1207
|
),
|
|
1206
1208
|
).then((result) => finalizeRunState(runState, result));
|