@ryan_nookpi/pi-extension-subagent 0.5.6 → 0.5.8

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/index.ts CHANGED
@@ -28,6 +28,7 @@
28
28
  */
29
29
 
30
30
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
31
+ import { SubagentAsyncTasks } from "./async-task-lifecycle.js";
31
32
  import { HANG_CHECK_INTERVAL_MS } from "./constants.js";
32
33
  import { SUBAGENT_COMMANDS, SUBAGENT_SHORTCUTS, type SubagentCommandName } from "./registration-manifest.js";
33
34
 
@@ -40,6 +41,7 @@ interface SubagentCore {
40
41
  }
41
42
 
42
43
  export default function (pi: ExtensionAPI) {
44
+ const asyncTasks = new SubagentAsyncTasks(pi);
43
45
  let core: SubagentCore | null = null;
44
46
  let corePromise: Promise<SubagentCore> | null = null;
45
47
  /** Serializes session lifecycle work so events apply in dispatch order. */
@@ -54,6 +56,7 @@ export default function (pi: ExtensionAPI) {
54
56
  import("./store.js"),
55
57
  ]);
56
58
  const store = storeMod.createStore();
59
+ store.asyncTasks = asyncTasks;
57
60
  const registrations = commands.registerAll(pi, store);
58
61
  core = { store, commands, registrations, escalation, lifecycle };
59
62
  return core;
@@ -116,11 +119,24 @@ export default function (pi: ExtensionAPI) {
116
119
  }, 0);
117
120
 
118
121
  let hangCheckTimer: ReturnType<typeof setInterval> | undefined;
122
+ let sessionGeneration = 0;
119
123
 
