@selesai/code 0.8.4 → 0.8.5

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 (96) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +7 -1
  3. package/dist/core/slash-commands.js +1 -0
  4. package/dist/defaults/models.json +41 -55
  5. package/dist/defaults/settings.json +7 -8
  6. package/dist/extensions/pi-subagents/CHANGELOG.md +26 -3
  7. package/dist/extensions/pi-subagents/README.md +2 -0
  8. package/dist/extensions/pi-subagents/docs/configuration.md +46 -2
  9. package/dist/extensions/pi-subagents/docs/observability.md +1 -1
  10. package/dist/extensions/pi-subagents/docs/tool-reference.md +1 -1
  11. package/dist/extensions/pi-subagents/package-lock.json +2 -2
  12. package/dist/extensions/pi-subagents/package.json +1 -1
  13. package/dist/extensions/pi-subagents/src/agents/agents.ts +9 -3
  14. package/dist/extensions/pi-subagents/src/extension/config.ts +6 -0
  15. package/dist/extensions/pi-subagents/src/extension/doctor.ts +40 -0
  16. package/dist/extensions/pi-subagents/src/extension/index.ts +21 -0
  17. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +5 -0
  18. package/dist/extensions/pi-subagents/src/extension/rpc.ts +3 -9
  19. package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
  20. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -1
  21. package/dist/extensions/pi-subagents/src/missions/lifecycle.ts +4 -7
  22. package/dist/extensions/pi-subagents/src/missions/store.ts +4 -4
  23. package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +6 -2
  24. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +374 -0
  25. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +9 -5
  26. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +117 -29
  27. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -1
  28. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +11 -5
  29. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +33 -15
  30. package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +104 -0
  31. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +17 -3
  32. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +5 -1
  33. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -3
  34. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +84 -36
  35. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +37 -2
  36. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +135 -22
  37. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +12 -0
  38. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +171 -0
  39. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +614 -187
  40. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +13 -4
  41. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +286 -0
  42. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +23 -0
  43. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  44. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +44 -1
  45. package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +280 -0
  46. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +4 -2
  47. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +19 -3
  48. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +17 -5
  49. package/dist/extensions/pi-subagents/src/shared/types.ts +92 -0
  50. package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -1
  51. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +7 -5
  52. package/dist/extensions/pi-subagents/src/tui/fleet.ts +225 -12
  53. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +22 -5
  54. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +87 -0
  55. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +96 -6
  56. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +28 -5
  57. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +8 -5
  58. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +83 -2
  59. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +211 -15
  60. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +41 -3
  61. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +314 -10
  62. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +31 -1
  63. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +317 -0
  64. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +21 -2
  65. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +16 -1
  66. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +6 -1
  67. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +75 -4
  68. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +18 -0
  69. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +29 -1
  70. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +31 -5
  71. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +139 -1
  72. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +10 -0
  73. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +2 -1
  74. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +171 -0
  75. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +30 -4
  76. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +14 -0
  77. package/dist/extensions/pi-subagents/test/unit/owned-process-tree.test.ts +69 -0
  78. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +83 -0
  79. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +2 -2
  80. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +55 -2
  81. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +12 -1
  82. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +7 -7
  83. package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +121 -0
  84. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +9 -0
  85. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -3
  86. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +28 -0
  87. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +10 -1
  88. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +39 -0
  89. package/dist/extensions/pi-subagents/test/unit/timeout-defaults.test.ts +65 -0
  90. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +61 -0
  91. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  92. package/dist/modes/interactive/interactive-mode.js +48 -1
  93. package/docs/quickstart.md +27 -43
  94. package/docs/settings.md +4 -0
  95. package/docs/usage.md +1 -0
  96. package/package.json +2 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@ All notable changes to `@selesai/code` will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.8.5] - 2026-08-14
