pi-subagents 0.21.2 → 0.21.3

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/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.21.3] - 2026-04-30
6
+
7
+ ### Fixed
8
+ - Debounce foreground `needs_attention` notices, make them non-triggering, and cancel them when the run finishes so stale chain-step alerts do not launch parent turns after completion.
9
+
5
10
  ## [0.21.2] - 2026-04-30
6
11
 
7
12
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.21.2",
3
+ "version": "0.21.3",
4
4
  "description": "Pi extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
@@ -261,6 +261,8 @@ without forcing each step to rediscover everything.
261
261
 
262
262
  ### Async/background
263
263
 
264
+ Use async mode whenever the parent agent should keep working while a child runs. A normal foreground `subagent(...)` call blocks the parent until the child completes; it is appropriate when the next parent step depends on the child result. If you say you will "ask a reviewer while I continue auditing" or otherwise run local work in parallel with a child, launch with `async: true`. Do not end your turn immediately after launching that async child if you promised to keep working; continue the local inspection or other independent work, then check the async run when its result is needed.
265
+
264
266
  ```typescript
265
267
  subagent({
266
268
  agent: "worker",
@@ -269,6 +271,18 @@ subagent({
269
271
  })
270
272
  ```
271
273
 
274
+ For review fanout where the parent continues a local audit:
275
+
276
+ ```typescript
277
+ const run = subagent({
278
+ agent: "reviewer",
279
+ task: "Review the current diff for correctness issues. Do not edit files.",
280
+ async: true,
281
+ context: "fresh"
282
+ })
283
+ // Continue local inspection, then later call status with the returned id.
284
+ ```
285
+
272
286
  Inspect async runs with `subagent({ action: "status", id: "..." })`, `subagent({ action: "status" })` for active runs, or the `/subagents-status` slash command.
273
287
 
274
288
  Use diagnostics when setup or child startup looks wrong:
