@clanker-code/pi-subagents 0.11.0 → 0.11.1

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
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.11.1] - 2026-06-28
11
+
12
+ ### Fixed
13
+ - **Depth 2+ subagents now appear in the TUI widget** — each child session's `DefaultResourceLoader` previously created its own isolated event bus, so lifecycle events (`subagents:created`, `subagents:started`, `subagents:completed`, `subagents:failed`) from depth 2+ agents never reached the parent's widget listener. A forwarding event bus now wraps the parent bus: the child gets its own isolated local bus, but lifecycle events are forwarded to the parent so the widget renders the full recursive agent tree.
14
+ - **Agent tool description shows next spawn depth instead of agent's own depth** — the `{{currentDepth}}` placeholder and recursive guideline in the Agent tool description now show `extensionDepth + 1` (the depth the *next* spawned agent would be at) instead of `extensionDepth` (the agent's own depth), eliminating the off-by-one confusion where a depth-1 agent displayed "1/4" but spawned agents at depth 2.
15
+ - **Event bus propagation covers all spawn paths** — RPC-spawned agents (`cross-extension-rpc.ts`), scheduled agents (`schedule.ts`), and `spawnAndWait` foreground agents now correctly receive the parent's event bus and recursive depth metadata, so lifecycle events from agents spawned via any path are visible in the parent widget.
16
+ - **Dashboard UI action handlers** — steer, abort, and view-result row actions in the subagents management modal now route to the correct handler functions instead of silently no-opping.
17
+ - **Dashboard UI duplicate module push guard** — the `ui_management` probe no longer pushes duplicate module entries when called multiple times in the same session.
18
+
19
+ ### Added
20
+ - **Dashboard UI model column** — the subagents management modal now shows the model used by each agent (e.g. "opus", "sonnet") in a dedicated column.
21
+
10
22
  ## [0.11.0] - 2026-06-28
11
23
 
12
24
  ### Added
@@ -8,6 +8,7 @@
8
8
  import type { Model } from "@earendil-works/pi-ai";
9
9
  import type { AgentSession, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
10
10
  import { type ToolActivity } from "./agent-runner.js";
11
+ import type { EventBus } from "./cross-extension-rpc.js";
11
12
  import { type AgentInvocation, type AgentRecord, type IsolationMode, type SubagentType, type ThinkingLevel } from "./types.js";
12
13
  export type OnAgentComplete = (record: AgentRecord) => void;
13
14
  export type OnAgentStart = (record: AgentRecord) => void;
@@ -69,6 +70,8 @@ interface SpawnOptions {
69
70
  }) => void;
70
71
  /** Called when the session successfully compacts. */
71
72
  onCompaction?: (info: CompactionInfo) => void;
73
+ /** Parent's event bus — shared with child sessions so lifecycle events propagate to the parent widget. */
74
+ eventBus?: EventBus;
72
75
  }
