pi-subagents 0.66.0 → 0.68.0

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.
Files changed (172) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +5 -4
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/reviewer.md +3 -2
  5. package/docs/agents.md +43 -15
  6. package/docs/configuration.md +67 -23
  7. package/docs/extension-api.md +38 -19
  8. package/docs/missions.md +10 -2
  9. package/docs/models.md +11 -79
  10. package/docs/observability.md +22 -12
  11. package/docs/standalone-background.md +59 -0
  12. package/docs/tool-reference.md +33 -20
  13. package/docs/watchdog.md +39 -10
  14. package/docs/workflows.md +37 -13
  15. package/index.ts +5 -2
  16. package/inspector-runner.mjs +2 -2
  17. package/package.json +4 -2
  18. package/prompts/parallel-review.md +1 -1
  19. package/runner-peer-loader.mjs +24 -0
  20. package/runner-peer-preload.mjs +32 -0
  21. package/skills/pi-subagents/SKILL.md +32 -21
  22. package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
  23. package/skills/pi-subagents/references/execution-controls.md +11 -9
  24. package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
  25. package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  26. package/skills/pi-subagents/references/prompting-and-roles.md +17 -13
  27. package/skills/pi-subagents/references/review-and-validation.md +3 -3
  28. package/src/agents/advertised-agent-prompt.ts +34 -3
  29. package/src/agents/agent-management.ts +57 -58
  30. package/src/agents/agent-serializer.ts +4 -3
  31. package/src/agents/agents.ts +190 -71
  32. package/src/agents/builtin-names.ts +1 -0
  33. package/src/agents/chain-serializer.ts +5 -0
  34. package/src/agents/runtime-agent-registry.ts +7 -6
  35. package/src/api/delegation.ts +4 -0
  36. package/src/api/preflight.ts +94 -59
  37. package/src/api/required-child-extensions.ts +6 -0
  38. package/src/api/shared-types.ts +2 -0
  39. package/src/extension/config.ts +10 -37
  40. package/src/extension/fanout-child.ts +66 -4
  41. package/src/extension/herdr-pi-bridge.ts +160 -0
  42. package/src/extension/index.ts +62 -39
  43. package/src/extension/public-execution.ts +7 -5
  44. package/src/extension/rpc.ts +4 -0
  45. package/src/extension/schemas.ts +81 -81
  46. package/src/extension/tool-description.ts +30 -82
  47. package/src/inspectors/actions.ts +148 -0
  48. package/src/inspectors/ghostty/actions.ts +74 -0
  49. package/src/inspectors/ghostty/plugin.ts +17 -0
  50. package/src/inspectors/herdr/actions.ts +99 -179
  51. package/src/inspectors/herdr/plugin.ts +20 -0
  52. package/src/inspectors/herdr/project-panes.ts +1 -1
  53. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  54. package/src/inspectors/plugins.ts +8 -0
  55. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  56. package/src/inspectors/types.ts +51 -0
  57. package/src/intercom/intercom-bridge.ts +50 -8
  58. package/src/intercom/native-supervisor-channel.ts +44 -31
  59. package/src/policy/authority.ts +4 -0
  60. package/src/profiles/profiles.ts +12 -6
  61. package/src/runs/background/active-async-capacity.ts +4 -0
  62. package/src/runs/background/active-run-index.ts +17 -1
  63. package/src/runs/background/async-execution.ts +348 -176
  64. package/src/runs/background/async-job-tracker.ts +8 -6
  65. package/src/runs/background/async-resume.ts +17 -12
  66. package/src/runs/background/async-status.ts +15 -4
  67. package/src/runs/background/auto-drain.ts +23 -10
  68. package/src/runs/background/binary-bootstrap.ts +38 -0
  69. package/src/runs/background/chain-append.ts +1 -1
  70. package/src/runs/background/chain-root-attachment.ts +14 -33
  71. package/src/runs/background/fleet-view.ts +30 -2
  72. package/src/runs/background/notify.ts +105 -7
  73. package/src/runs/background/owned-process-tree.ts +29 -2
  74. package/src/runs/background/result-files.ts +8 -4
  75. package/src/runs/background/result-watcher.ts +19 -2
  76. package/src/runs/background/run-child-session.ts +81 -33
  77. package/src/runs/background/run-status.ts +3 -0
  78. package/src/runs/background/runner-aliases.ts +12 -33
  79. package/src/runs/background/runner-child-launch.ts +6 -1
  80. package/src/runs/background/runner-child-sessions.ts +5 -4
  81. package/src/runs/background/runner-http-dispatcher.ts +119 -0
  82. package/src/runs/background/scheduled-runs.ts +51 -18
  83. package/src/runs/background/stale-run-reconciler.ts +35 -11
  84. package/src/runs/background/steering.ts +20 -2
  85. package/src/runs/background/subagent-runner.ts +441 -304
  86. package/src/runs/background/subagent-wait.ts +176 -28
  87. package/src/runs/background/wait-completions.ts +75 -27
  88. package/src/runs/background/wait-subscriptions.ts +9 -3
  89. package/src/runs/background/wait-tool.ts +5 -3
  90. package/src/runs/foreground/async-steering-action.ts +18 -7
  91. package/src/runs/foreground/async-stop-action.ts +93 -3
  92. package/src/runs/foreground/execution.ts +134 -248
  93. package/src/runs/foreground/foreground-history.ts +2 -1
  94. package/src/runs/foreground/prompt-audit.ts +3 -1
  95. package/src/runs/foreground/subagent-executor.ts +374 -178
  96. package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
  97. package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
  98. package/src/runs/shared/acceptance.ts +38 -11
  99. package/src/runs/shared/async-status-projection.ts +127 -33
  100. package/src/runs/shared/capability-ceiling.ts +2 -0
  101. package/src/runs/shared/child-hooks.ts +25 -10
  102. package/src/runs/shared/child-launch-plan.ts +15 -3
  103. package/src/runs/shared/child-launch.ts +28 -5
  104. package/src/runs/shared/child-lifecycle.ts +6 -3
  105. package/src/runs/shared/child-runtime-config.ts +8 -1
  106. package/src/runs/shared/child-session.ts +127 -52
  107. package/src/runs/shared/child-tool-plan.ts +142 -11
  108. package/src/runs/shared/completion-guard.ts +5 -3
  109. package/src/runs/shared/dynamic-fanout.ts +2 -2
  110. package/src/runs/shared/effective-system-prompt.ts +33 -0
  111. package/src/runs/shared/external-cli-contract.ts +11 -1
  112. package/src/runs/shared/external-cli-preflight.ts +6 -2
  113. package/src/runs/shared/external-cli-runner.ts +9 -7
  114. package/src/runs/shared/herdr-connection.ts +134 -0
  115. package/src/runs/shared/herdr-external-adapters.ts +169 -0
  116. package/src/runs/shared/herdr-machine.ts +279 -0
  117. package/src/runs/shared/herdr-pi-protocol.ts +59 -0
  118. package/src/runs/shared/herdr-placed-run.ts +263 -0
  119. package/src/runs/shared/llm-intent-arbiter.ts +12 -3
  120. package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
  121. package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -235
  122. package/src/runs/shared/model-scope.ts +1 -1
  123. package/src/runs/shared/nested-events.ts +11 -2
  124. package/src/runs/shared/orca-progress-tabs.ts +1 -1
  125. package/src/runs/shared/parallel-utils.ts +7 -2
  126. package/src/runs/shared/pi-spawn.ts +10 -0
  127. package/src/runs/shared/subagent-prompt-runtime.ts +12 -4
  128. package/src/runs/shared/task-intent.ts +46 -13
  129. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  130. package/src/runs/shared/worktree-setup-command.ts +27 -4
  131. package/src/runs/shared/worktree.ts +45 -15
  132. package/src/shared/child-cache-retention.ts +43 -0
  133. package/src/shared/fork-context.ts +15 -72
  134. package/src/shared/launch-contract.ts +68 -8
  135. package/src/shared/opencode-session-headers.ts +30 -0
  136. package/src/shared/pruned-fork.ts +1 -1
  137. package/src/shared/required-child-extensions.ts +81 -0
  138. package/src/shared/settings.ts +5 -2
  139. package/src/shared/shortcuts.ts +0 -4
  140. package/src/shared/types.ts +74 -30
  141. package/src/slash/delegation-adapters.ts +3 -1
  142. package/src/slash/delegation-request.ts +14 -0
  143. package/src/slash/slash-commands.ts +2 -7
  144. package/src/slash/subagents-admin.ts +24 -13
  145. package/src/tui/fleet-status.ts +164 -19
  146. package/src/tui/fleet.ts +16 -14
  147. package/src/tui/render.ts +168 -37
  148. package/src/watchdog/child-status.ts +28 -28
  149. package/src/watchdog/lsp-diagnostics.ts +1 -1
  150. package/src/watchdog/model-selection.ts +21 -1
  151. package/src/watchdog/permission-arbiter.ts +3 -1
  152. package/src/watchdog/register-child.ts +10 -2
  153. package/src/watchdog/register-main.ts +39 -35
  154. package/src/watchdog/render.ts +1 -1
  155. package/src/watchdog/review.ts +123 -74
  156. package/src/watchdog/rules.ts +1 -1
  157. package/src/watchdog/runtime.ts +100 -27
  158. package/src/watchdog/scope.ts +1 -1
  159. package/src/watchdog/settings.ts +3 -0
  160. package/src/watchdog/tool-actions.ts +13 -12
  161. package/src/watchdog/turn-delta.ts +23 -0
  162. package/src/watchdog/types.ts +5 -3
  163. package/src/watchdog/warning-format.ts +1 -1
  164. package/src/workflows/scripted-workflow.ts +279 -10
  165. package/src/workflows/workflow-checklist.ts +2 -2
  166. package/src/workflows/workflow-receipt.ts +21 -3
  167. package/src/workflows/workflow-resources.ts +13 -2
  168. package/runner-server-preload.mjs +0 -13
  169. package/src/runs/shared/model-exclusions.ts +0 -374
  170. package/src/runs/shared/readonly-model-continuation.ts +0 -69
  171. package/src/runs/shared/readonly-session-evidence.ts +0 -307
  172. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
