@arhen/pi-core-subagent 1.1.8 → 1.1.9

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 CHANGED
@@ -15,7 +15,7 @@ Built for one job: delegate work to isolated subagents **without bloating the pa
15
15
  - **In-process** — children are `AgentSession`s in the same runtime. No process spawn, no context bleed.
16
16
  - **Zero parent-context injection.** No catalog, no context hook. 6 slim tools total.
17
17
  - **Throttled updates** — widget/stream updates coalesce to ~6/s; no per-event deep clones.
18
- - **No silent hangs** — watchdog aborts children that produce no events for 90s; per-task timeout 10min.
18
+ - **No silent hangs** — watchdog aborts children that produce no events for 3 minutes; per-task timeout 10min.
19
19
 
20
20
  ## Install
21
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arhen/pi-core-subagent",
3
- "version": "1.1.8",
3
+ "version": "1.1.9",
4
4
  "type": "module",
5
5
  "description": "pi extension: fast in-process subagents with single/parallel/chain, background runs, intercom and agent-to-agent mailbox. Leader defines agents inline.",
6
6
  "license": "MIT",
package/src/child.ts CHANGED
@@ -98,8 +98,8 @@ export function createChildTools(taskId: string, handlers: ChildHandlers): ToolD
98
98
  if (messages.length === 0) return { content: [{ type: "text" as const, text: "No messages." }], details: {} };
99
99
  const body = messages
100
100
  .map((m) => `from ${m.from}: ${m.text}`)
101
- .join("\n")
102
- .slice(0, 4000);
101
+ .join("\n");
102
+ const capped = body.length > 4000 ? body.slice(0, 4000).replace(/[\uD800-\uDBFF]$/, "") : body; // multibyte-safe
103
103
  return { content: [{ type: "text" as const, text: body }], details: { messages } };
104
104
  },
105
105
  },