@@ -0,0 +1,92 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import { controlNotificationKey, formatControlNoticeMessage } from "../runs/shared/subagent-control.ts";
3
+ import type { ControlEvent, SubagentState } from "../shared/types.ts";
4
+
5
+ export const SUBAGENT_CONTROL_MESSAGE_TYPE = "subagent_control_notice";
6
+
7
+ export interface SubagentControlMessageDetails {
8
+ event: ControlEvent;
9
+ source?: "foreground" | "async";
10
+ asyncDir?: string;
11
+ childIntercomTarget?: string;
12
+ noticeText?: string;
13
+ }
14
+
15
+ export function controlNoticeTarget(details: SubagentControlMessageDetails): string | undefined {
16
+ return details.childIntercomTarget;
17
+ }
18
+
19
+ export function formatSubagentControlNotice(details: SubagentControlMessageDetails, content?: string): string {
20
+ return details.noticeText ?? content ?? formatControlNoticeMessage(details.event, controlNoticeTarget(details));
21
+ }
22
+
23
+ function noticeTimerKey(details: SubagentControlMessageDetails): string {
24
+ const childIntercomTarget = controlNoticeTarget(details);
25
+ return `${details.event.runId}:${controlNotificationKey(details.event, childIntercomTarget)}`;
26
+ }
27
+
28
+ export function clearPendingForegroundControlNotices(state: SubagentState, runId?: string): void {
29
+ const pending = state.pendingForegroundControlNotices;
30
+ if (!pending) return;
31
+ for (const [key, timer] of pending) {
32
+ if (runId !== undefined && !key.startsWith(`${runId}:`)) continue;
33
+ clearTimeout(timer);
34
+ pending.delete(key);
35
+ }
36
+ }
37
+
38
+ function deliverControlNotice(input: {
39
+ pi: Pick<ExtensionAPI, "sendMessage">;
40
+ visibleControlNotices: Set<string>;
41
+ details: SubagentControlMessageDetails;
42
+ }): void {
43
+ const childIntercomTarget = controlNoticeTarget(input.details);
44
+ const key = controlNotificationKey(input.details.event, childIntercomTarget);
45
+ if (input.visibleControlNotices.has(key)) return;
46
+ input.visibleControlNotices.add(key);
47
+ const noticeText = input.details.noticeText ?? formatControlNoticeMessage(input.details.event, childIntercomTarget);
48
+ input.pi.sendMessage(
49
+ {
50
+ customType: SUBAGENT_CONTROL_MESSAGE_TYPE,
51
+ content: noticeText,
52
+ display: true,
53
+ details: { ...input.details, childIntercomTarget, noticeText },
54
+ },
55
+ { triggerTurn: input.details.source !== "foreground" },
56
+ );
57
+ }
58
+
59
+ function isForegroundNoticeStillActionable(state: SubagentState, details: SubagentControlMessageDetails): boolean {
60
+ const control = state.foregroundControls.get(details.event.runId);
61
+ if (!control) return false;
62
+ if (control.currentAgent && control.currentAgent !== details.event.agent) return false;
63
+ if (details.event.index !== undefined && control.currentIndex !== details.event.index) return false;
64
+ return control.currentActivityState === "needs_attention";
65
+ }
66
+
67
+ export function handleSubagentControlNotice(input: {
68
+ pi: Pick<ExtensionAPI, "sendMessage">;
69
+ state: SubagentState;
70
+ visibleControlNotices: Set<string>;
71
+ details: SubagentControlMessageDetails;
72
+ foregroundDelayMs?: number;
73
+ }): void {
74
+ if (!input.details?.event || input.details.event.type === "active_long_running") return;
75
+ if (input.details.source !== "foreground") {
76
+ deliverControlNotice(input);
77
+ return;
78
+ }
79
+
80
+ const pending = input.state.pendingForegroundControlNotices ?? new Map<string, ReturnType<typeof setTimeout>>();
81
+ input.state.pendingForegroundControlNotices = pending;
82
+ const timerKey = noticeTimerKey(input.details);
83
+ const existing = pending.get(timerKey);
84
+ if (existing) clearTimeout(existing);
85
+ const timer = setTimeout(() => {
86
+ pending.delete(timerKey);
87
+ if (!isForegroundNoticeStillActionable(input.state, input.details)) return;
88
+ deliverControlNotice(input);
89
+ }, input.foregroundDelayMs ?? 1000);
90
+ timer.unref?.();
91
+ pending.set(timerKey, timer);
92
+ }
@@ -25,7 +25,6 @@ import { renderWidget, renderSubagentResult, stopResultAnimations, stopWidgetAni
25
25
  import { SubagentParams } from "./schemas.ts";
26
26
  import { createSubagentExecutor } from "../runs/foreground/subagent-executor.ts";
27
27
  import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
28
- import { controlNotificationKey, formatControlNoticeMessage } from "../runs/shared/subagent-control.ts";
29
28
  import { createResultWatcher } from "../runs/background/result-watcher.ts";
30
29
  import { registerSlashCommands } from "../slash/slash-commands.ts";
31
30
  import { registerPromptTemplateDelegationBridge } from "../slash/prompt-template-bridge.ts";
@@ -36,7 +35,6 @@ import registerSubagentNotify, { type SubagentNotifyDetails } from "../runs/back
36
35
  import { SUBAGENT_CHILD_ENV } from "../runs/shared/pi-args.ts";
37
36
  import { formatDuration, shortenPath } from "../shared/formatters.ts";
38
37
  import {
39
- type ControlEvent,
40
38
  type Details,
41
39
  type ExtensionConfig,
42
40
  type SubagentState,
@@ -49,6 +47,13 @@ import {
49
47
  SUBAGENT_CONTROL_EVENT,
50
48
  WIDGET_KEY,
51
49
  } from "../shared/types.ts";
50
+ import {
51
+ clearPendingForegroundControlNotices,
52
+ formatSubagentControlNotice,
53
+ handleSubagentControlNotice,
54
+ SUBAGENT_CONTROL_MESSAGE_TYPE,
55
+ type SubagentControlMessageDetails,
56
+ } from "./control-notices.ts";
52
57
 
53
58
  /**
54
59
  * Derive subagent session base directory from parent session file.
@@ -153,24 +158,6 @@ function createSlashResultComponent(
153
158
  return container;
154
159
  }
155
160
 
156
- const SUBAGENT_CONTROL_MESSAGE_TYPE = "subagent_control_notice";
157
-
158
- interface SubagentControlMessageDetails {
159
- event: ControlEvent;
160
- source?: "foreground" | "async";
161
- asyncDir?: string;
162
- childIntercomTarget?: string;
163
- noticeText?: string;
164
- }
165
-
166
- function controlNoticeTarget(details: SubagentControlMessageDetails): string | undefined {
167
- return details.childIntercomTarget;
168
- }
169
-
170
- function formatSubagentControlNotice(details: SubagentControlMessageDetails, content?: string): string {
171
- return details.noticeText ?? content ?? formatControlNoticeMessage(details.event, controlNoticeTarget(details));
172
- }
173
-
174
161
  function parseSubagentNotifyContent(content: string): SubagentNotifyDetails | undefined {
175
162
  const lines = content.split("\n");
176
163
  const header = lines[0] ?? "";
@@ -259,6 +246,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
259
246
  asyncJobs: new Map(),
260
247
  foregroundControls: new Map(),
261
248
  lastForegroundControlId: null,
249
+ pendingForegroundControlNotices: new Map(),
262
250
  cleanupTimers: new Map(),
263
251
  lastUiContext: null,
264
252
  poller: null,
@@ -283,6 +271,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
283
271
  const runtimeCleanup = () => {
284
272
  stopWidgetAnimation();
285
273
  stopResultAnimations();
274
+ clearPendingForegroundControlNotices(state);
286
275
  if (state.poller) {
287
276
  clearInterval(state.poller);
288
277
  state.poller = null;
@@ -497,22 +486,12 @@ DIAGNOSTICS:
497
486
  const visibleControlNotices = existingVisibleControlNotices instanceof Set ? existingVisibleControlNotices as Set<string> : new Set<string>();
498
487
  globalStore[controlNoticeSeenStoreKey] = visibleControlNotices;
499
488
  const controlEventHandler = (payload: unknown) => {
500
- const details = payload as SubagentControlMessageDetails;
501
- if (!details?.event || details.event.type === "active_long_running") return;
502
- const childIntercomTarget = controlNoticeTarget(details);
503
- const key = controlNotificationKey(details.event, childIntercomTarget);
504
- if (visibleControlNotices.has(key)) return;
505
- visibleControlNotices.add(key);
506
- const noticeText = details.noticeText ?? formatControlNoticeMessage(details.event, childIntercomTarget);
507
- pi.sendMessage(
508
- {
509
- customType: SUBAGENT_CONTROL_MESSAGE_TYPE,
510
- content: noticeText,
511
- display: true,
512
- details: { ...details, childIntercomTarget, noticeText },
513
- },
514
- { triggerTurn: true },
515
- );
489
+ handleSubagentControlNotice({
490
+ pi,
491
+ state,
492
+ visibleControlNotices,
493
+ details: payload as SubagentControlMessageDetails,
494
+ });
516
495
  };
517
496
  const eventUnsubscribes = [
518
497
  pi.events.on(SUBAGENT_ASYNC_STARTED_EVENT, handleStarted),
@@ -547,6 +526,7 @@ DIAGNOSTICS:
547
526
  state.currentSessionId = ctx.sessionManager.getSessionFile() ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
548
527
  state.lastUiContext = ctx;
549
528
  cleanupSessionArtifacts(ctx);
529
+ clearPendingForegroundControlNotices(state);
550
530
  resetJobs(ctx);
551
531
  restoreSlashFinalSnapshots(ctx.sessionManager.getEntries());
552
532
  };
@@ -569,6 +549,7 @@ DIAGNOSTICS:
569
549
  stopResultWatcher();
570
550
  if (state.poller) clearInterval(state.poller);
571
551
  state.poller = null;
552
+ clearPendingForegroundControlNotices(state);
572
553
  for (const timer of state.cleanupTimers.values()) {
573
554
  clearTimeout(timer);
574
555
  }
@@ -10,6 +10,7 @@ import { executeChain } from "./chain-execution.ts";
10
10
  import { resolveExecutionAgentScope } from "../../agents/agent-scope.ts";
11
11
  import { handleManagementAction } from "../../agents/agent-management.ts";
12
12
  import { buildDoctorReport } from "../../extension/doctor.ts";
13
+ import { clearPendingForegroundControlNotices } from "../../extension/control-notices.ts";
13
14
  import { runSync } from "./execution.ts";
14
15
  import { resolveModelCandidate } from "../shared/model-fallback.ts";
15
16
  import { aggregateParallelOutputs } from "../shared/parallel-utils.ts";
@@ -2067,6 +2068,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2067
2068
  return toExecutionErrorResult(effectiveParams, error);
2068
2069
  } finally {
2069
2070
  if (foregroundControl) {
2071
+ clearPendingForegroundControlNotices(deps.state, runId);
2070
2072
  deps.state.foregroundControls.delete(runId);
2071
2073
  if (deps.state.lastForegroundControlId === runId) {
2072
2074
  deps.state.lastForegroundControlId = null;
@@ -356,6 +356,7 @@ export interface SubagentState {
356
356
  interrupt?: () => boolean;
357
357
  }>;
358
358
  lastForegroundControlId: string | null;
359
+ pendingForegroundControlNotices?: Map<string, ReturnType<typeof setTimeout>>;
359
360
  cleanupTimers: Map<string, ReturnType<typeof setTimeout>>;
360
361
  lastUiContext: ExtensionContext | null;
361
362
  poller: NodeJS.Timeout | null;