pi-subagents 0.56.0 → 0.58.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 (108) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/agents/claude-code-writer.md +15 -0
  3. package/agents/claude-code.md +15 -0
  4. package/agents/codex-exec-writer.md +15 -0
  5. package/agents/codex-exec.md +15 -0
  6. package/agents/cursor-agent-writer.md +14 -0
  7. package/agents/cursor-agent.md +14 -0
  8. package/docs/agents.md +124 -21
  9. package/docs/configuration.md +37 -0
  10. package/docs/extension-api.md +41 -2
  11. package/docs/models.md +3 -3
  12. package/docs/observability.md +8 -7
  13. package/docs/tool-reference.md +14 -3
  14. package/docs/workflows.md +44 -0
  15. package/package.json +1 -1
  16. package/skills/pi-subagents/SKILL.md +2 -0
  17. package/skills/pi-subagents/references/execution-controls.md +2 -2
  18. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -0
  19. package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
  20. package/src/agents/agent-management.ts +36 -5
  21. package/src/agents/agent-refinements.ts +4 -4
  22. package/src/agents/agent-serializer.ts +5 -0
  23. package/src/agents/agents.ts +257 -51
  24. package/src/agents/builtin-names.ts +6 -0
  25. package/src/agents/runtime-agent-events.ts +70 -0
  26. package/src/agents/runtime-agent-registry.ts +18 -4
  27. package/src/api/agents.ts +10 -5
  28. package/src/api/preflight.ts +28 -3
  29. package/src/extension/config.ts +70 -0
  30. package/src/extension/doctor.ts +3 -3
  31. package/src/extension/index.ts +33 -8
  32. package/src/extension/public-execution.ts +29 -13
  33. package/src/extension/rpc.ts +55 -19
  34. package/src/extension/schemas.ts +6 -5
  35. package/src/extension/tool-description.ts +18 -12
  36. package/src/inspectors/herdr/actions.ts +2 -1
  37. package/src/inspectors/herdr/inspector-runner.ts +2 -10
  38. package/src/inspectors/herdr/session-roots-codec.ts +42 -0
  39. package/src/integrations/herdr-status.ts +51 -3
  40. package/src/runs/background/active-async-capacity.ts +77 -10
  41. package/src/runs/background/async-execution.ts +127 -19
  42. package/src/runs/background/async-job-tracker.ts +5 -0
  43. package/src/runs/background/async-resume.ts +6 -2
  44. package/src/runs/background/async-retention.ts +20 -3
  45. package/src/runs/background/async-status.ts +7 -0
  46. package/src/runs/background/chain-append.ts +2 -0
  47. package/src/runs/background/chain-root-attachment.ts +15 -1
  48. package/src/runs/background/fleet-view.ts +16 -10
  49. package/src/runs/background/inspect-rpc.ts +8 -8
  50. package/src/runs/background/notify.ts +26 -3
  51. package/src/runs/background/result-delivery-ownership.ts +45 -0
  52. package/src/runs/background/result-files.ts +27 -14
  53. package/src/runs/background/result-watcher.ts +36 -15
  54. package/src/runs/background/run-status.ts +33 -6
  55. package/src/runs/background/scheduled-runs.ts +7 -1
  56. package/src/runs/background/subagent-runner.ts +239 -56
  57. package/src/runs/background/wait-completions.ts +4 -0
  58. package/src/runs/foreground/execution.ts +92 -11
  59. package/src/runs/foreground/foreground-control.ts +6 -0
  60. package/src/runs/foreground/foreground-history.ts +22 -1
  61. package/src/runs/foreground/subagent-executor.ts +322 -102
  62. package/src/runs/foreground/workflow-detach-reconcile.ts +144 -18
  63. package/src/runs/shared/child-protocol.ts +21 -7
  64. package/src/runs/shared/claude-code-adapter.ts +129 -0
  65. package/src/runs/shared/codex-exec-adapter.ts +129 -0
  66. package/src/runs/shared/completion-guard.ts +4 -3
  67. package/src/runs/shared/cursor-agent-adapter.ts +114 -0
  68. package/src/runs/shared/dynamic-fanout.ts +3 -3
  69. package/src/runs/shared/external-cli-contract.ts +167 -0
  70. package/src/runs/shared/external-cli-preflight.ts +122 -0
  71. package/src/runs/shared/external-cli-runner.ts +348 -55
  72. package/src/runs/shared/fast-mode-extension.ts +5 -5
  73. package/src/runs/shared/launch-cwd.ts +16 -0
  74. package/src/runs/shared/long-running-guard.ts +2 -1
  75. package/src/runs/shared/mcp-config-sources.ts +386 -0
  76. package/src/runs/shared/mcp-direct-tool-allowlist.ts +155 -42
  77. package/src/runs/shared/model-exclusions.ts +69 -7
  78. package/src/runs/shared/model-fallback.ts +39 -5
  79. package/src/runs/shared/mutation-evidence.ts +7 -2
  80. package/src/runs/shared/nested-events.ts +3 -1
  81. package/src/runs/shared/nested-render.ts +2 -2
  82. package/src/runs/shared/parallel-utils.ts +8 -1
  83. package/src/runs/shared/pi-args.ts +61 -6
  84. package/src/runs/shared/process-signal.ts +13 -0
  85. package/src/runs/shared/run-history.ts +21 -1
  86. package/src/runs/shared/single-output.ts +17 -0
  87. package/src/runs/shared/subagent-prompt-runtime.ts +85 -9
  88. package/src/shared/fork-context.ts +21 -0
  89. package/src/shared/formatters.ts +13 -1
  90. package/src/shared/launch-contract.ts +4 -0
  91. package/src/shared/pruned-fork.ts +450 -0
  92. package/src/shared/session-file-trust.ts +19 -0
  93. package/src/shared/session-tokens.ts +14 -3
  94. package/src/shared/settings.ts +10 -2
  95. package/src/shared/shortcuts.ts +17 -0
  96. package/src/shared/types.ts +160 -10
  97. package/src/shared/utils.ts +6 -29
  98. package/src/shared/workflow-child-permit.ts +116 -0
  99. package/src/slash/delegation-adapters.ts +0 -1
  100. package/src/slash/slash-commands.ts +8 -6
  101. package/src/slash/subagents-admin.ts +3 -0
  102. package/src/tui/fleet-status.ts +27 -10
  103. package/src/tui/fleet-transcript.ts +11 -5
  104. package/src/tui/fleet.ts +28 -13
  105. package/src/tui/render.ts +55 -21
  106. package/src/workflows/scripted-workflow.ts +299 -31
  107. package/src/workflows/workflow-child-summary.ts +117 -0
  108. package/src/workflows/workflow-receipt.ts +155 -5