package/src/index.ts CHANGED
@@ -307,7 +307,8 @@ function makeSummary(run: RunSnapshot): string {
307
307
  const succeeded = run.tasks.filter((t) => t.status === "completed").length;
308
308
  const failed = run.tasks.filter((t) => t.status === "failed").length;
309
309
  const aborted = run.tasks.filter((t) => t.status === "aborted").length;
310
- const lines = [`Run ${run.id}: Subagents ${run.mode}${run.background ? " (background)" : ""} finished: ${succeeded}/${run.tasks.length} succeeded${failed ? `, ${failed} failed` : ""}${aborted ? `, ${aborted} aborted` : ""}.`];
310
+ const done = TERMINAL.includes(run.status) ? "finished" : "running";
311
+ const lines = [`Run ${run.id}: Subagents ${run.mode}${run.background ? " (background)" : ""} ${done}: ${succeeded}/${run.tasks.length} succeeded${failed ? `, ${failed} failed` : ""}${aborted ? `, ${aborted} aborted` : ""}.`];
311
312
  const usage = formatUsage(run.aggregateUsage);
312
313
  if (usage) lines.push(`Usage: ${usage}`);
313
314
  for (const task of run.tasks) {
@@ -495,7 +496,9 @@ class SubagentManager {
495
496
  const parentFile = getParentSessionFile(ctx);
496
497
  if (!parentFile) return;
497
498
  const sidecar = parentFile.replace(/\.jsonl$/, ".subagents.json");
498
- import("fs").then(({ writeFileSync }) => writeFileSync(sidecar, JSON.stringify(this.listRuns().slice(0, 50).map(cloneRun), null, 2)));
499
+ import("fs")
500
+ .then(({ writeFileSync }) => writeFileSync(sidecar, JSON.stringify(this.listRuns().slice(0, 50).map(cloneRun), null, 2)))
501
+ .catch(() => {}); // never surface as an unhandled rejection
499
502
  } catch {
500
503
  /* ignore */
501
504
  }
@@ -533,8 +536,7 @@ class SubagentManager {
533
536
  }
534
537
 
535
538
  // Widget: register-once + requestRender (todo-overlay pattern).
536
- // The component self-animates the spinner via its own 100ms interval;
537
- // scheduleWidget just throttles status changes into requestRender calls.
539
+ // scheduleWidget throttles status changes into requestRender calls.
538
540
  private widgetTui: TUI | null = null;
539
541
  /** Upsert a run into the widget's visible set (all runs, not just the latest). */
540
542
  private upsertWidgetRun(run: RunSnapshot | undefined): void {
@@ -600,7 +602,6 @@ class SubagentManager {
600
602
  onAskParent: async (_taskId, question) => {
601
603
  this.updateTask(run, task, { status: "awaiting_parent" }, ctx);
602
604
  this.liveChildren.get(`${run.id}:${task.id}`)?.touchWatchdog();
603
- this.notifyParent(run, "asked", { taskId: task.id, question });
604
605
  // A blocking run's parent can't reply mid-tool (followUp only fires after the
605
606
  // tool returns) — only background runs can truly wait for the answer.
606
607
  if (!run.background) {
@@ -608,10 +609,17 @@ class SubagentManager {
608
609
  this.liveChildren.get(`${run.id}:${task.id}`)?.touchWatchdog();
609
610
  return "Parent cannot answer while this run is blocking. Continue autonomously with your best judgment.";
610
611
  }
611
- const reply = await this.awaitParentReply(run.id, task.id);
612
- this.updateTask(run, task, { status: "running" }, ctx);
613
- this.liveChildren.get(`${run.id}:${task.id}`)?.touchWatchdog();
614
- return reply;
612
+ this.notifyParent(run, "asked", { taskId: task.id, question });
613
+ // M3: a waiting child is not stalled — keep the watchdog fed until the reply.
614
+ const keepAlive = setInterval(() => this.liveChildren.get(`${run.id}:${task.id}`)?.touchWatchdog(), 30_000);
615
+ try {
616
+ const reply = await this.awaitParentReply(run.id, task.id);
617
+ this.updateTask(run, task, { status: "running" }, ctx);
618
+ this.liveChildren.get(`${run.id}:${task.id}`)?.touchWatchdog();
619
+ return reply;
620
+ } finally {
621
+ clearInterval(keepAlive);
622
+ }
615
623
  },
616
624
  onNotifyParent: (_taskId, message, level) => {
617
625
  this.emit("subagent:intercom", { runId: run.id, taskId: task.id, kind: "notify", level, message });
@@ -793,7 +801,7 @@ class SubagentManager {
793
801
  runController?.signal.removeEventListener("abort", abortChild);
794
802
  };
795
803
  // Cancel may have landed during session creation — honor it before prompting.
796
- if (run.status === "aborted" || TERMINAL.includes(task.status)) {
804
+ if (run.status === "aborted" || TERMINAL.includes(task.status) || signal?.aborted) {
797
805
  await child.abort();
798
806
  throw new Error("Canceled by subagent_cancel");
799
807
  }
@@ -928,7 +936,7 @@ class SubagentManager {
928
936
  if (run.notifyPerTask && run.background && TERMINAL.includes(task.status)) {
929
937
  this.notifyTask(run, task, task.status as "completed" | "failed" | "aborted");
930
938
  }
931
- if (task.status !== "completed") break;
939
+ if (task.status !== "completed") break; // aborted/failed link stops the chain
932
940
  previous = task.finalText ?? "";
933
941
  }
934
942
  } else {
@@ -953,8 +961,11 @@ class SubagentManager {
953
961
  } else {
954
962
  this.clearWidget(ctx);
955
963
  }
956
- this.emit("subagent:run-completed", { runId: run.id, status: run.status, run: cloneRun(run), aggregateUsage: run.aggregateUsage });
957
- this.settleRun(run.id, run);
964
+ // L7: cancelRun already emitted + settled don't double-report.
965
+ if (this.settlers.has(run.id)) {
966
+ this.emit("subagent:run-completed", { runId: run.id, status: run.status, run: cloneRun(run), aggregateUsage: run.aggregateUsage });
967
+ this.settleRun(run.id, run);
968
+ }
958
969
  this.runControllers.delete(run.id);
959
970
  for (const task of run.tasks) this.mailboxes.close(`${run.id}:${task.id}`);
960
971
  this.persist(ctx);
@@ -1138,7 +1149,7 @@ export default function (pi: ExtensionAPI) {
1138
1149
  });
1139
1150
 
1140
1151
  pi.on("agent_start", (_event, ctx) => {
1141
- if (!manager.turnActivity) manager.clearWidget(ctx);
1152
+ if (!manager.turnActivity && !manager.hasActiveRun()) manager.clearWidget(ctx);
1142
1153
  manager.turnActivity = false;
1143
1154
  });
1144
1155