8
+
9
+ ### Added
10
+ - **`/settings-factory-reset` slash command.** Restores `~/.selesai/agent/settings.json` to the bundled factory defaults after a warning and confirmation. A backup is saved as `settings.json.bak` before the reset, credentials (`auth.json`), sessions, extensions, skills, and themes are untouched, and settings reload live without a restart.
11
+ - **Install from the public repository.** `npm install -g github:SelesaiInTech/selesai-code` now works: the package gained a `prepare` script that builds `dist` from source on install.
12
+
13
+ ### Changed
14
+ - **Simplified install guide.** The documented install command is now plain `npm install -g @selesai/code`. The previous `--ignore-scripts` advice was actively harmful: `@ast-grep/cli` (a runtime dependency) requires its `postinstall` to fetch its platform binary. Quickstart, README, and doc-web install pages updated, and quickstart no longer references the upstream `pi` binary or `~/.pi` paths.
15
+ - **Bumped default `retry.maxRetries` from 5 to 7** in bundled defaults.
16
+
7
17
  ## [0.8.4] - 2026-08-13
8
18
 
9
19
  ### Fixed
package/README.md CHANGED
@@ -7,7 +7,7 @@ The goal is simple: make a capable coding agent useful out of the box, maximize
7
7
  ## Install
8
8
 
9
9
  ```bash
10
- npm install -g --ignore-scripts @selesai/code
10
+ npm install -g @selesai/code
11
11
  selesai
12
12
  ```
13
13
 
@@ -17,6 +17,12 @@ For a one-off run using the latest published package:
17
17
  npx @selesai/code