@@ -9,11 +9,13 @@ import { resolveWaitToolConfig } from "../runs/background/wait-config.ts";
9
9
  import type { ChildRuntimeConfig } from "../runs/shared/child-runtime-config.ts";
10
10
  import { readNestedControlRequests, resolveInheritedNestedRoute, type NestedRoute, writeNestedControlResult } from "../runs/shared/nested-events.ts";
11
11
  import { deliverSubagentIntercomMessageEvent } from "../intercom/result-intercom.ts";
12
+ import { createNativeSupervisorChannel, NATIVE_SUPERVISOR_TOOL_NAME, resolveSupervisorChannelDir } from "../intercom/native-supervisor-channel.ts";
13
+ import { readStatus } from "../shared/utils.ts";
12
14
  import { resolveSubagentIntercomTarget } from "../intercom/intercom-bridge.ts";
13
15
  import { createSubagentParamsSchema } from "./schemas.ts";
14
16
  import { finalizeToolResult } from "./tool-result.ts";
15
17
  import { loadConfig, resolveAsyncByDefault } from "./config.ts";
16
- import { type Details, type SubagentState } from "../shared/types.ts";
18
+ import { SUBAGENT_ASYNC_STARTED_EVENT, type AsyncStartedEvent, type Details, type SubagentState } from "../shared/types.ts";
17
19
 
