@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.
Files changed (147) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +7 -1
  3. package/dist/core/slash-commands.js +1 -0
  4. package/dist/defaults/models.json +46 -55
  5. package/dist/defaults/settings.json +7 -8
  6. package/dist/extensions/agent-browser.test.ts +178 -2
  7. package/dist/extensions/agent-browser.ts +2 -0
  8. package/dist/extensions/copy-turn.test.ts +229 -0
  9. package/dist/extensions/copy-turn.ts +11 -0
  10. package/dist/extensions/grep-app/index.test.ts +356 -1
  11. package/dist/extensions/grep-app/index.ts +6 -1
  12. package/dist/extensions/handoff-new.test.ts +351 -161
  13. package/dist/extensions/handoff-new.ts +3 -0
  14. package/dist/extensions/inline-skills.test.ts +93 -0
  15. package/dist/extensions/inline-skills.ts +3 -0
  16. package/dist/extensions/pi-subagents/CHANGELOG.md +26 -3
  17. package/dist/extensions/pi-subagents/README.md +2 -0
  18. package/dist/extensions/pi-subagents/agents/builder.md +5 -2
  19. package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
  20. package/dist/extensions/pi-subagents/docs/configuration.md +54 -2
  21. package/dist/extensions/pi-subagents/docs/observability.md +1 -1
  22. package/dist/extensions/pi-subagents/docs/tool-reference.md +1 -1
  23. package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
  24. package/dist/extensions/pi-subagents/package-lock.json +2 -2
  25. package/dist/extensions/pi-subagents/package.json +1 -1
  26. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +1 -1
  27. package/dist/extensions/pi-subagents/src/agents/agents.ts +9 -3
  28. package/dist/extensions/pi-subagents/src/extension/config.ts +12 -0
  29. package/dist/extensions/pi-subagents/src/extension/doctor.ts +40 -0
  30. package/dist/extensions/pi-subagents/src/extension/index.ts +104 -14
  31. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +5 -0
  32. package/dist/extensions/pi-subagents/src/extension/rpc.ts +3 -9
  33. package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
  34. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -1
  35. package/dist/extensions/pi-subagents/src/missions/lifecycle.ts +4 -7
  36. package/dist/extensions/pi-subagents/src/missions/store.ts +4 -4
  37. package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +6 -2
  38. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +374 -0
  39. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +9 -5
  40. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +117 -29
  41. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +12 -6
  42. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -1
  43. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +14 -5
  44. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +33 -15
  45. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
  46. package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +104 -0
  47. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +17 -3
  48. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +19 -1
  49. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +2 -2
  50. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +5 -2
  51. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +12 -6
  52. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -3
  53. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +84 -36
  54. package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +2 -1
  55. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +37 -2
  56. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +135 -22
  57. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +12 -0
  58. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -4
  59. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +171 -0
  60. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +653 -196
  61. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +13 -4
  62. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +286 -0
  63. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +23 -0
  64. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  65. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +44 -1
  66. package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +280 -0
  67. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +4 -2
  68. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +19 -3
  69. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +17 -5
  70. package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +71 -0
  71. package/dist/extensions/pi-subagents/src/shared/types.ts +108 -0
  72. package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -1
  73. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +7 -5
  74. package/dist/extensions/pi-subagents/src/tui/fleet.ts +226 -13
  75. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +22 -5
  76. package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
  77. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +87 -0
  78. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +96 -6
  79. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +28 -5
  80. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +8 -5
  81. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +83 -2
  82. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +211 -15
  83. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +41 -3
  84. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +314 -10
  85. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +31 -1
  86. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +317 -0
  87. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +21 -2
  88. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +16 -1
  89. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +6 -1
  90. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +75 -4
  91. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +18 -0
  92. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +29 -1
  93. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +31 -5
  94. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +139 -1
  95. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +10 -0
  96. package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +103 -0
  97. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +22 -0
  98. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +2 -1
  99. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +171 -0
  100. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +30 -4
  101. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +14 -0
  102. package/dist/extensions/pi-subagents/test/unit/owned-process-tree.test.ts +69 -0
  103. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +83 -0
  104. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +2 -2
  105. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +55 -2
  106. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +12 -1
  107. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +7 -7
  108. package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +121 -0
  109. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +9 -0
  110. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -3
  111. package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +73 -0
  112. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +28 -0
  113. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +10 -1
  114. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +39 -0
  115. package/dist/extensions/pi-subagents/test/unit/timeout-defaults.test.ts +65 -0
  116. package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +45 -0
  117. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +61 -0
  118. package/dist/extensions/ponytail/index.js +11 -0
  119. package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
  120. package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
  121. package/dist/extensions/ponytail/test/extension.test.js +274 -140
  122. package/dist/extensions/ponytail/test/helpers.test.js +280 -92
  123. package/dist/extensions/question/index.ts +18 -0
  124. package/dist/extensions/question/question-list.ts +22 -0
  125. package/dist/extensions/question/tests/batch.test.ts +103 -65
  126. package/dist/extensions/question/tests/helpers.test.ts +68 -35
  127. package/dist/extensions/question/tests/question-list.test.ts +506 -204
  128. package/dist/extensions/question/tests/row-layout.test.ts +180 -114
  129. package/dist/extensions/question/tests/schemas.test.ts +42 -19
  130. package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
  131. package/dist/extensions/question/tests/wizard.test.ts +771 -50
  132. package/dist/extensions/rtk.test.ts +180 -1
  133. package/dist/extensions/tokenin-onboarding.ts +3 -0
  134. package/dist/extensions/undo.test.ts +720 -0
  135. package/dist/extensions/undo.ts +6 -0
  136. package/dist/extensions/web-agent-onboarding.test.ts +415 -0
  137. package/dist/extensions/workflow/extension.ts +3 -3
  138. package/dist/extensions/workflow/modes.ts +41 -19
  139. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  140. package/dist/modes/interactive/interactive-mode.js +48 -1
  141. package/dist/skills/pi-subagents/references/execution-controls.md +1 -1
  142. package/docs/plans/workflow-autoloop-reference.md +1 -2
  143. package/docs/plans/workflow-handoff-carryover.md +108 -0
  144. package/docs/quickstart.md +27 -43
  145. package/docs/settings.md +4 -0
  146. package/docs/usage.md +1 -0
  147. 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.47.1)