@@ -0,0 +1,70 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { registerRuntimeAgent, type RuntimeAgentDefinition, type RuntimeAgentRegistration } from "./runtime-agent-registry.ts";
3
+
4
+ export const RUNTIME_AGENT_REGISTER_EVENT = "pi-subagents:runtime-agent-register:v1";
5
+ export const RUNTIME_AGENT_REGISTER_VERSION = 1;
6
+
7
+ export type RuntimeAgentRegistrationResult =
8
+ | { ok: true; registration: RuntimeAgentRegistration }
9
+ | { ok: false; error: Error };
10
+
11
+ export interface RuntimeAgentRegistrationRequest {
12
+ version: 1;
13
+ name: string;
14
+ definition: RuntimeAgentDefinition;
15
+ result?: RuntimeAgentRegistrationResult;
16
+ }
17
+
18
+ export interface RegisterRuntimeAgentViaEventsInput {
19
+ pi: Pick<ExtensionAPI, "events">;
20
+ name: string;
21
+ definition: RuntimeAgentDefinition;
22
+ }
23
+
24
+ function errorFrom(value: unknown): Error {
25
+ return value instanceof Error ? value : new Error(String(value));
26
+ }
27
+
28
+ /** Register through the installed pi-subagents owner in this Pi process. */
29
+ export function registerAgentViaEvents(input: RegisterRuntimeAgentViaEventsInput): RuntimeAgentRegistration {
30
+ const request: RuntimeAgentRegistrationRequest = {
31
+ version: RUNTIME_AGENT_REGISTER_VERSION,
32
+ name: input.name,
33
+ definition: input.definition,
34
+ };
35
+ input.pi.events.emit(RUNTIME_AGENT_REGISTER_EVENT, request);
36
+ const result = request.result as unknown;
37
+ if (result === undefined) {
38
+ throw new Error("pi-subagents is not installed, not ready, or does not support runtime agent event registration.");
39
+ }
40
+ if (result && typeof result === "object" && !Array.isArray(result)) {
41
+ const candidate = result as Record<string, unknown>;
42
+ if (candidate.ok === true && candidate.registration && typeof candidate.registration === "object" && typeof (candidate.registration as { dispose?: unknown }).dispose === "function") {
43
+ return candidate.registration as RuntimeAgentRegistration;
44
+ }
45
+ if (candidate.ok === false && candidate.error instanceof Error) throw candidate.error;
46
+ }
47
+ throw new Error("pi-subagents returned a malformed runtime agent registration result.");
48
+ }
49
+
50
+ /** Install the process-local registration listener for the owning pi-subagents runtime. */
51
+ export function registerRuntimeAgentEventListener(pi: ExtensionAPI): () => void {
52
+ return pi.events.on(RUNTIME_AGENT_REGISTER_EVENT, (rawRequest) => {
53
+ if (!rawRequest || typeof rawRequest !== "object" || Array.isArray(rawRequest)) return;
54
+ const request = rawRequest as Record<string, unknown>;
55
+ if (request.result !== undefined) return;
56
+ try {
57
+ if (request.version !== RUNTIME_AGENT_REGISTER_VERSION) {
58
+ throw new Error(`Unsupported runtime agent registration event version '${String(request.version)}'.`);
59
+ }
60
+ const registration = registerRuntimeAgent({
61
+ pi,
62
+ name: request.name as string,
63
+ definition: request.definition as RuntimeAgentDefinition,
64
+ });
65
+ request.result = { ok: true, registration } satisfies RuntimeAgentRegistrationResult;
66
+ } catch (error) {
67
+ request.result = { ok: false, error: errorFrom(error) } satisfies RuntimeAgentRegistrationResult;
68
+ }
69
+ });
70
+ }
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { AcceptanceInput, AcceptanceRole, AgentRunnerConfig, OutputMode, ToolBudgetConfig, TurnBudgetConfig } from "../shared/types.ts";
3
+ import { CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL, isCodeOwnedExternalCliAdapterId, parseExternalCliCapabilityNarrowing, validateCodeOwnedProfileRunner } from "../runs/shared/external-cli-contract.ts";
3
4
  import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
4
5
  import { validatePermissionRules, type PermissionRules } from "../runs/shared/permissions.ts";
5
6
  import { validateToolBudgetConfig } from "../runs/shared/tool-budget.ts";