18
20
  function getSubagentSessionRoot(parentSessionFile: string | null): string {
19
21
  if (parentSessionFile) {
@@ -28,7 +30,7 @@ function expandTilde(p: string): string {
28
30
  return p.startsWith("~/") ? path.join(os.homedir(), p.slice(2)) : p;
29
31
  }
30
32
 
31
- function createChildSafeState(): SubagentState {
33
+ export function createChildSafeState(): SubagentState {
32
34
  return {
33
35
  baseCwd: "",
34
36
  currentSessionId: null,
@@ -141,7 +143,7 @@ function startNestedControlInboxListener(pi: ExtensionAPI, state: SubagentState,
141
143
  return () => clearInterval(timer);
142
144
  }
143
145
 
144
- /** Register the child-side `subagent` tool for fanout-authorized children. */
146
+ /** Register delegation and supervisor replies for fanout-authorized children. */
145
147
  export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI, childConfig: ChildRuntimeConfig): void {
146
148
  if (!childConfig.fanoutChild) return;
147
149
 
@@ -156,7 +158,40 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI, c
156
158
 
157
159
  const config = loadConfig();
158
160
  const waitToolConfig = resolveWaitToolConfig(config.waitTool);
159
- const state = createChildSafeState();
161
+ const state = childConfig.runtimeState ?? createChildSafeState();
162
+ const asyncChildren = new Map<string, { dir: string; agents: string[] }>();
163
+ const foregroundChannels = new Set<string>();
164
+ const supervisorChannel = createNativeSupervisorChannel(pi, state, {
165
+ getChannelDirs: () => {
166
+ const dirs = new Set<string>();
167
+ for (const run of state.foregroundControls.values()) {
168
+ for (const child of run.activeChildren?.values() ?? []) dirs.add(resolveSupervisorChannelDir(run.runId, child.agent, child.index));
169
+ }
170
+ for (const run of state.foregroundRuns?.values() ?? []) {
171
+ for (const child of run.children) {
172
+ if (child.status === "detached") dirs.add(resolveSupervisorChannelDir(run.runId, child.agent, child.index));
173
+ }
174
+ }
175
+ const retiringForeground = [...foregroundChannels].filter(dir => !dirs.has(dir));
176
+ for (const dir of dirs) foregroundChannels.add(dir);
177
+ for (const dir of retiringForeground) dirs.add(dir);
178
+ const retiringAsync: string[] = [];
179
+ for (const [id, child] of asyncChildren) {
180
+ const status = readStatus(child.dir);
181
+ if (status && status.state !== "queued" && status.state !== "running") retiringAsync.push(id);
182
+ for (const [index, agent] of child.agents.entries()) dirs.add(resolveSupervisorChannelDir(id, agent, index));
183
+ }
184
+ return {
185
+ dirs: [...dirs],
186
+ retire: () => {
187
+ for (const dir of retiringForeground) foregroundChannels.delete(dir);
188
+ for (const id of retiringAsync) asyncChildren.delete(id);
189
+ },
190
+ };
191
+ },
192
+ });
193
+ const hasPendingSupervisorRequest = supervisorChannel.hasPendingRequests;
194
+ childConfig.hasPendingSupervisorRequest = hasPendingSupervisorRequest;
160
195
  const executor = createSubagentExecutor({
161
196
  pi,
162
197
  state,
@@ -170,6 +205,8 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI, c
170
205
  discoverAgents,
171
206
  allowMutatingManagementActions: false,
172
207
  childRuntime: childConfig,
208
+ activateSupervisorTransport: supervisorChannel.activateTransport,
209
+ findPendingAsks: supervisorChannel.findPendingAsks,
173
210
  });
174
211
 
175
212
  const params = createSubagentParamsSchema();
@@ -188,6 +225,31 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI, c
188
225
  };
189
226
 
190
227
  pi.registerTool(tool);
228
+ let unsubscribeAsyncStarted: (() => void) | undefined;
229
+ pi.on("session_start", (_event, ctx) => {
230
+ supervisorChannel.registerTools();
231
+ // The host applies the explicit allowlist to dynamic registration too.
232
+ if (!pi.getAllTools().some(tool => tool.name === NATIVE_SUPERVISOR_TOOL_NAME)) return;
233
+ // Downward asks belong to this coordinator, not its parent or persisted session file.
234
+ state.supervisorOwnerSessionId = ctx.sessionManager.getSessionId() || null;
235
+ unsubscribeAsyncStarted = pi.events.on(SUBAGENT_ASYNC_STARTED_EVENT, (payload: unknown) => {
236
+ const info = payload as AsyncStartedEvent;
237
+ if (!info.id || !info.asyncDir || info.sessionId !== state.currentSessionId) return;
238
+ const agents = info.agents ?? (info.agent ? [info.agent] : []);
239
+ asyncChildren.set(info.id, { dir: info.asyncDir, agents });
240
+ supervisorChannel.activateTransport();
241
+ });
242
+ supervisorChannel.start();
243
+ supervisorChannel.activateTransport();
244
+ });
245
+ pi.on("session_shutdown", () => {
246
+ unsubscribeAsyncStarted?.();
247
+ asyncChildren.clear();
248
+ foregroundChannels.clear();
249
+ supervisorChannel.dispose();
250
+ if (childConfig.hasPendingSupervisorRequest === hasPendingSupervisorRequest) childConfig.hasPendingSupervisorRequest = undefined;
251
+ state.supervisorOwnerSessionId = null;
252
+ });
191
253
  const route = resolveNestedControlRoute(childConfig);
192
254
  if (!route) return;
193
255
  const listenerCleanupKey = "__piSubagentFanoutChildNestedControlInboxCleanups";
@@ -0,0 +1,160 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as net from "node:net";
4
+ import * as path from "node:path";
5
+ import { Type } from "typebox";
6
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
7
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
+ import packageJson from "../../package.json" with { type: "json" };
9
+ import { encodeHerdrPiFrame, HERDR_PI_MAX_FRAME_BYTES, HERDR_PI_PROTOCOL, HERDR_PI_RUN_ENV, HERDR_PI_RUNTIME_DIR_ENV, HerdrPiFrameDecoder, validateHerdrPiRunId, type HerdrPiFrame } from "../runs/shared/herdr-pi-protocol.ts";
10
+ import { discoverAgents } from "../agents/agents.ts";
11
+ import { buildSkillInjection, resolveSkills } from "../agents/skills.ts";
12
+ import { buildAgentMemoryInjection } from "../agents/agent-memory.ts";
13
+ import { appendAgentRefinementOverlay } from "../agents/agent-refinements.ts";
14
+ import { rewriteSubagentPrompt } from "../runs/shared/subagent-prompt-runtime.ts";
15
+ import { resolveExistingReadPaths } from "../shared/settings.ts";
16
+
17
+ interface PendingRequest { operation: "prompt" | "steer" | "follow-up" | "abort" | "supervisor-reply"; text?: string; supervisorId?: string }
18
+ interface BridgeContext {
19
+ isIdle(): boolean;
20
+ hasPendingMessages(): boolean;
21
+ getActiveTools?(): string[];
22
+ sessionManager?: { getSessionId?(): string; getSessionFile?(): string | undefined };
23
+ model?: { provider?: string; id?: string };
24
+ }
25
+
26
+ function gitEvidence(cwd: string): Record<string, unknown> | undefined {
27
+ const run = (args: string[]) => spawnSync("git", args, { cwd, encoding: "utf8", timeout: 5_000, maxBuffer: 128 * 1024 });
28
+ const inside = run(["rev-parse", "--is-inside-work-tree"]);
29
+ if (inside.status !== 0) return undefined;
30
+ const head = run(["rev-parse", "HEAD"]); const branch = run(["symbolic-ref", "--quiet", "--short", "HEAD"]); const dirty = run(["status", "--porcelain"]);
31
+ return { ...(head.status === 0 ? { head: head.stdout.trim() } : {}), ...(branch.status === 0 ? { branch: branch.stdout.trim() } : {}), dirty: Boolean(dirty.stdout.trim()) };
32
+ }
33
+
34
+ export function resolveRemoteHerdrResources(cwd: string, resources: { agent: string; skills?: string[]; toolCeiling?: string[]; reads?: string[] | false }, remoteDefaultTools: string[] = []): { agent: string; skills: string[]; tools: string[]; systemPrompt: string; inheritProjectContext: boolean; inheritGlobalContext: boolean; inheritSkills: boolean } {
35
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(resources.agent) || resources.skills?.some((name) => !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(name)) || resources.toolCeiling?.some((name) => !/^[A-Za-z0-9_-]{1,64}$/u.test(name)) || (resources.reads !== undefined && resources.reads !== false && (!Array.isArray(resources.reads) || resources.reads.some((name) => typeof name !== "string")))) throw new Error("Remote logical resource names are invalid.");
36
+ const discovered = discoverAgents(cwd, "both").agents.filter((agent) => agent.name === resources.agent || agent.aliases?.includes(resources.agent));
37
+ if (discovered.length !== 1) throw new Error(`Remote agent '${resources.agent}' was not found unambiguously in ${cwd}.`);
38
+ const agent = discovered[0]!; const names = resources.skills ?? agent.skills ?? [];
39
+ const skills = resolveSkills(names, cwd); if (skills.missing.length) throw new Error(`Remote Pi skills not found: ${skills.missing.join(", ")}. Configure them on the saved machine.`);
40
+ const skillPrompt = buildSkillInjection(skills.resolved);
41
+ const reads = resources.reads === undefined ? agent.defaultReads ?? false : resources.reads; const readPaths = Array.isArray(reads) ? resolveExistingReadPaths(reads, cwd) : [];
42
+ const systemPrompt = appendAgentRefinementOverlay(`${agent.systemPrompt}${buildAgentMemoryInjection(agent, cwd)}${skillPrompt ? `\n\n${skillPrompt}` : ""}${readPaths.length ? `\n\n[Read from: ${readPaths.join(", ")}]` : ""}`, { cwd, agentName: agent.name });
43
+ const denied = new Set(agent.excludeTools ?? []); const tools = (agent.tools === undefined ? remoteDefaultTools : agent.tools).filter((tool) => !denied.has(tool) && (!resources.toolCeiling || resources.toolCeiling.includes(tool))); if (agent.tools === undefined && !tools.length) throw new Error(`Remote agent '${agent.name}' resolved no default active tools within the inherited ceiling.`);
44
+ return { agent: agent.name, skills: names, tools, systemPrompt, inheritProjectContext: agent.inheritProjectContext, inheritGlobalContext: agent.inheritGlobalContext, inheritSkills: agent.inheritSkills };
45
+ }
46
+
47
+ export default function registerHerdrPiBridge(pi: ExtensionAPI): void {
48
+ const runId = validateHerdrPiRunId(process.env[HERDR_PI_RUN_ENV]);
49
+ const runDir = process.env[HERDR_PI_RUNTIME_DIR_ENV];
50
+ if (!runDir || !path.isAbsolute(runDir) || fs.lstatSync(runDir).isSymbolicLink()) throw new Error("Herdr bridge runtime directory was not authoritatively provisioned.");
51
+ fs.chmodSync(runDir, 0o700);
52
+ const socketPath = path.join(runDir, "bridge.sock");
53
+ const manifestPath = path.join(runDir, "manifest.json");
54
+ for (const candidate of [socketPath, manifestPath]) {
55
+ try { const stat = fs.lstatSync(candidate); if (stat.isSymbolicLink()) throw new Error(`Refusing symlink at '${candidate}'.`); fs.rmSync(candidate, { force: true }); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
56
+ }
57
+ let client: net.Socket | undefined;
58
+ let context: BridgeContext | undefined;
59
+ let nativeSessionId: string | undefined;
60
+ let finalAssistant: AgentMessage | undefined;
61
+ let activeRequestId: string | undefined;
62
+ let resolvedSystemPrompt: string | undefined;
63
+ let resolvedContextPolicy: { agent: string; inheritProjectContext: boolean; inheritGlobalContext: boolean; inheritSkills: boolean } | undefined;
64
+ let configured = false;
65
+ const pending = new Map<string, PendingRequest>();
66
+ const supervisor = new Map<string, { reason: "need_decision" | "interview_request" | "progress_update"; resolve(answer: string): void }>();
67
+ let evidenceCursor = 0; let journalBytes = 0; const journal: HerdrPiFrame[] = [];
68
+ const send = (frame: { type: string; [key: string]: unknown }) => {
69
+ const record = { protocol: HERDR_PI_PROTOCOL, runId, ...frame, cursor: ++evidenceCursor } as HerdrPiFrame; const size = Buffer.byteLength(JSON.stringify(record)); journal.push(record); journalBytes += size; while (journal.length > 512 || journalBytes > 8 * 1024 * 1024) { const removed = journal.shift(); if (removed) journalBytes -= Buffer.byteLength(JSON.stringify(removed)); }
70
+ if (!client || client.destroyed) return;
71
+ client.write(encodeHerdrPiFrame(record));
72
+ };
73
+ const sessionId = (ctx: BridgeContext) => ctx.sessionManager?.getSessionId?.();
74
+ const activeTools = (ctx: BridgeContext) => ctx.getActiveTools?.() ?? [];
75
+
76
+ pi.registerTool({
77
+ name: "contact_supervisor",
78
+ label: "Contact supervisor",
79
+ description: "Contact the parent/supervisor session for a blocking decision, structured interview, or progress update.",
80
+ parameters: Type.Object({ reason: Type.Union([Type.Literal("need_decision"), Type.Literal("interview_request"), Type.Literal("progress_update")]), message: Type.Optional(Type.String({ maxLength: 65_536 })), interview: Type.Optional(Type.Unknown()) }, { additionalProperties: false }),
81
+ execute: async (_toolCallId, input: { reason: "need_decision" | "interview_request" | "progress_update"; message?: string; interview?: unknown }) => {
82
+ if (input.reason !== "interview_request" && !input.message?.trim()) throw new Error("message is required for supervisor decisions and progress updates.");
83
+ const id = `sup_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
84
+ send({ type: "supervisor-request", requestId: id, reason: input.reason, message: input.message?.slice(0, 65_536) ?? "", ...(input.interview !== undefined ? { interview: input.interview } : {}), expectsReply: input.reason !== "progress_update", nativeSessionId });
85
+ if (input.reason === "progress_update") { await new Promise<string>((resolve) => supervisor.set(id, { reason: input.reason, resolve })); return { content: [{ type: "text", text: "Supervisor progress update queued." }], details: { delivered: true, requestId: id, reason: input.reason } }; }
86
+ const reason = input.reason as "need_decision" | "interview_request"; const answer = await new Promise<string>((resolve) => supervisor.set(id, { reason, resolve })); const details: Record<string, unknown> = { requestId: id, reason }; if (reason === "interview_request") try { details.structuredReply = JSON.parse(answer.replace(/^```(?:json)?\s*|\s*```$/giu, "")); } catch { details.structuredReplyParseError = "Supervisor interview reply was not valid JSON."; }
87
+ return { content: [{ type: "text", text: `**Reply from supervisor:**\n${answer}` }], details };
88
+ },
89
+ });
90
+
91
+ const execute = async (requestId: string, ctx: BridgeContext) => {
92
+ const request = pending.get(requestId);
93
+ if (!request) throw new Error(`Unknown or already consumed bridge request '${requestId}'.`);
94
+ pending.delete(requestId);
95
+ if (sessionId(ctx) !== nativeSessionId) throw new Error("Native Pi session identity changed.");
96
+ send({ type: "accepted", requestId, operation: request.operation, nativeSessionId });
97
+ const api = pi as unknown as { sendUserMessage(text: string, options?: { deliverAs?: "steer" | "followUp" }): void; abort(): void };
98
+ if (request.operation === "prompt") { activeRequestId = requestId; finalAssistant = undefined; api.sendUserMessage(request.text ?? ""); }
99
+ else if (request.operation === "steer") api.sendUserMessage(request.text ?? "", { deliverAs: "steer" });
100
+ else if (request.operation === "follow-up") api.sendUserMessage(request.text ?? "", { deliverAs: "followUp" });
101
+ else if (request.operation === "abort") api.abort();
102
+ else {
103
+ const entry = request.supervisorId ? supervisor.get(request.supervisorId) : undefined;
104
+ if (!entry) throw new Error("Unknown supervisor request.");
105
+ supervisor.delete(request.supervisorId!); entry.resolve(request.text ?? "");
106
+ }
107
+ send({ type: "control-ack", requestId, operation: request.operation, nativeSessionId });
108
+ };
109
+ pi.registerCommand("pi-subagents-bridge", { description: "Internal pane-native request dispatch", handler: (args, ctx) => execute(args.trim(), ctx as unknown as BridgeContext) });
110
+
111
+ const on = pi.on as unknown as (name: string, handler: (event: Record<string, unknown>, ctx: BridgeContext) => unknown) => void;
112
+ on("before_agent_start", (event) => resolvedSystemPrompt && resolvedContextPolicy ? { systemPrompt: `${rewriteSubagentPrompt(typeof event.systemPrompt === "string" ? event.systemPrompt : "", { inheritProjectContext: resolvedContextPolicy.inheritProjectContext, inheritGlobalContext: resolvedContextPolicy.inheritGlobalContext, inheritSkills: resolvedContextPolicy.inheritSkills })}\n\n<active_agent name=${JSON.stringify(resolvedContextPolicy.agent)}/>\n\n${resolvedSystemPrompt}` } : undefined);
113
+ on("session_start", (_event, ctx) => {
114
+ context = ctx; nativeSessionId = sessionId(ctx);
115
+ if (!nativeSessionId) throw new Error("Pane-native Pi bridge requires a persisted native session identity.");
116
+ fs.writeFileSync(manifestPath, JSON.stringify({ protocol: HERDR_PI_PROTOCOL, packageVersion: packageJson.version, runId, socketPath, nativeSessionId, pid: process.pid }), { mode: 0o600, flag: "wx" });
117
+ send({ type: "ready", packageVersion: packageJson.version, nativeSessionId, sessionFile: ctx.sessionManager?.getSessionFile?.(), cwd: process.cwd(), tools: activeTools(ctx), model: ctx.model?.provider && ctx.model.id ? `${ctx.model.provider}/${ctx.model.id}` : undefined, configured, activeRequestId, evidenceCursor, evidenceFloor: journal[0]?.cursor ?? evidenceCursor + 1, initialGit: gitEvidence(process.cwd()) });
118
+ });
119
+ on("message_end", (event) => {
120
+ const message = event.message as AgentMessage | undefined;
121
+ if (message?.role === "assistant") finalAssistant = message;
122
+ send({ type: "event", requestId: activeRequestId, event: { ...event, message }, nativeSessionId });
123
+ });
124
+ on("agent_settled", (_event, ctx) => { const requestId = activeRequestId; send({ type: "settled", requestId, nativeSessionId, idle: ctx.isIdle(), pending: ctx.hasPendingMessages(), finalAssistant, finalGit: gitEvidence(process.cwd()) }); if (ctx.isIdle() && !ctx.hasPendingMessages()) activeRequestId = undefined; });
125
+ on("session_shutdown", () => { server.close(); client?.destroy(); try { fs.rmSync(runDir, { recursive: true, force: true }); } catch {} });
126
+
127
+ const server = net.createServer((socket) => {
128
+ if (client && !client.destroyed) { socket.destroy(); return; }
129
+ client = socket; const decoder = new HerdrPiFrameDecoder(); let total = 0;
130
+ for (const record of journal) socket.write(encodeHerdrPiFrame(record));
131
+ socket.on("data", (chunk: Buffer) => {
132
+ total += chunk.byteLength; if (total > HERDR_PI_MAX_FRAME_BYTES * 32) { socket.destroy(new Error("Bridge byte budget exceeded.")); return; }
133
+ try {
134
+ for (const frame of decoder.push(chunk)) {
135
+ if (frame.runId !== runId) throw new Error("Bridge run identity mismatch.");
136
+ if (frame.type === "supervisor-delivered" && typeof frame.requestId === "string") { const entry = supervisor.get(frame.requestId); if (!entry || entry.reason !== "progress_update") throw new Error("Unknown supervisor delivery acknowledgement."); supervisor.delete(frame.requestId); entry.resolve("delivered"); continue; }
137
+ if (frame.type === "configure") {
138
+ if (configured || typeof frame.requestId !== "string" || !frame.resources || typeof frame.resources !== "object") throw new Error("Invalid or duplicate bridge configuration.");
139
+ const resources = frame.resources as { agent?: unknown; skills?: unknown; toolCeiling?: unknown; reads?: unknown };
140
+ const invalidSkills = resources.skills !== undefined && (!Array.isArray(resources.skills) || resources.skills.some((name) => typeof name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(name)));
141
+ const invalidReads = resources.reads !== undefined && resources.reads !== false && (!Array.isArray(resources.reads) || resources.reads.some((name) => typeof name !== "string"));
142
+ if (typeof resources.agent !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(resources.agent) || invalidSkills || invalidReads) throw new Error("Remote logical resource names are invalid.");
143
+ const resolved = resolveRemoteHerdrResources(process.cwd(), { agent: resources.agent, ...(resources.skills ? { skills: resources.skills as string[] } : {}), ...(resources.toolCeiling ? { toolCeiling: resources.toolCeiling as string[] } : {}), ...(resources.reads !== undefined ? { reads: resources.reads as string[] | false } : {}) }, context ? activeTools(context) : []); resolvedSystemPrompt = resolved.systemPrompt; resolvedContextPolicy = resolved;
144
+ (pi as unknown as { setActiveTools?(tools: string[]): void }).setActiveTools?.(resolved.tools); const acknowledged = context ? activeTools(context) : []; if (acknowledged.some((tool) => !resolved.tools.includes(tool)) || resolved.tools.some((tool) => !acknowledged.includes(tool))) throw new Error("Remote Pi did not apply the resolved active-tool set exactly.");
145
+ configured = true; send({ type: "configured", requestId: frame.requestId, nativeSessionId, agent: resolved.agent, skills: resolved.skills, model: context?.model?.provider && context.model.id ? `${context.model.provider}/${context.model.id}` : undefined, tools: acknowledged }); continue;
146
+ }
147
+ if (frame.type !== "prepare" || typeof frame.requestId !== "string" || pending.has(frame.requestId) || !configured) throw new Error("Invalid, duplicate, or unconfigured bridge request.");
148
+ const operation = frame.operation;
149
+ if (!(["prompt", "steer", "follow-up", "abort", "supervisor-reply"] as unknown[]).includes(operation)) throw new Error("Unsupported bridge operation.");
150
+ const text = frame.text; if (text !== undefined && (typeof text !== "string" || Buffer.byteLength(text) > 256 * 1024)) throw new Error("Bridge request text is invalid or too large.");
151
+ pending.set(frame.requestId, { operation: operation as PendingRequest["operation"], ...(text !== undefined ? { text } : {}), ...(typeof frame.supervisorId === "string" ? { supervisorId: frame.supervisorId } : {}) });
152
+ send({ type: "prepared", requestId: frame.requestId, nativeSessionId });
153
+ }
154
+ } catch (error) { socket.destroy(error as Error); }
155
+ });
156
+ socket.on("close", () => { if (client === socket) client = undefined; });
157
+ if (context && nativeSessionId) send({ type: "ready", packageVersion: packageJson.version, nativeSessionId, sessionFile: context.sessionManager?.getSessionFile?.(), cwd: process.cwd(), tools: activeTools(context), model: context.model?.provider && context.model.id ? `${context.model.provider}/${context.model.id}` : undefined, configured, activeRequestId, evidenceCursor, evidenceFloor: journal[0]?.cursor ?? evidenceCursor + 1, initialGit: gitEvidence(process.cwd()) });
158
+ });
159
+ server.listen(socketPath, () => { fs.chmodSync(socketPath, 0o600); });
160
+ }
@@ -30,8 +30,9 @@ import { getAgentDir } from "../shared/utils.ts";
30
30
  import { isStaleExtensionContextError, withCachedUiContext } from "../shared/extension-context.ts";
31
31
  import { currentCompletionOwnerId } from "../shared/completion-owner.ts";
32
32
  import { cleanupOldChainDirs } from "../shared/settings.ts";
33
- import { clearLegacyResultAnimationTimer, renderSubagentResult, renderSubagentSummary } from "../tui/render.ts";
33
+ import { clearLegacyResultAnimationTimer, renderSubagentResult, renderSubagentSummary, setInlineWorkflowCoverage } from "../tui/render.ts";
34
34
  import { openSubagentFleet } from "../tui/fleet.ts";
35
+ import { createBuiltinInspectorPlugins } from "../inspectors/plugins.ts";
35
36
  import { SubagentFleetStatus, resolveFleetViewPlacement } from "../tui/fleet-status.ts";
36
37
  import { createSubagentParamsSchema } from "./schemas.ts";
37
38
  import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
@@ -70,7 +71,7 @@ import { SUBAGENT_CHILD_ENV, SUBAGENT_PARENT_SESSION_ENV } from "../runs/shared/
70
71
  import { disposeChildSessions } from "../runs/shared/child-session.ts";
71
72
  import { resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
72
73
  import { formatDuration, shortenPath } from "../shared/formatters.ts";
73
- import { applyModelExclusionsConfig, loadConfig, resolveAsyncByDefault, resolveScheduledStoreRoot } from "./config.ts";
74
+ import { loadConfig, resolveAsyncByDefault, resolveScheduledStoreRoot } from "./config.ts";
74
75
  import { buildSubagentToolDescription, buildSubagentToolPromptMetadata } from "./tool-description.ts";
75
76
  import { formatWorkflowPreflightSummary, normalizeWorkflowPreflight } from "../workflows/workflow-preflight.ts";
76
77
  import { finalizeToolResult } from "./tool-result.ts";
@@ -336,14 +337,18 @@ function createSlashResultComponent(
336
337
  theme: ExtensionContext["ui"]["theme"],
337
338
  rendererConfig?: MainWindowRendererConfig,
338
339
  foregroundDetachShortcut?: string,
340
+ getCurrentTheme: () => ExtensionContext["ui"]["theme"] = () => theme,
339
341
  ): Container {
340
342
  const container = new Container();
341
343
  let lastVersion = -1;
344
+ let lastTheme: ExtensionContext["ui"]["theme"] | undefined;
342
345
  container.render = (width: number): string[] => {
343
346
  const snapshot = getSlashRenderableSnapshot(details);
344
- if (snapshot.version !== lastVersion || isSlashResultRunning(snapshot.result)) {
347
+ const currentTheme = getCurrentTheme();
348
+ if (snapshot.version !== lastVersion || currentTheme !== lastTheme || isSlashResultRunning(snapshot.result)) {
345
349
  lastVersion = snapshot.version;
346
- rebuildSlashResultContainer(container, snapshot.result, options, theme, rendererConfig, foregroundDetachShortcut);
350
+ lastTheme = currentTheme;
351
+ rebuildSlashResultContainer(container, snapshot.result, options, currentTheme, rendererConfig, foregroundDetachShortcut);
347
352
  }
348
353
  return Container.prototype.render.call(container, width);
349
354
  };
@@ -426,8 +431,6 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
426
431
  cleanupOldChainDirs();
427
432
 
428
433
  const config = loadConfig();
429
- // Apply the process-wide exclusion TTL before any child launch can record a model failure.
430
- applyModelExclusionsConfig(config);
431
434
  const waitToolConfig = resolveWaitToolConfig(config.waitTool);
432
435
  const asyncByDefault = resolveAsyncByDefault(config);
433
436
  const fleetViewEnabled = config.fleetView !== false;
@@ -437,14 +440,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
437
440
  const tempArtifactsDir = getArtifactsDir(null);
438
441
  const artifactCleanupDays = config.artifactConfig?.cleanupDays ?? DEFAULT_ARTIFACT_CONFIG.cleanupDays;
439
442
  cleanupAllArtifactDirs(artifactCleanupDays);
440
- const resultIndexCleanupTimer = setTimeout(() => {
441
- try {
442
- cleanupResultIndexes(DIRS.results);
443
- } catch (error) {
444
- console.error("Failed to clean stale subagent result indexes:", error);
445
- }
446
- }, 30_000);
447
- resultIndexCleanupTimer.unref?.();
443
+ let resultIndexCleanupTimer: ReturnType<typeof setTimeout> | undefined;
448
444
 
449
445
  const state: SubagentState = {
450
446
  baseCwd: "",
@@ -503,7 +499,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
503
499
  const ctx = withLastUiContext((current) => current);
504
500
  if (!ctx) return;
505
501
  try {
506
- await openSubagentFleet(ctx, state, { initialKey: itemKey, asyncDirRoot: DIRS.async, resultsDir: DIRS.results, fleetKeybindings: config.fleetKeybindings });
502
+ await openSubagentFleet(ctx, state, { initialKey: itemKey, asyncDirRoot: DIRS.async, resultsDir: DIRS.results, fleetKeybindings: config.fleetKeybindings, inspectorPlugins: createBuiltinInspectorPlugins() });
507
503
  } catch (error) {
508
504
  if (isStaleExtensionContextError(error)) {
509
505
  if (state.lastUiContext === ctx) state.lastUiContext = null;
@@ -511,7 +507,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
511
507
  }
512
508
  throw error;
513
509
  }
514
- }, { placement: fleetViewPlacement })
510
+ }, { placement: fleetViewPlacement, onWorkflowCoverageChange: setInlineWorkflowCoverage })
515
511
  : undefined;
516
512
  let executorScheduled: ((id: string, params: SubagentParamsLike, signal: AbortSignal, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>) | undefined;
517
513
  let goalTurnId = 0;
@@ -590,23 +586,39 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
590
586
  const { startResultWatcher, transitionResultDelivery, primeExistingResults, stopResultWatcher } = resultWatcher;
591
587
  refreshResultDelivery = resultWatcher.refreshResultDelivery;
592
588
  const asyncRetentionAbort = new AbortController();
593
- const asyncRetentionTimer = setTimeout(async () => {
594
- try {
595
- await cleanupAsyncRetention({
596
- asyncDirRoot: DIRS.async,
597
- resultsDir: DIRS.results,
598
- signal: asyncRetentionAbort.signal,
599
- protectedRunIds: new Set([
600
- ...state.asyncJobs.keys(),
601
- ...(state.workflowControllers?.keys() ?? []),
602
- ...scheduledRunManager.referencedAsyncRunIds(),
603
- ]),
604
- });
605
- } catch (error) {
606
- console.error("Failed to clean retained async subagent state:", error);
589
+ let asyncRetentionTimer: ReturnType<typeof setTimeout> | undefined;
590
+ const startSessionMaintenance = () => {
591
+ if (!resultIndexCleanupTimer) {
592
+ resultIndexCleanupTimer = setTimeout(() => {
593
+ try {
594
+ cleanupResultIndexes(DIRS.results);
595
+ } catch (error) {
596
+ console.error("Failed to clean stale subagent result indexes:", error);
597
+ }
598
+ }, 30_000);
599
+ resultIndexCleanupTimer.unref?.();
600
+ }
601
+ waitSubscriptionManager.start();
602
+ if (!asyncRetentionTimer) {
603
+ asyncRetentionTimer = setTimeout(async () => {
604
+ try {
605
+ await cleanupAsyncRetention({
606
+ asyncDirRoot: DIRS.async,
607
+ resultsDir: DIRS.results,
608
+ signal: asyncRetentionAbort.signal,
609
+ protectedRunIds: new Set([
610
+ ...state.asyncJobs.keys(),
611
+ ...(state.workflowControllers?.keys() ?? []),
612
+ ...scheduledRunManager.referencedAsyncRunIds(),
613
+ ]),
614
+ });
615
+ } catch (error) {
616
+ console.error("Failed to clean retained async subagent state:", error);
617
+ }
618
+ }, ASYNC_RETENTION_DELAY_MS);
619
+ asyncRetentionTimer.unref?.();
607
620
  }
608
- }, ASYNC_RETENTION_DELAY_MS);
609
- asyncRetentionTimer.unref?.();
621
+ };
610
622
 
611
623
  const executorDeps: Parameters<typeof createSubagentExecutor>[0] = {
612
624
  pi,
@@ -648,7 +660,14 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
648
660
  pi.registerMessageRenderer<SlashMessageDetails>(SLASH_RESULT_TYPE, (message, options, theme) => {
649
661
  const details = resolveSlashMessageDetails(message.details);
650
662
  if (!details) return undefined;
651
- return createSlashResultComponent(details, options, theme, config.mainWindowRenderer, config.foregroundDetachShortcut);
663
+ return createSlashResultComponent(
664
+ details,
665
+ options,
666
+ theme,
667
+ config.mainWindowRenderer,
668
+ config.foregroundDetachShortcut,
669
+ () => state.lastUiContext?.ui.theme ?? theme,
670
+ );
652
671
  });
653
672
 
654
673
  pi.registerMessageRenderer<undefined>(SLASH_TEXT_RESULT_TYPE, (message, _options, _theme) => {
@@ -800,9 +819,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
800
819
  pi.registerTool(tool);
801
820
 
802
821
  pi.on("before_agent_start", (event, ctx) => {
803
- const selectedTools = event.systemPromptOptions.selectedTools ?? pi.getActiveTools();
822
+ const selectedTools = event.systemPromptOptions?.selectedTools ?? (typeof pi.getActiveTools === "function" ? pi.getActiveTools() : []);
804
823
  const sessionId = state.currentSessionId ?? resolveCurrentSessionId(ctx.sessionManager);
805
- const advertisedPrompt = selectedTools.includes("subagent")
824
+ const advertisedPrompt = Array.isArray(selectedTools) && selectedTools.includes("subagent")
806
825
  ? buildAdvertisedAgentPrompt(advertisedAgents, resolveCurrentSubagentCapabilityCeiling(sessionId))
807
826
  : undefined;
808
827
  const systemPrompt = appendAdvertisedAgentPrompt(event.systemPrompt, advertisedPrompt);
@@ -812,7 +831,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
812
831
  registerWaitTool(pi, state, waitToolConfig.enabled, waitSubscriptionManager, waitToolConfig.defaultTimeoutMs);
813
832
 
814
833
  pi.on("agent_end", async (_event, ctx) => {
815
- if (!ctx.hasUI) await drainOutstandingWork({ state, events: pi.events });
834
+ if (!ctx.hasUI) await drainOutstandingWork({ state, events: pi.events, hasPendingSupervisorRequest: supervisorChannel.hasPendingRequests });
816
835
  const ownerSessionId = state.currentSessionId;
817
836
  if (!ownerSessionId) return;
818
837
  goalTurnId += 1;
@@ -1029,8 +1048,10 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
1029
1048
  state.workflowControllers?.clear();
1030
1049
  state.workflowChildStops?.clear();
1031
1050
  clearRuntimeAgentsForPi(pi);
1032
- clearTimeout(resultIndexCleanupTimer);
1033
- clearTimeout(asyncRetentionTimer);
1051
+ if (resultIndexCleanupTimer) clearTimeout(resultIndexCleanupTimer);
1052
+ resultIndexCleanupTimer = undefined;
1053
+ if (asyncRetentionTimer) clearTimeout(asyncRetentionTimer);
1054
+ asyncRetentionTimer = undefined;
1034
1055
  asyncRetentionAbort.abort();
1035
1056
  stopResultWatcher();
1036
1057
  resultDeliveryOwnership.clear();
@@ -1124,7 +1145,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
1124
1145
  if (event.reason !== "manual") suspendWidgetsForCompaction();
1125
1146
  });
1126
1147
 
1127
- pi.on("session_compact", () => {
1148
+ pi.on("session_compact", (event) => {
1149
+ if (event.reason !== "manual") return;
1128
1150
  const hasActiveAsyncWork = [...state.asyncJobs.values()].some((job) => job.status === "queued" || job.status === "running");
1129
1151
  if (!hasActiveAsyncWork || !withLastUiContext(() => true)) return;
1130
1152
  pi.sendMessage(
@@ -1139,6 +1161,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
1139
1161
 
1140
1162
  pi.on("session_start", (event, ctx) => {
1141
1163
  installRuntime(ctx);
1164
+ startSessionMaintenance();
1142
1165
  const recovering = event.reason === "startup" || event.reason === "reload" || event.reason === "resume";
1143
1166
  resetSessionState(ctx, recovering, event.previousSessionFile);
1144
1167
  releaseHostSessionLiveness();
@@ -89,16 +89,18 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
89
89
  if (hasNamedWorkflow && (params.workflowScript !== undefined || params.workflowScriptPath !== undefined)) {
90
90
  return { ok: false, error: "workflow is mutually exclusive with workflowScript and workflowScriptPath.", mode: "workflow" };
91
91
  }
92
- if (!hasNamedWorkflow && params.args !== undefined) {
93
- return { ok: false, error: "args requires a named workflow resource.", mode: "workflow" };
94
- }
95
92
  const hasWorkflowInput = params.workflowScript !== undefined || params.workflowScriptPath !== undefined || hasNamedWorkflow;
93
+ if (!hasWorkflowInput && params.args !== undefined) {
94
+ return { ok: false, error: "args requires workflow, workflowScript, or workflowScriptPath.", mode: "workflow" };
95
+ }
96
96
  const hasCapacityOverride = params.globalConcurrencyLimit !== undefined || params.maxSubagentSpawnsPerRun !== undefined;
97
97
  if (hasCapacityOverride) {
98
98
  const capacityOverrideError = validateWorkflowCapacityOverrides(params);
99
99
  if (capacityOverrideError) return { ok: false, error: capacityOverrideError, mode: params.action === undefined ? "workflow" : "management" };
100
- if (params.action !== undefined || hasNamedWorkflow || (params.workflowScript === undefined && params.workflowScriptPath === undefined)) {
101
- return { ok: false, error: "Workflow capacity overrides are only supported on top-level workflowScript or workflowScriptPath calls.", mode: params.action === undefined ? "workflow" : "management" };
100
+ const validatesSpawnBudget = typeof params.action === "string" && params.action.trim() === "validate"
101
+ && params.globalConcurrencyLimit === undefined && params.maxSubagentSpawnsPerRun !== undefined;
102
+ if ((params.action !== undefined && !validatesSpawnBudget) || hasNamedWorkflow || (params.workflowScript === undefined && params.workflowScriptPath === undefined)) {
103
+ return { ok: false, error: "Workflow capacity overrides are only supported on top-level workflowScript or workflowScriptPath calls; validate accepts maxSubagentSpawnsPerRun for static budget checks.", mode: params.action === undefined ? "workflow" : "management" };
102
104
  }
103
105
  }
104
106
  if (params.preflight !== undefined && !hasWorkflowInput) {
@@ -499,9 +499,13 @@ function manageParams(params: unknown): SubagentParamsLike {
499
499
  if (requiresId && typeof input.id !== "string") {
500
500
  throw new SubagentRpcError("invalid_params", `RPC manage ${action} requires id.`);
501
501
  }
502
+ if (action === "schedule.run" && input.quiet !== undefined && typeof input.quiet !== "boolean") {
503
+ throw new SubagentRpcError("invalid_params", "RPC manage quiet must be a boolean.");
504
+ }
502
505
  const output: SubagentParamsLike = {
503
506
  action,
504
507
  ...(typeof input.id === "string" ? { id: input.id.trim() } : {}),
508
+ ...(action === "schedule.run" && input.quiet === true ? { quiet: true } : {}),
505
509
  };
506
510
  assertSubagentParams(output, "RPC manage params");
507
511
  return output;