pi-subagents 0.47.0 → 0.48.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 +37 -0
- package/README.md +2 -0
- package/agents/reviewer.md +3 -4
- package/docs/configuration.md +46 -2
- package/docs/observability.md +1 -1
- package/docs/tool-reference.md +1 -1
- package/package.json +1 -1
- package/src/agents/agents.ts +9 -3
- package/src/extension/config.ts +16 -0
- package/src/extension/doctor.ts +40 -0
- package/src/extension/index.ts +30 -11
- package/src/extension/public-execution.ts +5 -0
- package/src/extension/rpc.ts +7 -9
- package/src/extension/schemas.ts +4 -4
- package/src/intercom/intercom-bridge.ts +4 -1
- package/src/intercom/native-supervisor-channel.ts +102 -4
- package/src/missions/lifecycle.ts +4 -7
- package/src/missions/store.ts +4 -4
- package/src/missions/workflow-state.ts +6 -2
- package/src/runs/background/active-async-capacity.ts +374 -0
- package/src/runs/background/active-run-index.ts +46 -0
- package/src/runs/background/async-execution.ts +117 -29
- package/src/runs/background/async-job-tracker.ts +279 -134
- package/src/runs/background/async-resume.ts +7 -1
- package/src/runs/background/async-status.ts +33 -4
- package/src/runs/background/chain-append.ts +33 -15
- package/src/runs/background/control-channel.ts +55 -17
- package/src/runs/background/owned-process-tree.ts +104 -0
- package/src/runs/background/process-terminal.ts +17 -3
- package/src/runs/background/result-watcher.ts +87 -6
- package/src/runs/background/run-status.ts +23 -1
- package/src/runs/background/scheduled-runs.ts +23 -0
- package/src/runs/background/stale-run-reconciler.ts +10 -3
- package/src/runs/background/subagent-runner.ts +75 -33
- package/src/runs/foreground/async-dismiss-action.ts +85 -0
- package/src/runs/foreground/async-steering-action.ts +6 -7
- 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 +171 -0
- package/src/runs/foreground/subagent-executor.ts +648 -195
- package/src/runs/shared/acceptance.ts +13 -4
- package/src/runs/shared/llm-intent-arbiter.ts +286 -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/subagent-prompt-runtime.ts +24 -5
- package/src/runs/shared/task-intent.ts +19 -3
- package/src/runs/shared/worktree.ts +17 -5
- package/src/shared/artifacts.ts +1 -1
- package/src/shared/file-coalescer.ts +9 -0
- package/src/shared/types.ts +97 -1
- package/src/shared/utils.ts +3 -1
- package/src/tui/fleet-status.ts +7 -5
- package/src/tui/fleet.ts +225 -12
- package/src/workflows/scripted-workflow.ts +26 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,15 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.48.0] - 2026-08-13
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Add a durable per-run child fan-out budget with a default cap of 64 across static, dynamic, workflow, and nested child admissions. Thanks to @asjer for #1031.
|
|
9
|
+
- Add an opt-in per-session cap for concurrently active top-level async runs, with atomic admission, resume transfer, status/Fleet/RPC/doctor visibility, and release gated by the verified process-terminal behavior from #1030. Thanks to @asjer for #1029.
|
|
10
|
+
- Add a live Prompt Audit drawer to Fleet for current-session foreground children. Prompt text is visible in the drawer, kept outside serializable Fleet state, and redacted from foreground input, transcript, metadata, result, progress, and run-history artifacts (#1021).
|
|
11
|
+
- Add a global `timeoutMs` config option that sets the default run deadline for single, parallel, and chain launches (foreground, plus plain single-agent async) when neither the call nor the selected agent provides a timeout. It reaches parallel (`tasks: [...]`) and chain launches, which never adopt an agent's frontmatter `timeoutMs` (that default applies to single-agent launches only), so a long fan-out no longer falls back to the built-in 30-minute default and gets killed mid-run. Explicit call `timeoutMs`/`maxRuntimeMs` and agent frontmatter defaults still win; composite async runs stay unbounded at the top level by design. Thanks to @shaharmor for #1018.
|
|
12
|
+
- Add a `PI_SUBAGENT_TASK_DELIVERY` environment setting (`auto` | `file`, default `auto`) controlling how the task text reaches child Pi processes. `file` writes the task to a temp `task.md` referenced as `@<path>` instead of embedding it in argv, for hosts where endpoint protection (EDR) pre-execution command-line scanning denies children whose argv embeds a long natural-language task. Thanks to @yanqianglu for #1028.
|
|
13
|
+
- Escalate startup retries to file task delivery after an unexplained zero-activity `SIGKILL` child exit, so EDR-denied launches self-heal on retry in both foreground and background runs. Thanks to @yanqianglu for #1028.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Open Fleet Prompt Audit with the authored task visible by default and show a short live task summary in the normal Fleet detail pane (#1021).
|
|
17
|
+
- Use full task-text hashes for LLM intent arbiter memoization so same-prefix review and implementation tasks cannot share a cached verdict.
|
|
18
|
+
- Terminate async Pi writers as owned POSIX process groups on stop and timeout, and keep terminal process proof unknown until process-tree exit is verified. Thanks to @asjer for #1030.
|
|
19
|
+
- Explain when a requested mission is scoped to another worktree by naming the current project root and mission directory (#1024).
|
|
20
|
+
- Preserve the configured output reference when explicit acceptance rejects an otherwise completed foreground child, so useful reports remain available (#1023).
|
|
21
|
+
- Reject configured worktree base directories inside the agent extensions directory, including symlink aliases (#1014).
|
|
22
|
+
- Align unnamed intercom fallback orchestrator targets with pi-intercom's 18-character registered presence names so subagents without an explicit session name can reach their orchestrator. Thanks to @mystery4f for #1017.
|
|
23
|
+
- Stop reading hyphenated adjectives like "must-fix items" or "should-fix tests" as implementation intent, which made the completion mutation guard hard-fail read-only review runs with a false "completed without making edits" error. Severity compounds (must|should|needs + dash + verb) are stripped before verb matching across every mutation pattern (incl. update/add/apply/make/do siblings), the acceptance-level write-capability check, and the patch-scope pattern, while CLI flags ("eslint --fix", "prettier --write") and clause-level dashes ("branch—fix it") keep their write intent. Thanks to @MarcusNeufeldt for #1020.
|
|
24
|
+
- Add an optional LLM intent arbiter: when the completion guard is about to hard-fail a run that made no edits, a model decides — from the task text alone, never the child's own report — whether the task actually instructed file changes; only a confident read-only verdict rescues the run, before any failure state is published. Covers single, parallel, and chain foreground runs; enabled by default; set `PI_SUBAGENTS_LLM_INTENT_ARBITER=0` to disable. Thanks to @MarcusNeufeldt for #1020.
|
|
25
|
+
- Tolerate empty-string entries in acceptance-report string-array fields instead of rejecting the whole report. Thanks to @hjiang for #1015.
|
|
26
|
+
- Let single external-cli workflow children ignore inherited Pi models so model-less external runners start instead of failing preflight. Thanks to @twosunnus for #1016.
|
|
27
|
+
|
|
28
|
+
## [0.47.1] - 2026-08-12
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- Honor configured artifact cleanup retention days and let `0` disable artifact cleanup. Thanks to @elecnix for #1012.
|
|
32
|
+
- Add a display-only dismiss action for reload-recovered running workflows without claiming or attempting to stop their work (#1010).
|
|
33
|
+
- Stop the bundled reviewer from inheriting chain-only plan/progress reads in ad-hoc review runs. Thanks to @Ostii for #1000.
|
|
34
|
+
- Remove mutation-capable tools from the bundled reviewer so read-only review lanes have a hard launch-time tool boundary (#1007).
|
|
35
|
+
- Show the requested child agent in workflow started trace entries. Thanks to @albertgwo for #1001.
|
|
36
|
+
|
|
5
37
|
## [0.47.0] - 2026-08-11
|
|
6
38
|
|
|
7
39
|
### Changed
|
|
40
|
+
- Avoid fully parsing stale cross-session result files during watcher recovery and reduce healthy watcher safety scans.
|
|
41
|
+
- Index active async runs so status restoration no longer scans all historical run directories.
|
|
42
|
+
- Refresh active async job state from filesystem events while reserving polling for slow liveness repair.
|
|
8
43
|
- Add optional strict model-scope enforcement that rejects inherited and fallback models outside the configured allowlist. Thanks to @antonioc-cl for #995.
|
|
9
44
|
- Trim legacy chain-control schema fields and guidance by default, saving 1,319 `o200k_base` tokens from the serialized default tool schema plus description versus `legacyChainControls: true`. Thanks to @tajquitgenius for #977.
|
|
10
45
|
- Move project-scoped pi-subagents storage from `.pi-subagents/` to `.pi/subagents/` for cleaner project roots. Thanks to @yceachan for #971.
|
|
11
46
|
- Clarify the accepted mission launch object contract for tool callers.
|
|
12
47
|
- Reduce repeated async status parsing, workflow trace projection, and constrained widget rendering work.
|
|
48
|
+
- Coalesce rapid running-status writes while keeping terminal and attention status changes durable immediately.
|
|
13
49
|
- Keep parsed async statuses cached beyond 50 runs while preserving per-read freshness checks. Thanks to @bcanvural for #982.
|
|
50
|
+
- Prefer native control inbox watchers over per-process 250 ms polling, with polling retained as a fallback.
|
|
14
51
|
|
|
15
52
|
### Fixed
|
|
16
53
|
- Omit missing configured read files from child task instructions.
|
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/agents/reviewer.md
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: reviewer
|
|
3
3
|
description: Versatile review specialist for code diffs, plans, proposed solutions, codebase health, and PR/issue validation
|
|
4
|
-
tools: read, grep, find, ls,
|
|
4
|
+
tools: read, grep, find, ls, intercom
|
|
5
5
|
thinking: high
|
|
6
6
|
systemPromptMode: replace
|
|
7
7
|
inheritProjectContext: true
|
|
8
8
|
inheritSkills: false
|
|
9
|
-
defaultReads: plan.md, progress.md
|
|
10
9
|
---
|
|
11
10
|
|
|
12
11
|
You are a disciplined review subagent. Your job is to inspect, evaluate, and report findings with evidence. You do not guess; you verify from the code, tests, docs, or requirements.
|
|
@@ -51,9 +50,9 @@ Review a PR or issue by understanding the context, then verifying:
|
|
|
51
50
|
- Tests and docs are updated as needed.
|
|
52
51
|
|
|
53
52
|
## Working rules
|
|
54
|
-
- Read the plan
|
|
53
|
+
- Read the relevant files first. Read plan and progress when the task supplies them.
|
|
55
54
|
- Repo-local `progress.md` files are allowed scratch/memory files. Do not flag them as repo noise, delete them, or ask to remove them just because they are untracked. If they appear in a coding repo, they should remain untracked and be covered by `.gitignore`.
|
|
56
|
-
-
|
|
55
|
+
- Do not use shell commands or write files. Report any test or Git command that a supervisor must run.
|
|
57
56
|
- Do not invent issues. Only report problems you can justify from evidence.
|
|
58
57
|
- Prefer small corrective edits over broad rewrites.
|
|
59
58
|
- If everything looks good, say so plainly.
|
package/docs/configuration.md
CHANGED
|
@@ -115,6 +115,18 @@ This is different from `waitTool.enabled=false`, which returns immediately witho
|
|
|
115
115
|
|
|
116
116
|
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
117
|
|
|
118
|
+
## `timeoutMs`
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{ "timeoutMs": 3600000 }
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
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*.
|
|
125
|
+
|
|
126
|
+
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.
|
|
127
|
+
|
|
128
|
+
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.
|
|
129
|
+
|
|
118
130
|
## `globalConcurrencyLimit`
|
|
119
131
|
|
|
120
132
|
```json
|
|
@@ -131,9 +143,31 @@ Caps simultaneously running children inside existing durable legacy multi-child
|
|
|
131
143
|
|
|
132
144
|
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
145
|
|
|
134
|
-
`subagent({ action: "status" })`, fleet status, and `subagent({ action: "doctor" })` expose used, effective limit, remaining capacity, grants, and the remaining grant allowance.
|
|
146
|
+
`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.
|
|
147
|
+
|
|
148
|
+
## `maxSubagentSpawnsPerRun`
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{ "maxSubagentSpawnsPerRun": 64 }
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
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`.
|
|
155
|
+
|
|
156
|
+
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`.
|
|
135
157
|
|
|
136
|
-
|
|
158
|
+
## `maxActiveAsyncRunsPerSession`
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{ "maxActiveAsyncRunsPerSession": 4 }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
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.
|
|
165
|
+
|
|
166
|
+
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.
|
|
167
|
+
|
|
168
|
+
This limit bounds current top-level async load. It is separate from cumulative `maxSubagentSpawnsPerSession`, `maxSubagentSpawnsPerRun`, and `globalConcurrencyLimit`.
|
|
169
|
+
|
|
170
|
+
`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
171
|
|
|
138
172
|
## `scheduledRuns`
|
|
139
173
|
|
|
@@ -196,6 +230,16 @@ export PI_SUBAGENT_PI_BINARY=/path/to/pi-or-wrapper
|
|
|
196
230
|
|
|
197
231
|
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
232
|
|
|
233
|
+
## `PI_SUBAGENT_TASK_DELIVERY`
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
export PI_SUBAGENT_TASK_DELIVERY=file # auto | file (default: auto)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
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.
|
|
240
|
+
|
|
241
|
+
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`.
|
|
242
|
+
|
|
199
243
|
## `intercomBridge`
|
|
200
244
|
|
|
201
245
|
```json
|
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
|
|
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
|
@@ -1015,7 +1015,10 @@ function applyBuiltinOverride(
|
|
|
1015
1015
|
};
|
|
1016
1016
|
|
|
1017
1017
|
if (override.description !== undefined) next.description = override.description;
|
|
1018
|
-
if (override.model !== undefined) {
|
|
1018
|
+
if (override.model !== undefined) {
|
|
1019
|
+
if (override.model === false) delete next.model; else next.model = override.model;
|
|
1020
|
+
delete next.modelSource;
|
|
1021
|
+
}
|
|
1019
1022
|
if (override.fallbackModels !== undefined) { if (override.fallbackModels === false) delete next.fallbackModels; else next.fallbackModels = [...override.fallbackModels]; }
|
|
1020
1023
|
if (override.thinking !== undefined) { if (override.thinking === false) delete next.thinking; else next.thinking = override.thinking; }
|
|
1021
1024
|
if (override.systemPromptMode !== undefined) next.systemPromptMode = override.systemPromptMode;
|
|
@@ -1133,8 +1136,11 @@ function applyCustomAgentOverride(
|
|
|
1133
1136
|
mutable().description = override.description;
|
|
1134
1137
|
anyFilled = true;
|
|
1135
1138
|
}
|
|
1136
|
-
if (override.model !== undefined) {
|
|
1137
|
-
|
|
1139
|
+
if (override.model !== undefined && !agentHasFrontmatterField(agent, "model")) {
|
|
1140
|
+
const target = mutable();
|
|
1141
|
+
if (override.model === false) delete target.model; else target.model = override.model;
|
|
1142
|
+
delete target.modelSource;
|
|
1143
|
+
anyFilled = true;
|
|
1138
1144
|
}
|
|
1139
1145
|
if (override.fallbackModels !== undefined) {
|
|
1140
1146
|
fill(
|
package/src/extension/config.ts
CHANGED
|
@@ -37,6 +37,15 @@ function validateFleetKeybindingsConfig(value: unknown): void {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function validateArtifactConfig(value: unknown): void {
|
|
41
|
+
if (value === undefined) return;
|
|
42
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.artifactConfig must be a JSON object");
|
|
43
|
+
const cleanupDays = (value as Record<string, unknown>).cleanupDays;
|
|
44
|
+
if (cleanupDays !== undefined && (typeof cleanupDays !== "number" || !Number.isInteger(cleanupDays) || cleanupDays < 0)) {
|
|
45
|
+
throw new Error("config.artifactConfig.cleanupDays must be a non-negative integer");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
40
49
|
function validateConfig(config: Record<string, unknown>): void {
|
|
41
50
|
if (config.artifactDir !== undefined && !ARTIFACT_DIR_PREFERENCES.has(config.artifactDir as ArtifactDirPreference)) {
|
|
42
51
|
throw new Error(`config.artifactDir must be "project", "session", or "temp"`);
|
|
@@ -44,11 +53,18 @@ function validateConfig(config: Record<string, unknown>): void {
|
|
|
44
53
|
if (config.legacyChainControls !== undefined && typeof config.legacyChainControls !== "boolean") {
|
|
45
54
|
throw new Error("config.legacyChainControls must be a boolean");
|
|
46
55
|
}
|
|
56
|
+
if (config.maxActiveAsyncRunsPerSession !== undefined
|
|
57
|
+
&& (typeof config.maxActiveAsyncRunsPerSession !== "number"
|
|
58
|
+
|| !Number.isInteger(config.maxActiveAsyncRunsPerSession)
|
|
59
|
+
|| config.maxActiveAsyncRunsPerSession < 0)) {
|
|
60
|
+
throw new Error("config.maxActiveAsyncRunsPerSession must be a non-negative integer");
|
|
61
|
+
}
|
|
47
62
|
validateMissionStoreConfig(config.missions);
|
|
48
63
|
validateAuthorityPolicy(config.authorityPolicy);
|
|
49
64
|
validatePermissionConfig(config.permissions);
|
|
50
65
|
validateScheduledRunsConfig(config.scheduledRuns);
|
|
51
66
|
validateFleetKeybindingsConfig(config.fleetKeybindings);
|
|
67
|
+
validateArtifactConfig(config.artifactConfig);
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
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
|
"",
|
package/src/extension/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { SubagentFleetStatus, resolveFleetViewPlacement } from "../tui/fleet-sta
|
|
|
30
30
|
import { createSubagentParamsSchema } from "./schemas.ts";
|
|
31
31
|
import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
32
32
|
import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
|
|
33
|
+
import { getActiveAsyncCapacitySnapshot, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
|
|
33
34
|
import { createResultWatcher } from "../runs/background/result-watcher.ts";
|
|
34
35
|
import { createScheduledRunManager } from "../runs/background/scheduled-runs.ts";
|
|
35
36
|
import { registerSlashCommands } from "../slash/slash-commands.ts";
|
|
@@ -65,6 +66,7 @@ import {
|
|
|
65
66
|
SLASH_TEXT_RESULT_TYPE,
|
|
66
67
|
SUBAGENT_ASYNC_COMPLETE_EVENT,
|
|
67
68
|
SUBAGENT_ASYNC_STARTED_EVENT,
|
|
69
|
+
SUBAGENT_PROCESS_TERMINAL_EVENT,
|
|
68
70
|
SUBAGENT_CONTROL_EVENT,
|
|
69
71
|
SUBAGENT_STEERING_NOTICE_EVENT,
|
|
70
72
|
WIDGET_KEY,
|
|
@@ -366,7 +368,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
366
368
|
const asyncWidgetEnabled = config.asyncWidget !== false;
|
|
367
369
|
const summaryInlineToolDisplay = config.inlineToolDisplay === "summary";
|
|
368
370
|
const tempArtifactsDir = getArtifactsDir(null);
|
|
369
|
-
|
|
371
|
+
const artifactCleanupDays = config.artifactConfig?.cleanupDays ?? DEFAULT_ARTIFACT_CONFIG.cleanupDays;
|
|
372
|
+
cleanupAllArtifactDirs(artifactCleanupDays);
|
|
370
373
|
|
|
371
374
|
const state: SubagentState = {
|
|
372
375
|
baseCwd: "",
|
|
@@ -383,6 +386,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
383
386
|
granted: 0,
|
|
384
387
|
grantHistory: [],
|
|
385
388
|
},
|
|
389
|
+
activeAsyncCapacity: { used: 0, limit: resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession) ?? 0 },
|
|
386
390
|
asyncJobs: new Map(),
|
|
387
391
|
fleetJobs: new Map(),
|
|
388
392
|
foregroundRuns: new Map(),
|
|
@@ -430,6 +434,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
430
434
|
},
|
|
431
435
|
resolveCapabilityCeiling: (sessionId) => resolveCurrentSubagentCapabilityCeiling(sessionId),
|
|
432
436
|
});
|
|
437
|
+
const { ensurePoller, refreshWidget, handleStarted, handleComplete, resetJobs, restoreActiveJobs, dispose: disposeAsyncJobTracker } = createAsyncJobTracker(pi, state, DIRS.async, {
|
|
438
|
+
widgetEnabled: asyncWidgetEnabled,
|
|
439
|
+
});
|
|
433
440
|
const { startResultWatcher, primeExistingResults, stopResultWatcher } = createResultWatcher(
|
|
434
441
|
pi,
|
|
435
442
|
state,
|
|
@@ -438,6 +445,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
438
445
|
{
|
|
439
446
|
notifier: completionNotifier,
|
|
440
447
|
observeCompletion: (result) => scheduledRunManager.handleAsyncCompletion(result),
|
|
448
|
+
observedCompletionRunIds: () => scheduledRunManager.observedCompletionRunIds(),
|
|
441
449
|
deliverIntercomResults: config.intercomBridge?.resultDelivery === true,
|
|
442
450
|
},
|
|
443
451
|
);
|
|
@@ -451,16 +459,10 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
451
459
|
supervisorChannel.dispose();
|
|
452
460
|
waitSubscriptionManager.dispose();
|
|
453
461
|
fleetStatus?.dispose();
|
|
454
|
-
|
|
455
|
-
clearInterval(state.poller);
|
|
456
|
-
state.poller = null;
|
|
457
|
-
}
|
|
462
|
+
disposeAsyncJobTracker();
|
|
458
463
|
};
|
|
459
464
|
globalStore[runtimeCleanupStoreKey] = runtimeCleanup;
|
|
460
465
|
|
|
461
|
-
const { ensurePoller, refreshWidget, handleStarted, handleComplete, resetJobs, restoreActiveJobs } = createAsyncJobTracker(pi, state, DIRS.async, {
|
|
462
|
-
widgetEnabled: asyncWidgetEnabled,
|
|
463
|
-
});
|
|
464
466
|
const executor = createSubagentExecutor({
|
|
465
467
|
pi,
|
|
466
468
|
state,
|
|
@@ -678,12 +680,17 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
678
680
|
};
|
|
679
681
|
const asyncCompleteHandler = (payload: unknown) => {
|
|
680
682
|
handleComplete(payload);
|
|
683
|
+
refreshActiveAsyncCapacity();
|
|
681
684
|
scheduledRunManager.handleAsyncCompletion(payload);
|
|
682
685
|
fleetStatus?.refresh();
|
|
683
686
|
};
|
|
684
687
|
const eventUnsubscribes = [
|
|
685
688
|
pi.events.on(SUBAGENT_ASYNC_STARTED_EVENT, asyncStartedHandler),
|
|
686
689
|
pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, asyncCompleteHandler),
|
|
690
|
+
pi.events.on(SUBAGENT_PROCESS_TERMINAL_EVENT, () => {
|
|
691
|
+
refreshActiveAsyncCapacity();
|
|
692
|
+
fleetStatus?.refresh();
|
|
693
|
+
}),
|
|
687
694
|
pi.events.on(SUBAGENT_CONTROL_EVENT, controlEventHandler),
|
|
688
695
|
pi.events.on(SUBAGENT_STEERING_NOTICE_EVENT, steeringNoticeHandler),
|
|
689
696
|
herdrStatusBridge.dispose,
|
|
@@ -708,7 +715,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
708
715
|
try {
|
|
709
716
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
710
717
|
if (sessionFile) {
|
|
711
|
-
cleanupOldArtifacts(getArtifactsDir(sessionFile),
|
|
718
|
+
cleanupOldArtifacts(getArtifactsDir(sessionFile), artifactCleanupDays);
|
|
712
719
|
}
|
|
713
720
|
} catch {
|
|
714
721
|
// Cleanup failures should not block session lifecycle events.
|
|
@@ -729,6 +736,18 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
729
736
|
fleetStatus?.refresh();
|
|
730
737
|
};
|
|
731
738
|
|
|
739
|
+
const refreshActiveAsyncCapacity = () => {
|
|
740
|
+
if (!state.currentSessionId) {
|
|
741
|
+
state.activeAsyncCapacity = { used: 0, limit: resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession) ?? 0 };
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(
|
|
745
|
+
state.currentSessionId,
|
|
746
|
+
resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession),
|
|
747
|
+
{ liveWorkflowRunIds: new Set(state.workflowControllers?.keys() ?? []) },
|
|
748
|
+
);
|
|
749
|
+
};
|
|
750
|
+
|
|
732
751
|
const resetSessionState = (ctx: ExtensionContext, recovering: boolean) => {
|
|
733
752
|
state.widgetsSuspended = false;
|
|
734
753
|
state.baseCwd = ctx.cwd;
|
|
@@ -754,6 +773,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
754
773
|
}
|
|
755
774
|
}
|
|
756
775
|
state.lastUiContext = ctx;
|
|
776
|
+
refreshActiveAsyncCapacity();
|
|
757
777
|
cleanupSessionArtifacts(ctx);
|
|
758
778
|
state.foregroundControls.clear();
|
|
759
779
|
state.lastForegroundControlId = null;
|
|
@@ -823,8 +843,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
823
843
|
delete globalStore[eventUnsubscribeStoreKey];
|
|
824
844
|
}
|
|
825
845
|
scheduledRunManager.stop();
|
|
826
|
-
|
|
827
|
-
state.poller = null;
|
|
846
|
+
disposeAsyncJobTracker();
|
|
828
847
|
for (const timer of state.cleanupTimers.values()) {
|
|
829
848
|
clearTimeout(timer);
|
|
830
849
|
}
|
|
@@ -11,6 +11,8 @@ export interface PublicSubagentExecutionParams {
|
|
|
11
11
|
workflowScript?: unknown;
|
|
12
12
|
resume?: unknown;
|
|
13
13
|
clarify?: unknown;
|
|
14
|
+
runFanoutBudget?: unknown;
|
|
15
|
+
runFanoutAdmitted?: unknown;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export type PublicSubagentExecutionMode = "workflow" | "management";
|
|
@@ -24,6 +26,9 @@ export type PublicSubagentExecutionNormalization<T> =
|
|
|
24
26
|
* Internal runs.run children and structured owned delegation bypass this boundary.
|
|
25
27
|
*/
|
|
26
28
|
export function normalizePublicSubagentExecution<T extends PublicSubagentExecutionParams>(params: T): PublicSubagentExecutionNormalization<T> {
|
|
29
|
+
if (params.runFanoutBudget !== undefined || params.runFanoutAdmitted !== undefined) {
|
|
30
|
+
return { ok: false, error: "Public execution does not accept internal run fan-out fields.", mode: params.workflowScript !== undefined ? "workflow" : "management" };
|
|
31
|
+
}
|
|
27
32
|
const action = params.action;
|
|
28
33
|
if (action !== undefined && (typeof action !== "string" || !action.trim())) {
|
|
29
34
|
return { ok: false, error: "action must be a non-empty management/control action, or omit action and use workflowScript.", mode: "management" };
|
package/src/extension/rpc.ts
CHANGED
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
import { sanitizeDisplayText, truncateDisplayText } from "../shared/display-text.ts";
|
|
20
20
|
import { readStatus } from "../shared/utils.ts";
|
|
21
21
|
import { SubagentParams } from "./schemas.ts";
|
|
22
|
-
import { formatWorkflowJsonPreview } from "../workflows/scripted-workflow.ts";
|
|
23
22
|
import { normalizePublicSubagentExecution } from "./public-execution.ts";
|
|
24
23
|
|
|
25
24
|
export const SUBAGENT_RPC_PROTOCOL_VERSION = 1;
|
|
@@ -91,6 +90,7 @@ export interface SubagentRpcFleetStatus {
|
|
|
91
90
|
entries: SubagentRpcFleetEntry[];
|
|
92
91
|
/** Total active children before the bounded entries window. */
|
|
93
92
|
totalActive: number;
|
|
93
|
+
topLevelAsyncCapacity: { used: number; limit: number };
|
|
94
94
|
omitted: number;
|
|
95
95
|
}
|
|
96
96
|
|
|
@@ -154,7 +154,7 @@ function buildFleetStatus(
|
|
|
154
154
|
}
|
|
155
155
|
if (!state || !authoritativeSessionId || state.currentSessionId !== authoritativeSessionId) {
|
|
156
156
|
keyState.keys.clear();
|
|
157
|
-
return { version: 1, entries: [], totalActive: 0, omitted: 0 };
|
|
157
|
+
return { version: 1, entries: [], totalActive: 0, topLevelAsyncCapacity: { used: 0, limit: 0 }, omitted: 0 };
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
let totalActive = 0;
|
|
@@ -173,7 +173,6 @@ function buildFleetStatus(
|
|
|
173
173
|
effort: child.thinking,
|
|
174
174
|
startedAt: child.startedAt,
|
|
175
175
|
tokens: { input: child.inputTokens ?? 0, output: child.outputTokens ?? 0, total: child.tokens ?? 0 },
|
|
176
|
-
goal: child.description ?? control.description,
|
|
177
176
|
});
|
|
178
177
|
} else {
|
|
179
178
|
addCandidate({
|
|
@@ -183,7 +182,6 @@ function buildFleetStatus(
|
|
|
183
182
|
effort: control.thinking,
|
|
184
183
|
startedAt: control.startedAt,
|
|
185
184
|
tokens: { input: control.inputTokens ?? 0, output: control.outputTokens ?? 0, total: control.tokens ?? 0 },
|
|
186
|
-
goal: control.description,
|
|
187
185
|
});
|
|
188
186
|
}
|
|
189
187
|
}
|
|
@@ -191,13 +189,11 @@ function buildFleetStatus(
|
|
|
191
189
|
if (job.sessionId !== authoritativeSessionId || !activeState(job.status)) continue;
|
|
192
190
|
const startedAt = job.startedAt ?? job.updatedAt;
|
|
193
191
|
if (job.mode === "workflow") {
|
|
194
|
-
const latestEmit = job.workflow?.emits?.length ? formatWorkflowJsonPreview(job.workflow.emits.at(-1), 120) : undefined;
|
|
195
192
|
addCandidate({
|
|
196
193
|
internalKey: `async:${job.asyncId}`,
|
|
197
194
|
agent: "workflow",
|
|
198
195
|
startedAt,
|
|
199
196
|
tokens: job.totalTokens,
|
|
200
|
-
goal: latestEmit !== undefined ? `latest emit: ${latestEmit}` : job.description,
|
|
201
197
|
});
|
|
202
198
|
continue;
|
|
203
199
|
}
|
|
@@ -214,7 +210,6 @@ function buildFleetStatus(
|
|
|
214
210
|
agent: job.mode ?? "subagent",
|
|
215
211
|
startedAt,
|
|
216
212
|
tokens: job.totalTokens,
|
|
217
|
-
goal: job.description,
|
|
218
213
|
});
|
|
219
214
|
continue;
|
|
220
215
|
}
|
|
@@ -230,7 +225,6 @@ function buildFleetStatus(
|
|
|
230
225
|
effort: step.thinking,
|
|
231
226
|
startedAt: step.startedAt ?? startedAt,
|
|
232
227
|
tokens: step.tokens ?? (steps.length === 1 ? job.totalTokens : undefined),
|
|
233
|
-
goal: job.description,
|
|
234
228
|
});
|
|
235
229
|
}
|
|
236
230
|
}
|
|
@@ -271,7 +265,7 @@ function buildFleetStatus(
|
|
|
271
265
|
if (!activeKeys.has(internalKey)) keyState.keys.delete(internalKey);
|
|
272
266
|
}
|
|
273
267
|
const omitted = Math.max(0, totalActive - entries.length);
|
|
274
|
-
return { version: 1, entries, totalActive, omitted };
|
|
268
|
+
return { version: 1, entries, totalActive, topLevelAsyncCapacity: state.activeAsyncCapacity ?? { used: 0, limit: 0 }, omitted };
|
|
275
269
|
}
|
|
276
270
|
|
|
277
271
|
interface RegisterSubagentRpcBridgeOptions {
|
|
@@ -490,6 +484,10 @@ function stopAsyncRun(
|
|
|
490
484
|
throw new SubagentRpcError("not_found", `Async run '${initialRunId}' was not found in the active session.`);
|
|
491
485
|
}
|
|
492
486
|
|
|
487
|
+
if (initialStatus.mode === "workflow" && initialStatus.state === "running") {
|
|
488
|
+
throw new SubagentRpcError("invalid_state", `Workflow ${initialRunId} is not controlled by this extension runtime; reload recovery cannot stop it safely.`);
|
|
489
|
+
}
|
|
490
|
+
|
|
493
491
|
let status;
|
|
494
492
|
try {
|
|
495
493
|
status = reconcileAsyncRun(location.asyncDir, { resultsDir, kill: options.kill, now: options.now }).status;
|
package/src/extension/schemas.ts
CHANGED
|
@@ -263,10 +263,10 @@ const SubagentParamProperties = {
|
|
|
263
263
|
})),
|
|
264
264
|
name: Type.Optional(Type.String({ description: "Human-readable name for action='schedule.create'." })),
|
|
265
265
|
id: Type.Optional(Type.String({
|
|
266
|
-
description: "Run id or prefix for status, interrupt, stop, resume, steer, append-step, approve-checkpoint, reject-checkpoint, mission.attach-run, or the decision id for mission.resolve-decision."
|
|
266
|
+
description: "Run id or prefix for status, interrupt, stop, dismiss, resume, steer, append-step, approve-checkpoint, reject-checkpoint, mission.attach-run, or the decision id for mission.resolve-decision."
|
|
267
267
|
})),
|
|
268
268
|
runId: Type.Optional(Type.String({
|
|
269
|
-
description: "Target run ID for interrupt, stop, resume, steer, append-step, approve-checkpoint, reject-checkpoint, or mission.attach-run. Prefer id for new calls."
|
|
269
|
+
description: "Target run ID for interrupt, stop, dismiss, resume, steer, append-step, approve-checkpoint, reject-checkpoint, or mission.attach-run. Prefer id for new calls."
|
|
270
270
|
})),
|
|
271
271
|
dir: Type.Optional(Type.String({
|
|
272
272
|
description: "Async run directory for action='status', action='stop', action='resume', or action='steer'."
|
|
@@ -324,8 +324,8 @@ const SubagentParamProperties = {
|
|
|
324
324
|
description: "'fresh' or 'fork' to branch from parent session. Explicit context overrides every child in the invocation. If omitted, each requested agent uses its own defaultContext; agents without defaultContext: 'fork' run fresh.",
|
|
325
325
|
})),
|
|
326
326
|
async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
|
|
327
|
-
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "
|
|
328
|
-
maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias timeoutMs. Foreground
|
|
327
|
+
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Timeout. Foreground and single async runs use config timeoutMs, else 30m; async composites have no default parent deadline. Alias maxRuntimeMs." })),
|
|
328
|
+
maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias timeoutMs. Foreground and single async runs use config timeoutMs, else 30m; async composites have no default parent deadline." })),
|
|
329
329
|
turnBudget: Type.Optional(TurnBudgetOverride),
|
|
330
330
|
toolBudget: Type.Optional(ToolBudgetOverride),
|
|
331
331
|
usageBudget: Type.Optional(UsageBudgetOverride),
|
|
@@ -63,7 +63,10 @@ export function resolveIntercomSessionTarget(sessionName: string | undefined, se
|
|
|
63
63
|
if (trimmedName) return trimmedName;
|
|
64
64
|
const fallbackSessionId = intercomSessionId?.trim() || sessionId;
|
|
65
65
|
const normalizedSessionId = fallbackSessionId.startsWith("session-") ? fallbackSessionId.slice("session-".length) : fallbackSessionId;
|
|
66
|
-
|
|
66
|
+
// NOTE: keep slice length in sync with pi-intercom's resolveIntercomPresenceName
|
|
67
|
+
// (index.ts: DEFAULT_UNNAMED_SESSION_ALIAS_PREFIX + slice(0, 18)); mismatched lengths
|
|
68
|
+
// make fallback orchestrator targets unresolvable ("Session not found").
|
|
69
|
+
return `${DEFAULT_INTERCOM_TARGET_PREFIX}-${normalizedSessionId.slice(0, 18)}`;
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
function sanitizeIntercomTargetPart(value: string): string {
|