pi-subagents 0.47.1 → 0.49.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.
- package/CHANGELOG.md +51 -0
- package/README.md +2 -0
- package/docs/agents.md +1 -0
- package/docs/configuration.md +74 -7
- package/docs/missions.md +4 -2
- package/docs/observability.md +27 -3
- package/docs/tool-reference.md +1 -1
- package/package.json +1 -1
- package/src/agents/agents.ts +36 -15
- package/src/api/preflight.ts +1 -1
- package/src/extension/config.ts +26 -0
- package/src/extension/doctor.ts +40 -0
- package/src/extension/index.ts +39 -8
- package/src/extension/public-execution.ts +35 -4
- package/src/extension/rpc.ts +3 -9
- package/src/extension/schemas.ts +10 -9
- package/src/extension/tool-description.ts +10 -8
- package/src/inspectors/herdr/actions.ts +11 -2
- package/src/inspectors/herdr/inspector-runner.ts +16 -3
- package/src/intercom/intercom-bridge.ts +7 -2
- package/src/missions/lifecycle.ts +4 -7
- package/src/missions/store.ts +12 -7
- package/src/missions/workflow-state.ts +6 -2
- package/src/runs/background/active-async-capacity.ts +431 -0
- package/src/runs/background/active-run-index.ts +9 -5
- package/src/runs/background/async-execution.ts +141 -65
- package/src/runs/background/async-job-tracker.ts +4 -0
- package/src/runs/background/async-resume.ts +18 -2
- package/src/runs/background/async-status.ts +11 -5
- package/src/runs/background/chain-append.ts +33 -15
- package/src/runs/background/fleet-view.ts +18 -5
- package/src/runs/background/owned-process-tree.ts +104 -0
- package/src/runs/background/process-terminal.ts +17 -3
- package/src/runs/background/resume-guidance.ts +27 -7
- package/src/runs/background/retained-children.ts +14 -6
- package/src/runs/background/run-status.ts +101 -5
- package/src/runs/background/stale-run-reconciler.ts +3 -3
- package/src/runs/background/subagent-runner.ts +60 -32
- package/src/runs/foreground/chain-execution.ts +37 -2
- package/src/runs/foreground/execution.ts +90 -19
- package/src/runs/foreground/foreground-control.ts +12 -0
- package/src/runs/foreground/prompt-audit.ts +172 -0
- package/src/runs/foreground/subagent-executor.ts +829 -203
- package/src/runs/shared/acceptance.ts +13 -4
- package/src/runs/shared/completion-guard.ts +17 -1
- package/src/runs/shared/llm-intent-arbiter.ts +302 -0
- package/src/runs/shared/parallel-utils.ts +2 -0
- package/src/runs/shared/pi-args.ts +44 -1
- package/src/runs/shared/run-fanout-budget.ts +280 -0
- package/src/runs/shared/single-output.ts +4 -2
- package/src/runs/shared/task-intent.ts +19 -3
- package/src/runs/shared/worktree.ts +17 -5
- package/src/shared/agent-stream-options.ts +5 -0
- package/src/shared/artifacts.ts +2 -6
- package/src/shared/display-text.ts +50 -0
- package/src/shared/node-executable.ts +21 -0
- package/src/shared/types.ts +115 -4
- package/src/shared/utils.ts +3 -1
- package/src/tui/fleet-status.ts +7 -5
- package/src/tui/fleet-transcript.ts +1 -48
- package/src/tui/fleet.ts +228 -13
- package/src/tui/render.ts +86 -39
- package/src/watchdog/permission-arbiter.ts +2 -1
- package/src/watchdog/review.ts +4 -3
- package/src/workflows/chat-progress.ts +8 -2
- package/src/workflows/scripted-workflow.ts +66 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.49.0] - 2026-08-13
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Run a single child with `{ agent, task? }` when a full workflow script is not needed (#1059).
|
|
9
|
+
- Adjust FleetView spacing and collapsed result height from the main window. Thanks to @pierre-mgmt for #1048.
|
|
10
|
+
- Inspect async run state with `debug.run`, without exposing prompts, secrets, or transcripts (#1037).
|
|
11
|
+
- Let builtin role overrides keep Pi's normal tools and extensions with `tools: "inherit"`. Thanks to @estanexanavsem for #1047 and @davidarny for #1049.
|
|
12
|
+
- Add simple terminal examples for FleetView, the async widget, and inline tool display. Thanks to @czottmann for #1050.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- Clean up active-run limits and artifact packaging code without changing behavior.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Trust live Herdr session roots only when the parent executor registered them for that async run.
|
|
19
|
+
- Let a workflow child disable the intercom bridge for one run with `intercomBridge: { mode: "off" }`, while normal async completion still works. Thanks to @jaudiger for #1072.
|
|
20
|
+
- Recover sibling children after a detached workflow fails (#1066).
|
|
21
|
+
- Show child session transcripts in standalone Herdr inspectors when the transcript is in a trusted session folder (#1069).
|
|
22
|
+
- Keep watchdog reviews, permission checks, Prompt Audit rewriting, and completion intent checks on the authenticated provider stream across the Pi 0.81 and 0.84 APIs. Thanks to @nuzayets for #1067.
|
|
23
|
+
- Mark children stopped by a parent workflow as stopped, not failed, and keep the stop reason (#1060).
|
|
24
|
+
- Keep subagent artifacts and automatic mission records out of project worktrees by default, so read-only workflows leave the tree clean (#1062).
|
|
25
|
+
- Make parents wait at dependency barriers after async launches, so child results are available before dependent work continues. Thanks to @exuanbo for #1045.
|
|
26
|
+
- Keep wait callers alive for intercom replies instead of reviving a detached wrapper. Thanks to @yayamaz for #1053.
|
|
27
|
+
- Keep workflow summary reports separate from child reports, and reject report path collisions before launch (#1038).
|
|
28
|
+
- Accept no-edit implementation challenge passes when the writer says the current solution is already best (#1054).
|
|
29
|
+
- Make the mutation guard safer for LLM intent checks, long tasks, and provider authentication. Thanks to @MarcusNeufeldt for #1044.
|
|
30
|
+
- Launch Herdr inspector panes with Node when Pi runs as a standalone executable. Thanks to @kevinpita for #1051.
|
|
31
|
+
- Sanitize async, nested, and result transcript output before showing it in terminal views. Thanks to @riesbri for #1046.
|
|
32
|
+
|
|
33
|
+
## [0.48.0] - 2026-08-13
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
- Limit each run to 64 child launches by default, so accidental fan-out loops stop before they create too many children. Thanks to @asjer for #1031.
|
|
37
|
+
- Add an optional limit for how many top-level async runs one session can have active at the same time. Fleet, status, RPC, and doctor now show the limit and current usage. Thanks to @asjer for #1029.
|
|
38
|
+
- Add a live Prompt Audit drawer to Fleet for foreground children owned by the current session. It shows the prompt on screen without saving it to status files, history, transcripts, metadata, results, progress, or run artifacts (#1021).
|
|
39
|
+
- Add a global `timeoutMs` setting for default run deadlines on foreground launches and plain single-agent async runs. It applies when a launch or agent does not set its own timeout, and it prevents long foreground fan-outs from falling back to the built-in 30-minute limit. Composite async runs stay unbounded at the top level. Thanks to @shaharmor for #1018.
|
|
40
|
+
- Add `PI_SUBAGENT_TASK_DELIVERY=auto|file` for hosts that block child processes when the task text appears in the command line. File mode writes the task to a temporary `task.md` and passes that path instead. Thanks to @yanqianglu for #1028.
|
|
41
|
+
- Retry with file-based task delivery after a child exits with no activity, which helps recover from endpoint protection tools that block long command lines. Thanks to @yanqianglu for #1028.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
- Open Fleet Prompt Audit with the original task visible by default, and show a short task summary in the normal Fleet detail pane (#1021).
|
|
45
|
+
- Use the full task text when caching LLM intent decisions, so similar tasks with the same prefix cannot share the wrong answer.
|
|
46
|
+
- Stop async Pi writer processes as full process groups, and only mark process cleanup as proven after the process tree has actually exited. Thanks to @asjer for #1030.
|
|
47
|
+
- Explain when a mission belongs to another worktree, including both the current project root and the mission directory (#1024).
|
|
48
|
+
- Keep the configured output reference when explicit acceptance rejects a foreground child, so useful reports remain available (#1023).
|
|
49
|
+
- Reject worktree base directories inside the agent extensions directory, including symlinked paths (#1014).
|
|
50
|
+
- Make unnamed intercom fallback targets match pi-intercom's registered name length, so subagents without a custom session name can still reach their parent. Thanks to @mystery4f for #1017.
|
|
51
|
+
- Stop treating phrases like "must-fix items" or "should-fix tests" as instructions to edit files during read-only review tasks. Thanks to @MarcusNeufeldt for #1020.
|
|
52
|
+
- Add an optional LLM check before the mutation guard fails a foreground single, parallel, or chain child that made no edits. If the task was actually read-only, the run now completes instead of failing. Thanks to @MarcusNeufeldt for #1020.
|
|
53
|
+
- Accept empty strings inside acceptance-report string arrays instead of rejecting the full report. Thanks to @hjiang for #1015.
|
|
54
|
+
- Let single external-CLI workflow children start without inheriting a Pi model, so model-less external runners do not fail preflight. Thanks to @twosunnus for #1016.
|
|
55
|
+
|
|
5
56
|
## [0.47.1] - 2026-08-12
|
|
6
57
|
|
|
7
58
|
### Fixed
|
package/README.md
CHANGED
|
@@ -93,6 +93,8 @@ In the TUI, a persistent FleetView below the editor keeps active work visible. `
|
|
|
93
93
|
|
|
94
94
|
Details, keybindings, and the machine-readable run artifacts are in [Observability](https://github.com/nicobailon/pi-subagents/blob/main/docs/observability.md).
|
|
95
95
|
|
|
96
|
+
For bounded orchestration, `maxSubagentSpawnsPerRun` limits cumulative logical children in one run tree. It defaults to 64 and stays separate from active concurrency and the session-wide cumulative spawn budget. See [Configuration](https://github.com/nicobailon/pi-subagents/blob/main/docs/configuration.md#maxsubagentspawnsperrun).
|
|
97
|
+
|
|
96
98
|
## If something feels off
|
|
97
99
|
|
|
98
100
|
```text
|
package/docs/agents.md
CHANGED
|
@@ -77,6 +77,7 @@ Supported override fields: `description`, `model`, `fallbackModels`, `thinking`,
|
|
|
77
77
|
|
|
78
78
|
- `description` replaces the discovered description for builtin and custom agents, which lets list output show deployment-specific routing or model metadata.
|
|
79
79
|
- Use `defaultContext: false` or `acceptanceRole: false` to clear an inherited override.
|
|
80
|
+
- Use `tools: "inherit"` on a builtin when that one role should omit its bundled tool allowlist and receive Pi's normal builtins and ambient extensions. This keeps strict tools as the default for other builtins.
|
|
80
81
|
- Project overrides beat user overrides.
|
|
81
82
|
- Matching user and project agents also receive override fields that their frontmatter leaves unset, so a shared project config agent can keep the persona while local settings choose the model.
|
|
82
83
|
|
package/docs/configuration.md
CHANGED
|
@@ -44,6 +44,29 @@ Defaults to `false`. The default registered model-facing tool schema and descrip
|
|
|
44
44
|
|
|
45
45
|
Controls the `subagent` tool result shown inline in chat. The default, `"rich"`, shows live child activity and expands to detailed output. `"summary"` keeps the inline result at one stable row for running, completed, failed, stopped, and paused runs; it does not animate, show elapsed time, preview child output, or change when Pi's expand key is pressed. FleetView remains available for live progress and detailed inspection.
|
|
46
46
|
|
|
47
|
+
## `mainWindowRenderer`
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"mainWindowRenderer": {
|
|
52
|
+
"horizontalSpacing": 0,
|
|
53
|
+
"compactResultMaxLines": 4
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Controls only the main chat `subagent` call/result renderer. It does not change child execution, orchestration, FleetView, artifacts, transcripts, or model-facing content.
|
|
59
|
+
|
|
60
|
+
`horizontalSpacing` is an integer from `0` to `4`. The default preserves current spacing. Set it to `0` to remove the extra spaces before compact result details and between parts of the call row.
|
|
61
|
+
|
|
62
|
+
`compactResultMaxLines` is a positive integer. It caps only collapsed rich-result rows and adds an expand hint when rows are hidden. Expanded output remains uncapped.
|
|
63
|
+
|
|
64
|
+
With `"summary"`, a tool result looks like this:
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
✓ reviewer · completed
|
|
68
|
+
```
|
|
69
|
+
|
|
47
70
|
## `asyncByDefault`
|
|
48
71
|
|
|
49
72
|
```json
|
|
@@ -115,6 +138,18 @@ This is different from `waitTool.enabled=false`, which returns immediately witho
|
|
|
115
138
|
|
|
116
139
|
Forces depth-0 internal single, parallel, and chain runs into background mode and bypasses launch UI by forcing `clarify: false`. Nested calls keep their own inherited settings.
|
|
117
140
|
|
|
141
|
+
## `timeoutMs`
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{ "timeoutMs": 3600000 }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Global default runtime deadline, in milliseconds, for subagent runs. It replaces the built-in 30-minute backstop for foreground launches (single, parallel, chain, and workflowScript) and plain single-agent async runs whenever no call-level `timeoutMs`/`maxRuntimeMs` applies. For single-agent launches, selected agent frontmatter `timeoutMs` still wins. This only moves the *default*.
|
|
148
|
+
|
|
149
|
+
Use it when foreground orchestration or plain async single-agent runs need a longer default than 30 minutes. It does not set async composite top-level deadlines, and it does not replace async fan-out child deadlines.
|
|
150
|
+
|
|
151
|
+
Composite async runs (async chains, parallel tasks, and scripted workflows) stay unbounded at the top level by design. Their runner children are bounded individually by their own agent or runner defaults, so this value does not cap them. Must be a positive integer no greater than `2147483647` (the largest delay a Node.js timer can honor, roughly 24.8 days); invalid or out-of-range values are ignored and the built-in defaults apply.
|
|
152
|
+
|
|
118
153
|
## `globalConcurrencyLimit`
|
|
119
154
|
|
|
120
155
|
```json
|
|
@@ -131,9 +166,31 @@ Caps simultaneously running children inside existing durable legacy multi-child
|
|
|
131
166
|
|
|
132
167
|
Optionally caps the total number of child subagent launches during one parent session, including completed and failed children, parallel task counts, static chain steps, and bounded dynamic fanout children. Sessions are unlimited by default. Set this value to `0` to disable a configured cap. `PI_SUBAGENT_MAX_SPAWNS_PER_SESSION` overrides the config for a process and follows the same positive-cap/zero-unlimited semantics.
|
|
133
168
|
|
|
134
|
-
`subagent({ action: "status" })`, fleet status, and `subagent({ action: "doctor" })` expose used, effective limit, remaining capacity, grants, and the remaining grant allowance.
|
|
169
|
+
`subagent({ action: "status" })`, fleet status, and `subagent({ action: "doctor" })` expose used, effective limit, remaining capacity, grants, and the remaining grant allowance for this budget. A user may explicitly call `subagent({ action: "grant-spawn-budget", additional: 10 })` from the root interactive parent after all children settle and confirm the native prompt. Grants are additive: they never erase cumulative usage, are rejected for unlimited sessions and child/headless callers, and total granted capacity cannot exceed the original configured cap. Compaction remains part of the same logical parent session and does not reset usage or grants; starting a new parent session does.
|
|
135
170
|
|
|
136
|
-
|
|
171
|
+
## `maxSubagentSpawnsPerRun`
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{ "maxSubagentSpawnsPerRun": 64 }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Caps cumulative logical child admissions in one top-level run tree. The default is `64`. `PI_SUBAGENT_MAX_SPAWNS_PER_RUN` overrides the config when it is a positive integer. Invalid, zero, or missing values fall back to the configured positive value or `64`.
|
|
178
|
+
|
|
179
|
+
The budget counts single launches, expanded `tasks`/`count`, static chain steps and parallel groups, actual dynamic `expand` items, appended chain steps, workflow children, and nested child calls. Static and materialized dynamic groups are admitted atomically. Startup retries, model fallback, and retained-child resume reuse the original logical child claim. Claims are never released or refunded. This cap is independent from the session-wide cumulative spawn budget and `globalConcurrencyLimit`.
|
|
180
|
+
|
|
181
|
+
## `maxActiveAsyncRunsPerSession`
|
|
182
|
+
|
|
183
|
+
```json
|
|
184
|
+
{ "maxActiveAsyncRunsPerSession": 4 }
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Optionally caps concurrently active top-level async runs owned by one parent session. Unset or `0` keeps the existing unlimited behavior. A positive integer reserves one slot before an async single, parallel, chain, or workflow creates run artifacts or starts children. Foreground runs and nested/workflow children do not reserve another slot.
|
|
188
|
+
|
|
189
|
+
Queued, running, paused, and needs-attention runs retain capacity. Runner-backed slots release only after terminal logical state and matching observed process-terminal proof from #1030. Missing, malformed, or unknown cleanup proof retains the slot. A terminal async workflow releases after its controller is gone and every launched child is accounted for: awaited foreground children are covered by workflow settlement, while actual background children still require observed process-terminal proof. Resume transfers the source slot without a second charge. Dismissal and history cleanup do not release capacity.
|
|
190
|
+
|
|
191
|
+
This limit bounds current top-level async load. It is separate from cumulative `maxSubagentSpawnsPerSession`, `maxSubagentSpawnsPerRun`, and `globalConcurrencyLimit`.
|
|
192
|
+
|
|
193
|
+
`subagent({ action: "status" })`, fleet status, and `subagent({ action: "doctor" })` expose used, effective limit, and remaining active capacity. Static chains and parallel calls fail before creating run artifacts or starting partial work when their declared capacity cannot fit. Later retries or unbounded dynamic work are not guaranteed by that preflight.
|
|
137
194
|
|
|
138
195
|
## `scheduledRuns`
|
|
139
196
|
|
|
@@ -196,6 +253,16 @@ export PI_SUBAGENT_PI_BINARY=/path/to/pi-or-wrapper
|
|
|
196
253
|
|
|
197
254
|
Overrides the command used to launch child Pi processes. Package wrappers can set this to their own `pi`/agent binary so subagents inherit wrapper flags, environment setup, and bundled resources without relying on `PATH` ordering. Empty or whitespace-only values are ignored.
|
|
198
255
|
|
|
256
|
+
## `PI_SUBAGENT_TASK_DELIVERY`
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
export PI_SUBAGENT_TASK_DELIVERY=file # auto | file (default: auto)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Controls how the task text reaches the child Pi process. `auto` (default) passes short tasks as an inline argv token and writes tasks longer than 8000 characters to a temp `task.md` referenced as `@<path>`. `file` always uses a temp file, keeping the task out of argv entirely.
|
|
263
|
+
|
|
264
|
+
Use `file` on hosts where endpoint protection (EDR) pre-execution scanning denies child processes whose command line embeds a long natural-language task — that denial surfaces as an immediate zero-activity `SIGKILL`. Independently of this setting, startup retries automatically escalate to file delivery after an unexplained zero-activity `SIGKILL`. Empty, whitespace-only, or unrecognized values fall back to `auto`.
|
|
265
|
+
|
|
199
266
|
## `intercomBridge`
|
|
200
267
|
|
|
201
268
|
```json
|
|
@@ -253,7 +320,6 @@ stdin is a JSON object with `repoRoot`, `worktreePath`, `agentCwd`, `branch`, `i
|
|
|
253
320
|
{
|
|
254
321
|
"missions": {
|
|
255
322
|
"enabled": true,
|
|
256
|
-
"directory": ".pi/subagents/missions",
|
|
257
323
|
"globalIndex": true,
|
|
258
324
|
"retainTerminal": 200
|
|
259
325
|
}
|
|
@@ -262,7 +328,8 @@ stdin is a JSON object with `repoRoot`, `worktreePath`, `agentCwd`, `branch`, `i
|
|
|
262
328
|
|
|
263
329
|
Automatic missions are enabled by default for ordinary launches with a task. Use per-launch `mission: false` for intentionally ephemeral work, or set `enabled: false` to disable automatic creation globally; explicit mission actions and `missionId`/`mission` launch fields still work.
|
|
264
330
|
|
|
265
|
-
-
|
|
331
|
+
- Mission records default to a project-keyed directory under pi's agent directory (`~/.pi/agent/missions/projects/<project-hash>/`). This keeps the project worktree clean.
|
|
332
|
+
- `directory` may be absolute, `~/...`, or project-relative. Set it to `.pi/subagents/missions` to opt in to project-scoped records.
|
|
266
333
|
- `retainTerminal` is a positive count (default `200`); pruning removes only the oldest completed, failed, or cancelled records and their pointers, never planned, active, waiting, needs-decision, or corrupt records.
|
|
267
334
|
- The user-global index contains pointers only; missing-record pointers self-heal when globally listed. Set `globalIndex: false` to disable writes or `globalIndexDir` to redirect it.
|
|
268
335
|
|
|
@@ -291,11 +358,11 @@ Each fixed action resolves to `"auto"`, `"confirm"`, or `"forbid"`. This is inte
|
|
|
291
358
|
|
|
292
359
|
Controls where subagent artifact files (inputs, outputs, transcripts, metadata) are stored:
|
|
293
360
|
|
|
294
|
-
- `"project"
|
|
295
|
-
- `"session"
|
|
361
|
+
- `"project"`: writes to `<cwd>/.pi/subagents/artifacts/`.
|
|
362
|
+
- `"session"` (default): stores artifacts under pi's session directory (`~/.pi/agent/sessions/<session>/subagent-artifacts/`), keeping the working directory clean. It falls back to the OS temp directory when no session file exists.
|
|
296
363
|
- `"temp"`: uses the OS temp directory.
|
|
297
364
|
|
|
298
|
-
This preference also controls the default chain scratch directory. `"project"` uses `<cwd>/.pi/subagents/chain-runs/`, while `"session"` and `"temp"` use the user-scoped temp chain directory.
|
|
365
|
+
This preference also controls the default chain scratch directory. `"project"` uses `<cwd>/.pi/subagents/chain-runs/`, while the default `"session"` and `"temp"` use the user-scoped temp chain directory.
|
|
299
366
|
|
|
300
367
|
The `"session"` option uses the same directory that `cleanupAllArtifactDirs` already scans for age-based cleanup, so artifacts are still cleaned up automatically. Temporary chain directories are cleaned up separately after 24 hours.
|
|
301
368
|
|
package/docs/missions.md
CHANGED
|
@@ -11,7 +11,9 @@ Missions are durable wrappers around runs. The noun map:
|
|
|
11
11
|
- **Run** — one actual subagent execution.
|
|
12
12
|
- **Receipt** — proof or a link for an external outcome, such as a PR, CI check, deployment, or release.
|
|
13
13
|
|
|
14
|
-
Ordinary workflow launches create one enclosing mission by default, with detailed JSON records under
|
|
14
|
+
Ordinary workflow launches create one enclosing mission by default, with detailed JSON records under `~/.pi/agent/missions/projects/<project-hash>/` linking objectives, run ids, lifecycle status, decisions, artifact paths, and delivery receipts. Workflow children do not create separate missions. Each workflow child attempt is stored in the enclosing mission with its stable workflow key, run id when known, agent, task metadata, timestamps, session and artifact paths, and latest status heartbeat.
|
|
15
|
+
|
|
16
|
+
Records created under the old default `<project>/.pi/subagents/missions` stay on disk. Continue them by setting `missions.directory` to that path for the project or by copying the record into the new agent-dir project store. There is no automatic migration.
|
|
15
17
|
|
|
16
18
|
Behavior:
|
|
17
19
|
|
|
@@ -19,7 +21,7 @@ Behavior:
|
|
|
19
21
|
- Human receipts end with `Mission: <id> (<status>)`, while JSON/structured output text stays unchanged and `details.missionId` is authoritative.
|
|
20
22
|
- Pass `mission: false` for an intentionally ephemeral workflow. It creates no mission for the workflow or its children and has no `state` global.
|
|
21
23
|
- Set `missions.enabled: false` to disable automatic mission creation; explicit mission fields and actions still work.
|
|
22
|
-
- A workflow with a mission can use `await state.get(key)` and `await state.set(key, value)` for durable JSON state. Missing keys return `undefined`. Keys use the same format as `runs.run` keys. Each set takes the state-file lock, reads the latest file, merges the key, and atomically writes `<
|
|
24
|
+
- A workflow with a mission can use `await state.get(key)` and `await state.set(key, value)` for durable JSON state. Missing keys return `undefined`. Keys use the same format as `runs.run` keys. Each set takes the state-file lock, reads the latest file, merges the key, and atomically writes `<mission-directory>/<mission-id>/state.json`. The complete file cannot exceed 256 KiB. Each workflow caches the file on its first `get`. A `mission:false` workflow has no `state` global.
|
|
23
25
|
|
|
24
26
|
An explicit `mission` object must have exactly one non-empty `title` or `summary`. `objective` and `labels` are optional. When supplied, `goal` must be `true` and requires `budget: { tokens: <positive integer> }`.
|
|
25
27
|
|
package/docs/observability.md
CHANGED
|
@@ -4,7 +4,7 @@ Where running subagents show up, how to inspect them, and the files and events t
|
|
|
4
4
|
|
|
5
5
|
## Foreground runs
|
|
6
6
|
|
|
7
|
-
Foreground runs stream progress in the conversation while they run. They default to a generous 30-minute wall-clock timeout when neither the call nor the selected agent provides a timeout; explicit `timeoutMs`/`maxRuntimeMs` and agent defaults win.
|
|
7
|
+
Foreground runs stream progress in the conversation while they run. They default to a generous 30-minute wall-clock timeout when neither the call nor the selected agent provides a timeout; a global [`timeoutMs`](configuration.md#timeoutms) config replaces that default, and explicit `timeoutMs`/`maxRuntimeMs` and agent defaults win.
|
|
8
8
|
|
|
9
9
|
Live progress shows compact detail for single, chain, and parallel modes: current tool, recent output, token counts, aggregate cost, duration, activity freshness, current-tool duration, and chain graph metadata when available.
|
|
10
10
|
|
|
@@ -23,13 +23,37 @@ subagent({ action: "status", id: "..." }) // one run
|
|
|
23
23
|
|
|
24
24
|
Or ask naturally: "Show me the current async runs."
|
|
25
25
|
|
|
26
|
+
The under-editor async widget gives a short view while work runs. Its expand key follows your Pi keybinding:
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
async subagent worker · background
|
|
30
|
+
● worker
|
|
31
|
+
● Step 1/1: worker · running
|
|
32
|
+
⎿ read: src/auth.ts | 2.0s
|
|
33
|
+
Press configured-expand-key for live detail
|
|
34
|
+
```
|
|
35
|
+
|
|
26
36
|
To inspect one background child in text, use `subagent({ action: "status", id: "...", view: "transcript" })`; add `index` for a specific child in a parallel or chain run.
|
|
27
37
|
|
|
28
38
|
## FleetView
|
|
29
39
|
|
|
30
40
|
In the TUI, a persistent FleetView below the editor keeps active work visible as a compact summary. Set `fleetViewPlacement` to `"aboveEditor"` to move it above the editor.
|
|
31
41
|
|
|
32
|
-
|
|
42
|
+
```text
|
|
43
|
+
2 active agents · ↓ 4.2k tokens · ↓/← to inspect
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
After you expand it:
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
↑↓/jk select · enter inspect · esc back
|
|
50
|
+
|
|
51
|
+
> main
|
|
52
|
+
scout · running 1m 12s · ↓ 2.8k tokens
|
|
53
|
+
reviewer · running 38s · ↓ 1.4k tokens
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
When the focused editor is empty, press `↓` or `←` to expand the summary into `main` plus active children with agent name, state, elapsed time, and token totals. Then use `↑`/`↓` or `j`/`k` to select a child and `Enter` to inspect it. Printable navigation keys are never intercepted before activation.
|
|
33
57
|
|
|
34
58
|
FleetView replaces the legacy above-editor async widget by default. Successful background completions stay quiet so inactive Pi tabs are not marked unread, while failed or paused completions still notify the originating session. Parallel runs show every active child independently. Chains with parallel groups keep their grouped shape in progress and results, so failed or paused agents stay visible next to completed ones. When a child is explicitly allowed to fan out with `tools: subagent`, its nested runs appear under that parent child in the main status tree instead of being hidden inside the child process.
|
|
35
59
|
|
|
@@ -126,7 +150,7 @@ Foreground and async runners share bounded child-protocol handling:
|
|
|
126
150
|
|
|
127
151
|
## Chain and debug artifacts
|
|
128
152
|
|
|
129
|
-
Each chain run creates a scratch directory under its resolved chain root. With the default `artifactDir: "
|
|
153
|
+
Each chain run creates a scratch directory under its resolved chain root. With the default `artifactDir: "session"` or with `"temp"`, it is user-scoped temp storage. With `artifactDir: "project"`, the root is `<cwd>/.pi/subagents/chain-runs/`:
|
|
130
154
|
|
|
131
155
|
```text
|
|
132
156
|
<tmpdir>/pi-subagents-<scope>/chain-runs/{runId}/
|
package/docs/tool-reference.md
CHANGED
|
@@ -43,7 +43,7 @@ Parameters and actions for the `subagent` tool. These are what the LLM passes wh
|
|
|
43
43
|
| `agentScope` | `user \| project \| both` | `both` | Agent discovery scope. Project wins on collisions. |
|
|
44
44
|
| `async` | boolean | default-on | Background execution. Workflows default to background and accept `async:false` as an explicit foreground escape hatch. |
|
|
45
45
|
| `chatProgress` | `auto \| off \| live-card` | `auto` | WorkflowScript chat projection. `auto` renders a live in-chat card only for watched foreground workflows in the same Git repository, including managed worktrees; it is off otherwise. Explicit `live-card` requires `async:false` and the same Git repository. |
|
|
46
|
-
| `timeoutMs` / `maxRuntimeMs` | number | 30 min foreground
|
|
46
|
+
| `timeoutMs` / `maxRuntimeMs` | number | config `timeoutMs`, else 30 min foreground / single-agent async | Optional run-level max runtime in milliseconds. When omitted, the global [`timeoutMs`](configuration.md#timeoutms) config provides the default; absent that, foreground and plain single-agent async runs fall back to 30 minutes, while composite async runs (chains, parallel tasks, workflows) stay unbounded at the top level. |
|
|
47
47
|
| `turnBudget` | object | none | Optional assistant-turn budget `{ maxTurns, graceTurns }`. At `maxTurns` the child is warned to wrap up. After the grace window (default 1), termination occurs at the next assistant boundary; a response that starts tool work records `termination-deferred` until a later boundary. Partial output is returned on abort. |
|
|
48
48
|
| `toolBudget` | object | none | Optional child tool-call budget `{ soft?, hard, block? }`. At `soft` the child is nudged to finalize. After `hard`, configured tools are blocked; `block` defaults to `read`, `grep`, `find`, and `ls`, while `"*"` blocks every tool call. Final assistant text is never blocked. |
|
|
49
49
|
| `usageBudget` | object | none | Optional root-only reported-usage budget `{ tokens?: { soft?, hard }, costUsd?: { soft?, hard } }`. Soft limits are status-only. Hard limits prevent later child launches after reported usage is reconciled; already-running children are not stopped and no reservations are made. |
|
package/package.json
CHANGED
package/src/agents/agents.ts
CHANGED
|
@@ -92,7 +92,7 @@ interface BuiltinAgentOverrideConfig {
|
|
|
92
92
|
disabled?: boolean;
|
|
93
93
|
systemPrompt?: string;
|
|
94
94
|
skills?: string[] | false;
|
|
95
|
-
tools?: string[] | false;
|
|
95
|
+
tools?: string[] | false | "inherit";
|
|
96
96
|
extensions?: string[] | false;
|
|
97
97
|
subagentOnlyExtensions?: string[] | false;
|
|
98
98
|
completionGuard?: boolean;
|
|
@@ -602,7 +602,7 @@ function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentO
|
|
|
602
602
|
...(override.disabled !== undefined ? { disabled: override.disabled } : {}),
|
|
603
603
|
...(override.systemPrompt !== undefined ? { systemPrompt: override.systemPrompt } : {}),
|
|
604
604
|
...(override.skills !== undefined ? { skills: override.skills === false ? false : [...override.skills] } : {}),
|
|
605
|
-
...(override.tools !== undefined ? { tools: override.tools
|
|
605
|
+
...(override.tools !== undefined ? { tools: Array.isArray(override.tools) ? [...override.tools] : override.tools } : {}),
|
|
606
606
|
...(override.extensions !== undefined ? { extensions: override.extensions === false ? false : [...override.extensions] } : {}),
|
|
607
607
|
...(override.subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions: override.subagentOnlyExtensions === false ? false : [...override.subagentOnlyExtensions] } : {}),
|
|
608
608
|
...(override.completionGuard !== undefined ? { completionGuard: override.completionGuard } : {}),
|
|
@@ -738,6 +738,17 @@ function parseOverrideStringArrayOrFalse(
|
|
|
738
738
|
return items;
|
|
739
739
|
}
|
|
740
740
|
|
|
741
|
+
function parseToolsOverride(
|
|
742
|
+
value: unknown,
|
|
743
|
+
meta: { filePath: string; name: string },
|
|
744
|
+
): BuiltinAgentOverrideConfig["tools"] | undefined {
|
|
745
|
+
if (typeof value === "string" && value.trim() === "inherit") return "inherit";
|
|
746
|
+
if (value === undefined || value === false || Array.isArray(value)) {
|
|
747
|
+
return parseOverrideStringArrayOrFalse(value, { ...meta, field: "tools" });
|
|
748
|
+
}
|
|
749
|
+
throw new Error(`Builtin override '${meta.name}' in '${meta.filePath}' has invalid 'tools'; expected an array of strings, "inherit", or false.`);
|
|
750
|
+
}
|
|
751
|
+
|
|
741
752
|
function parseBuiltinOverrideEntry(
|
|
742
753
|
name: string,
|
|
743
754
|
value: unknown,
|
|
@@ -845,7 +856,7 @@ function parseBuiltinOverrideEntry(
|
|
|
845
856
|
const skills = parseOverrideStringArrayOrFalse(input.skills, { filePath, name, field: "skills" });
|
|
846
857
|
if (skills !== undefined) override.skills = skills;
|
|
847
858
|
|
|
848
|
-
const tools =
|
|
859
|
+
const tools = parseToolsOverride(input.tools, { filePath, name });
|
|
849
860
|
if (tools !== undefined) override.tools = tools;
|
|
850
861
|
|
|
851
862
|
const extensions = parseOverrideStringArrayOrFalse(input.extensions, { filePath, name, field: "extensions" });
|
|
@@ -1004,6 +1015,17 @@ function applySubagentDefaults(
|
|
|
1004
1015
|
);
|
|
1005
1016
|
}
|
|
1006
1017
|
|
|
1018
|
+
function applyToolsOverride(target: AgentConfig, toolsOverride: string[] | false | "inherit"): void {
|
|
1019
|
+
if (toolsOverride === "inherit") {
|
|
1020
|
+
delete target.tools;
|
|
1021
|
+
delete target.mcpDirectTools;
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
const { tools, mcpDirectTools } = splitToolList(toolsOverride === false ? [] : toolsOverride);
|
|
1025
|
+
if (tools === undefined) delete target.tools; else target.tools = tools;
|
|
1026
|
+
if (mcpDirectTools === undefined) delete target.mcpDirectTools; else target.mcpDirectTools = mcpDirectTools;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1007
1029
|
function applyBuiltinOverride(
|
|
1008
1030
|
agent: AgentConfig,
|
|
1009
1031
|
override: BuiltinAgentOverrideConfig,
|
|
@@ -1015,7 +1037,10 @@ function applyBuiltinOverride(
|
|
|
1015
1037
|
};
|
|
1016
1038
|
|
|
1017
1039
|
if (override.description !== undefined) next.description = override.description;
|
|
1018
|
-
if (override.model !== undefined) {
|
|
1040
|
+
if (override.model !== undefined) {
|
|
1041
|
+
if (override.model === false) delete next.model; else next.model = override.model;
|
|
1042
|
+
delete next.modelSource;
|
|
1043
|
+
}
|
|
1019
1044
|
if (override.fallbackModels !== undefined) { if (override.fallbackModels === false) delete next.fallbackModels; else next.fallbackModels = [...override.fallbackModels]; }
|
|
1020
1045
|
if (override.thinking !== undefined) { if (override.thinking === false) delete next.thinking; else next.thinking = override.thinking; }
|
|
1021
1046
|
if (override.systemPromptMode !== undefined) next.systemPromptMode = override.systemPromptMode;
|
|
@@ -1026,11 +1051,7 @@ function applyBuiltinOverride(
|
|
|
1026
1051
|
if (override.disabled !== undefined) next.disabled = override.disabled;
|
|
1027
1052
|
if (override.systemPrompt !== undefined) next.systemPrompt = override.systemPrompt;
|
|
1028
1053
|
if (override.skills !== undefined) { if (override.skills === false) delete next.skills; else next.skills = [...override.skills]; }
|
|
1029
|
-
if (override.tools !== undefined)
|
|
1030
|
-
const { tools, mcpDirectTools } = splitToolList(override.tools === false ? [] : override.tools);
|
|
1031
|
-
if (tools === undefined) delete next.tools; else next.tools = tools;
|
|
1032
|
-
if (mcpDirectTools === undefined) delete next.mcpDirectTools; else next.mcpDirectTools = mcpDirectTools;
|
|
1033
|
-
}
|
|
1054
|
+
if (override.tools !== undefined) applyToolsOverride(next, override.tools);
|
|
1034
1055
|
if (override.extensions !== undefined) { if (override.extensions === false) delete next.extensions; else next.extensions = [...override.extensions]; }
|
|
1035
1056
|
if (override.subagentOnlyExtensions !== undefined) { if (override.subagentOnlyExtensions === false) delete next.subagentOnlyExtensions; else next.subagentOnlyExtensions = [...override.subagentOnlyExtensions]; }
|
|
1036
1057
|
if (override.completionGuard !== undefined) next.completionGuard = override.completionGuard;
|
|
@@ -1133,8 +1154,11 @@ function applyCustomAgentOverride(
|
|
|
1133
1154
|
mutable().description = override.description;
|
|
1134
1155
|
anyFilled = true;
|
|
1135
1156
|
}
|
|
1136
|
-
if (override.model !== undefined) {
|
|
1137
|
-
|
|
1157
|
+
if (override.model !== undefined && !agentHasFrontmatterField(agent, "model")) {
|
|
1158
|
+
const target = mutable();
|
|
1159
|
+
if (override.model === false) delete target.model; else target.model = override.model;
|
|
1160
|
+
delete target.modelSource;
|
|
1161
|
+
anyFilled = true;
|
|
1138
1162
|
}
|
|
1139
1163
|
if (override.fallbackModels !== undefined) {
|
|
1140
1164
|
fill(
|
|
@@ -1169,10 +1193,7 @@ function applyCustomAgentOverride(
|
|
|
1169
1193
|
fill("skills", ["skill", "skills"], override.skills === false ? undefined : [...override.skills]);
|
|
1170
1194
|
}
|
|
1171
1195
|
if (override.tools !== undefined && !agentHasFrontmatterField(agent, "tools")) {
|
|
1172
|
-
|
|
1173
|
-
const target = mutable();
|
|
1174
|
-
if (tools === undefined) delete target.tools; else target.tools = tools;
|
|
1175
|
-
if (mcpDirectTools === undefined) delete target.mcpDirectTools; else target.mcpDirectTools = mcpDirectTools;
|
|
1196
|
+
applyToolsOverride(mutable(), override.tools);
|
|
1176
1197
|
anyFilled = true;
|
|
1177
1198
|
}
|
|
1178
1199
|
if (override.extensions !== undefined) {
|
package/src/api/preflight.ts
CHANGED
|
@@ -285,7 +285,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
285
285
|
return { ok: false, code: "denied_required_tool", message, diagnostics };
|
|
286
286
|
}
|
|
287
287
|
const artifactsEnabled = input.artifacts !== false;
|
|
288
|
-
const artifactsDir = artifactsEnabled ? getArtifactsDir(input.parentSessionFile ?? null, effectiveCwd, input.artifactDir
|
|
288
|
+
const artifactsDir = artifactsEnabled ? getArtifactsDir(input.parentSessionFile ?? null, effectiveCwd, input.artifactDir) : undefined;
|
|
289
289
|
const artifactPaths = artifactsDir ? getArtifactPaths(artifactsDir, runId, agent.name, 0) : undefined;
|
|
290
290
|
const outputPath = resolveSingleOutputPath(behavior.output, effectiveCwd, effectiveCwd, artifactsDir ? path.join(artifactsDir, "outputs", runId) : undefined);
|
|
291
291
|
const sessionRoot = input.sessionDir ? path.resolve(input.sessionDir) : input.sessionRoot ? path.join(path.resolve(input.sessionRoot), runId) : undefined;
|
package/src/extension/config.ts
CHANGED
|
@@ -46,6 +46,25 @@ function validateArtifactConfig(value: unknown): void {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
function validateMainWindowRendererConfig(value: unknown): void {
|
|
50
|
+
if (value === undefined) return;
|
|
51
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.mainWindowRenderer must be a JSON object");
|
|
52
|
+
const rendererConfig = value as Record<string, unknown>;
|
|
53
|
+
if (rendererConfig.horizontalSpacing !== undefined
|
|
54
|
+
&& (typeof rendererConfig.horizontalSpacing !== "number"
|
|
55
|
+
|| !Number.isInteger(rendererConfig.horizontalSpacing)
|
|
56
|
+
|| rendererConfig.horizontalSpacing < 0
|
|
57
|
+
|| rendererConfig.horizontalSpacing > 4)) {
|
|
58
|
+
throw new Error("config.mainWindowRenderer.horizontalSpacing must be an integer from 0 to 4");
|
|
59
|
+
}
|
|
60
|
+
if (rendererConfig.compactResultMaxLines !== undefined
|
|
61
|
+
&& (typeof rendererConfig.compactResultMaxLines !== "number"
|
|
62
|
+
|| !Number.isInteger(rendererConfig.compactResultMaxLines)
|
|
63
|
+
|| rendererConfig.compactResultMaxLines < 1)) {
|
|
64
|
+
throw new Error("config.mainWindowRenderer.compactResultMaxLines must be a positive integer");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
49
68
|
function validateConfig(config: Record<string, unknown>): void {
|
|
50
69
|
if (config.artifactDir !== undefined && !ARTIFACT_DIR_PREFERENCES.has(config.artifactDir as ArtifactDirPreference)) {
|
|
51
70
|
throw new Error(`config.artifactDir must be "project", "session", or "temp"`);
|
|
@@ -53,12 +72,19 @@ function validateConfig(config: Record<string, unknown>): void {
|
|
|
53
72
|
if (config.legacyChainControls !== undefined && typeof config.legacyChainControls !== "boolean") {
|
|
54
73
|
throw new Error("config.legacyChainControls must be a boolean");
|
|
55
74
|
}
|
|
75
|
+
if (config.maxActiveAsyncRunsPerSession !== undefined
|
|
76
|
+
&& (typeof config.maxActiveAsyncRunsPerSession !== "number"
|
|
77
|
+
|| !Number.isInteger(config.maxActiveAsyncRunsPerSession)
|
|
78
|
+
|| config.maxActiveAsyncRunsPerSession < 0)) {
|
|
79
|
+
throw new Error("config.maxActiveAsyncRunsPerSession must be a non-negative integer");
|
|
80
|
+
}
|
|
56
81
|
validateMissionStoreConfig(config.missions);
|
|
57
82
|
validateAuthorityPolicy(config.authorityPolicy);
|
|
58
83
|
validatePermissionConfig(config.permissions);
|
|
59
84
|
validateScheduledRunsConfig(config.scheduledRuns);
|
|
60
85
|
validateFleetKeybindingsConfig(config.fleetKeybindings);
|
|
61
86
|
validateArtifactConfig(config.artifactConfig);
|
|
87
|
+
validateMainWindowRendererConfig(config.mainWindowRenderer);
|
|
62
88
|
}
|
|
63
89
|
|
|
64
90
|
export function getConfigPath(): string {
|
package/src/extension/doctor.ts
CHANGED
|
@@ -3,6 +3,8 @@ 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";
|
|
7
|
+
import { decodeRunFanoutBudgetDescriptor, formatRunFanoutBudget, getRunFanoutBudgetSnapshot, RUN_FANOUT_BUDGET_ENV } from "../runs/shared/run-fanout-budget.ts";
|
|
6
8
|
import { diagnoseIntercomBridge, type IntercomBridgeDiagnostic } from "../intercom/intercom-bridge.ts";
|
|
7
9
|
import { discoverAvailableSkills, type SkillSource } from "../agents/skills.ts";
|
|
8
10
|
import {
|
|
@@ -11,6 +13,8 @@ import {
|
|
|
11
13
|
TEMP_ROOT_DIR,
|
|
12
14
|
type ExtensionConfig,
|
|
13
15
|
type SubagentState,
|
|
16
|
+
normalizeMaxSubagentSpawnsPerRun,
|
|
17
|
+
resolveMaxSubagentSpawnsPerRun,
|
|
14
18
|
} from "../shared/types.ts";
|
|
15
19
|
|
|
16
20
|
interface DoctorPaths {
|
|
@@ -175,6 +179,36 @@ function formatSpawnBudgetSection(input: DoctorReportInput): string[] {
|
|
|
175
179
|
];
|
|
176
180
|
}
|
|
177
181
|
|
|
182
|
+
function formatRunFanoutSection(input: DoctorReportInput): string[] {
|
|
183
|
+
try {
|
|
184
|
+
const inherited = decodeRunFanoutBudgetDescriptor(process.env[RUN_FANOUT_BUDGET_ENV]);
|
|
185
|
+
if (inherited) {
|
|
186
|
+
return [`- usage: ${formatRunFanoutBudget(getRunFanoutBudgetSnapshot(inherited)).replace(/^Run fan-out: /, "")}`, `- root run: ${inherited.rootRunId}`, "- reset boundary: cumulative claims are never released; a new top-level run creates a new budget"];
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
return [`- inherited budget: invalid — ${errorText(error)}`];
|
|
190
|
+
}
|
|
191
|
+
const configured = resolveMaxSubagentSpawnsPerRun(input.config.maxSubagentSpawnsPerRun);
|
|
192
|
+
const source = normalizeMaxSubagentSpawnsPerRun(process.env.PI_SUBAGENT_MAX_SPAWNS_PER_RUN) !== undefined
|
|
193
|
+
? "environment"
|
|
194
|
+
: normalizeMaxSubagentSpawnsPerRun(input.config.maxSubagentSpawnsPerRun) !== undefined ? "config" : "default";
|
|
195
|
+
return [`- configured limit: ${configured} (${source})`, "- usage: available after a run starts", "- reset boundary: cumulative claims are never released; a new top-level run creates a new budget"];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function formatActiveAsyncCapacitySection(input: DoctorReportInput): string[] {
|
|
199
|
+
const limit = resolveMaxActiveAsyncRunsPerSession(input.config.maxActiveAsyncRunsPerSession);
|
|
200
|
+
const sessionId = input.currentSessionId ?? input.state.currentSessionId;
|
|
201
|
+
const snapshot = sessionId
|
|
202
|
+
? getActiveAsyncCapacitySnapshot(sessionId, limit, { liveWorkflowRunIds: new Set(input.state.workflowControllers?.keys() ?? []) })
|
|
203
|
+
: { used: 0, limit: limit ?? 0 };
|
|
204
|
+
input.state.activeAsyncCapacity = snapshot;
|
|
205
|
+
return [
|
|
206
|
+
`- usage: ${snapshot.used}/${snapshot.limit || "unlimited"} used`,
|
|
207
|
+
"- scope: top-level async runs in the current parent session; foreground and nested workflow children are not charged again",
|
|
208
|
+
"- release: terminal logical state plus verified process exit; missing or unknown cleanup proof retains capacity",
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
|
|
178
212
|
function formatPermissionSystemSection(): string[] {
|
|
179
213
|
const lines: string[] = [];
|
|
180
214
|
const parentSession = process.env["PI_SUBAGENT_PARENT_SESSION"] ?? "";
|
|
@@ -215,6 +249,12 @@ export function buildDoctorReport(input: DoctorReportInput): string {
|
|
|
215
249
|
"Spawn budget",
|
|
216
250
|
...formatSpawnBudgetSection(input),
|
|
217
251
|
"",
|
|
252
|
+
"Run fan-out budget",
|
|
253
|
+
...formatRunFanoutSection(input),
|
|
254
|
+
"",
|
|
255
|
+
"Active async capacity",
|
|
256
|
+
...formatActiveAsyncCapacitySection(input),
|
|
257
|
+
"",
|
|
218
258
|
"Permission system",
|
|
219
259
|
...formatPermissionSystemSection(),
|
|
220
260
|
"",
|