73
76
  interface ResumeOptions {
74
77
  signal?: AbortSignal;
@@ -218,6 +218,7 @@ export class AgentManager {
218
218
  },
219
219
  depth: record.depth,
220
220
  parentAgentId: record.parentAgentId,
221
+ eventBus: options.eventBus,
221
222
  onSessionCreated: (session) => {
222
223
  record.session = session;
223
224
  // Flush any steers that arrived before the session was ready
@@ -4,6 +4,7 @@
4
4
  import type { Model } from "@earendil-works/pi-ai";
5
5
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
6
6
  import { type AgentSession, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
7
+ import type { EventBus } from "./cross-extension-rpc.js";
7
8
  import { type SubagentType, type ThinkingLevel } from "./types.js";
8
9
  /**
9
10
  * Tool names registered by THIS extension. Single source of truth so the
@@ -19,6 +20,13 @@ export declare const SUBAGENT_TOOL_NAMES: {
19
20
  readonly CLEAR_SUBAGENTS: "clear_subagents";
20
21
  readonly LIST_MODELS: "list_models";
21
22
  };
23
+ /**
24
+ * Create a forwarding event bus for a child session.
25
+ * The child gets its own local bus for emit/on, but lifecycle events
26
+ * (subagents:*) are also forwarded to the parent bus so the parent widget
27
+ * can display depth 2+ agents.
28
+ */
29
+ export declare function createForwardingEventBus(parentBus: EventBus): EventBus;
22
30
  export declare function getCurrentExtensionDepth(): number;
23
31
  export declare function getCurrentExtensionAgentId(): string | undefined;
24
32
  export declare function getCurrentExtensionParentAgentId(): string | undefined;
@@ -131,6 +139,9 @@ export interface RunOptions {
131
139
  depth?: number;
132
140
  /** Parent subagent id when spawned recursively from another subagent. */
133
141
  parentAgentId?: string;
142
+ /** Parent's event bus — shared with the child session so lifecycle events
143
+ * (subagents:created, subagents:started, etc.) propagate to the parent widget. */
144
+ eventBus?: EventBus;
134
145
  }
135
146
  export interface RunResult {
136
147
  responseText: string;
@@ -4,7 +4,7 @@
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { homedir } from "node:os";
6
6
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
7
- import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
7
+ import { createAgentSession, createEventBus, DefaultResourceLoader, getAgentDir, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
8
8
  import { BUILTIN_TOOL_NAMES, getAgentConfig, getConfig, getMemoryToolNames, getReadOnlyMemoryToolNames, getToolNamesForType } from "./agent-types.js";
9
9
  import { buildParentContext, extractText } from "./context.js";
10
10
  import { DEFAULT_AGENTS } from "./default-agents.js";
@@ -40,6 +40,38 @@ const RECURSIVE_TOOL_NAMES = [
40
40
  SUBAGENT_TOOL_NAMES.STEER,
41
41
  ];
42
42
  const EXTENSION_DEPTH_KEY = Symbol.for("pi-subagents:extension-depth");
43
+ /** Lifecycle event names that should propagate from child to parent sessions. */
44
+ const FORWARDABLE_EVENTS = new Set([
45
+ "subagents:created",
46
+ "subagents:started",
47
+ "subagents:completed",
48
+ "subagents:failed",
49
+ "subagents:compacted",
50
+ ]);
51
+ /**
52
+ * Create a forwarding event bus for a child session.
53
+ * The child gets its own local bus for emit/on, but lifecycle events
54
+ * (subagents:*) are also forwarded to the parent bus so the parent widget
55
+ * can display depth 2+ agents.
56
+ */
57
+ export function createForwardingEventBus(parentBus) {
58
+ // Use the parent's EventBus factory to create a properly isolated local bus
59
+ const localBus = createEventBus();
60
+ return {
61
+ on(event, handler) {
62
+ // Subscribe to local bus only — child doesn't see parent/sibling events
63
+ return localBus.on(event, handler);
64
+ },
65
+ emit(event, data) {
66
+ // Always emit on local bus for child's own listeners
67
+ localBus.emit(event, data);
68
+ // Forward lifecycle events to parent bus for parent widget visibility
69
+ if (FORWARDABLE_EVENTS.has(event)) {
70
+ parentBus.emit(event, data);
71
+ }
72
+ },
73
+ };
74
+ }
43
75
  const AUTO_EXPOSE_EXTENSION_NAMES = new Set(["pi-c2c"]);
44
76
  let extensionDepthLoadChain = Promise.resolve();
45
77
  const packageNameCache = new Map();
@@ -438,6 +470,10 @@ export async function runAgent(ctx, type, prompt, options) {
438
470
  }),
439
471
  };
440
472
  };
473
+ // Create a forwarding event bus so the child session's lifecycle events
474
+ // (subagents:created, subagents:started, etc.) propagate to the parent's
475
+ // event bus — making depth 2+ agents visible in the parent widget.
476
+ const childEventBus = options.eventBus ? createForwardingEventBus(options.eventBus) : undefined;
441
477
  const loader = new DefaultResourceLoader({
442
478
  cwd: configCwd,
443
479
  agentDir,
@@ -450,6 +486,7 @@ export async function runAgent(ctx, type, prompt, options) {
450
486
  noContextFiles: true,
451
487
  systemPromptOverride: () => systemPrompt,
452
488
  appendSystemPromptOverride: () => [],
489
+ eventBus: childEventBus,
453
490
  });
454
491
  await withLoadingExtensionDepth(depth, options.agentId, options.parentAgentId, () => loader.reload());
455
492
  // Plain entries in `tools:` are expected to be built-in names (extension tools
@@ -2,7 +2,13 @@ import type { ToolDescriptionMode } from "./settings.js";
2
2
  export declare function getModelLabelFromConfig(model: string): string;
3
3
  export interface AgentToolDescriptionOptions {
4
4
  mode: ToolDescriptionMode;
5
- extensionDepth: number;
5
+ /**
6
+ * Depth at which the NEXT spawned subagent will run.
7
+ * This is `extensionDepth + 1` — the agent's own depth plus one.
8
+ * Displayed as "Current recursive depth" in the tool description so the
9
+ * LLM sees the depth of the agent it is about to create, not its own depth.
10
+ */
11
+ nextSubagentDepth: number;
6
12
  schedulingEnabled: boolean;
7
13
  }
8
14
  export declare function buildScheduleGuideline(schedulingEnabled: boolean): string;
@@ -39,7 +39,7 @@ export function buildScheduleGuideline(schedulingEnabled) {
39
39
  }
40
40
  export function buildAgentToolDescription(options) {
41
41
  const scheduleGuideline = buildScheduleGuideline(options.schedulingEnabled);
42
- const recursiveGuideline = `Recursive agents are allowed through depth ${MAX_RECURSIVE_DEPTH}. Current recursive depth: ${options.extensionDepth}/${MAX_RECURSIVE_DEPTH}.`;
42
+ const recursiveGuideline = `Recursive agents are allowed through depth ${MAX_RECURSIVE_DEPTH}. Current recursive depth: ${options.nextSubagentDepth}/${MAX_RECURSIVE_DEPTH}.`;
43
43
  const compactAgentToolDescription = `Launch an autonomous agent for complex, multi-step tasks. Agent types:
44
44
  ${buildCompactTypeListText()}
45
45
 
@@ -49,7 +49,7 @@ Notes:
49
49
  - description: 3-5 words (shown in UI). Prompts must be self-contained — the agent has not seen this conversation.
50
50
  - Parallel work: one message, multiple Agent calls; they all run in the background. You are notified when agents finish — never poll or sleep.
51
51
  - Background by default: when you have useful independent work, launch it and continue. Doing nothing while an agent runs is worse than letting background work proceed.
52
- - Recursive agents: current depth ${options.extensionDepth}/${MAX_RECURSIVE_DEPTH}; you may spawn subagents until depth ${MAX_RECURSIVE_DEPTH}.
52
+ - Recursive agents: current depth ${options.nextSubagentDepth}/${MAX_RECURSIVE_DEPTH}; you may spawn subagents until depth ${MAX_RECURSIVE_DEPTH}.
53
53
  - The result is not shown to the user — summarize it for them. Verify an agent's claimed code changes before reporting work done.
54
54
  - resume continues a previous agent by ID; steer_subagent messages a running one.
55
55
  - list_models enumerates the model registry the \`model:\` param accepts — call it before picking a model explicitly.
@@ -105,7 +105,7 @@ Terse command-style prompts produce shallow, generic work.
105
105
  compactTypeList: buildCompactTypeListText,
106
106
  agentDir: getAgentDir,
107
107
  scheduleGuideline: () => scheduleGuideline,
108
- currentDepth: () => String(options.extensionDepth),
108
+ currentDepth: () => String(options.nextSubagentDepth),
109
109
  maxDepth: () => String(MAX_RECURSIVE_DEPTH),
110
110
  recursiveGuideline: () => recursiveGuideline,
111
111
  };
@@ -33,6 +33,10 @@ export interface RpcDeps {
33
33
  pi: unknown;
34
34
  getCtx: () => unknown | undefined;
35
35
  manager: SpawnCapable;
36
+ /** Default recursive depth for RPC-spawned subagents in this session. */
37
+ depth?: number;
38
+ /** Parent subagent id for RPC-spawned subagents in this session. */
39
+ parentAgentId?: string;
36
40
  }
37
41
  export interface RpcHandle {
38
42
  unsubPing: () => void;
@@ -66,7 +66,17 @@ export function registerRpcHandlers(deps) {
66
66
  }
67
67
  normalizedOptions = { ...normalizedOptions, model: resolved };
68
68
  }
69
- return { id: manager.spawn(pi, ctx, type, prompt, normalizedOptions) };
69
+ const spawnOptions = {
70
+ ...normalizedOptions,
71
+ eventBus: events,
72
+ depth: normalizedOptions.depth ?? deps.depth,
73
+ parentAgentId: normalizedOptions.parentAgentId ?? deps.parentAgentId,
74
+ };
75
+ if (spawnOptions.depth === undefined)
76
+ delete spawnOptions.depth;
77
+ if (spawnOptions.parentAgentId === undefined)
78
+ delete spawnOptions.parentAgentId;
79
+ return { id: manager.spawn(pi, ctx, type, prompt, spawnOptions) };
70
80
  });
71
81
  const unsubStop = handleRpc(events, "subagents:rpc:stop", ({ agentId }) => {
72
82
  if (!manager.abort(agentId))
@@ -24,6 +24,7 @@ function buildAgentRow(record) {
24
24
  id: record.id,
25
25
  type: getDisplayName(record.type),
26
26
  description: record.description ?? "",
27
+ model: record.invocation?.modelName ?? "—",
27
28
  status: record.status,
28
29
  toolUses: record.toolUses ?? 0,
29
30
  tokens: totalTokens > 0 ? formatTokenCount(totalTokens) : "—",
@@ -56,7 +57,14 @@ export function registerDashboardModules(pi, manager) {
56
57
  }, INVALIDATE_DEBOUNCE_MS);
57
58
  }
58
59
  // ── 1. Module Discovery (ui:list-modules) ──────────────────────────
60
+ // Guard against duplicate pushes: the bridge may call refreshUiModules
61
+ // multiple times per probe cycle when multiple sessions each register
62
+ // their own ui:invalidate listener. Check if our modules are already
63
+ // present before pushing.
59
64
  pi.events.on("ui:list-modules", ((probe) => {
65
+ const alreadyContributed = probe.modules.some((m) => m.kind === "management-modal" && m.id === MODULE_ID);
66
+ if (alreadyContributed)
67
+ return;
60
68
  const agents = manager.listAgents();
61
69
  const running = agents.filter(a => a.status === "running").length;
62
70
  const completed = agents.filter(a => a.status === "completed").length;
@@ -96,6 +104,7 @@ export function registerDashboardModules(pi, manager) {
96
104
  { key: "id", label: "ID", kind: "text", width: 120 },
97
105
  { key: "type", label: "Type", kind: "text", width: 100 },
98
106
  { key: "description", label: "Description", kind: "text" },
107
+ { key: "model", label: "Model", kind: "text", width: 80 },
99
108
  { key: "status", label: "Status", kind: "text", width: 90 },
100
109
  { key: "toolUses", label: "Tools", kind: "number", width: 60 },
101
110
  { key: "tokens", label: "Tokens", kind: "text", width: 80 },
@@ -148,20 +157,25 @@ export function registerDashboardModules(pi, manager) {
148
157
  pi.events.on("subagents:ui:refresh", (() => {
149
158
  scheduleInvalidate();
150
159
  }));
151
- // View Result: emit the result as a toast so the dashboard shows it
160
+ // View Result: return the agent's result as table rows so the modal
161
+ // displays it. The bridge's synchronous fast path calls `_reply(items)`
162
+ // when `data.items` is populated by the handler — do NOT call
163
+ // `scheduleInvalidate()` here as the subsequent re-probe would
164
+ // overwrite the returned rows with the original table data.
152
165
  pi.events.on("subagents:ui:view-result", ((data) => {
153
- const agentId = data.params?.id ?? data.id;
166
+ // Bridge spreads msg.params into data; row identity is at data.row.id.
167
+ const agentId = data.row?.id ?? data.id;
168
+ if (!agentId)
169
+ return;
154
170
  const record = manager.getRecord(agentId);
155
- if (!record) {
156
- pi.events.emit("ui:invalidate", { id: MODULE_ID });
171
+ if (!record)
157
172
  return;
158
- }
159
173
  const resultText = record.result?.trim() || "No output yet.";
160
174
  const preview = resultText.length > 2000
161
175
  ? resultText.slice(0, 2000) + "\n…(truncated)"
162
176
  : resultText;
163
- pi.events.emit("ui:invalidate", { id: MODULE_ID });
164
- // Return result as items so it shows in a detail view
177
+ // Populate data.items the bridge's synchronous fast path forwards
178
+ // this as a `ui_data_list` message back to the dashboard.
165
179
  data.items = [{
166
180
  id: record.id,
167
181
  type: getDisplayName(record.type),
@@ -171,25 +185,36 @@ export function registerDashboardModules(pi, manager) {
171
185
  outputFile: record.outputFile ?? "",
172
186
  }];
173
187
  }));
174
- // Abort: signal the agent to stop
188
+ // Abort: stop the running agent via the manager's abort() method
189
+ // which properly cancels the AbortController and cleans up state.
175
190
  pi.events.on("subagents:ui:abort", ((data) => {
176
- const agentId = data.params?.id ?? data.id;
177
- const record = manager.getRecord(agentId);
178
- if (record?.session) {
179
- // Use the session's abort mechanism
180
- try {
181
- record.session.dispose?.();
182
- }
183
- catch {
184
- // Ignore disposal errors
185
- }
186
- }
191
+ const agentId = data.row?.id ?? data.id;
192
+ if (!agentId)
193
+ return;
194
+ manager.abort(agentId);
187
195
  scheduleInvalidate();
188
196
  }));
189
- // Steer: for now just invalidate full steer requires a prompt input
190
- // which the management-modal form view could support in the future
191
- pi.events.on("subagents:ui:steer", ((_data) => {
192
- // TODO: Could open a form view for entering the steer message
197
+ // Steer: send a steering message to a running agent's session.
198
+ // The management-modal row action carries the row identity; we steer
199
+ // with a default "Continue" nudge. A future form view could accept
200
+ // custom text.
201
+ pi.events.on("subagents:ui:steer", ((data) => {
202
+ const agentId = data.row?.id ?? data.id;
203
+ if (!agentId)
204
+ return;
205
+ const record = manager.getRecord(agentId);
206
+ if (!record)
207
+ return;
208
+ if (record.status === "running" && record.session) {
209
+ // Session is live — steer immediately
210
+ record.session.steer("Continue").catch(() => { });
211
+ }
212
+ else if (record.status === "queued") {
213
+ // Session not yet created — queue the steer for flush on start
214
+ if (!record.pendingSteers)
215
+ record.pendingSteers = [];
216
+ record.pendingSteers.push("Continue");
217
+ }
193
218
  scheduleInvalidate();
194
219
  }));
195
220
  // ── 4. Invalidate on agent lifecycle events ────────────────────────
package/dist/index.js CHANGED
@@ -379,7 +379,7 @@ export default function (pi) {
379
379
  return; // sessionId not yet available — try again on next event
380
380
  const path = resolveStorePath(ctx.cwd, sessionId);
381
381
  const store = new ScheduleStore(path);
382
- scheduler.start(pi, ctx, manager, store);
382
+ scheduler.start(pi, ctx, manager, store, { depth: nextSubagentDepth, parentAgentId: extensionAgentId });
383
383
  pi.events.emit("subagents:scheduler_ready", { sessionId, jobCount: store.list().length });
384
384
  }
385
385
  catch (err) {
@@ -412,6 +412,8 @@ export default function (pi) {
412
412
  pi,
413
413
  getCtx: () => currentCtx,
414
414
  manager,
415
+ depth: nextSubagentDepth,
416
+ parentAgentId: extensionAgentId,
415
417
  });
416
418
  // Broadcast readiness so extensions loaded after us can discover us
417
419
  pi.events.emit("subagents:ready", {});
@@ -594,7 +596,7 @@ export default function (pi) {
594
596
  const scheduleParam = isSchedulingEnabled() ? scheduleParamShape : {};
595
597
  const agentToolDescription = buildAgentToolDescription({
596
598
  mode: getToolDescriptionMode(),
597
- extensionDepth,
599
+ nextSubagentDepth,
598
600
  schedulingEnabled: isSchedulingEnabled(),
599
601
  });
600
602
  pi.registerTool(defineTool({
@@ -872,6 +874,7 @@ export default function (pi) {
872
874
  invocation: agentInvocation,
873
875
  depth: nextSubagentDepth,
874
876
  parentAgentId: extensionAgentId,
877
+ eventBus: pi.events,
875
878
  outputFileForAgent: (agentId) => createOutputFilePath(ctx.cwd, agentId, ctx.sessionManager.getSessionId()),
876
879
  onOutputFileCreated: (outputFile, agentId) => writeInitialEntry(outputFile, agentId, P.prompt, ctx.cwd),
877
880
  ...bgCallbacks,
@@ -1632,6 +1635,9 @@ Write the file using the write tool. Only write the file, nothing else.`;
1632
1635
  const record = await manager.spawnAndWait(pi, ctx, "general-purpose", generatePrompt, {
1633
1636
  description: `Generate ${name} agent`,
1634
1637
  maxTurns: 5,
1638
+ eventBus: pi.events,
1639
+ depth: nextSubagentDepth,
1640
+ parentAgentId: extensionAgentId,
1635
1641
  });
1636
1642
  if (record.status === "error") {
1637
1643
  ctx.ui.notify(`Generation failed: ${record.error}`, "warning");
@@ -51,6 +51,12 @@ export interface NewJobInput {
51
51
  isolated?: boolean;
52
52
  isolation?: IsolationMode;
53
53
  }
54
+ interface SchedulerSpawnDefaults {
55
+ /** Recursive depth for scheduled subagents fired from this session. */
56
+ depth?: number;
57
+ /** Parent subagent id for scheduled subagents fired from this session. */
58
+ parentAgentId?: string;
59
+ }
54
60
  export declare class SubagentScheduler {
55
61
  private jobs;
56
62
  private intervals;
@@ -58,8 +64,9 @@ export declare class SubagentScheduler {
58
64
  private pi;
59
65
  private ctx;
60
66
  private manager;
67
+ private spawnDefaults;
61
68
  /** Start the scheduler: bind to a session's store and arm enabled jobs. */
62
- start(pi: ExtensionAPI, ctx: ExtensionContext, manager: AgentManager, store: ScheduleStore): void;
69
+ start(pi: ExtensionAPI, ctx: ExtensionContext, manager: AgentManager, store: ScheduleStore, spawnDefaults?: SchedulerSpawnDefaults): void;
63
70
  /** Stop all timers; drop refs. Safe to call repeatedly. */
64
71
  stop(): void;
65
72
  /** True if start() has bound a store and the scheduler is active. */
@@ -107,3 +114,4 @@ export declare class SubagentScheduler {
107
114
  /** "10s"/"5m"/"1h"/"2d" → milliseconds. */
108
115
  static parseInterval(s: string): number | null;
109
116
  }
117
+ export {};
package/dist/schedule.js CHANGED
@@ -24,12 +24,14 @@ export class SubagentScheduler {
24
24
  pi;
25
25
  ctx;
26
26
  manager;
27
+ spawnDefaults = {};
27
28
  /** Start the scheduler: bind to a session's store and arm enabled jobs. */
28
- start(pi, ctx, manager, store) {
29
+ start(pi, ctx, manager, store, spawnDefaults = {}) {
29
30
  this.pi = pi;
30
31
  this.ctx = ctx;
31
32
  this.manager = manager;
32
33
  this.store = store;
34
+ this.spawnDefaults = spawnDefaults;
33
35
  for (const job of store.list()) {
34
36
  if (job.enabled)
35
37
  this.scheduleJob(job);
@@ -47,6 +49,7 @@ export class SubagentScheduler {
47
49
  this.pi = undefined;
48
50
  this.ctx = undefined;
49
51
  this.manager = undefined;
52
+ this.spawnDefaults = {};
50
53
  }
51
54
  /** True if start() has bound a store and the scheduler is active. */
52
55
  isActive() {
@@ -222,6 +225,9 @@ export class SubagentScheduler {
222
225
  isolated: job.isolated,
223
226
  thinkingLevel: job.thinking,
224
227
  isolation: job.isolation,
228
+ eventBus: pi.events,
229
+ depth: this.spawnDefaults.depth,
230
+ parentAgentId: this.spawnDefaults.parentAgentId,
225
231
  });
226
232
  }
227
233
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clanker-code/pi-subagents",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "A pi extension extension that brings smart Claude Code-style autonomous sub-agents to pi.",
5
5
  "author": "clankercode",
6
6
  "license": "MIT",
@@ -12,6 +12,7 @@ import { isAbsolute } from "node:path";
12
12
  import type { Model } from "@earendil-works/pi-ai";
13
13
  import type { AgentSession, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
14
14
  import { resumeAgent, runAgent, type ToolActivity } from "./agent-runner.js";
15
+ import type { EventBus } from "./cross-extension-rpc.js";
15
16
  import { type AgentInvocation, type AgentRecord, type IsolationMode, MAX_RECURSIVE_DEPTH, type SubagentType, type ThinkingLevel } from "./types.js";
16
17
  import { addUsage } from "./usage.js";
17
18
  import { cleanupWorktree, createWorktree, pruneWorktrees, } from "./worktree.js";
@@ -102,6 +103,8 @@ interface SpawnOptions {
102
103
  onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
103
104
  /** Called when the session successfully compacts. */
104
105
  onCompaction?: (info: CompactionInfo) => void;
106
+ /** Parent's event bus — shared with child sessions so lifecycle events propagate to the parent widget. */
107
+ eventBus?: EventBus;
105
108
  }
106
109
 
107
110
  interface ResumeOptions {
@@ -310,6 +313,7 @@ export class AgentManager {
310
313
  },
311
314
  depth: record.depth,
312
315
  parentAgentId: record.parentAgentId,
316
+ eventBus: options.eventBus,
313
317
  onSessionCreated: (session) => {
314
318
  record.session = session;
315
319
  // Flush any steers that arrived before the session was ready
@@ -11,6 +11,7 @@ import {
11
11
  type AgentSession,
12
12
  type AgentSessionEvent,
13
13
  createAgentSession,
14
+ createEventBus,
14
15
  DefaultResourceLoader,
15
16
  type ExtensionAPI,
16
17
  getAgentDir,
@@ -19,6 +20,7 @@ import {
19
20
  } from "@earendil-works/pi-coding-agent";
20
21
  import { BUILTIN_TOOL_NAMES, getAgentConfig, getConfig, getMemoryToolNames, getReadOnlyMemoryToolNames, getToolNamesForType } from "./agent-types.js";
21
22
  import { buildParentContext, extractText } from "./context.js";
23
+ import type { EventBus } from "./cross-extension-rpc.js";
22
24
  import { DEFAULT_AGENTS } from "./default-agents.js";
23
25
  import { detectEnv } from "./env.js";
24
26
  import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
@@ -55,6 +57,40 @@ const RECURSIVE_TOOL_NAMES: string[] = [
55
57
  ];
56
58
 
57
59
  const EXTENSION_DEPTH_KEY = Symbol.for("pi-subagents:extension-depth");
60
+
61
+ /** Lifecycle event names that should propagate from child to parent sessions. */
62
+ const FORWARDABLE_EVENTS = new Set([
63
+ "subagents:created",
64
+ "subagents:started",
65
+ "subagents:completed",
66
+ "subagents:failed",
67
+ "subagents:compacted",
68
+ ]);
69
+
70
+ /**
71
+ * Create a forwarding event bus for a child session.
72
+ * The child gets its own local bus for emit/on, but lifecycle events
73
+ * (subagents:*) are also forwarded to the parent bus so the parent widget
74
+ * can display depth 2+ agents.
75
+ */
76
+ export function createForwardingEventBus(parentBus: EventBus): EventBus {
77
+ // Use the parent's EventBus factory to create a properly isolated local bus
78
+ const localBus = createEventBus();
79
+ return {
80
+ on(event, handler) {
81
+ // Subscribe to local bus only — child doesn't see parent/sibling events
82
+ return localBus.on(event, handler);
83
+ },
84
+ emit(event, data) {
85
+ // Always emit on local bus for child's own listeners
86
+ localBus.emit(event, data);
87
+ // Forward lifecycle events to parent bus for parent widget visibility
88
+ if (FORWARDABLE_EVENTS.has(event)) {
89
+ parentBus.emit(event, data);
90
+ }
91
+ },
92
+ };
93
+ }
58
94
  const AUTO_EXPOSE_EXTENSION_NAMES = new Set(["pi-c2c"]);
59
95
  let extensionDepthLoadChain: Promise<void> = Promise.resolve();
60
96
  const packageNameCache = new Map<string, string[]>();
@@ -365,6 +401,9 @@ export interface RunOptions {
365
401
  depth?: number;
366
402
  /** Parent subagent id when spawned recursively from another subagent. */
367
403
  parentAgentId?: string;
404
+ /** Parent's event bus — shared with the child session so lifecycle events
405
+ * (subagents:created, subagents:started, etc.) propagate to the parent widget. */
406
+ eventBus?: EventBus;
368
407
  }
369
408
 
370
409
  export interface RunResult {
@@ -555,6 +594,11 @@ export async function runAgent(
555
594
  };
556
595
  };
557
596
 
597
+ // Create a forwarding event bus so the child session's lifecycle events
598
+ // (subagents:created, subagents:started, etc.) propagate to the parent's
599
+ // event bus — making depth 2+ agents visible in the parent widget.
600
+ const childEventBus = options.eventBus ? createForwardingEventBus(options.eventBus) : undefined;
601
+
558
602
  const loader = new DefaultResourceLoader({
559
603
  cwd: configCwd,
560
604
  agentDir,
@@ -567,6 +611,7 @@ export async function runAgent(
567
611
  noContextFiles: true,
568
612
  systemPromptOverride: () => systemPrompt,
569
613
  appendSystemPromptOverride: () => [],
614
+ eventBus: childEventBus,
570
615
  });
571
616
  await withLoadingExtensionDepth(depth, options.agentId, options.parentAgentId, () => loader.reload());
572
617
 
@@ -44,7 +44,13 @@ const buildCompactTypeListText = () =>
44
44
 
45
45
  export interface AgentToolDescriptionOptions {
46
46
  mode: ToolDescriptionMode;
47
- extensionDepth: number;
47
+ /**
48
+ * Depth at which the NEXT spawned subagent will run.
49
+ * This is `extensionDepth + 1` — the agent's own depth plus one.
50
+ * Displayed as "Current recursive depth" in the tool description so the
51
+ * LLM sees the depth of the agent it is about to create, not its own depth.
52
+ */
53
+ nextSubagentDepth: number;
48
54
  schedulingEnabled: boolean;
49
55
  }
50
56
 
@@ -56,7 +62,7 @@ export function buildScheduleGuideline(schedulingEnabled: boolean): string {
56
62
 
57
63
  export function buildAgentToolDescription(options: AgentToolDescriptionOptions): string {
58
64
  const scheduleGuideline = buildScheduleGuideline(options.schedulingEnabled);
59
- const recursiveGuideline = `Recursive agents are allowed through depth ${MAX_RECURSIVE_DEPTH}. Current recursive depth: ${options.extensionDepth}/${MAX_RECURSIVE_DEPTH}.`;
65
+ const recursiveGuideline = `Recursive agents are allowed through depth ${MAX_RECURSIVE_DEPTH}. Current recursive depth: ${options.nextSubagentDepth}/${MAX_RECURSIVE_DEPTH}.`;
60
66
 
61
67
  const compactAgentToolDescription = `Launch an autonomous agent for complex, multi-step tasks. Agent types:
62
68
  ${buildCompactTypeListText()}
@@ -67,7 +73,7 @@ Notes:
67
73
  - description: 3-5 words (shown in UI). Prompts must be self-contained — the agent has not seen this conversation.
68
74
  - Parallel work: one message, multiple Agent calls; they all run in the background. You are notified when agents finish — never poll or sleep.
69
75
  - Background by default: when you have useful independent work, launch it and continue. Doing nothing while an agent runs is worse than letting background work proceed.
70
- - Recursive agents: current depth ${options.extensionDepth}/${MAX_RECURSIVE_DEPTH}; you may spawn subagents until depth ${MAX_RECURSIVE_DEPTH}.
76
+ - Recursive agents: current depth ${options.nextSubagentDepth}/${MAX_RECURSIVE_DEPTH}; you may spawn subagents until depth ${MAX_RECURSIVE_DEPTH}.
71
77
  - The result is not shown to the user — summarize it for them. Verify an agent's claimed code changes before reporting work done.
72
78
  - resume continues a previous agent by ID; steer_subagent messages a running one.
73
79
  - list_models enumerates the model registry the \`model:\` param accepts — call it before picking a model explicitly.
@@ -125,7 +131,7 @@ Terse command-style prompts produce shallow, generic work.
125
131
  compactTypeList: buildCompactTypeListText,
126
132
  agentDir: getAgentDir,
127
133
  scheduleGuideline: () => scheduleGuideline,
128
- currentDepth: () => String(options.extensionDepth),
134
+ currentDepth: () => String(options.nextSubagentDepth),
129
135
  maxDepth: () => String(MAX_RECURSIVE_DEPTH),
130
136
  recursiveGuideline: () => recursiveGuideline,
131
137
  };
@@ -36,6 +36,10 @@ export interface RpcDeps {
36
36
  pi: unknown; // passed through to manager.spawn
37
37
  getCtx: () => unknown | undefined; // returns current ExtensionContext
38
38
  manager: SpawnCapable;
39
+ /** Default recursive depth for RPC-spawned subagents in this session. */
40
+ depth?: number;
41
+ /** Parent subagent id for RPC-spawned subagents in this session. */
42
+ parentAgentId?: string;
39
43
  }
40
44
 
41
45
  export interface RpcHandle {
@@ -108,7 +112,16 @@ export function registerRpcHandlers(deps: RpcDeps): RpcHandle {
108
112
  normalizedOptions = { ...normalizedOptions, model: resolved };
109
113
  }
110
114
 
111
- return { id: manager.spawn(pi, ctx, type, prompt, normalizedOptions) };
115
+ const spawnOptions = {
116
+ ...normalizedOptions,
117
+ eventBus: events,
118
+ depth: normalizedOptions.depth ?? deps.depth,
119
+ parentAgentId: normalizedOptions.parentAgentId ?? deps.parentAgentId,
120
+ };
121
+ if (spawnOptions.depth === undefined) delete spawnOptions.depth;
122
+ if (spawnOptions.parentAgentId === undefined) delete spawnOptions.parentAgentId;
123
+
124
+ return { id: manager.spawn(pi, ctx, type, prompt, spawnOptions) };
112
125
  },
113
126
  );
114
127
 
@@ -77,6 +77,7 @@ function buildAgentRow(record: any) {
77
77
  id: record.id,
78
78
  type: getDisplayName(record.type),
79
79
  description: record.description ?? "",
80
+ model: record.invocation?.modelName ?? "—",
80
81
  status: record.status,
81
82
  toolUses: record.toolUses ?? 0,
82
83
  tokens: totalTokens > 0 ? formatTokenCount(totalTokens) : "—",
@@ -110,7 +111,16 @@ export function registerDashboardModules(pi: ExtensionAPI, manager: AgentManager
110
111
  }
111
112
 
112
113
  // ── 1. Module Discovery (ui:list-modules) ──────────────────────────
114
+ // Guard against duplicate pushes: the bridge may call refreshUiModules
115
+ // multiple times per probe cycle when multiple sessions each register
116
+ // their own ui:invalidate listener. Check if our modules are already
117
+ // present before pushing.
113
118
  pi.events.on("ui:list-modules", ((probe: ModuleProbe) => {
119
+ const alreadyContributed = probe.modules.some(
120
+ (m: any) => m.kind === "management-modal" && m.id === MODULE_ID,
121
+ );
122
+ if (alreadyContributed) return;
123
+
114
124
  const agents = manager.listAgents();
115
125
  const running = agents.filter(a => a.status === "running").length;
116
126
  const completed = agents.filter(a => a.status === "completed").length;
@@ -150,6 +160,7 @@ export function registerDashboardModules(pi: ExtensionAPI, manager: AgentManager
150
160
  { key: "id", label: "ID", kind: "text", width: 120 },
151
161
  { key: "type", label: "Type", kind: "text", width: 100 },
152
162
  { key: "description", label: "Description", kind: "text" },
163
+ { key: "model", label: "Model", kind: "text", width: 80 },
153
164
  { key: "status", label: "Status", kind: "text", width: 90 },
154
165
  { key: "toolUses", label: "Tools", kind: "number", width: 60 },
155
166
  { key: "tokens", label: "Tokens", kind: "text", width: 80 },
@@ -206,23 +217,25 @@ export function registerDashboardModules(pi: ExtensionAPI, manager: AgentManager
206
217
  scheduleInvalidate();
207
218
  }) as any);
208
219
 
209
- // View Result: emit the result as a toast so the dashboard shows it
220
+ // View Result: return the agent's result as table rows so the modal
221
+ // displays it. The bridge's synchronous fast path calls `_reply(items)`
222
+ // when `data.items` is populated by the handler — do NOT call
223
+ // `scheduleInvalidate()` here as the subsequent re-probe would
224
+ // overwrite the returned rows with the original table data.
210
225
  pi.events.on("subagents:ui:view-result", ((data: any) => {
211
- const agentId = data.params?.id ?? data.id;
226
+ // Bridge spreads msg.params into data; row identity is at data.row.id.
227
+ const agentId = data.row?.id ?? data.id;
228
+ if (!agentId) return;
212
229
  const record = manager.getRecord(agentId);
213
- if (!record) {
214
- pi.events.emit("ui:invalidate", { id: MODULE_ID });
215
- return;
216
- }
230
+ if (!record) return;
217
231
 
218
232
  const resultText = record.result?.trim() || "No output yet.";
219
233
  const preview = resultText.length > 2000
220
234
  ? resultText.slice(0, 2000) + "\n…(truncated)"
221
235
  : resultText;
222
236
 
223
- pi.events.emit("ui:invalidate", { id: MODULE_ID });
224
-
225
- // Return result as items so it shows in a detail view
237
+ // Populate data.items the bridge's synchronous fast path forwards
238
+ // this as a `ui_data_list` message back to the dashboard.
226
239
  data.items = [{
227
240
  id: record.id,
228
241
  type: getDisplayName(record.type),
@@ -233,25 +246,33 @@ export function registerDashboardModules(pi: ExtensionAPI, manager: AgentManager
233
246
  }];
234
247
  }) as any);
235
248
 
236
- // Abort: signal the agent to stop
249
+ // Abort: stop the running agent via the manager's abort() method
250
+ // which properly cancels the AbortController and cleans up state.
237
251
  pi.events.on("subagents:ui:abort", ((data: any) => {
238
- const agentId = data.params?.id ?? data.id;
239
- const record = manager.getRecord(agentId);
240
- if (record?.session) {
241
- // Use the session's abort mechanism
242
- try {
243
- record.session.dispose?.();
244
- } catch {
245
- // Ignore disposal errors
246
- }
247
- }
252
+ const agentId = data.row?.id ?? data.id;
253
+ if (!agentId) return;
254
+ manager.abort(agentId);
248
255
  scheduleInvalidate();
249
256
  }) as any);
250
257
 
251
- // Steer: for now just invalidate full steer requires a prompt input
252
- // which the management-modal form view could support in the future
253
- pi.events.on("subagents:ui:steer", ((_data: any) => {
254
- // TODO: Could open a form view for entering the steer message
258
+ // Steer: send a steering message to a running agent's session.
259
+ // The management-modal row action carries the row identity; we steer
260
+ // with a default "Continue" nudge. A future form view could accept
261
+ // custom text.
262
+ pi.events.on("subagents:ui:steer", ((data: any) => {
263
+ const agentId = data.row?.id ?? data.id;
264
+ if (!agentId) return;
265
+ const record = manager.getRecord(agentId);
266
+ if (!record) return;
267
+
268
+ if (record.status === "running" && record.session) {
269
+ // Session is live — steer immediately
270
+ record.session.steer("Continue").catch(() => {});
271
+ } else if (record.status === "queued") {
272
+ // Session not yet created — queue the steer for flush on start
273
+ if (!record.pendingSteers) record.pendingSteers = [];
274
+ record.pendingSteers.push("Continue");
275
+ }
255
276
  scheduleInvalidate();
256
277
  }) as any);
257
278
 
package/src/index.ts CHANGED
@@ -444,7 +444,7 @@ export default function (pi: ExtensionAPI) {
444
444
  if (!sessionId) return; // sessionId not yet available — try again on next event
445
445
  const path = resolveStorePath(ctx.cwd, sessionId);
446
446
  const store = new ScheduleStore(path);
447
- scheduler.start(pi, ctx, manager, store);
447
+ scheduler.start(pi, ctx, manager, store, { depth: nextSubagentDepth, parentAgentId: extensionAgentId });
448
448
  pi.events.emit("subagents:scheduler_ready", { sessionId, jobCount: store.list().length });
449
449
  } catch (err) {
450
450
  // Scheduling is non-essential — log and move on so the rest of the
@@ -478,6 +478,8 @@ export default function (pi: ExtensionAPI) {
478
478
  pi,
479
479
  getCtx: () => currentCtx,
480
480
  manager,
481
+ depth: nextSubagentDepth,
482
+ parentAgentId: extensionAgentId,
481
483
  });
482
484
 
483
485
  // Broadcast readiness so extensions loaded after us can discover us
@@ -683,7 +685,7 @@ export default function (pi: ExtensionAPI) {
683
685
 
684
686
  const agentToolDescription = buildAgentToolDescription({
685
687
  mode: getToolDescriptionMode(),
686
- extensionDepth,
688
+ nextSubagentDepth,
687
689
  schedulingEnabled: isSchedulingEnabled(),
688
690
  });
689
691
 
@@ -1016,6 +1018,7 @@ export default function (pi: ExtensionAPI) {
1016
1018
  invocation: agentInvocation,
1017
1019
  depth: nextSubagentDepth,
1018
1020
  parentAgentId: extensionAgentId,
1021
+ eventBus: pi.events,
1019
1022
  outputFileForAgent: (agentId) => createOutputFilePath(ctx.cwd, agentId, ctx.sessionManager.getSessionId()),
1020
1023
  onOutputFileCreated: (outputFile, agentId) => writeInitialEntry(outputFile, agentId, P.prompt!, ctx.cwd),
1021
1024
  ...bgCallbacks,
@@ -1826,6 +1829,9 @@ Write the file using the write tool. Only write the file, nothing else.`;
1826
1829
  const record = await manager.spawnAndWait(pi, ctx, "general-purpose", generatePrompt, {
1827
1830
  description: `Generate ${name} agent`,
1828
1831
  maxTurns: 5,
1832
+ eventBus: pi.events,
1833
+ depth: nextSubagentDepth,
1834
+ parentAgentId: extensionAgentId,
1829
1835
  });
1830
1836
 
1831
1837
  if (record.status === "error") {
package/src/schedule.ts CHANGED
@@ -45,6 +45,13 @@ export interface NewJobInput {
45
45
  isolation?: IsolationMode;
46
46
  }
47
47
 
48
+ interface SchedulerSpawnDefaults {
49
+ /** Recursive depth for scheduled subagents fired from this session. */
50
+ depth?: number;
51
+ /** Parent subagent id for scheduled subagents fired from this session. */
52
+ parentAgentId?: string;
53
+ }
54
+
48
55
  export class SubagentScheduler {
49
56
  private jobs = new Map<string, Cron>();
50
57
  private intervals = new Map<string, NodeJS.Timeout>();
@@ -52,13 +59,21 @@ export class SubagentScheduler {
52
59
  private pi: ExtensionAPI | undefined;
53
60
  private ctx: ExtensionContext | undefined;
54
61
  private manager: AgentManager | undefined;
62
+ private spawnDefaults: SchedulerSpawnDefaults = {};
55
63
 
56
64
  /** Start the scheduler: bind to a session's store and arm enabled jobs. */
57
- start(pi: ExtensionAPI, ctx: ExtensionContext, manager: AgentManager, store: ScheduleStore): void {
65
+ start(
66
+ pi: ExtensionAPI,
67
+ ctx: ExtensionContext,
68
+ manager: AgentManager,
69
+ store: ScheduleStore,
70
+ spawnDefaults: SchedulerSpawnDefaults = {},
71
+ ): void {
58
72
  this.pi = pi;
59
73
  this.ctx = ctx;
60
74
  this.manager = manager;
61
75
  this.store = store;
76
+ this.spawnDefaults = spawnDefaults;
62
77
 
63
78
  for (const job of store.list()) {
64
79
  if (job.enabled) this.scheduleJob(job);
@@ -75,6 +90,7 @@ export class SubagentScheduler {
75
90
  this.pi = undefined;
76
91
  this.ctx = undefined;
77
92
  this.manager = undefined;
93
+ this.spawnDefaults = {};
78
94
  }
79
95
 
80
96
  /** True if start() has bound a store and the scheduler is active. */
@@ -247,6 +263,9 @@ export class SubagentScheduler {
247
263
  isolated: job.isolated,
248
264
  thinkingLevel: job.thinking,
249
265
  isolation: job.isolation,
266
+ eventBus: pi.events,
267
+ depth: this.spawnDefaults.depth,
268
+ parentAgentId: this.spawnDefaults.parentAgentId,
250
269
  });
251
270
  } catch (err) {
252
271
  const error = err instanceof Error ? err.message : String(err);
package/bugs.txt DELETED
@@ -1,57 +0,0 @@
1
- # pi-subagents bugs
2
-
3
- ## 2026-06-24: subagent-spawned reviewer c2c alias registrations route to coordinator
4
-
5
- Observed in `/home/xertrov/src/autoplanet-harness` session while coordinating nested pi Agent reviewers.
6
-
7
- Symptom:
8
- - Coordinator received transcript notifications like `Subagent <id> registered as pi-...` for reviewer subagents spawned by an implementer subagent.
9
- - The spawning implementer subagent did **not** receive those c2c alias registration notifications.
10
- - This made the coordinator see aliases for subagents it did not directly start, while the parent subagent could not map its Agent handles to c2c aliases.
11
-
12
- Concrete example:
13
- - Coordinator directly started T244/G9 implementer: agent `81a4688f-82ce-40e`, alias `pi-8391d3-ae5887a`.
14
- - T244 spawned reviewer/rereviewer Agent subagents with IDs:
15
- - `436193fa-ef20-4d0`
16
- - `97bddbcf-53a8-48c`
17
- - `56a58df4-c91c-48a`
18
- - `82ae1d14-707d-415`
19
- - Coordinator received registrations for aliases including `pi-8391d3-a723e9a`, `pi-8391d3-a0a8a00`, `pi-8391d3-ab7ed6f`, `pi-8391d3-ab167dc`.
20
- - T244 reported: its transcript only showed Agent result handles/output paths/completion summaries; it did not see `Subagent <id> registered as pi-...` messages and could not map reviewer IDs to c2c aliases.
21
-
22
- Expected behavior:
23
- - Alias registration notifications for a subagent spawned by another subagent should be delivered to the spawning subagent's transcript, or at least to both the spawning subagent and coordinator with parent/owner metadata.
24
- - The notification should include parent/spawner identity so coordinators can distinguish direct children from nested reviewer subagents.
25
-
26
- Impact:
27
- - Coordinator receives noisy/ambiguous registrations for unknown aliases.
28
- - Spawning subagent cannot c2c-message its own reviewers by alias or report alias ownership accurately.
29
- - Requires extra coordinator debugging messages to identify ownership.
30
-
31
- Suggested fix:
32
- - Route `Subagent <id> registered as <alias>` to the agent that invoked the Agent tool.
33
- - Add fields like `parent_agent_id`, `parent_alias`, and maybe `root_coordinator_alias` to registration notifications.
34
- - If root coordinator must also receive nested registrations, label them explicitly as nested.
35
-
36
- Additional observation after filing:
37
- - Coordinator later received another nested-looking registration: `Subagent 8262e0d6-457e-49c registered as pi-8391d3-ab41e8a`, again without the coordinator directly starting that subagent.
38
- - Confirmed with T245/G6 implementer `pi-8391d3-a861bf0`: it spawned reviewer Agent IDs `8262e0d6-457e-49c` and `14d29ca7-348e-437`, while coordinator received alias registrations `pi-8391d3-ab41e8a` and `pi-8391d3-aa90cd5`; T245 reported it had not received any c2c alias registration notifications for them.
39
- - After crash recovery restart, coordinator received another nested-looking registration: `Subagent 4f396572-cab5-42c registered as pi-8391d3-a52ab8a`; likely spawned by a recovery worker/reviewer rather than directly by coordinator.
40
- - Coordinator also received `Subagent 09dc9e7f-f960-411 registered as pi-8391d3-a219275` during recovery worker activity, again likely nested/reviewer notification routed to coordinator.
41
- - Coordinator also received `Subagent ee594feb-8fd1-458 registered as pi-8391d3-a8f6d40` while nested reviewer activity was ongoing; likely another nested alias notification routed to coordinator.
42
- - Coordinator also received `Subagent dd29fe5b-357a-48f registered as pi-8391d3-afd6cfc` during T246 nested rereview activity; likely another nested alias notification routed to coordinator.
43
- - Coordinator received nested-looking registrations `Subagent 9aa66b38-3ba8-405 registered as pi-8391d3-a6cefa4` and `Subagent d33bb514-be85-4a5 registered as pi-8391d3-ae99126` during active worker review activity.
44
- - Coordinator received nested-looking registration `Subagent ab7cb925-d76c-489 registered as pi-8391d3-a8c6afd` during active worker review activity.
45
- - Coordinator received nested-looking registration `Subagent ba23eaaf-a706-459 registered as pi-8391d3-af5d393` during active worker review activity.
46
- - Coordinator received nested-looking registration `Subagent adf52e5e-862e-4c6 registered as pi-8391d3-aa897ea` during active worker review activity.
47
- - Coordinator received nested-looking registration `Subagent e6eb1662-8223-458 registered as pi-8391d3-adb2231` during active worker review activity.
48
- - Coordinator received nested-looking registration `Subagent 565012aa-43b6-407 registered as pi-8391d3-a66be3e` during active worker review activity.
49
- - Coordinator received nested-looking registration `Subagent 603b211a-9daa-421 registered as pi-8391d3-aa92c43` during active worker review activity.
50
- - Coordinator received nested-looking registration `Subagent 734f3fa3-958b-4bd registered as pi-8391d3-a028432` during active T248 review activity.
51
- - Coordinator received nested-looking registration `Subagent 2d565584-fcbc-496 registered as pi-8391d3-ac6e43c` during active T250/T251/T252 review activity.
52
- - Coordinator received nested-looking registration `Subagent 5e7ee0d1-5112-4b9 registered as pi-8391d3-a7722c2` during active T250/T251/T252 review activity.
53
- - Coordinator received nested-looking registration `Subagent eae7393b-dfb4-445 registered as pi-8391d3-a2a9d62` during active T250/T251/T252 review activity.
54
- - Coordinator received nested-looking registration `Subagent 6629a0fe-daef-401 registered as pi-8391d3-ad0b4e5` during active T250 review activity.
55
- - Coordinator received nested-looking registration `Subagent 49eb5d11-178d-4d2 registered as pi-8391d3-a979978` during active T250 review activity.
56
- - Coordinator received nested-looking registration `Subagent bf846951-6b2f-4b5 registered as pi-8391d3-aabc2c5` during active T250 rereview activity.
57
- - Coordinator received nested-looking registration `Subagent 3250e286-4fd4-4a2 registered as pi-8391d3-a78b404` during active T250 rereview activity.