5
+ ## Selesai fork (0.48.0)
6
6
 
7
- Selesai vendors pi-subagents 0.47.1 and layers its own branding and additions on top:
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.47.1 features are all preserved)
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
- Changed files: ...
31
- Validation: ...
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. 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.
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
- 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.
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; none async | Optional run-level max runtime in milliseconds. Foreground uses 30 minutes when omitted. Async runs have no default timeout, including async workflows. |
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.47.1",
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.47.1",
9
+ "version": "0.48.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "jiti": "2.7.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.47.1",
3
+ "version": "0.48.0",
4
4
  "description": "Selesai extension for single-agent delegation, chains, parallel groups, and scripted multi-agent workflows",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
@@ -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↔review round 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`.
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) { if (override.model === false) delete next.model; else next.model = override.model; }
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
- fill("model", ["model"], override.model === false ? undefined : override.model);
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 ownerSessionId = state.currentSessionId;
631
- if (!ownerSessionId) return;
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, ownerSessionId);
636
- for (const notice of collectGoalContinuationNotices({ location, ownerSessionId, retainedChildren, turnId: goalTurnId })) {
637
- handleSubagentControlNotice({
638
- pi,
639
- state,
640
- visibleControlNotices: new Set(),
641
- details: { source: "goal", event: notice.event, noticeText: notice.message },
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 resetSessionState = (ctx: ExtensionContext, recovering: boolean) => {
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; background subagent results will arrive separately when ready.",
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
- resetSessionState(ctx, recovering);
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" };