@selesai/code 0.8.4 → 0.8.6
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 +22 -0
- package/README.md +7 -1
- package/dist/core/slash-commands.js +1 -0
- package/dist/defaults/models.json +46 -55
- package/dist/defaults/settings.json +7 -8
- package/dist/extensions/agent-browser.test.ts +178 -2
- package/dist/extensions/agent-browser.ts +2 -0
- package/dist/extensions/copy-turn.test.ts +229 -0
- package/dist/extensions/copy-turn.ts +11 -0
- package/dist/extensions/grep-app/index.test.ts +356 -1
- package/dist/extensions/grep-app/index.ts +6 -1
- package/dist/extensions/handoff-new.test.ts +351 -161
- package/dist/extensions/handoff-new.ts +3 -0
- package/dist/extensions/inline-skills.test.ts +93 -0
- package/dist/extensions/inline-skills.ts +3 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +26 -3
- package/dist/extensions/pi-subagents/README.md +2 -0
- package/dist/extensions/pi-subagents/agents/builder.md +5 -2
- package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
- package/dist/extensions/pi-subagents/docs/configuration.md +54 -2
- package/dist/extensions/pi-subagents/docs/observability.md +1 -1
- package/dist/extensions/pi-subagents/docs/tool-reference.md +1 -1
- package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
- package/dist/extensions/pi-subagents/package-lock.json +2 -2
- package/dist/extensions/pi-subagents/package.json +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +1 -1
- package/dist/extensions/pi-subagents/src/agents/agents.ts +9 -3
- package/dist/extensions/pi-subagents/src/extension/config.ts +12 -0
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +40 -0
- package/dist/extensions/pi-subagents/src/extension/index.ts +104 -14
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +5 -0
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +3 -9
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -1
- package/dist/extensions/pi-subagents/src/missions/lifecycle.ts +4 -7
- package/dist/extensions/pi-subagents/src/missions/store.ts +4 -4
- package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +6 -2
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +374 -0
- package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +9 -5
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +117 -29
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +12 -6
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +14 -5
- package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +33 -15
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +104 -0
- package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +17 -3
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +19 -1
- package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +12 -6
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -3
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +84 -36
- package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +37 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +135 -22
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +12 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -4
- package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +171 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +653 -196
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +13 -4
- package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +286 -0
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +23 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +44 -1
- package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +280 -0
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +4 -2
- package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +19 -3
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +17 -5
- package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +71 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +108 -0
- package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -1
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +7 -5
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +226 -13
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +22 -5
- package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
- package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +87 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +96 -6
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +28 -5
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +8 -5
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +83 -2
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +211 -15
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +41 -3
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +314 -10
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +31 -1
- package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +317 -0
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +21 -2
- package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +16 -1
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +6 -1
- package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +75 -4
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +18 -0
- package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +29 -1
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +31 -5
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +139 -1
- package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +10 -0
- package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +103 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +22 -0
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +2 -1
- package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +171 -0
- package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +30 -4
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/owned-process-tree.test.ts +69 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +83 -0
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +55 -2
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +12 -1
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +7 -7
- package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +121 -0
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +9 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -3
- package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +73 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +10 -1
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +39 -0
- package/dist/extensions/pi-subagents/test/unit/timeout-defaults.test.ts +65 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +45 -0
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +61 -0
- package/dist/extensions/ponytail/index.js +11 -0
- package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
- package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
- package/dist/extensions/ponytail/test/extension.test.js +274 -140
- package/dist/extensions/ponytail/test/helpers.test.js +280 -92
- package/dist/extensions/question/index.ts +18 -0
- package/dist/extensions/question/question-list.ts +22 -0
- package/dist/extensions/question/tests/batch.test.ts +103 -65
- package/dist/extensions/question/tests/helpers.test.ts +68 -35
- package/dist/extensions/question/tests/question-list.test.ts +506 -204
- package/dist/extensions/question/tests/row-layout.test.ts +180 -114
- package/dist/extensions/question/tests/schemas.test.ts +42 -19
- package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
- package/dist/extensions/question/tests/wizard.test.ts +771 -50
- package/dist/extensions/rtk.test.ts +180 -1
- package/dist/extensions/tokenin-onboarding.ts +3 -0
- package/dist/extensions/undo.test.ts +720 -0
- package/dist/extensions/undo.ts +6 -0
- package/dist/extensions/web-agent-onboarding.test.ts +415 -0
- package/dist/extensions/workflow/extension.ts +3 -3
- package/dist/extensions/workflow/modes.ts +41 -19
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.js +48 -1
- package/dist/skills/pi-subagents/references/execution-controls.md +1 -1
- package/docs/plans/workflow-autoloop-reference.md +1 -2
- package/docs/plans/workflow-handoff-carryover.md +108 -0
- package/docs/quickstart.md +27 -43
- package/docs/settings.md +4 -0
- package/docs/usage.md +1 -0
- package/package.json +5 -1
|
@@ -22,6 +22,9 @@ function getSkillName(command: SkillCommand): string {
|
|
|
22
22
|
|
|
23
23
|
function getInlineSkillToken(textBeforeCursor: string): string | undefined {
|
|
24
24
|
const match = textBeforeCursor.match(/(?:^|[^a-z0-9_-])\$([a-z0-9-]*)$/i);
|
|
25
|
+
// Group 1 is always present when the regex matches (it can be empty but
|
|
26
|
+
// never undefined), so the ?? fallback is unreachable.
|
|
27
|
+
/* v8 ignore next 1 */
|
|
25
28
|
return match ? match[1] ?? "" : undefined;
|
|
26
29
|
}
|
|
27
30
|
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
## Selesai fork (0.
|
|
5
|
+
## Selesai fork (0.48.0)
|
|
6
6
|
|
|
7
|
-
Selesai vendors pi-subagents 0.
|
|
7
|
+
Selesai vendors pi-subagents 0.48.0 and layers its own branding and additions on top:
|
|
8
8
|
|
|
9
9
|
### Branding
|
|
10
10
|
- Host package renamed from `@earendil-works/pi-coding-agent` to `@selesai/code`.
|
|
@@ -12,7 +12,7 @@ Selesai vendors pi-subagents 0.47.1 and layers its own branding and additions on
|
|
|
12
12
|
- Extension env vars renamed `PI_SUBAGENT*` → `SELESAI_SUBAGENT*` (legacy `PI_SUBAGENT*` values still honored via `src/shared/env.ts`).
|
|
13
13
|
- Home directory is never treated as a project root for subagent config resolution.
|
|
14
14
|
|
|
15
|
-
### Selesai additions (upstream 0.
|
|
15
|
+
### Selesai additions (upstream 0.48.0 features are all preserved)
|
|
16
16
|
- Agent roster extended with Selesai personas: `architect`, `builder`, `commentator`, `explorer`, `recapper` (upstream `worker`, `reviewer`, `scout`, `oracle`, `delegate`, `researcher` remain). `researcher` is the Selesai host-compatible brief generator.
|
|
17
17
|
- Declarative `chain` / `tasks` execution and saved `.chain.md` / `.chain.json` workflows promoted to first-class documented surfaces (machinery ships in 0.47.1).
|
|
18
18
|
- New slash commands: `/chain`, `/parallel`, `/run-chain`, `/chain-prompts`.
|
|
@@ -22,6 +22,29 @@ Selesai vendors pi-subagents 0.47.1 and layers its own branding and additions on
|
|
|
22
22
|
- Prompt templates: `parallel-context-build`, `parallel-handoff-plan`.
|
|
23
23
|
- `chatProgress` extended with `terminal` / `milestones` projections.
|
|
24
24
|
|
|
25
|
+
## [0.48.0] - 2026-08-13
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- 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.
|
|
29
|
+
- 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.
|
|
30
|
+
- 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).
|
|
31
|
+
- 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.
|
|
32
|
+
- Add a `SELESAI_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.
|
|
33
|
+
- 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.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- 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).
|
|
37
|
+
- Use full task-text hashes for LLM intent arbiter memoization so same-prefix review and implementation tasks cannot share a cached verdict.
|
|
38
|
+
- 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.
|
|
39
|
+
- Explain when a requested mission is scoped to another worktree by naming the current project root and mission directory (#1024).
|
|
40
|
+
- Preserve the configured output reference when explicit acceptance rejects an otherwise completed foreground child, so useful reports remain available (#1023).
|
|
41
|
+
- Reject configured worktree base directories inside the agent extensions directory, including symlink aliases (#1014).
|
|
42
|
+
- 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.
|
|
43
|
+
- 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.
|
|
44
|
+
- 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 `SELESAI_SUBAGENTS_LLM_INTENT_ARBITER=0` to disable. Thanks to @MarcusNeufeldt for #1020.
|
|
45
|
+
- Tolerate empty-string entries in acceptance-report string-array fields instead of rejecting the whole report. Thanks to @hjiang for #1015.
|
|
46
|
+
- 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.
|
|
47
|
+
|
|
25
48
|
## [0.47.1] - 2026-08-12
|
|
26
49
|
|
|
27
50
|
### Fixed
|
|
@@ -99,6 +99,8 @@ In the TUI, a persistent FleetView below the editor keeps active work visible. `
|
|
|
99
99
|
|
|
100
100
|
Details, keybindings, and the machine-readable run artifacts are in [Observability](https://github.com/nicobailon/pi-subagents/blob/main/docs/observability.md).
|
|
101
101
|
|
|
102
|
+
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).
|
|
103
|
+
|
|
102
104
|
## If something feels off
|
|
103
105
|
|
|
104
106
|
```text
|
|
@@ -21,12 +21,15 @@ Rules:
|
|
|
21
21
|
- If a required product, architecture, or scope decision is not approved: when the injected bridge instructions make `contact_supervisor` available, use it with `reason: "need_decision"` and wait; otherwise stop, do not guess, and report the exact blocking decision in your final response.
|
|
22
22
|
- Do not launch subagents. Do not send routine completion handoffs.
|
|
23
23
|
- Do not claim success without making the requested edits, unless you are blocked and report why.
|
|
24
|
+
- If the task specifies a progress file path, append a `## Round N` entry to that file before finishing (use the round number from the task; if none is given, count existing `## Round` entries and add one). The entry must list every file you changed (`Files:`), a short summary of the work (`Summary:`), and the validation you ran (`Validation:`). If the task names no progress file, skip this.
|
|
24
25
|
|
|
25
26
|
Before finishing, verify the requirement, changed files, and relevant tests/checks.
|
|
26
27
|
|
|
27
28
|
Final response:
|
|
28
29
|
|
|
29
30
|
Implemented: ...
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
Progress:
|
|
32
|
+
Files: ... (every file changed)
|
|
33
|
+
Summary: ... (one or two lines on what was done)
|
|
34
|
+
Validation: ... (checks run and outcome)
|
|
32
35
|
Open risks/questions: ...
|
|
@@ -14,7 +14,7 @@ acceptanceRole: read-only
|
|
|
14
14
|
|
|
15
15
|
You are a review-only subagent. Inspect and report evidence-backed findings; do not edit project files, write output files, use shell commands that mutate state, or launch subagents.
|
|
16
16
|
|
|
17
|
-
Review the supplied target directly. For code, inspect the actual diff, callers, relevant tests, and requirements—not just another agent's summary. Use `bash` only for read-only inspection or test commands.
|
|
17
|
+
Review the supplied target directly. If the task names a progress file, read it first and scope your review to its latest round entry: inspect the diff restricted to the files that entry lists (`git diff -- <files>`). Older entries are already reviewed—re-inspect only files the latest entry repeats. If no progress file is named, or it is missing or empty, review the full uncommitted diff. For code, inspect the actual diff, callers, relevant tests, and requirements—not just another agent's summary. Use `bash` only for read-only inspection or test commands.
|
|
18
18
|
|
|
19
19
|
Check:
|
|
20
20
|
- correctness, regressions, edge cases, and plan/requirement adherence;
|
|
@@ -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,39 @@ 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. `SELESAI_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`
|
|
135
149
|
|
|
136
|
-
|
|
150
|
+
```json
|
|
151
|
+
{ "maxSubagentSpawnsPerRun": 64 }
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Caps cumulative logical child admissions in one top-level run tree. The default is `64`. `SELESAI_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`.
|
|
157
|
+
|
|
158
|
+
## `maxWorkflowAutoRelaunches`
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{ "maxWorkflowAutoRelaunches": 12 }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Maximum automatic relaunches when an async scripted workflow ends with a fan-out `budget` result before the goal is clean. Each relaunch re-runs the same `workflowScript` as a fresh top-level run with a new fan-out budget, the same mission, and the same progress file, so the loop keeps building until the commentator reports clean. Defaults to 12; `0` means unlimited. When the cap is reached, the workflow completes as `failed` with a note to raise the cap or relaunch manually. Foreground workflows are unaffected — their budget results surface to the parent inline.
|
|
165
|
+
|
|
166
|
+
## `maxActiveAsyncRunsPerSession`
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{ "maxActiveAsyncRunsPerSession": 4 }
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
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.
|
|
173
|
+
|
|
174
|
+
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.
|
|
175
|
+
|
|
176
|
+
This limit bounds current top-level async load. It is separate from cumulative `maxSubagentSpawnsPerSession`, `maxSubagentSpawnsPerRun`, and `globalConcurrencyLimit`.
|
|
177
|
+
|
|
178
|
+
`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
179
|
|
|
138
180
|
## `scheduledRuns`
|
|
139
181
|
|
|
@@ -196,6 +238,16 @@ export SELESAI_SUBAGENT_PI_BINARY=/path/to/pi-or-wrapper
|
|
|
196
238
|
|
|
197
239
|
Overrides the command used to launch child Selesai 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
240
|
|
|
241
|
+
## `SELESAI_SUBAGENT_TASK_DELIVERY`
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
export SELESAI_SUBAGENT_TASK_DELIVERY=file # auto | file (default: auto)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
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.
|
|
248
|
+
|
|
249
|
+
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`.
|
|
250
|
+
|
|
199
251
|
## `intercomBridge`
|
|
200
252
|
|
|
201
253
|
```json
|
|
@@ -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
|
|
|
@@ -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. |
|
|
@@ -62,7 +62,7 @@ return runs.run("test", { agent: "worker", task });
|
|
|
62
62
|
|
|
63
63
|
A plain workflow creates one enclosing mission by default. Its children do not create separate missions. The result exposes the id as `details.missionId`, and human-readable output ends with `Mission: <id> (<status>)`. Pass `mission:false` for an ephemeral workflow with no mission or durable `state` global.
|
|
64
64
|
|
|
65
|
-
For watched same-repo workflows, pass `async:false` to show the live in-chat workflow card. `chatProgress` can force `off` or `live-card` when the automatic policy is not what you want. Foreground workflows default to a 30-minute timeout; async workflows have no default timeout. See the [tool reference](tool-reference.md) for the full parameter list.
|
|
65
|
+
For watched same-repo workflows, pass `async:false` to show the live in-chat workflow card. `chatProgress` can force `off` or `live-card` when the automatic policy is not what you want. Foreground workflows default to a 30-minute timeout; async workflows have no default timeout. Async workflows that end with a fan-out `budget` result auto-relaunch as a fresh top-level run (new fan-out budget, same mission and progress file) until the commentator reports clean or the `maxWorkflowAutoRelaunches` cap is reached (default 12; 0 = unlimited). See the [tool reference](tool-reference.md) for the full parameter list.
|
|
66
66
|
|
|
67
67
|
The legacy `/chain`, `/parallel`, and `/run-chain` commands are not registered.
|
|
68
68
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pi-subagents",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.48.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"jiti": "2.7.0",
|
|
@@ -55,7 +55,7 @@ its resolved launch context as `[fresh]` or `[fork]`. Aggregate headers show
|
|
|
55
55
|
|
|
56
56
|
`workflowScript` is the scripted orchestration surface. Use `runs.run(key, { agent, task, ... })` for one child, `runs.all([...])` for parallel children, and ordinary JavaScript for sequence, branching, filtering, retries, and aggregation. Scripts are ordinary JavaScript statement bodies, so use an explicit return such as `workflowScript: "return runs.run('main', { agent: 'worker', task: '...' })"` for a useful one-child result. Prefer a single scripted workflow whenever the parent is starting a coordinated wave, such as multiple reviews, review plus gate monitor, worker then monitor setup, cross-repo prep lanes, or a fanout that the parent will consume together; use the declarative `chain` / `tasks` modes when the shape is known up front.
|
|
57
57
|
|
|
58
|
-
**Workflow modes as `workflowScript` auto-loops.** The bundled `workflow` extension ships its four mode shapes (task, prototype, quicktype, loop) as `workflowScript` auto-loops launched via `/workflow-*`. Each mode runs its phases as `runs.run` steps and loops the build
|
|
58
|
+
**Workflow modes as `workflowScript` auto-loops.** The bundled `workflow` extension ships its four mode shapes (task, prototype, quicktype, loop) as `workflowScript` auto-loops launched via `/workflow-*`. Each mode runs its phases as `runs.run` steps and loops the build → review → fix round (a blocking review triggers a fix round that addresses only the findings) until the commentator reports clean. There are no checkpoints (one-shot-and-sleep); durable state is the auto-created mission per launch; recover via `mission.list`/`status`.
|
|
59
59
|
|
|
60
60
|
```js
|
|
61
61
|
subagent({
|
|
@@ -1024,7 +1024,10 @@ function applyBuiltinOverride(
|
|
|
1024
1024
|
};
|
|
1025
1025
|
|
|
1026
1026
|
if (override.description !== undefined) next.description = override.description;
|
|
1027
|
-
if (override.model !== undefined) {
|
|
1027
|
+
if (override.model !== undefined) {
|
|
1028
|
+
if (override.model === false) delete next.model; else next.model = override.model;
|
|
1029
|
+
delete next.modelSource;
|
|
1030
|
+
}
|
|
1028
1031
|
if (override.fallbackModels !== undefined) { if (override.fallbackModels === false) delete next.fallbackModels; else next.fallbackModels = [...override.fallbackModels]; }
|
|
1029
1032
|
if (override.thinking !== undefined) { if (override.thinking === false) delete next.thinking; else next.thinking = override.thinking; }
|
|
1030
1033
|
if (override.systemPromptMode !== undefined) next.systemPromptMode = override.systemPromptMode;
|
|
@@ -1142,8 +1145,11 @@ function applyCustomAgentOverride(
|
|
|
1142
1145
|
mutable().description = override.description;
|
|
1143
1146
|
anyFilled = true;
|
|
1144
1147
|
}
|
|
1145
|
-
if (override.model !== undefined) {
|
|
1146
|
-
|
|
1148
|
+
if (override.model !== undefined && !agentHasFrontmatterField(agent, "model")) {
|
|
1149
|
+
const target = mutable();
|
|
1150
|
+
if (override.model === false) delete target.model; else target.model = override.model;
|
|
1151
|
+
delete target.modelSource;
|
|
1152
|
+
anyFilled = true;
|
|
1147
1153
|
}
|
|
1148
1154
|
if (override.fallbackModels !== undefined) {
|
|
1149
1155
|
fill(
|
|
@@ -53,6 +53,18 @@ function validateConfig(config: Record<string, unknown>): void {
|
|
|
53
53
|
if (config.legacyChainControls !== undefined && typeof config.legacyChainControls !== "boolean") {
|
|
54
54
|
throw new Error("config.legacyChainControls must be a boolean");
|
|
55
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
|
+
}
|
|
62
|
+
if (config.maxWorkflowAutoRelaunches !== undefined
|
|
63
|
+
&& (typeof config.maxWorkflowAutoRelaunches !== "number"
|
|
64
|
+
|| !Number.isInteger(config.maxWorkflowAutoRelaunches)
|
|
65
|
+
|| config.maxWorkflowAutoRelaunches < 0)) {
|
|
66
|
+
throw new Error("config.maxWorkflowAutoRelaunches must be a non-negative integer");
|
|
67
|
+
}
|
|
56
68
|
validateMissionStoreConfig(config.missions);
|
|
57
69
|
validateAuthorityPolicy(config.authorityPolicy);
|
|
58
70
|
validatePermissionConfig(config.permissions);
|
|
@@ -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.SELESAI_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["SELESAI_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
|
"",
|
|
@@ -16,6 +16,7 @@ import { randomUUID } from "node:crypto";
|
|
|
16
16
|
import * as fs from "node:fs";
|
|
17
17
|
import * as os from "node:os";
|
|
18
18
|
import * as path from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
19
20
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
20
21
|
import { keyText, type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@selesai/code";
|
|
21
22
|
import { Box, Container, Spacer, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
|
|
@@ -23,6 +24,7 @@ import { discoverAgents } from "../agents/agents.ts";
|
|
|
23
24
|
import { ensureAccessibleDir } from "../shared/accessible-dir.ts";
|
|
24
25
|
import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
|
|
25
26
|
import { resolveCurrentSessionId } from "../shared/session-identity.ts";
|
|
27
|
+
import { resolveSessionLineage } from "../shared/session-lineage.ts";
|
|
26
28
|
import { cleanupOldChainDirs } from "../shared/settings.ts";
|
|
27
29
|
import { clearLegacyResultAnimationTimer, renderSubagentResult, renderSubagentSummary } from "../tui/render.ts";
|
|
28
30
|
import { openSubagentFleet } from "../tui/fleet.ts";
|
|
@@ -31,6 +33,7 @@ import { createSubagentParamsSchema } from "./schemas.ts";
|
|
|
31
33
|
import { validateChainInput } from "./chain-validation.ts";
|
|
32
34
|
import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
33
35
|
import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
|
|
36
|
+
import { getActiveAsyncCapacitySnapshot, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
|
|
34
37
|
import { createResultWatcher } from "../runs/background/result-watcher.ts";
|
|
35
38
|
import { createScheduledRunManager } from "../runs/background/scheduled-runs.ts";
|
|
36
39
|
import { registerSlashCommands } from "../slash/slash-commands.ts";
|
|
@@ -66,6 +69,7 @@ import {
|
|
|
66
69
|
SLASH_TEXT_RESULT_TYPE,
|
|
67
70
|
SUBAGENT_ASYNC_COMPLETE_EVENT,
|
|
68
71
|
SUBAGENT_ASYNC_STARTED_EVENT,
|
|
72
|
+
SUBAGENT_PROCESS_TERMINAL_EVENT,
|
|
69
73
|
SUBAGENT_CONTROL_EVENT,
|
|
70
74
|
SUBAGENT_STEERING_NOTICE_EVENT,
|
|
71
75
|
WIDGET_KEY,
|
|
@@ -374,6 +378,12 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
374
378
|
const artifactCleanupDays = config.artifactConfig?.cleanupDays ?? DEFAULT_ARTIFACT_CONFIG.cleanupDays;
|
|
375
379
|
cleanupAllArtifactDirs(artifactCleanupDays);
|
|
376
380
|
|
|
381
|
+
// Replay guard for lineage result adoption: accept result files written at
|
|
382
|
+
// most this long before the adoption window opened (covers completions that
|
|
383
|
+
// raced the session switch). Older files were left behind by a dead watcher
|
|
384
|
+
// and are ignored until their owning session is resumed.
|
|
385
|
+
const ADOPTED_RESULT_SLACK_MS = 60_000;
|
|
386
|
+
|
|
377
387
|
const state: SubagentState = {
|
|
378
388
|
baseCwd: "",
|
|
379
389
|
currentSessionId: null,
|
|
@@ -389,6 +399,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
389
399
|
granted: 0,
|
|
390
400
|
grantHistory: [],
|
|
391
401
|
},
|
|
402
|
+
activeAsyncCapacity: { used: 0, limit: resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession) ?? 0 },
|
|
392
403
|
asyncJobs: new Map(),
|
|
393
404
|
fleetJobs: new Map(),
|
|
394
405
|
foregroundRuns: new Map(),
|
|
@@ -627,19 +638,26 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
627
638
|
|
|
628
639
|
pi.on("agent_end", async (_event, ctx) => {
|
|
629
640
|
if (!ctx.hasUI) await drainOutstandingWork({ state, events: pi.events });
|
|
630
|
-
const
|
|
631
|
-
|
|
641
|
+
const ownerSessionIds = state.sessionLineage?.length
|
|
642
|
+
? state.sessionLineage
|
|
643
|
+
: state.currentSessionId ? [state.currentSessionId] : [];
|
|
644
|
+
if (ownerSessionIds.length === 0) return;
|
|
632
645
|
goalTurnId += 1;
|
|
633
646
|
try {
|
|
634
647
|
const location = resolveMissionStoreLocation({ projectRoot: state.baseCwd, ...(config.missions ? { config: config.missions } : {}) });
|
|
635
|
-
const retainedChildren = listRetainedChildren(DIRS.async,
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
648
|
+
const retainedChildren = listRetainedChildren(DIRS.async, ownerSessionIds);
|
|
649
|
+
const emittedMissions = new Set<string>();
|
|
650
|
+
for (const ownerSessionId of ownerSessionIds) {
|
|
651
|
+
for (const notice of collectGoalContinuationNotices({ location, ownerSessionId, retainedChildren, turnId: goalTurnId })) {
|
|
652
|
+
if (emittedMissions.has(notice.missionId)) continue;
|
|
653
|
+
emittedMissions.add(notice.missionId);
|
|
654
|
+
handleSubagentControlNotice({
|
|
655
|
+
pi,
|
|
656
|
+
state,
|
|
657
|
+
visibleControlNotices: new Set(),
|
|
658
|
+
details: { source: "goal", event: notice.event, noticeText: notice.message },
|
|
659
|
+
});
|
|
660
|
+
}
|
|
643
661
|
}
|
|
644
662
|
} catch (error) {
|
|
645
663
|
console.error("Failed to evaluate goal missions:", error);
|
|
@@ -647,6 +665,11 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
647
665
|
});
|
|
648
666
|
|
|
649
667
|
registerSlashCommands(pi, state, { fleetKeybindings: config.fleetKeybindings });
|
|
668
|
+
pi.on("resources_discover", () => {
|
|
669
|
+
return {
|
|
670
|
+
promptPaths: [fileURLToPath(new URL("../../prompts", import.meta.url))],
|
|
671
|
+
};
|
|
672
|
+
});
|
|
650
673
|
|
|
651
674
|
const eventUnsubscribeStoreKey = "__piSubagentEventUnsubscribes";
|
|
652
675
|
const controlNoticeSeenStoreKey = "__piSubagentVisibleControlNotices";
|
|
@@ -689,12 +712,17 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
689
712
|
};
|
|
690
713
|
const asyncCompleteHandler = (payload: unknown) => {
|
|
691
714
|
handleComplete(payload);
|
|
715
|
+
refreshActiveAsyncCapacity();
|
|
692
716
|
scheduledRunManager.handleAsyncCompletion(payload);
|
|
693
717
|
fleetStatus?.refresh();
|
|
694
718
|
};
|
|
695
719
|
const eventUnsubscribes = [
|
|
696
720
|
pi.events.on(SUBAGENT_ASYNC_STARTED_EVENT, asyncStartedHandler),
|
|
697
721
|
pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, asyncCompleteHandler),
|
|
722
|
+
pi.events.on(SUBAGENT_PROCESS_TERMINAL_EVENT, () => {
|
|
723
|
+
refreshActiveAsyncCapacity();
|
|
724
|
+
fleetStatus?.refresh();
|
|
725
|
+
}),
|
|
698
726
|
pi.events.on(SUBAGENT_CONTROL_EVENT, controlEventHandler),
|
|
699
727
|
pi.events.on(SUBAGENT_STEERING_NOTICE_EVENT, steeringNoticeHandler),
|
|
700
728
|
herdrStatusBridge.dispose,
|
|
@@ -740,12 +768,57 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
740
768
|
fleetStatus?.refresh();
|
|
741
769
|
};
|
|
742
770
|
|
|
743
|
-
const
|
|
771
|
+
const refreshActiveAsyncCapacity = () => {
|
|
772
|
+
if (!state.currentSessionId) {
|
|
773
|
+
state.activeAsyncCapacity = { used: 0, limit: resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession) ?? 0 };
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(
|
|
777
|
+
state.currentSessionId,
|
|
778
|
+
resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession),
|
|
779
|
+
{ liveWorkflowRunIds: new Set(state.workflowControllers?.keys() ?? []) },
|
|
780
|
+
);
|
|
781
|
+
const adoptedActive = [...state.asyncJobs.values()].filter((job) => job.adopted && (job.status === "queued" || job.status === "running")).length;
|
|
782
|
+
if (adoptedActive > 0) {
|
|
783
|
+
state.activeAsyncCapacity = {
|
|
784
|
+
...state.activeAsyncCapacity,
|
|
785
|
+
used: state.activeAsyncCapacity.used + adoptedActive,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
const formatActiveWorkflowSummary = (target: SubagentState, intro: string): string => {
|
|
791
|
+
const active = [...target.asyncJobs.values()]
|
|
792
|
+
.filter((job) => job.status === "queued" || job.status === "running")
|
|
793
|
+
.sort((left, right) => (left.startedAt ?? 0) - (right.startedAt ?? 0));
|
|
794
|
+
const lines = active.map((job) => {
|
|
795
|
+
const label = job.mode === "workflow"
|
|
796
|
+
? `workflow[${job.workflowKey ?? "script"}]`
|
|
797
|
+
: `${job.mode ?? "run"}${job.agents?.length ? ` ${job.agents.join(", ")}` : ""}`;
|
|
798
|
+
const step = job.currentStep !== undefined
|
|
799
|
+
? ` · step ${job.currentStep + 1}/${job.chainStepCount ?? job.stepsTotal ?? "?"}`
|
|
800
|
+
: "";
|
|
801
|
+
const activity = job.currentTool ? ` · last: ${job.currentTool}` : "";
|
|
802
|
+
const adopted = job.adopted ? " (carried over from a previous session)" : "";
|
|
803
|
+
return `- ${job.asyncId} ${label}${step}${activity}${adopted}`;
|
|
804
|
+
});
|
|
805
|
+
return [
|
|
806
|
+
intro,
|
|
807
|
+
...lines,
|
|
808
|
+
"Monitor with subagent status <id>; interrupt/resume/steer by run id; children.list lists completed steps. Results arrive automatically.",
|
|
809
|
+
].join("\n");
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
const resetSessionState = (ctx: ExtensionContext, recovering: boolean, previousSessionFile?: string | null) => {
|
|
744
813
|
state.widgetsSuspended = false;
|
|
745
814
|
state.baseCwd = ctx.cwd;
|
|
746
815
|
goalTurnId = 0;
|
|
747
816
|
state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
|
|
748
817
|
state.parentSessionFile = ctx.sessionManager.getSessionFile();
|
|
818
|
+
state.sessionLineage = resolveSessionLineage({ sessionManager: ctx.sessionManager }, { parentHint: previousSessionFile ?? null });
|
|
819
|
+
state.handoffResumePending = false;
|
|
820
|
+
if (state.sessionLineage.length > 1) state.adoptedResultWindowStart = Date.now() - ADOPTED_RESULT_SLACK_MS;
|
|
821
|
+
else state.adoptedResultWindowStart = undefined;
|
|
749
822
|
state.subagentSpawns = {
|
|
750
823
|
sessionId: state.currentSessionId,
|
|
751
824
|
count: 0,
|
|
@@ -765,12 +838,14 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
765
838
|
}
|
|
766
839
|
}
|
|
767
840
|
state.lastUiContext = ctx;
|
|
841
|
+
refreshActiveAsyncCapacity();
|
|
768
842
|
cleanupSessionArtifacts(ctx);
|
|
769
843
|
state.foregroundControls.clear();
|
|
770
844
|
state.lastForegroundControlId = null;
|
|
771
845
|
resetJobs(ctx);
|
|
772
846
|
restoreForegroundRunHistory(state, { resultsDir: DIRS.results });
|
|
773
|
-
restoreActiveJobs(ctx);
|
|
847
|
+
const adoptedJobs = restoreActiveJobs(ctx);
|
|
848
|
+
state.handoffResumePending = adoptedJobs > 0 && state.lastUiContext?.hasUI === true;
|
|
774
849
|
scheduledRunManager.bindSession(ctx);
|
|
775
850
|
restoreSlashFinalSnapshots(ctx.sessionManager.getEntries());
|
|
776
851
|
waitSubscriptionManager.restore();
|
|
@@ -786,6 +861,17 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
786
861
|
|
|
787
862
|
pi.on("agent_settled", () => {
|
|
788
863
|
resumeWidgetsAfterCompaction();
|
|
864
|
+
if (state.handoffResumePending && state.lastUiContext?.hasUI === true) {
|
|
865
|
+
state.handoffResumePending = false;
|
|
866
|
+
pi.sendMessage(
|
|
867
|
+
{
|
|
868
|
+
customType: "subagent-handoff-resume",
|
|
869
|
+
content: formatActiveWorkflowSummary(state, "Session handoff detected. The previous session's background workflows were carried over:"),
|
|
870
|
+
display: false,
|
|
871
|
+
},
|
|
872
|
+
{ triggerTurn: true },
|
|
873
|
+
);
|
|
874
|
+
}
|
|
789
875
|
});
|
|
790
876
|
|
|
791
877
|
pi.on("session_before_compact", (event) => {
|
|
@@ -798,7 +884,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
798
884
|
pi.sendMessage(
|
|
799
885
|
{
|
|
800
886
|
customType: "subagent-compaction-resume",
|
|
801
|
-
content: "Compaction is complete. Resume the parent task now;
|
|
887
|
+
content: formatActiveWorkflowSummary(state, "Compaction is complete. Resume the parent task now; results for the runs below arrive separately when ready:"),
|
|
802
888
|
display: false,
|
|
803
889
|
},
|
|
804
890
|
{ triggerTurn: true },
|
|
@@ -807,7 +893,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
807
893
|
|
|
808
894
|
pi.on("session_start", (event, ctx) => {
|
|
809
895
|
const recovering = event.reason === "startup" || event.reason === "reload" || event.reason === "resume";
|
|
810
|
-
|
|
896
|
+
const previousSessionFile = event.reason === "new" || event.reason === "fork" ? event.previousSessionFile : null;
|
|
897
|
+
resetSessionState(ctx, recovering, previousSessionFile);
|
|
811
898
|
herdrStatusBridge.sessionStarted({
|
|
812
899
|
hasUI: ctx.hasUI === true,
|
|
813
900
|
runs: activeHerdrRuns(),
|
|
@@ -821,6 +908,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
821
908
|
stopResultWatcher();
|
|
822
909
|
state.currentSessionId = null;
|
|
823
910
|
state.parentSessionFile = null;
|
|
911
|
+
state.sessionLineage = undefined;
|
|
912
|
+
state.adoptedResultWindowStart = undefined;
|
|
913
|
+
state.handoffResumePending = false;
|
|
824
914
|
completionNotifier.dispose();
|
|
825
915
|
delete process.env[SUBAGENT_PARENT_SESSION_ENV];
|
|
826
916
|
for (const unsubscribe of eventUnsubscribes) {
|
|
@@ -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";
|
|
@@ -26,6 +28,9 @@ export type PublicSubagentExecutionNormalization<T> =
|
|
|
26
28
|
* children and structured owned delegation bypass this boundary.
|
|
27
29
|
*/
|
|
28
30
|
export function normalizePublicSubagentExecution<T extends PublicSubagentExecutionParams>(params: T): PublicSubagentExecutionNormalization<T> {
|
|
31
|
+
if (params.runFanoutBudget !== undefined || params.runFanoutAdmitted !== undefined) {
|
|
32
|
+
return { ok: false, error: "Public execution does not accept internal run fan-out fields.", mode: params.workflowScript !== undefined ? "workflow" : "management" };
|
|
33
|
+
}
|
|
29
34
|
const action = params.action;
|
|
30
35
|
if (action !== undefined && (typeof action !== "string" || !action.trim())) {
|
|
31
36
|
return { ok: false, error: "action must be a non-empty management/control action, or omit action and execute directly.", mode: "management" };
|