@arhen/pi-core-subagent 1.3.46 → 1.3.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.
package/README.md CHANGED
@@ -93,11 +93,10 @@ Define agents inline per call, or reference a named agent file (see [Agent files
93
93
  }
94
94
  ```
95
95
 
96
- Parallel — mixed toolsets, siblings can talk via mailbox:
96
+ Parallel — mixed toolsets, siblings can talk via mailbox (intercom is always on):
97
97
 
98
98
  ```json
99
99
  {
100
- "allowIntercom": true,
101
100
  "tasks": [
102
101
  { "agent": "researcher", "prompt": "You find facts. Cite paths.", "task": "Map the auth flow", "write": false },
103
102
  { "agent": "implementer", "prompt": "You make minimal changes.", "task": "Implement POST /api/upload", "write": true }
@@ -272,14 +271,13 @@ flowchart LR
272
271
 
273
272
  And the rule that keeps this from becoming ceremony: **zero `needs` anywhere = plain parallel.** No waves, no gates, no graph vocabulary imposed on flat work.
274
273
 
275
- Background (default) + intercom — the run returns a runId immediately; you stay steerable while it works:
274
+ Background (default) + intercom — the run returns a runId immediately; you stay steerable while it works. Children can always ask you questions and message each other:
276
275
 
277
276
  ```json
278
277
  {
279
278
  "agent": "auditor",
280
279
  "prompt": "You audit dependencies.",
281
- "task": "Audit package.json for outdated deps",
282
- "allowIntercom": true
280
+ "task": "Audit package.json for outdated deps"
283
281
  }
284
282
  ```
285
283
 
@@ -289,7 +287,7 @@ Background (default) + intercom — the run returns a runId immediately; you sta
289
287
 
290
288
  | Tool | Purpose |
291
289
  |---|---|
292
- | `subagent` | single / `tasks` (parallel or graph via `needs`) / `chain` (`{previous}`); every run is background — returns a runId, completion notifies you; `autoAwait:true` parks the call until the run finishes and returns the final result inline; `allowIntercom:true` enables child talk tools; `notifyPerTask` (default true) wakes you as each task completes |
290
+ | `subagent` | single / `tasks` (parallel or graph via `needs`) / `chain` (`{previous}`); every run is background — returns a runId, completion notifies you; `autoAwait:true` parks the call until the run finishes and returns the final result inline; children always carry talk tools (ask/notify/mailbox); `notifyPerTask` (default true) wakes you as each task completes |
293
291
  | `subagent_status` | live per-task snapshot (non-blocking), including each child's session file path |
294
292
  | `subagent_result` | full output of a run or one task |
295
293
  | `await_subagent` | block until a run finishes (optional `timeoutMs`) |
@@ -299,9 +297,9 @@ Background (default) + intercom — the run returns a runId immediately; you sta
299
297
 
300
298
  ### Per-task fields
301
299
 
302
- `agent` (name you invent — required), `task` (required), `prompt` (system prompt, optional — minimal default used), `write` (toolset, default read-only), plus optional `model` (`provider/model-id`), `thinking` (validated enum: `off|minimal|low|medium|high|xhigh|max`), `tools` (explicit allowlist), `cwd`, `maxRuntimeMs`, `id`, `needs` (dependency edges — see [Graph mode](#graph-mode--needs)). Top-level only: `autoAwait`, `notifyPerTask`, `allowIntercom`, `concurrency`.
300
+ `agent` (name you invent — required), `task` (required), `prompt` (system prompt, optional — minimal default used), `write` (toolset, default read-only), plus optional `model` (`provider/model-id`), `thinking` (validated enum: `off|minimal|low|medium|high|xhigh|max`), `tools` (explicit allowlist), `cwd`, `maxRuntimeMs`, `id`, `needs` (dependency edges — see [Graph mode](#graph-mode--needs)). Top-level only: `autoAwait`, `notifyPerTask`, `concurrency`.
303
301
 
304
- ### Child talk tools (when `allowIntercom: true`)
302
+ ### Child talk tools (always on)
305
303
 
306
304
  | Tool | Meaning |
307
305
  |---|---|
@@ -315,7 +313,7 @@ Background (default) + intercom — the run returns a runId immediately; you sta
315
313
  ## Commands
316
314
 
317
315
  - `/subagents` — list runs; `/subagents peek` (or `ctrl+shift+a`) — browsable pane
318
- - `/subagents auto-limit on|off` — toggle leader-imposed `maxRuntimeMs` caps (persists to `~/.pi/agent/subagents-config.json`; default on). `off` strips ALL task timeouts: tasks run unlimited until done, stalled, or aborted — only for runs where a hard bound is genuinely required is a cap kept (none, when off). Bare `/subagents auto-limit` shows the current state.
316
+ - `/subagents auto-limit on|off` — toggle leader-imposed `maxRuntimeMs` caps (persists to `~/.pi/agent/subagents-config.json`; default **off**). `on` gives tasks without an explicit `maxRuntimeMs` the 1 h default ceiling; `off` raises the ceiling to 6 h (still a ceiling — an unbounded child would pin the run forever). Bare `/subagents auto-limit` shows the current state.
319
317
 
320
318
  ## Peek — `/subagents peek` or `ctrl+shift+a`
321
319
 
@@ -356,7 +354,7 @@ The extension has no multiplexer integration and does not want one: it exposes t
356
354
 
357
355
  - Parent tools: 6 schemas with short descriptions. **No catalog, no context hook** — nothing injected per request.
358
356
  - Background completion: 3-line notice. Full text only via `subagent_result`.
359
- - Children: isolated sessions; talk tools injected only when `allowIntercom`; each child's prompt states its own task id and its siblings' so mailbox addressing works. Model resolution: explicit `provider/model-id` or bare id via the pi model registry → the parent's current model → settings default. Thinking levels validated against the resolved model's `thinkingLevelMap`.
357
+ - Children: isolated sessions; talk tools always injected; each child's prompt states its own task id and its siblings' so mailbox addressing works. Model resolution: explicit `provider/model-id` or bare id via the pi model registry → the parent's current model → settings default. Thinking levels validated against the resolved model's `thinkingLevelMap`.
360
358
 
361
359
  ## What this is built on
362
360
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arhen/pi-core-subagent",
3
- "version": "1.3.46",
3
+ "version": "1.3.48",
4
4
  "type": "module",
5
5
  "description": "pi extension: fast in-process subagents with a dependency-graph scheduler (needs edges gate tasks and carry upstream output into dependent prompts), plus background runs, intercom and agent-to-agent mailbox. Leader defines agents inline.",
6
6
  "license": "MIT",
package/src/child.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Child-session tools.
3
3
  *
4
- * When `allowIntercom: true`, children get four talk tools:
4
+ * Every child gets four talk tools:
5
5
  * - ask_parent blocking Q&A with the leader (parent)
6
6
  * - notify_parent one-way message to the leader
7
7
  * - send_agent_message one-way message to another subagent's mailbox
package/src/index.ts CHANGED
@@ -81,12 +81,12 @@ export default function (pi: ExtensionAPI) {
81
81
  if (value === "on" || value === "off") {
82
82
  const next = manager.setAutoLimit(value === "on");
83
83
  ctx.ui.notify(
84
- `auto-limit ${next ? "on" : "off"} — ${next ? "leader-imposed" : "no"} maxRuntimeMs caps apply to tasks (off = unlimited until done).`,
84
+ `auto-limit ${next ? "on" : "off"} — ${next ? "tasks without an explicit maxRuntimeMs get the 1 h default ceiling" : "raised 6 h ceiling applies (no 1 h cap)"}.`,
85
85
  "info",
86
86
  );
87
87
  } else {
88
88
  ctx.ui.notify(
89
- `auto-limit is ${manager.autoLimitOn ? "on" : "off"} — use \`/subagents auto-limit on|off\` (off = tasks run unlimited until done).`,
89
+ `auto-limit is ${manager.autoLimitOn ? "on (1 h default ceiling)" : "off (6 h ceiling)"} — use \`/subagents auto-limit on|off\`.`,
90
90
  "info",
91
91
  );
92
92
  }
@@ -157,7 +157,7 @@ export default function (pi: ExtensionAPI) {
157
157
  // ponytail: this string is billed on every request. No example block — an example
158
158
  // biases the model toward one shape; guidelines + JSON schema describe all of them.
159
159
  description:
160
- "Run isolated subagents (own context, own session). You invent each agent: name, optional system prompt, toolset (read-only default, write:true to edit). Use `agent`+`task` for one, `tasks` for many. `needs` declares dependency edges: a task waits for its needs and receives their outputs prepended to its prompt. If a user agent file in `.agents/agents`, `.claude/agents`, or `.pi/agents` (project dirs, then home) has a `description` matching the spawn goal (name + task), that file is authoritative: body = system prompt, frontmatter `model`/`tools` apply, inline prompt/model/tools ignored. No match → the inline definition stands. Write agents run in an isolated git worktree: on completion the result reports the branch + changed files — review, then merge with `git merge --no-ff <branch>` (merged branches are cleaned automatically). Every run is background: the call returns a runId immediately and completion notifies you — do NOT park waiting on it. If you have no other work, end your turn; the completion notice wakes you with the results. Set autoAwait:true only when the very next step in the SAME turn consumes the result. allowIntercom:true lets children talk to you and each other.",
160
+ "Run isolated subagents (own context, own session). You invent each agent: name, optional system prompt, toolset (read-only default, write:true to edit). Use `agent`+`task` for one, `tasks` for many. `needs` declares dependency edges: a task waits for its needs and receives their outputs prepended to its prompt. If a user agent file in `.agents/agents`, `.claude/agents`, or `.pi/agents` (project dirs, then home) has a `description` matching the spawn goal (name + task), that file is authoritative: body = system prompt, frontmatter `model`/`tools` apply, inline prompt/model/tools ignored. No match → the inline definition stands. Write agents run in an isolated git worktree: on completion the result reports the branch + changed files — review, then merge with `git merge --no-ff <branch>` (merged branches are cleaned automatically). Every run is background: the call returns a runId immediately and completion notifies you — do NOT park waiting on it. If you have no other work, end your turn; the completion notice wakes you with the results. Set autoAwait:true only when the very next step in the SAME turn consumes the result. Children always carry talk tools: they can ask you questions, notify you, and message siblings.",
161
161
  promptSnippet: "Define and delegate work to specialized subagents.",
162
162
  promptGuidelines: [
163
163
  "Use subagent when independent review, testing, research, or parallel analysis improves quality.",
@@ -172,11 +172,10 @@ export default function (pi: ExtensionAPI) {
172
172
  "Never block with nothing to do: if you have no work left after spawning, end your turn. Task completion notifies you and wakes a fresh turn with the results — await_subagent/autoAwait in that situation only burns time and tokens.",
173
173
  "autoAwait:true only when the same turn must consume the result immediately (e.g. you spawn a reviewer and then must act on its verdict before replying). Otherwise spawn background and read results from the completion notice, or subagent_result when you come back.",
174
174
  "await_subagent is for the rare case where you have parallel work of your own and need to sync at a specific point — not the default follow-up to a spawn.",
175
- "allowIntercom:true only when a child may need to ask you something.",
176
175
  ],
177
176
  parameters: SubagentParams,
178
177
  executionMode: "parallel", // sibling subagent calls run concurrently, not serialized
179
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
178
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
180
179
  const typed = params as SubagentParamsShape;
181
180
  const details = manager.startInBackground(typed, ctx);
182
181
  if (typed.autoAwait) {
@@ -237,7 +236,7 @@ export default function (pi: ExtensionAPI) {
237
236
  : args.agent
238
237
  ? `single ${args.agent}`
239
238
  : "preparing…";
240
- const flags = [args.autoAwait ? "await" : "bg", args.allowIntercom ? "a2a" : ""].filter(Boolean).join(" · ");
239
+ const flags = args.autoAwait ? "await" : "bg";
241
240
  // Params used, dimmed: model, thinking, toolset, per-task write count.
242
241
  const tasks = args.tasks ?? args.chain ?? [];
243
242
  const writeCount = tasks.filter((t) => t.write).length;
@@ -269,7 +268,7 @@ export default function (pi: ExtensionAPI) {
269
268
  })
270
269
  .join("");
271
270
  return new Text(
272
- `${theme.fg("toolTitle", theme.bold("subagent"))} ${theme.fg("accent", mode)}${flags ? ` ${theme.fg("muted", `[${flags}]`)}` : ""}${params}${graphLine}${plan}`,
271
+ `${theme.fg("toolTitle", theme.bold("subagent"))} ${theme.fg("accent", mode)} ${theme.fg("muted", `[${flags}]`)}${params}${graphLine}${plan}`,
273
272
  0,
274
273
  0,
275
274
  );
package/src/manager.ts CHANGED
@@ -321,8 +321,8 @@ export class SubagentManager {
321
321
  /** Set by clearRuns — blocks late persists from erasing the sidecar. */
322
322
  private cleared = false;
323
323
 
324
- /** When false, strip leader-imposed maxRuntimeMs so tasks run unlimited — toggle via `/subagents auto-limit on|off`. */
325
- private autoLimit = true;
324
+ /** When true, tasks without an explicit maxRuntimeMs get the 1 h default ceiling; when false (default) the raised 6 h ceiling applies — toggle via `/subagents auto-limit on|off`. */
325
+ private autoLimit = false;
326
326
 
327
327
  turnActivity = false;
328
328
 
@@ -850,7 +850,7 @@ export class SubagentManager {
850
850
  const allowedTools = input.write ? WRITE_TOOLS : READONLY_TOOLS;
851
851
  const fileTools = file?.tools?.filter((t) => allowedTools.includes(t));
852
852
  const baseTools = fileTools?.length ? fileTools : (input.tools ?? allowedTools);
853
- const tools = [...baseTools, ...(run.allowIntercom ? CHILD_TALK_TOOLS : [])];
853
+ const tools = [...baseTools, ...CHILD_TALK_TOOLS];
854
854
  // Isolation follows the DELIVERED toolset, never the raw request: explicit
855
855
  // tools: [bash] without write:true still gets a worktree, and a file that
856
856
  // narrowed the child to read-only never gets the commit/merge ceremony.
@@ -990,9 +990,7 @@ export class SubagentManager {
990
990
  const worktreeNote = wt
991
991
  ? ` You work in an isolated git worktree (branch ${wt.branch})${task.stackedOn ? `, stacked on ${task.stackedOn} (its changes are already in your tree)` : ""}. Never run git commands that switch branches, create branches, or move the worktree (git switch/checkout/branch/worktree). The extension commits your changes when you finish. git status/diff are fine for inspecting your own changes. node_modules is a SHARED symlink to the main checkout: never install, upgrade, or delete dependencies (no npm/bun/yarn/pnpm install, no \`rm -rf node_modules\`) — those writes escape your worktree and damage the user's project. If the task truly needs a dependency change, edit the manifest only and say so in your answer.`
992
992
  : "";
993
- const subagentInstruction = run.allowIntercom
994
- ? `You are running as a subagent. Your bash tool already executes in the project working directory — never prefix commands with \`cd\`. Do not call subagent/delegation tools unless the parent explicitly asks. Return a concise final answer. You MAY use ask_parent only when truly blocked on information only the parent has; notify_parent for one-way updates; send_agent_message/poll_agent_messages to coordinate with siblings. Your mailbox address and siblings: ${task.roster ?? "(none)"}. Use the exact task ids (e.g. task_2) as send_agent_message targets. Siblings run independently and may start late or finish early — never block indefinitely on their replies: poll at most 5 times, then proceed with your best judgment. A gated sibling (marked ↳ waits in the graph) may not be running yet; do not wait for it. Stalled waits get the whole run killed. When your work is done, call notify_parent ONCE with a concise result summary — key findings, verdicts, file:line evidence — so the leader can start consuming your output before the run finishes.${worktreeNote}`
995
- : `You are running as a subagent. Your bash tool already executes in the project working directory — never prefix commands with \`cd\`. Do not call subagent/delegation tools unless the parent explicitly asks. Return a concise final answer for the parent agent.${worktreeNote}`;
993
+ const subagentInstruction = `You are running as a subagent. Your bash tool already executes in the project working directory — never prefix commands with \`cd\`. Do not call subagent/delegation tools unless the parent explicitly asks. Return a concise final answer. You MAY use ask_parent only when truly blocked on information only the parent has; notify_parent for one-way updates; send_agent_message/poll_agent_messages to coordinate with siblings. Your mailbox address and siblings: ${task.roster ?? "(none)"}. Use the exact task ids (e.g. task_2) as send_agent_message targets. Siblings run independently and may start late or finish early — never block indefinitely on their replies: poll at most 5 times, then proceed with your best judgment. A gated sibling (marked ↳ waits in the graph) may not be running yet; do not wait for it. Stalled waits get the whole run killed. When your work is done, call notify_parent ONCE with a concise result summary — key findings, verdicts, file:line evidence — so the leader can start consuming your output before the run finishes.${worktreeNote}`;
996
994
 
997
995
  const loader = new DefaultResourceLoader({
998
996
  cwd: childCwd,
@@ -1005,9 +1003,7 @@ export class SubagentManager {
1005
1003
  });
1006
1004
  await loader.reload();
1007
1005
 
1008
- const customTools: ToolDefinition[] = run.allowIntercom
1009
- ? createChildTools(task.id, this.makeChildHandlers(run, task, ctx))
1010
- : [];
1006
+ const customTools: ToolDefinition[] = createChildTools(task.id, this.makeChildHandlers(run, task, ctx));
1011
1007
 
1012
1008
  const created = await createAgentSession({
1013
1009
  cwd: childCwd,
@@ -1324,7 +1320,6 @@ export class SubagentManager {
1324
1320
  id: newId("run"),
1325
1321
  mode,
1326
1322
  status: "queued",
1327
- allowIntercom: Boolean(params.allowIntercom),
1328
1323
  notifyPerTask: params.notifyPerTask ?? true,
1329
1324
  createdAt: Date.now(),
1330
1325
  concurrency: Math.max(1, Math.min(params.concurrency ?? DEFAULT_CONCURRENCY, MAX_CONCURRENCY)),
package/src/peek.ts CHANGED
@@ -48,9 +48,7 @@ function stripThinking(text: string): string {
48
48
  .trim();
49
49
  }
50
50
 
51
- // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping real terminal escapes is the point
52
51
  const ANSI = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
53
- // biome-ignore lint/suspicious/noControlCharactersInRegex: same
54
52
  const CONTROL = /[\x00-\x08\x0b-\x1f\x7f]/g;
55
53
 
56
54
  /** Tool output is terminal output: it carries colour escapes and carriage returns
package/src/schemas.ts CHANGED
@@ -67,9 +67,6 @@ export const SubagentParams = Type.Object({
67
67
  default: true,
68
68
  }),
69
69
  ),
70
- allowIntercom: Type.Optional(
71
- Type.Boolean({ description: "Let children ask you questions, notify you, and message sibling subagents" }),
72
- ),
73
70
  });
74
71
 
75
72
  /** Derived from the schemas — single source of truth, no hand-maintained mirror. */
package/src/types.ts CHANGED
@@ -68,7 +68,6 @@ export interface RunSnapshot {
68
68
  id: string;
69
69
  mode: RunMode;
70
70
  status: RunStatus;
71
- allowIntercom: boolean;
72
71
  notifyPerTask: boolean;
73
72
  createdAt: number;
74
73
  startedAt?: number;
package/src/worktree.ts CHANGED
@@ -392,10 +392,23 @@ export function ownerAlive(path: string, ownedHere?: (path: string) => boolean):
392
392
  }
393
393
 
394
394
  /** Stable per-boot id, so a recycled pid from before a reboot can't look alive.
395
- * Compute ONE floor on the expressed seconds, not two — separate floors of
396
- * walltime and uptime flip by ±1 around integer boundaries and would read a
397
- * live marker as dead on a cross-second read. */
395
+ * Prefer the OS's own boot identity — the clock formula (Date.now - uptime)
396
+ * breaks on NTP-stepped clocks (CI runners): the step flips the floor and a
397
+ * live marker suddenly reads as pre-reboot/dead. The formula stays as the
398
+ * last-resort fallback: one floor on the expressed seconds, not two —
399
+ * separate floors of walltime and uptime flip by ±1 around integer
400
+ * boundaries and would read a live marker as dead on a cross-second read. */
398
401
  function bootId(): string {
402
+ try {
403
+ if (process.platform === "linux") return readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
404
+ if (process.platform === "darwin") {
405
+ // kern.boottime = "{ sec = 1756…, usec = … }" — sec alone is stable per boot.
406
+ const out = execFileSync("sysctl", ["-n", "kern.boottime"], { encoding: "utf8" });
407
+ return out.match(/sec = (\d+)/)?.[1] ?? out.trim();
408
+ }
409
+ } catch {
410
+ /* fall through to the formula */
411
+ }
399
412
  return String(Math.floor((Date.now() - uptime() * 1000) / 1000));
400
413
  }
401
414