18
18
  ```
19
19
 
20
+ To install directly from the public repository (builds the latest `main` from source):
21
+
22
+ ```bash
23
+ npm install -g github:SelesaiInTech/selesai-code
24
+ ```
25
+
20
26
  The published npm package is **`@selesai/code`** and its executable is **`selesai`**. Selesai uses `~/.selesai/agent` for user state and `.selesai/` for project-local settings and resources.
21
27
 
22
28
  Start with a prompt:
@@ -1,6 +1,7 @@
1
1
  import { APP_NAME } from "../config.js";
2
2
  export const BUILTIN_SLASH_COMMANDS = [
3
3
  { name: "settings", description: "Open settings menu" },
4
+ { name: "settings-factory-reset", description: "Reset settings to factory defaults (with confirmation)" },
4
5
  { name: "model", description: "Select model (opens selector UI)", argumentHint: "<provider/model>" },
5
6
  { name: "scoped-models", description: "Enable/disable models for Ctrl+P cycling" },
6
7
  { name: "export", description: "Export session (HTML default, or specify path: .html/.jsonl)" },
@@ -15,13 +15,7 @@
15
15
  "name": "DeepSeek V4 Flash",
16
16
  "reasoning": true,
17
17
  "input": ["text"],
18
- "contextWindow": 1000000,
19
- "cost": {
20
- "input": 0.15,
21
- "output": 0.3,
22
- "cacheRead": 0.003,
23
- "cacheWrite": 0
24
- },
18
+ "contextWindow": 512000,
25
19
  "thinkingLevelMap": {
26
20
  "minimal": null,
27
21
  "low": null,
@@ -37,50 +31,38 @@
37
31
  }
38
32
  },
39
33
  {
40
- "id": "kimi-k3",
41
- "name": "Kimi K3",
34
+ "id": "deepseek-v4-pro",
35
+ "name": "DeepSeek V4 pro",
42
36
  "reasoning": true,
43
- "input": ["text", "image"],
44
- "contextWindow": 262128,
45
- "cost": {
46
- "input": 3,
47
- "output": 15,
48
- "cacheRead": 1.5,
49
- "cacheWrite": 0
37
+ "input": ["text"],
38
+ "contextWindow": 512000,
39
+ "thinkingLevelMap": {
40
+ "minimal": null,
41
+ "low": null,
42
+ "medium": null,
43
+ "high": null,
44
+ "xhigh": null,
45
+ "max": "max"
50
46
  },
51
47
  "compat": {
52
48
  "supportsDeveloperRole": false,
53
- "supportsReasoningEffort": false
49
+ "requiresReasoningContentOnAssistantMessages": true,
50
+ "thinkingFormat": "deepseek"
54
51
  }
55
52
  },
56
53
  {
57
- "id": "kimi-k2.7-code",
58
- "name": "Kimi K2.7 Code",
54
+ "id": "kimi-k3",
55
+ "name": "Kimi K3",
59
56
  "reasoning": true,
60
57
  "input": ["text", "image"],
61
- "contextWindow": 262128,
62
- "cost": {
63
- "input": 0.95,
64
- "output": 4,
65
- "cacheRead": 0.2375,
66
- "cacheWrite": 0
67
- },
68
- "compat": {
69
- "supportsDeveloperRole": false,
70
- "supportsReasoningEffort": false
71
- }
72
- },
73
- {
74
- "id": "qwen3.5-397b",
75
- "name": "Qwen3.5 397B",
76
- "reasoning": true,
77
- "input": ["text"],
78
- "contextWindow": 262128,
79
- "cost": {
80
- "input": 0.69,
81
- "output": 4.14,
82
- "cacheRead": 0.1725,
83
- "cacheWrite": 0
58
+ "contextWindow": 512000,
59
+ "thinkingLevelMap": {
60
+ "minimal": null,
61
+ "low": "low",
62
+ "medium": null,
63
+ "high": "high",
64
+ "xhigh": null,
65
+ "max": "max"
84
66
  },
85
67
  "compat": {
86
68
  "supportsDeveloperRole": false,
@@ -88,16 +70,18 @@
88
70
  }
89
71
  },
90
72
  {
91
- "id": "qwen3.6-35b",
92
- "name": "Qwen3.6 35B",
73
+ "id": "kimi-k2.7-code",
74
+ "name": "Kimi K2.7 Code",
93
75
  "reasoning": true,
94
76
  "input": ["text", "image"],
95
- "contextWindow": 131056,
96
- "cost": {
97
- "input": 0.29,
98
- "output": 1.15,
99
- "cacheRead": 0.0725,
100
- "cacheWrite": 0
77
+ "contextWindow": 262128,
78
+ "thinkingLevelMap": {
79
+ "minimal": null,
80
+ "low": "low",
81
+ "medium": null,
82
+ "high": "high",
83
+ "xhigh": null,
84
+ "max": "max"
101
85
  },
102
86
  "compat": {
103
87
  "supportsDeveloperRole": false,
@@ -110,11 +94,13 @@
110
94
  "reasoning": true,
111
95
  "input": ["text"],
112
96
  "contextWindow": 1000000,
113
- "cost": {
114
- "input": 1.45,
115
- "output": 4.5,
116
- "cacheRead": 0.3625,
117
- "cacheWrite": 0
97
+ "thinkingLevelMap": {
98
+ "minimal": null,
99
+ "low": "low",
100
+ "medium": null,
101
+ "high": "high",
102
+ "xhigh": null,
103
+ "max": null
118
104
  },
119
105
  "compat": {
120
106
  "supportsDeveloperRole": false,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "autoHandoff": {
3
- "enabled": false,
4
- "thresholdTokens": 128000
3
+ "enabled": true,
4
+ "thresholdTokens": 256000
5
5
  },
6
6
  "collapseChangelog": true,
7
7
  "compaction": {
@@ -9,19 +9,18 @@
9
9
  "keepRecentTokens": 32000,
10
10
  "reserveTokens": 64000
11
11
  },
12
- "defaultModel": "glm-5.2",
12
+ "defaultModel": "deepseek-v4-flash",
13
13
  "defaultProvider": "tokenin",
14
- "defaultThinkingLevel": "high",
14
+ "defaultThinkingLevel": "max",
15
15
  "doubleEscapeAction": "tree",
16
16
  "editorPaddingX": 0,
17
17
  "outputPad": 0,
18
- "showCacheMissNotices": true,
18
+ "showCacheMissNotices": false,
19
19
  "followUpMode": "one-at-a-time",
20
20
  "hideThinkingBlock": false,
21
21
  "images": {
22
22
  "autoResize": true
23
23
  },
24
- "lastChangelogVersion": "0.80.2",
25
24
  "packages": [
26
25
  "git:github.com/MasuRii/pi-tool-display",
27
26
  "git:github.com/omaclaren/pi-markdown-preview"
@@ -29,7 +28,7 @@
29
28
  "quietStartup": true,
30
29
  "retry": {
31
30
  "enabled": true,
32
- "maxRetries": 3
31
+ "maxRetries": 7
33
32
  },
34
33
  "steeringMode": "all",
35
34
  "terminal": {
@@ -39,7 +38,7 @@
39
38
  "subagents": {
40
39
  "agentOverrides": {
41
40
  "architect": {
42
- "model": "tokenin/kimi-k3:high"
41
+ "model": "tokenin/deepseek-v4-pro:max"
43
42
  },
44
43
  "builder": {
45
44
  "model": "tokenin/deepseek-v4-flash:max"
@@ -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
@@ -115,6 +115,18 @@ This is different from `waitTool.enabled=false`, which returns immediately witho
115
115
 
116
116
  Forces depth-0 internal single, parallel, and chain runs into background mode and bypasses launch UI by forcing `clarify: false`. Nested calls keep their own inherited settings.
117
117
 
118
+ ## `timeoutMs`
119
+
120
+ ```json
121
+ { "timeoutMs": 3600000 }
122
+ ```
123
+
124
+ Global default runtime deadline, in milliseconds, for subagent runs. It replaces the built-in 30-minute backstop for foreground launches (single, parallel, chain, and workflowScript) and plain single-agent async runs whenever no call-level `timeoutMs`/`maxRuntimeMs` applies. For single-agent launches, selected agent frontmatter `timeoutMs` still wins. This only moves the *default*.
125
+
126
+ Use it when foreground orchestration or plain async single-agent runs need a longer default than 30 minutes. It does not set async composite top-level deadlines, and it does not replace async fan-out child deadlines.
127
+
128
+ Composite async runs (async chains, parallel tasks, and scripted workflows) stay unbounded at the top level by design. Their runner children are bounded individually by their own agent or runner defaults, so this value does not cap them. Must be a positive integer no greater than `2147483647` (the largest delay a Node.js timer can honor, roughly 24.8 days); invalid or out-of-range values are ignored and the built-in defaults apply.
129
+
118
130
  ## `globalConcurrencyLimit`
119
131
 
120
132
  ```json