@@ -26,6 +27,7 @@ export interface RuntimeAgentDefinition {
26
27
  thinking?: string | false;
27
28
  systemPromptMode?: "append" | "replace";
28
29
  inheritProjectContext?: boolean;
30
+ inheritGlobalContext?: boolean;
29
31
  inheritSkills?: boolean;
30
32
  defaultContext?: AgentDefaultContext;
31
33
  defaultAsync?: boolean;
@@ -39,6 +41,7 @@ export interface RuntimeAgentDefinition {
39
41
  skillPath?: readonly string[];
40
42
  extensions?: readonly string[];
41
43
  subagentOnlyExtensions?: readonly string[];
44
+ mutationTools?: readonly string[];
42
45
  output?: string;
43
46
  outputMode?: OutputMode;
44
47
  defaultReads?: readonly string[];
@@ -169,12 +172,15 @@ function validateRunner(value: unknown): AgentRunnerConfig | undefined {
169
172
  if (runner.type !== "external-cli") throw new Error("Runtime agent definition runner.type must be 'pi', 'external-cli', or 'external-job'.");
170
173
  if (typeof runner.command !== "string" || !runner.command.trim()) throw new Error("Runtime agent definition external-cli runner requires a non-empty command string.");
171
174
  if (runner.args !== undefined && (!Array.isArray(runner.args) || runner.args.some((arg) => typeof arg !== "string"))) throw new Error("Runtime agent definition external-cli runner args must be an array of strings.");
175
+ if (runner.adapter !== undefined && !isCodeOwnedExternalCliAdapterId(runner.adapter)) throw new Error(`Runtime agent definition external-cli runner adapter must be ${CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL}.`);
176
+ if (runner.adapter !== undefined && Array.isArray(runner.args) && runner.args.length > 0) throw new Error(`Runtime agent definition ${runner.adapter} adapter owns its argv; runner args are not supported.`);
172
177
  if (runner.promptDelivery !== undefined && runner.promptDelivery !== "stdin") throw new Error("Runtime agent definition external-cli runner promptDelivery must be 'stdin'.");
173
- const supported = new Set(["type", "command", "args", "promptDelivery"]);
178
+ const capabilities = parseExternalCliCapabilityNarrowing(runner.capabilities, "Runtime agent definition external-cli runner capabilities");
179
+ const supported = new Set(["type", "adapter", "command", "args", "promptDelivery", "capabilities"]);
174
180
  const unknown = Object.keys(runner).filter((key) => !supported.has(key));
175
181
  if (unknown.length > 0) throw new Error(`Runtime agent definition external-cli runner has unsupported fields: ${unknown.join(", ")}.`);
176
182
  const args = runner.args as string[] | undefined;
177
- return { type: "external-cli", command: runner.command.trim(), ...(args?.length ? { args } : {}), ...(runner.promptDelivery ? { promptDelivery: "stdin" as const } : {}) };
183
+ return { type: "external-cli", ...(isCodeOwnedExternalCliAdapterId(runner.adapter) ? { adapter: runner.adapter } : {}), command: runner.command.trim(), ...(args?.length ? { args } : {}), ...(runner.promptDelivery ? { promptDelivery: "stdin" as const } : {}), ...(capabilities ? { capabilities } : {}) };
178
184
  }
179
185
 
180
186
  function validateTurnBudget(value: unknown): TurnBudgetConfig | undefined {
@@ -200,9 +206,9 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
200
206
  const definition = value as Record<string, unknown>;
201
207
  const supported = new Set([
202
208
  "description", "systemPrompt", "aliases", "tools", "mcpDirectTools", "model", "fallbackModels", "thinking",
203
- "systemPromptMode", "inheritProjectContext", "inheritSkills", "defaultContext", "defaultAsync", "defaultTimeoutMs",
209
+ "systemPromptMode", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "defaultContext", "defaultAsync", "defaultTimeoutMs",
204
210
  "defaultToolTimeoutMs", "defaultTurnBudget", "defaultAcceptance", "acceptanceRole", "runner", "skills", "skillPath",
205
- "extensions", "subagentOnlyExtensions", "output", "outputMode", "defaultReads", "defaultProgress", "interactive",
211
+ "extensions", "subagentOnlyExtensions", "mutationTools", "output", "outputMode", "defaultReads", "defaultProgress", "interactive",
206
212
  "maxSubagentDepth", "completionGuard", "toolBudget", "permissions",
207
213
  ]);
208
214
  const unknown = Object.keys(definition).filter((key) => !supported.has(key));
@@ -223,6 +229,7 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
223
229
  const model = validateOptionalString(definition.model, "Runtime agent definition model");
224
230
  const fallbackModels = validateStringList(definition.fallbackModels, "Runtime agent definition fallbackModels");
225
231
  const inheritProjectContext = validateBoolean(definition.inheritProjectContext, "Runtime agent definition inheritProjectContext");
232
+ const inheritGlobalContext = validateBoolean(definition.inheritGlobalContext, "Runtime agent definition inheritGlobalContext");
226
233
  const inheritSkills = validateBoolean(definition.inheritSkills, "Runtime agent definition inheritSkills");
227
234
  const defaultAsync = validateBoolean(definition.defaultAsync, "Runtime agent definition defaultAsync");
228
235
  const defaultTimeoutMs = validatePositiveInteger(definition.defaultTimeoutMs, "Runtime agent definition defaultTimeoutMs");
@@ -234,6 +241,7 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
234
241
  const skillPath = validateStringList(definition.skillPath, "Runtime agent definition skillPath");
235
242
  const extensions = validateStringList(definition.extensions, "Runtime agent definition extensions");
236
243
  const subagentOnlyExtensions = validateStringList(definition.subagentOnlyExtensions, "Runtime agent definition subagentOnlyExtensions");
244
+ const mutationTools = validateStringList(definition.mutationTools, "Runtime agent definition mutationTools");
237
245
  const output = validateOptionalString(definition.output, "Runtime agent definition output");
238
246
  const defaultReads = validateStringList(definition.defaultReads, "Runtime agent definition defaultReads");
239
247
  const defaultProgress = validateBoolean(definition.defaultProgress, "Runtime agent definition defaultProgress");
@@ -253,6 +261,7 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
253
261
  ...(thinking !== undefined ? { thinking: thinking as string | false } : {}),
254
262
  ...(systemPromptMode !== undefined ? { systemPromptMode: systemPromptMode as "append" | "replace" } : {}),
255
263
  ...(inheritProjectContext !== undefined ? { inheritProjectContext } : {}),
264
+ ...(inheritGlobalContext !== undefined ? { inheritGlobalContext } : {}),
256
265
  ...(inheritSkills !== undefined ? { inheritSkills } : {}),
257
266
  ...(defaultContext !== undefined ? { defaultContext: defaultContext as AgentDefaultContext } : {}),
258
267
  ...(defaultAsync !== undefined ? { defaultAsync } : {}),
@@ -266,6 +275,7 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
266
275
  ...(skillPath ? { skillPath } : {}),
267
276
  ...(extensions ? { extensions } : {}),
268
277
  ...(subagentOnlyExtensions ? { subagentOnlyExtensions } : {}),
278
+ ...(mutationTools ? { mutationTools } : {}),
269
279
  ...(output ? { output } : {}),
270
280
  ...(outputMode !== undefined ? { outputMode: outputMode as OutputMode } : {}),
271
281
  ...(defaultReads ? { defaultReads } : {}),
@@ -330,6 +340,7 @@ function toAgentConfig(name: string, definition: RuntimeAgentDefinition): AgentC
330
340
  ...(definition.thinking !== undefined ? { thinking: definition.thinking } : {}),
331
341
  systemPromptMode: definition.systemPromptMode ?? defaultSystemPromptMode(name),
332
342
  inheritProjectContext: definition.inheritProjectContext ?? defaultInheritProjectContext(name),
343
+ inheritGlobalContext: definition.inheritGlobalContext ?? false,
333
344
  inheritSkills: definition.inheritSkills ?? defaultInheritSkills(),
334
345
  ...(definition.defaultContext !== undefined ? { defaultContext: definition.defaultContext } : {}),
335
346
  ...(definition.defaultAsync !== undefined ? { defaultAsync: definition.defaultAsync } : {}),
@@ -345,6 +356,7 @@ function toAgentConfig(name: string, definition: RuntimeAgentDefinition): AgentC
345
356
  ...(definition.skillPath !== undefined ? { skillPath: [...definition.skillPath] } : {}),
346
357
  ...(definition.extensions !== undefined ? { extensions: [...definition.extensions] } : {}),
347
358
  ...(definition.subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions: [...definition.subagentOnlyExtensions] } : {}),
359
+ ...(definition.mutationTools !== undefined ? { mutationTools: [...definition.mutationTools] } : {}),
348
360
  ...(definition.output !== undefined ? { output: definition.output } : {}),
349
361
  ...(definition.outputMode !== undefined ? { outputMode: definition.outputMode } : {}),
350
362
  ...(definition.defaultReads !== undefined ? { defaultReads: [...definition.defaultReads] } : {}),
@@ -364,6 +376,8 @@ export function registerRuntimeAgent(input: RegisterRuntimeAgentInput): RuntimeA
364
376
  const name = validateString(input.name, "Runtime agent name", MAX_AGENT_NAME_LENGTH);
365
377
  const definition = validateDefinition(input.definition);
366
378
  const agent = toAgentConfig(name, definition);
379
+ const profileError = validateCodeOwnedProfileRunner(agent);
380
+ if (profileError) throw new Error(profileError);
367
381
  assertNoBuiltinCollision(agent);
368
382
  const current = registry();
369
383
  const records = current.byPi.get(pi) ?? [];
package/src/api/agents.ts CHANGED
@@ -1,7 +1,12 @@
1
- import { registerRuntimeAgent, type RegisterRuntimeAgentInput, type RuntimeAgentDefinition, type RuntimeAgentRegistration } from "../agents/runtime-agent-registry.ts";
1
+ import type { RegisterRuntimeAgentInput, RuntimeAgentDefinition, RuntimeAgentRegistration } from "../agents/runtime-agent-registry.ts";
2
+ export { registerRuntimeAgent as registerAgent } from "../agents/runtime-agent-registry.ts";
3
+ export {
4
+ RUNTIME_AGENT_REGISTER_EVENT,
5
+ RUNTIME_AGENT_REGISTER_VERSION,
6
+ registerAgentViaEvents,
7
+ type RegisterRuntimeAgentViaEventsInput,
8
+ type RuntimeAgentRegistrationRequest,
9
+ type RuntimeAgentRegistrationResult,
10
+ } from "../agents/runtime-agent-events.ts";
2
11
 
3
12
  export type { RegisterRuntimeAgentInput, RuntimeAgentDefinition, RuntimeAgentRegistration };
4
-
5
- export function registerAgent(input: RegisterRuntimeAgentInput): RuntimeAgentRegistration {
6
- return registerRuntimeAgent(input);
7
- }
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { discoverAgents, discoverAgentsAll, findBlockingAgentDiagnostic, resolveAgentName, type AgentConfig, type AgentScope, type AgentSource } from "../agents/agents.ts";
4
+ import { discoverAgents, discoverAgentsAll, findBlockingAgentDiagnostic, formatUnknownAgentError, resolveAgentName, unknownAgentDiagnosticContext, type AgentConfig, type AgentScope, type AgentSource } from "../agents/agents.ts";
5
5
  import { resolveExecutionAgentScope } from "../agents/agent-scope.ts";
6
6
  import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../agents/skills.ts";
7
7
  import { buildAgentMemoryInjection } from "../agents/agent-memory.ts";
@@ -42,8 +42,10 @@ export type SubagentLaunchContractReasonCode =
42
42
  | "thinking_ceiling"
43
43
  | "invalid_extension_bindings";
44
44
 
45
+ export type SubagentLaunchContractDiagnosticCode = SubagentLaunchContractReasonCode | "host_required" | "snapshot_warning" | "workspace_scope_authority";
46
+
45
47
  export interface SubagentLaunchContractDiagnostic {
46
- code: SubagentLaunchContractReasonCode | "host_required" | "snapshot_warning";
48
+ code: SubagentLaunchContractDiagnosticCode;
47
49
  severity: "error" | "warning" | "host-required";
48
50
  message: string;
49
51
  }
@@ -157,6 +159,7 @@ export interface SubagentLaunchContract {
157
159
  thinkingCeiling?: ThinkingLevel;
158
160
  systemPromptMode: AgentConfig["systemPromptMode"];
159
161
  inheritProjectContext: boolean;
162
+ inheritGlobalContext: boolean;
160
163
  inheritSkills: boolean;
161
164
  skills: SubagentLaunchContractSkills;
162
165
  tools: SubagentLaunchContractTools;
@@ -204,6 +207,24 @@ function resolveLaunchContractContext(input: SubagentLaunchContractInput, agent:
204
207
  });
205
208
  }
206
209
 
210
+ function taskWorkspaceScopeAuthorityDiagnostic(task: string | undefined): SubagentLaunchContractDiagnostic | undefined {
211
+ if (!task) return undefined;
212
+ const text = task.replace(/\s+/g, " ").trim();
213
+ if (!text) return undefined;
214
+ const createsWorkspacePackage = /\b(?:add|create|introduce|make|set up)\b.{0,80}\b(?:new\s+)?(?:workspace\s+)?package\b/i.test(text)
215
+ || /\b(?:new\s+)?package\b.{0,80}\b(?:workspace|monorepo)\b/i.test(text);
216
+ if (!createsWorkspacePackage) return undefined;
217
+ const packageOnlyAuthority = /\b(?:only|solely)\b.{0,40}\b(?:edit|change|modify|touch|write(?:\s+to)?)\b.{0,80}\b(?:package(?:\s+directory)?|packages\/[\w.-]+)\b/i.test(text)
218
+ || /\b(?:do not|don't|must not)\b.{0,40}\b(?:edit|change|modify|touch|write(?:\s+to)?)\b.{0,80}\b(?:root|workspace|lockfile|metadata)\b/i.test(text)
219
+ || /\bwithout\b.{0,40}\b(?:root|workspace|lockfile|metadata)\b.{0,40}\b(?:edit|change|modification|write)s?\b/i.test(text);
220
+ if (!packageOnlyAuthority) return undefined;
221
+ return {
222
+ code: "workspace_scope_authority",
223
+ severity: "warning",
224
+ message: "Task asks for a workspace package change while limiting authority to package-scope edits. New workspace packages often need root workspace metadata or lockfile changes, so confirm that authority before launch.",
225
+ };
226
+ }
227
+
207
228
  function candidateList(inputAgent: string, selected: AgentConfig | undefined, cwd: string): SubagentLaunchContractAgentCandidate[] {
208
229
  const all = discoverAgentsAll(cwd);
209
230
  return [...all.builtin, ...all.package, ...all.user, ...all.project]
@@ -221,6 +242,8 @@ function candidateList(inputAgent: string, selected: AgentConfig | undefined, cw
221
242
 
222
243
  export async function resolveSubagentLaunchContract(input: SubagentLaunchContractInput): Promise<SubagentLaunchContractResult> {
223
244
  const diagnostics: SubagentLaunchContractDiagnostic[] = [];
245
+ const authorityDiagnostic = taskWorkspaceScopeAuthorityDiagnostic(input.task);
246
+ if (authorityDiagnostic) diagnostics.push(authorityDiagnostic);
224
247
  const effectiveCwd = path.resolve(input.cwd);
225
248
  try {
226
249
  if (!fs.statSync(effectiveCwd).isDirectory()) {
@@ -251,7 +274,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
251
274
  return { ok: false, code: "ambiguous_agent", message: resolvedAgent.error, diagnostics };
252
275
  }
253
276
  if (!resolvedAgent.agent) {
254
- return { ok: false, code: "missing_agent", message: `Unknown agent: ${input.agent}`, diagnostics };
277
+ return { ok: false, code: "missing_agent", message: formatUnknownAgentError(input.agent, unknownAgentDiagnosticContext(discovered)), diagnostics };
255
278
  }
256
279
  const agent = resolvedAgent.agent;
257
280
  let extensionBindings: ExtensionBindings | undefined;
@@ -400,6 +423,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
400
423
  ...(thinkingCeiling ? { thinkingCeiling } : {}),
401
424
  systemPromptMode: agent.systemPromptMode,
402
425
  inheritProjectContext: agent.inheritProjectContext,
426
+ inheritGlobalContext: agent.inheritGlobalContext,
403
427
  inheritSkills: agent.inheritSkills,
404
428
  skills: {
405
429
  requested: requestedSkills,
@@ -455,6 +479,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
455
479
  systemPrompt: effectiveSystemPrompt,
456
480
  systemPromptMode: agent.systemPromptMode,
457
481
  inheritProjectContext: agent.inheritProjectContext,
482
+ inheritGlobalContext: agent.inheritGlobalContext,
458
483
  inheritSkills: agent.inheritSkills,
459
484
  skills: requestedSkills,
460
485
  tools: toolPlan.effectiveToolAllowlist,
@@ -6,7 +6,9 @@ import { FLEET_KEYBINDING_ACTIONS, type ArtifactDirPreference, type ExtensionCon
6
6
  import { validateMissionStoreConfig } from "../missions/store.ts";
7
7
  import { validateAuthorityPolicy } from "../policy/authority.ts";
8
8
  import { getAgentDir } from "../shared/utils.ts";
9
+ import { DEFAULT_MODEL_EXCLUSION_TTL_MS, MAX_MODEL_EXCLUSION_TTL_MS, setDefaultTTL } from "../runs/shared/model-exclusions.ts";
9
10
  import { validatePermissionConfig } from "../runs/shared/permissions.ts";
11
+ import { MAX_ABANDONED_SLOT_RELEASE_AFTER_MS, MIN_ABANDONED_SLOT_RELEASE_AFTER_MS } from "../runs/background/active-async-capacity.ts";
10
12
 
11
13
  const ARTIFACT_DIR_PREFERENCES = new Set<ArtifactDirPreference>(["project", "session", "temp"]);
12
14
  const FLEET_KEYBINDING_ACTION_SET = new Set<string>(FLEET_KEYBINDING_ACTIONS);
@@ -16,6 +18,23 @@ const BASE_KEY_IDS = new Set([
16
18
  ...Object.values(Key).flatMap((value) => typeof value === "string" ? [value.toLowerCase()] : []),
17
19
  ]);
18
20
 
21
+ class PrunedForkConfigError extends Error {}
22
+
23
+ function validateForkContextConfig(value: unknown): void {
24
+ if (value === undefined) return;
25
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.forkContext must be a JSON object");
26
+ const config = value as Record<string, unknown>;
27
+ if (config.mode !== undefined && config.mode !== "full" && config.mode !== "pruned") {
28
+ throw new Error('config.forkContext.mode must be "full" or "pruned"');
29
+ }
30
+ if (config.model !== undefined && (typeof config.model !== "string" || !config.model.trim())) {
31
+ throw new PrunedForkConfigError("config.forkContext.model must be a non-empty string");
32
+ }
33
+ if (config.mode === "pruned" && config.model === undefined) {
34
+ throw new PrunedForkConfigError('config.forkContext.model is required when config.forkContext.mode is "pruned"');
35
+ }
36
+ }
37
+
19
38
  function isValidKeyId(value: string): boolean {
20
39
  if (value !== value.trim()) return false;
21
40
  const parts = value.toLowerCase().split("+");
@@ -62,6 +81,31 @@ function validateArtifactConfig(value: unknown): void {
62
81
  }
63
82
  }
64
83
 
84
+ function validateCapacityConfig(value: unknown): void {
85
+ if (value === undefined) return;
86
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.capacity must be a JSON object");
87
+ const abandonedSlotReleaseAfterMs = (value as Record<string, unknown>).abandonedSlotReleaseAfterMs;
88
+ if (abandonedSlotReleaseAfterMs !== undefined
89
+ && abandonedSlotReleaseAfterMs !== false
90
+ && (typeof abandonedSlotReleaseAfterMs !== "number"
91
+ || !Number.isInteger(abandonedSlotReleaseAfterMs)
92
+ || abandonedSlotReleaseAfterMs < MIN_ABANDONED_SLOT_RELEASE_AFTER_MS
93
+ || abandonedSlotReleaseAfterMs > MAX_ABANDONED_SLOT_RELEASE_AFTER_MS)) {
94
+ throw new Error(`config.capacity.abandonedSlotReleaseAfterMs must be false or an integer from ${MIN_ABANDONED_SLOT_RELEASE_AFTER_MS} to ${MAX_ABANDONED_SLOT_RELEASE_AFTER_MS}`);
95
+ }
96
+ }
97
+
98
+ /** Validate the user-controlled TTL policy before it reaches the exclusion store. */
99
+ // TEST:test/unit/pi-coding-agent-dir.test.ts[loads and applies model exclusion TTL config]
100
+ function validateModelExclusionsConfig(value: unknown): void {
101
+ if (value === undefined) return;
102
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.modelExclusions must be a JSON object");
103
+ const defaultTtlMs = (value as Record<string, unknown>).defaultTtlMs;
104
+ if (defaultTtlMs !== undefined && (typeof defaultTtlMs !== "number" || !Number.isFinite(defaultTtlMs) || defaultTtlMs <= 0 || defaultTtlMs > MAX_MODEL_EXCLUSION_TTL_MS)) {
105
+ throw new Error(`config.modelExclusions.defaultTtlMs must be a finite positive number no greater than ${MAX_MODEL_EXCLUSION_TTL_MS}`);
106
+ }
107
+ }
108
+
65
109
  function validateOrcaProgressTabsConfig(value: unknown): void {
66
110
  if (value === undefined) return;
67
111
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.orcaProgressTabs must be a JSON object");
@@ -97,6 +141,7 @@ function validateConfig(config: Record<string, unknown>): void {
97
141
  if (config.defaultSubagentContext !== undefined && config.defaultSubagentContext !== "fresh" && config.defaultSubagentContext !== "fork") {
98
142
  throw new Error('config.defaultSubagentContext must be "fresh" or "fork"');
99
143
  }
144
+ validateForkContextConfig(config.forkContext);
100
145
  if (config.foregroundDetachShortcut !== undefined
101
146
  && (typeof config.foregroundDetachShortcut !== "string" || !isValidKeyId(config.foregroundDetachShortcut))) {
102
147
  throw new Error("config.foregroundDetachShortcut must be a valid keybinding string such as \"ctrl+b\"");
@@ -119,6 +164,8 @@ function validateConfig(config: Record<string, unknown>): void {
119
164
  validateScheduledRunsConfig(config.scheduledRuns);
120
165
  validateFleetKeybindingsConfig(config.fleetKeybindings);
121
166
  validateArtifactConfig(config.artifactConfig);
167
+ validateCapacityConfig(config.capacity);
168
+ validateModelExclusionsConfig(config.modelExclusions);
122
169
  validateMainWindowRendererConfig(config.mainWindowRenderer);
123
170
  validateOrcaProgressTabsConfig(config.orcaProgressTabs);
124
171
  }
@@ -150,6 +197,28 @@ export function updateConfig(updater: (config: ExtensionConfig) => ExtensionConf
150
197
  return next;
151
198
  }
152
199
 
200
+ /**
201
+ * Resolve the default TTL that the process-wide exclusion store should use.
202
+ *
203
+ * @param config Extension configuration after validation.
204
+ * @returns The configured TTL in milliseconds, or the built-in 24-hour default.
205
+ */
206
+ // TEST:test/unit/pi-coding-agent-dir.test.ts[loads and applies model exclusion TTL config]
207
+ export function resolveModelExclusionTTL(config: Pick<ExtensionConfig, "modelExclusions">): number {
208
+ return config.modelExclusions?.defaultTtlMs ?? DEFAULT_MODEL_EXCLUSION_TTL_MS;
209
+ }
210
+
211
+ /**
212
+ * Apply the configured exclusion policy to the process-wide model store.
213
+ *
214
+ * @param config Extension configuration after validation.
215
+ * @returns Nothing.
216
+ */
217
+ // TEST:test/unit/pi-coding-agent-dir.test.ts[loads and applies model exclusion TTL config]
218
+ export function applyModelExclusionsConfig(config: Pick<ExtensionConfig, "modelExclusions">): void {
219
+ setDefaultTTL(resolveModelExclusionTTL(config), { shortenExisting: config.modelExclusions?.defaultTtlMs !== undefined });
220
+ }
221
+
153
222
  export function resolveAsyncByDefault(config: Pick<ExtensionConfig, "asyncByDefault">): boolean {
154
223
  return config.asyncByDefault !== false;
155
224
  }
@@ -159,6 +228,7 @@ export function loadConfig(): ExtensionConfig {
159
228
  try {
160
229
  return readConfigForUpdate(configPath);
161
230
  } catch (error) {
231
+ if (error instanceof PrunedForkConfigError) throw error;
162
232
  console.error(`Failed to load subagent config from '${configPath}':`, error);
163
233
  }
164
234
  return {};
@@ -3,7 +3,7 @@ import * as path from "node:path";
3
3
  import { discoverAgentsAll, type AgentSource } from "../agents/agents.ts";
4
4
  import { isAsyncAvailable } from "../runs/background/async-execution.ts";
5
5
  import { formatSpawnBudgetSummary, getSpawnBudgetSnapshot } from "../runs/shared/spawn-budget.ts";
6
- import { getActiveAsyncCapacitySnapshot, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
6
+ import { getActiveAsyncCapacitySnapshot, resolveAbandonedSlotReleaseAfterMs, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
7
7
  import { decodeRunFanoutBudgetDescriptor, formatRunFanoutBudget, getRunFanoutBudgetSnapshot, RUN_FANOUT_BUDGET_ENV } from "../runs/shared/run-fanout-budget.ts";
8
8
  import { diagnoseIntercomBridge, type IntercomBridgeDiagnostic } from "../intercom/intercom-bridge.ts";
9
9
  import { discoverAvailableSkills, type SkillSource } from "../agents/skills.ts";
@@ -197,13 +197,13 @@ function formatActiveAsyncCapacitySection(input: DoctorReportInput): string[] {
197
197
  const limit = resolveMaxActiveAsyncRunsPerSession(input.config.maxActiveAsyncRunsPerSession);
198
198
  const sessionId = input.currentSessionId ?? input.state.currentSessionId;
199
199
  const snapshot = sessionId
200
- ? getActiveAsyncCapacitySnapshot(sessionId, limit, { liveWorkflowRunIds: new Set(input.state.workflowControllers?.keys() ?? []) })
200
+ ? getActiveAsyncCapacitySnapshot(sessionId, limit, { liveWorkflowRunIds: new Set(input.state.workflowControllers?.keys() ?? []), abandonedSlotReleaseAfterMs: resolveAbandonedSlotReleaseAfterMs(input.config.capacity?.abandonedSlotReleaseAfterMs) })
201
201
  : { used: 0, limit: limit ?? 0 };
202
202
  input.state.activeAsyncCapacity = snapshot;
203
203
  return [
204
204
  `- usage: ${snapshot.used}/${snapshot.limit || "unlimited"} used`,
205
205
  "- scope: top-level async runs in the current parent session; foreground and nested workflow children are not charged again",
206
- "- release: terminal logical state plus verified process exit; missing or unknown cleanup proof retains capacity",
206
+ "- release: terminal state plus matching observed process-terminal proof, or abandoned-timeout for failed runs with a dead runner PID and stale activity when enabled; false keeps unknown-proof slots",
207
207
  ];
208
208
  }
209
209
 
@@ -9,7 +9,7 @@
9
9
  * Toggle: async parameter (default: true; set asyncByDefault:false in config.json to opt out)
10
10
  *
11
11
  * Config file: ~/.pi/agent/extensions/subagent/config.json
12
- * { "asyncByDefault": true, "defaultSubagentContext": "fork", "forceTopLevelAsync": true, "maxSubagentDepth": 1, "intercomBridge": { "mode": "always", "instructionFile": "./intercom-bridge.md" }, "worktreeSetupHook": "./scripts/setup-worktree.mjs" }
12
+ * { "asyncByDefault": true, "defaultSubagentContext": "fork", "forkContext": { "mode": "pruned", "model": "provider/model" }, "forceTopLevelAsync": true, "maxSubagentDepth": 1, "intercomBridge": { "mode": "always", "instructionFile": "./intercom-bridge.md" }, "worktreeSetupHook": "./scripts/setup-worktree.mjs" }
13
13
  */
14
14
 
15
15
  import { randomUUID } from "node:crypto";
@@ -21,9 +21,11 @@ import { keyText, type ExtensionAPI, type ExtensionContext, type ToolDefinition
21
21
  import { Box, Container, Spacer, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
22
22
  import { discoverAgents, discoverAgentsAll, type AgentConfig, type AgentScope } from "../agents/agents.ts";
23
23
  import { clearRuntimeAgentsForPi, listRuntimeAgentConfigs, mergeRuntimeAgents } from "../agents/runtime-agent-registry.ts";
24
+ import { registerRuntimeAgentEventListener } from "../agents/runtime-agent-events.ts";
24
25
  import { ensureAccessibleDir } from "../shared/accessible-dir.ts";
25
26
  import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
26
27
  import { resolveCurrentSessionId } from "../shared/session-identity.ts";
28
+ import { getAgentDir } from "../shared/utils.ts";
27
29
  import { currentCompletionOwnerId } from "../shared/completion-owner.ts";
28
30
  import { cleanupOldChainDirs } from "../shared/settings.ts";
29
31
  import { clearLegacyResultAnimationTimer, renderSubagentResult, renderSubagentSummary } from "../tui/render.ts";
@@ -32,10 +34,11 @@ import { SubagentFleetStatus, resolveFleetViewPlacement } from "../tui/fleet-sta
32
34
  import { createSubagentParamsSchema } from "./schemas.ts";
33
35
  import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
34
36
  import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
35
- import { getActiveAsyncCapacitySnapshot, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
37
+ import { getActiveAsyncCapacitySnapshot, resolveAbandonedSlotReleaseAfterMs, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
36
38
  import { cleanupResultIndexes, missionObserverResultCandidateFiles } from "../runs/background/result-files.ts";
37
39
  import { ASYNC_RETENTION_DELAY_MS, cleanupAsyncRetention } from "../runs/background/async-retention.ts";
38
40
  import { createResultWatcher } from "../runs/background/result-watcher.ts";
41
+ import { createResultDeliveryOwnership } from "../runs/background/result-delivery-ownership.ts";
39
42
  import { createScheduledRunManager } from "../runs/background/scheduled-runs.ts";
40
43
  import { registerSlashCommands } from "../slash/slash-commands.ts";
41
44
  import { registerPromptTemplateDelegationBridge } from "../slash/prompt-template-bridge.ts";
@@ -56,7 +59,7 @@ import { formatSteeringNotice, handleSubagentSteeringNotice, SUBAGENT_STEERING_M
56
59
  import { SUBAGENT_CHILD_ENV, SUBAGENT_PARENT_SESSION_ENV } from "../runs/shared/pi-args.ts";
57
60
  import { resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
58
61
  import { formatDuration, shortenPath } from "../shared/formatters.ts";
59
- import { loadConfig, resolveAsyncByDefault, resolveScheduledStoreRoot } from "./config.ts";
62
+ import { applyModelExclusionsConfig, loadConfig, resolveAsyncByDefault, resolveScheduledStoreRoot } from "./config.ts";
60
63
  import { buildSubagentToolDescription, buildSubagentToolPromptMetadata } from "./tool-description.ts";
61
64
  import { finalizeToolResult } from "./tool-result.ts";
62
65
  import { collectGoalContinuationNotices } from "../missions/goal-driver.ts";
@@ -382,9 +385,13 @@ export function projectActiveHerdrRuns(state: SubagentState): HerdrStatusRun[] {
382
385
  .filter((job) => active(job.status))
383
386
  .map((job) => {
384
387
  const children = job.mode === "workflow" ? foregroundChildrenByWorkflow.get(job.asyncId) : undefined;
388
+ const currentStep = job.steps?.find((step) => step.status === "running")
389
+ ?? (job.currentStep !== undefined ? job.steps?.[job.currentStep] : undefined)
390
+ ?? job.steps?.find((step) => step.status === "pending");
385
391
  return {
386
392
  id: job.asyncId,
387
393
  agents: children?.length ? children.map((child) => child.agent) : job.agents,
394
+ ...(currentStep?.label ? { taskLabel: currentStep.label } : {}),
388
395
  needsAttention: job.activityState === "needs_attention" || children?.some((child) => child.needsAttention),
389
396
  };
390
397
  });
@@ -401,6 +408,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
401
408
  cleanupOldChainDirs();
402
409
 
403
410
  const config = loadConfig();
411
+ // Apply the process-wide exclusion TTL before any child launch can record a model failure.
412
+ applyModelExclusionsConfig(config);
404
413
  const waitToolConfig = resolveWaitToolConfig(config.waitTool);
405
414
  const asyncByDefault = resolveAsyncByDefault(config);
406
415
  const fleetViewEnabled = config.fleetView !== false;
@@ -459,7 +468,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
459
468
  const supervisorChannel = createNativeSupervisorChannel(pi, state);
460
469
  const waitSubscriptionManager = createWaitSubscriptionManager(pi, state);
461
470
  const mainWatchdog = registerMainWatchdog(pi);
462
- const completionNotifier = registerSubagentNotify(pi, state, { batchConfig: config.completionBatch });
471
+ const resultDeliveryOwnership = createResultDeliveryOwnership(state);
472
+ const completionNotifier = registerSubagentNotify(pi, state, { batchConfig: config.completionBatch, ownership: resultDeliveryOwnership });
463
473
  const fleetStatus = fleetViewEnabled
464
474
  ? new SubagentFleetStatus(state, async (itemKey) => {
465
475
  const ctx = state.lastUiContext;
@@ -521,6 +531,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
521
531
  10 * 60 * 1000,
522
532
  {
523
533
  notifier: completionNotifier,
534
+ ownership: resultDeliveryOwnership,
524
535
  observeCompletion: (result) => scheduledRunManager.handleAsyncCompletion(result),
525
536
  observedCompletionRunIds: () => scheduledRunManager.observedCompletionRunIds(),
526
537
  hasDeliveryDemand: hasResultDeliveryDemand,
@@ -528,7 +539,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
528
539
  resultScanLogging: config.resultScanLogging,
529
540
  },
530
541
  );
531
- const { startResultWatcher, primeExistingResults, stopResultWatcher } = resultWatcher;
542
+ const { startResultWatcher, transitionResultDelivery, primeExistingResults, stopResultWatcher } = resultWatcher;
532
543
  refreshResultDelivery = resultWatcher.refreshResultDelivery;
533
544
  const asyncRetentionAbort = new AbortController();
534
545
  const asyncRetentionTimer = setTimeout(async () => {
@@ -695,6 +706,12 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
695
706
  0,
696
707
  0,
697
708
  );
709
+ if (args.workflowScriptPath)
710
+ return new Text(
711
+ `${title}${gap}${theme.fg("accent", args.workflowScriptPath)}${args.async === true ? `${gap}${theme.fg("warning", "[async]")}` : ""}`,
712
+ 0,
713
+ 0,
714
+ );
698
715
  const asyncLabel = args.async === true ? `${gap}${theme.fg("warning", "[async]")}` : "";
699
716
  return new Text(
700
717
  `${title}${gap}${theme.fg("accent", args.agent || "?")}${asyncLabel}`,
@@ -778,6 +795,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
778
795
  fleetStatus?.refresh();
779
796
  };
780
797
  const eventUnsubscribes = [
798
+ registerRuntimeAgentEventListener(pi),
781
799
  pi.events.on(SUBAGENT_ASYNC_STARTED_EVENT, asyncStartedHandler),
782
800
  pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, asyncCompleteHandler),
783
801
  pi.events.on(SUBAGENT_PROCESS_TERMINAL_EVENT, () => {
@@ -794,7 +812,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
794
812
  if (event.toolName !== "subagent") return;
795
813
  if (!ctx.hasUI) return;
796
814
  state.lastUiContext = ctx;
815
+ const activeJobCount = state.asyncJobs.size;
797
816
  restoreActiveJobs(ctx);
817
+ if (state.asyncJobs.size > activeJobCount) herdrStatusBridge.syncRuns();
798
818
  fleetStatus?.setContext(ctx);
799
819
  fleetStatus?.refresh();
800
820
  if (state.asyncJobs.size > 0) {
@@ -836,16 +856,20 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
836
856
  state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(
837
857
  state.currentSessionId,
838
858
  resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession),
839
- { liveWorkflowRunIds: new Set(state.workflowControllers?.keys() ?? []) },
859
+ { liveWorkflowRunIds: new Set(state.workflowControllers?.keys() ?? []), abandonedSlotReleaseAfterMs: resolveAbandonedSlotReleaseAfterMs(config.capacity?.abandonedSlotReleaseAfterMs) },
840
860
  );
841
861
  };
842
862
 
843
- const resetSessionState = (ctx: ExtensionContext, recovering: boolean) => {
863
+ const resetSessionState = (ctx: ExtensionContext, recovering: boolean, previousSessionFile?: string) => {
844
864
  state.widgetsSuspended = false;
845
865
  state.baseCwd = ctx.cwd;
846
866
  goalTurnId = 0;
867
+ const previousRuntimeSessionId = state.currentSessionId;
868
+ resultDeliveryOwnership.claimPredecessor(previousSessionFile, previousRuntimeSessionId);
847
869
  state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
870
+ transitionResultDelivery();
848
871
  state.parentSessionFile = ctx.sessionManager.getSessionFile();
872
+ state.trustedSessionFileRoot = state.parentSessionFile ? path.join(getAgentDir(), "sessions") : undefined;
849
873
  state.trustedSessionRoots = [...new Set([
850
874
  ...(config.defaultSessionDir ? [path.resolve(expandTilde(config.defaultSessionDir))] : []),
851
875
  ...(state.parentSessionFile ? [getSubagentSessionRoot(state.parentSessionFile)] : []),
@@ -920,6 +944,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
920
944
  clearTimeout(asyncRetentionTimer);
921
945
  asyncRetentionAbort.abort();
922
946
  stopResultWatcher();
947
+ resultDeliveryOwnership.clear();
923
948
  completionNotifier.dispose();
924
949
  mainWatchdog.dispose();
925
950
  scheduledRunManager.stop();
@@ -1020,7 +1045,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
1020
1045
  pi.on("session_start", (event, ctx) => {
1021
1046
  installRuntime(ctx);
1022
1047
  const recovering = event.reason === "startup" || event.reason === "reload" || event.reason === "resume";
1023
- resetSessionState(ctx, recovering);
1048
+ resetSessionState(ctx, recovering, event.previousSessionFile);
1024
1049
  herdrStatusBridge.sessionStarted({
1025
1050
  hasUI: ctx.hasUI === true,
1026
1051
  runs: activeHerdrRuns(),