120
124
  pi.on("session_start", (_event, ctx) => {
121
- enqueue((c) => {
125
+ const generation = ++sessionGeneration;
126
+ const sessionId = ctx.sessionManager.getSessionId();
127
+ // Fence tool admission synchronously, before lazy lifecycle work can yield.
128
+ try {
129
+ if (pi.events) asyncTasks.bind(sessionId);
130
+ } catch (error) {
131
+ process.stderr.write(`[subagent] session binding failed: ${error instanceof Error ? error.message : error}\n`);
132
+ return;
133
+ }
134
+ enqueue(async (c) => {
135
+ if (generation !== sessionGeneration) return;
136
+ if (pi.events) await asyncTasks.provider.whenDiscovered();
137
+ if (generation !== sessionGeneration || ctx.sessionManager.getSessionId() !== sessionId) return;
122
138
  c.store.disposed = false;
123
- c.commands.handleSessionStart(pi, c.store, ctx as unknown as ExtensionContext);
139
+ c.commands.handleSessionStart(asyncTasks.wrap(pi), c.store, ctx as unknown as ExtensionContext);
124
140
  c.escalation.maybeRegisterAskMaster(pi, ctx);
125
141
  });
126
142
  if (!hangCheckTimer) {
@@ -131,6 +147,8 @@ export default function (pi: ExtensionAPI) {
131
147
  });
132
148
 
133
149
  pi.on("session_shutdown", async (event) => {
150
+ ++sessionGeneration;
151
+ asyncTasks.shutdown();
134
152
  if (hangCheckTimer) {
135
153
  clearInterval(hangCheckTimer);
136
154
  hangCheckTimer = undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryan_nookpi/pi-extension-subagent",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
4
4
  "description": "Asynchronous subagent delegation for pi with run, batch, chain, and continuation workflows.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -39,6 +39,8 @@
39
39
  "format.ts",
40
40
  "group-pending.ts",
41
41
  "index.ts",
42
+ "async-task-provider.ts",
43
+ "async-task-lifecycle.ts",
42
44
  "lifecycle.ts",
43
45
  "invocation-queue.ts",
44
46
  "live-preview.ts",
package/run-utils.ts CHANGED
@@ -242,18 +242,18 @@ export function snapshotBatchGroup(
242
242
  let failed = 0;
243
243
  let aborted = 0;
244
244
  const members: FinishedGroupMember[] = batch.runIds.map((runId) => {
245
- const run = store.commandRuns.get(runId);
245
+ const run = batch.completedRuns.get(runId) ?? store.commandRuns.get(runId);
246
246
  if (!run) {
247
247
  if (batch.abortedRunIds.has(runId)) aborted++;
248
248
  else if (batch.failedRunIds.has(runId)) failed++;
249
249
  return {
250
250
  summaryLine: `#${runId} [gone] (run no longer available)`,
251
- output: batch.pendingResults.get(runId)?.trim() || "(no output)",
251
+ output: "(no output)",
252
252
  };
253
253
  }
254
254
  if (run.errorClass === "aborted" || batch.abortedRunIds.has(runId)) aborted++;
255
255
  else if (run.status === "error" || batch.failedRunIds.has(runId)) failed++;
256
- return { summaryLine: formatCommandRunSummary(run), output: memberOutput(run, batch.pendingResults.get(runId)) };
256
+ return { summaryLine: formatCommandRunSummary(run), output: memberOutput(run) };
257
257
  });
258
258
  return {
259
259
  groupId: batch.batchId,
package/runner.ts CHANGED
@@ -7,6 +7,7 @@ import { spawn } from "node:child_process";
7
7
  import * as fs from "node:fs";
8
8
  import type { Message } from "@earendil-works/pi-ai";
9
9
  import type { AgentConfig } from "./agents.js";
10
+ import { currentResourceObserver, trackSubagentRunner } from "./async-task-lifecycle.js";
10
11
  import { buildClaudeArgs } from "./claude-args.js";
11
12
  import { createSidecarWriter } from "./claude-sidecar-writer.js";
12
13
  import {
@@ -43,6 +44,7 @@ import {
43
44
  } from "./utils/agent-utils.js";
44
45
 
45
46
  export interface RunSingleAgentSessionConfig {
47
+ asyncRunId?: number;
46
48
  sessionFile?: string;
47
49
  resumeSessionId?: string;
48
50
  sidecarSessionFile?: string;
@@ -198,7 +200,21 @@ export function computeAgentAliasHints(agents: AgentConfig[]): string {
198
200
 
199
201
  // ─── Single Agent Execution ──────────────────────────────────────────────────
200
202
 
201
- export async function runSingleAgent(
203
+ export async function runSingleAgent(...args: Parameters<typeof runSingleAgentUntracked>): Promise<SingleResult> {
204
+ const config = typeof args[8] === "object" ? args[8] : undefined;
205
+ return trackSubagentRunner(
206
+ args[2],
207
+ args[5],
208
+ (signal) => {
209
+ const invocation: Parameters<typeof runSingleAgentUntracked> = [...args];
210
+ invocation[5] = signal;
211
+ return runSingleAgentUntracked(...invocation);
212
+ },
213
+ config?.asyncRunId,
214
+ );
215
+ }
216
+
217
+ async function runSingleAgentUntracked(
202
218
  defaultCwd: string,
203
219
  agents: AgentConfig[],
204
220
  agentName: string,
@@ -209,6 +225,7 @@ export async function runSingleAgent(
209
225
  makeDetails: (results: SingleResult[]) => SubagentDetails,
210
226
  sessionConfig?: string | RunSingleAgentSessionConfig,
211
227
  ): Promise<SingleResult> {
228
+ currentResourceObserver()("settled");
212
229
  const normalizedSessionConfig = normalizeRunSessionConfig(sessionConfig);
213
230
  const sessionFile = normalizedSessionConfig.sessionFile;
214
231
  const resumeSessionId = normalizedSessionConfig.resumeSessionId;
@@ -246,7 +263,10 @@ export async function runSingleAgent(
246
263
 
247
264
  if (agent.runtime === "claude") {
248
265
  if (resolveClaudeRuntimeMode(defaultCwd) === "sdk") {
266
+ // SDK query.close() has no process-exit acknowledgement.
267
+ currentResourceObserver()("unknown");
249
268
  const { runClaudeAgentViaSdk } = await import("./claude-sdk-runner.js");
269
+ if (signal?.aborted) throw new Error("Subagent was aborted");
250
270
  return runClaudeAgentViaSdk(
251
271
  defaultCwd,
252
272
  agent,
@@ -300,6 +320,7 @@ async function runClaudeAgent(
300
320
  sidecarSessionFile?: string,
301
321
  onDiagnostic?: RunnerDiagnosticSink,
302
322
  ): Promise<SingleResult> {
323
+ const observeResource = currentResourceObserver();
303
324
  try {
304
325
  validateClaudeRuntimeModel(agent.model);
305
326
  } catch (err: any) {
@@ -392,7 +413,9 @@ async function runClaudeAgent(
392
413
  });
393
414
 
394
415
  const exitCode = await new Promise<number>((resolve) => {
416
+ if (signal?.aborted) throw new Error("Subagent was aborted");
395
417
  const proc = spawn("claude", args, { cwd: defaultCwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
418
+ observeResource("active");
396
419
  let buffer = "";
397
420
  let procExited = false;
398
421
  let settled = false;
@@ -564,6 +587,7 @@ async function runClaudeAgent(
564
587
  });
565
588
 
566
589
  proc.on("exit", (code, terminationSignal) => {
590
+ observeResource("settled");
567
591
  procExited = true;
568
592
  lastExitCode = normalizeChildExitCode(code, terminationSignal, lastExitCode);
569
593
  lastProcessSignal = terminationSignal;
@@ -583,6 +607,7 @@ async function runClaudeAgent(
583
607
  });
584
608
 
585
609
  proc.on("close", (code, terminationSignal) => {
610
+ observeResource("settled");
586
611
  procExited = true;
587
612
  lastProcessSignal = terminationSignal ?? lastProcessSignal;
588
613
  diagnose({ event: "close", childPid: proc.pid, code, signal: terminationSignal });
@@ -593,6 +618,7 @@ async function runClaudeAgent(
593
618
  });
594
619
 
595
620
  proc.on("error", (error) => {
621
+ observeResource(proc.pid ? "unknown" : "settled");
596
622
  procExited = true;
597
623
  stderrBuf += `[runner] process error: ${error?.message || String(error)}\n`;
598
624
  diagnose({
@@ -658,6 +684,7 @@ async function runPiAgent(
658
684
  persistedSessionBaseOffset = 0,
659
685
  onDiagnostic?: RunnerDiagnosticSink,
660
686
  ): Promise<SingleResult> {
687
+ const observeResource = currentResourceObserver();
661
688
  const args: string[] = ["--mode", "json", "-p"];
662
689
  if (sessionFile) args.push("--session", sessionFile);
663
690
  else args.push("--no-session");
@@ -721,7 +748,9 @@ async function runPiAgent(
721
748
  args.push(normalizeTaskForSubagentPrompt(task));
722
749
 
723
750
  const exitCode = await new Promise<number>((resolve) => {
751
+ if (signal?.aborted) throw new Error("Subagent was aborted");
724
752
  const proc = spawn("pi", args, { cwd: defaultCwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
753
+ observeResource("active");
725
754
  let buffer = "";
726
755
  let procExited = false;
727
756
  let settled = false;
@@ -1089,6 +1118,7 @@ async function runPiAgent(
1089
1118
  });
1090
1119
 
1091
1120
  proc.on("exit", (code, terminationSignal) => {
1121
+ observeResource("settled");
1092
1122
  procExited = true;
1093
1123
  lastExitCode = normalizeChildExitCode(code, terminationSignal, lastExitCode);
1094
1124
  lastProcessSignal = terminationSignal;
@@ -1105,6 +1135,7 @@ async function runPiAgent(
1105
1135
  });
1106
1136
 
1107
1137
  proc.on("close", (code, terminationSignal) => {
1138
+ observeResource("settled");
1108
1139
  procExited = true;
1109
1140
  lastProcessSignal = terminationSignal ?? lastProcessSignal;
1110
1141
  diagnose({ event: "close", childPid: proc.pid, code, signal: terminationSignal });
@@ -1113,6 +1144,7 @@ async function runPiAgent(
1113
1144
  });
1114
1145
 
1115
1146
  proc.on("error", (error) => {
1147
+ observeResource(proc.pid ? "unknown" : "settled");
1116
1148
  procExited = true;
1117
1149
  appendStderrDiagnostic(currentResult, `process error: ${error?.message || String(error)}`);
1118
1150
  diagnose({
package/store.ts CHANGED
@@ -18,6 +18,7 @@ import type { WidgetRenderCtx } from "./widget.js";
18
18
  export const COLLAPSED_ITEM_COUNT = 10;
19
19
 
20
20
  export interface SubagentStore {
21
+ asyncTasks?: import("./async-task-lifecycle.js").SubagentAsyncTasks;
21
22
  /** True after session_shutdown; async callbacks must not use captured runtime APIs. */
22
23
  disposed: boolean;
23
24
  commandRuns: Map<number, CommandRunState>;