@@ -131,9 +143,31 @@ Caps simultaneously running children inside existing durable legacy multi-child
131
143
 
132
144
  Optionally caps the total number of child subagent launches during one parent session, including completed and failed children, parallel task counts, static chain steps, and bounded dynamic fanout children. Sessions are unlimited by default. Set this value to `0` to disable a configured cap. `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`
149
+
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`.
135
157
 
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.
158
+ ## `maxActiveAsyncRunsPerSession`
159
+
160
+ ```json
161
+ { "maxActiveAsyncRunsPerSession": 4 }
162
+ ```
163
+
164
+ Optionally caps concurrently active top-level async runs owned by one parent session. Unset or `0` keeps the existing unlimited behavior. A positive integer reserves one slot before an async single, parallel, chain, or workflow creates run artifacts or starts children. Foreground runs and nested/workflow children do not reserve another slot.
165
+
166
+ Queued, running, paused, and needs-attention runs retain capacity. Runner-backed slots release only after terminal logical state and matching observed process-terminal proof from #1030. Missing, malformed, or unknown cleanup proof retains the slot. A terminal async workflow releases after its controller is gone and every launched child is accounted for: awaited foreground children are covered by workflow settlement, while actual background children still require observed process-terminal proof. Resume transfers the source slot without a second charge. Dismissal and history cleanup do not release capacity.
167
+
168
+ This limit bounds current top-level async load. It is separate from cumulative `maxSubagentSpawnsPerSession`, `maxSubagentSpawnsPerRun`, and `globalConcurrencyLimit`.
169
+
170
+ `subagent({ action: "status" })`, fleet status, and `subagent({ action: "doctor" })` expose used, effective limit, and remaining active capacity. Static chains and parallel calls fail before creating run artifacts or starting partial work when their declared capacity cannot fit. Later retries or unbounded dynamic work are not guaranteed by that preflight.
137
171
 
138
172
  ## `scheduledRuns`
139
173
 
@@ -196,6 +230,16 @@ export SELESAI_SUBAGENT_PI_BINARY=/path/to/pi-or-wrapper
196
230
 
197
231
  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
232
 
233
+ ## `SELESAI_SUBAGENT_TASK_DELIVERY`
234
+
235
+ ```bash
236
+ export SELESAI_SUBAGENT_TASK_DELIVERY=file # auto | file (default: auto)
237
+ ```
238
+
239
+ Controls how the task text reaches the child Pi process. `auto` (default) passes short tasks as an inline argv token and writes tasks longer than 8000 characters to a temp `task.md` referenced as `@<path>`. `file` always uses a temp file, keeping the task out of argv entirely.
240
+
241
+ Use `file` on hosts where endpoint protection (EDR) pre-execution scanning denies child processes whose command line embeds a long natural-language task — that denial surfaces as an immediate zero-activity `SIGKILL`. Independently of this setting, startup retries automatically escalate to file delivery after an unexplained zero-activity `SIGKILL`. Empty, whitespace-only, or unrecognized values fall back to `auto`.
242
+
199
243
  ## `intercomBridge`
200
244
 
201
245
  ```json
@@ -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. |
@@ -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",
@@ -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,12 @@ 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
+ }
56
62
  validateMissionStoreConfig(config.missions);
57
63
  validateAuthorityPolicy(config.authorityPolicy);
58
64
  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
  "",
@@ -31,6 +31,7 @@ import { createSubagentParamsSchema } from "./schemas.ts";
31
31
  import { validateChainInput } from "./chain-validation.ts";
32
32
  import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
33
33
  import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
34
+ import { getActiveAsyncCapacitySnapshot, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
34
35
  import { createResultWatcher } from "../runs/background/result-watcher.ts";
35
36
  import { createScheduledRunManager } from "../runs/background/scheduled-runs.ts";
36
37
  import { registerSlashCommands } from "../slash/slash-commands.ts";
@@ -66,6 +67,7 @@ import {
66
67
  SLASH_TEXT_RESULT_TYPE,
67
68
  SUBAGENT_ASYNC_COMPLETE_EVENT,
68
69
  SUBAGENT_ASYNC_STARTED_EVENT,
70
+ SUBAGENT_PROCESS_TERMINAL_EVENT,
69
71
  SUBAGENT_CONTROL_EVENT,
70
72
  SUBAGENT_STEERING_NOTICE_EVENT,
71
73
  WIDGET_KEY,
@@ -389,6 +391,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
389
391
  granted: 0,
390
392
  grantHistory: [],
391
393
  },
394
+ activeAsyncCapacity: { used: 0, limit: resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession) ?? 0 },
392
395
  asyncJobs: new Map(),
393
396
  fleetJobs: new Map(),
394
397
  foregroundRuns: new Map(),
@@ -689,12 +692,17 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
689
692
  };
690
693
  const asyncCompleteHandler = (payload: unknown) => {
691
694
  handleComplete(payload);
695
+ refreshActiveAsyncCapacity();
692
696
  scheduledRunManager.handleAsyncCompletion(payload);
693
697
  fleetStatus?.refresh();
694
698
  };
695
699
  const eventUnsubscribes = [
696
700
  pi.events.on(SUBAGENT_ASYNC_STARTED_EVENT, asyncStartedHandler),
697
701
  pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, asyncCompleteHandler),
702
+ pi.events.on(SUBAGENT_PROCESS_TERMINAL_EVENT, () => {
703
+ refreshActiveAsyncCapacity();
704
+ fleetStatus?.refresh();
705
+ }),
698
706
  pi.events.on(SUBAGENT_CONTROL_EVENT, controlEventHandler),
699
707
  pi.events.on(SUBAGENT_STEERING_NOTICE_EVENT, steeringNoticeHandler),
700
708
  herdrStatusBridge.dispose,
@@ -740,6 +748,18 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
740
748
  fleetStatus?.refresh();
741
749
  };
742
750
 
751
+ const refreshActiveAsyncCapacity = () => {
752
+ if (!state.currentSessionId) {
753
+ state.activeAsyncCapacity = { used: 0, limit: resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession) ?? 0 };
754
+ return;
755
+ }
756
+ state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(
757
+ state.currentSessionId,
758
+ resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession),
759
+ { liveWorkflowRunIds: new Set(state.workflowControllers?.keys() ?? []) },
760
+ );
761
+ };
762
+
743
763
  const resetSessionState = (ctx: ExtensionContext, recovering: boolean) => {
744
764
  state.widgetsSuspended = false;
745
765
  state.baseCwd = ctx.cwd;
@@ -765,6 +785,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
765
785
  }
766
786
  }
767
787
  state.lastUiContext = ctx;
788
+ refreshActiveAsyncCapacity();
768
789
  cleanupSessionArtifacts(ctx);
769
790
  state.foregroundControls.clear();
770
791
  state.lastForegroundControlId = null;
@@ -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" };