lsd-pi 1.3.2 → 1.3.7

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 (205) hide show
  1. package/dist/cli.js +2 -1
  2. package/dist/lsd-settings-manager.d.ts +2 -0
  3. package/dist/lsd-settings-manager.js +5 -0
  4. package/dist/resource-loader.js +33 -3
  5. package/dist/resources/extensions/browser-tools/tools/codegen.js +5 -5
  6. package/dist/resources/extensions/browser-tools/tools/navigation.js +107 -178
  7. package/dist/resources/extensions/browser-tools/tools/network-mock.js +112 -167
  8. package/dist/resources/extensions/browser-tools/tools/pages.js +182 -234
  9. package/dist/resources/extensions/browser-tools/tools/refs.js +202 -461
  10. package/dist/resources/extensions/browser-tools/tools/session.js +176 -323
  11. package/dist/resources/extensions/browser-tools/tools/state-persistence.js +91 -154
  12. package/dist/resources/extensions/browser-tools/utils.js +1 -1
  13. package/dist/resources/extensions/cache-timer/index.js +3 -2
  14. package/dist/resources/extensions/slash-commands/extension-manifest.json +2 -2
  15. package/dist/resources/extensions/slash-commands/fast.js +73 -0
  16. package/dist/resources/extensions/slash-commands/index.js +2 -0
  17. package/dist/resources/extensions/slash-commands/plan.js +37 -12
  18. package/dist/resources/extensions/subagent/background-job-manager.js +13 -0
  19. package/dist/resources/extensions/subagent/in-process-runner.js +387 -0
  20. package/dist/resources/extensions/subagent/index.js +278 -626
  21. package/dist/resources/extensions/subagent/legacy-runner.js +503 -0
  22. package/dist/resources/extensions/voice/index.js +96 -36
  23. package/dist/resources/extensions/voice/push-to-talk.js +26 -0
  24. package/dist/welcome-screen.js +2 -2
  25. package/package.json +1 -1
  26. package/packages/pi-agent-core/dist/agent.d.ts +19 -0
  27. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  28. package/packages/pi-agent-core/dist/agent.js +16 -0
  29. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  30. package/packages/pi-agent-core/src/agent.ts +32 -2
  31. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts +34 -1
  32. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts.map +1 -1
  33. package/packages/pi-ai/dist/providers/openai-codex-responses.js +32 -4
  34. package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
  35. package/packages/pi-ai/dist/providers/openai-codex-responses.test.js +127 -16
  36. package/packages/pi-ai/dist/providers/openai-codex-responses.test.js.map +1 -1
  37. package/packages/pi-ai/dist/providers/openai-responses.d.ts +8 -1
  38. package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  39. package/packages/pi-ai/dist/providers/openai-responses.fast-mode.test.d.ts +2 -0
  40. package/packages/pi-ai/dist/providers/openai-responses.fast-mode.test.d.ts.map +1 -0
  41. package/packages/pi-ai/dist/providers/openai-responses.fast-mode.test.js +67 -0
  42. package/packages/pi-ai/dist/providers/openai-responses.fast-mode.test.js.map +1 -0
  43. package/packages/pi-ai/dist/providers/openai-responses.js +21 -3
  44. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  45. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  46. package/packages/pi-ai/dist/providers/simple-options.js +2 -0
  47. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  48. package/packages/pi-ai/dist/types.d.ts +5 -0
  49. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  50. package/packages/pi-ai/dist/types.js.map +1 -1
  51. package/packages/pi-ai/src/providers/openai-codex-responses.test.ts +143 -20
  52. package/packages/pi-ai/src/providers/openai-codex-responses.ts +47 -4
  53. package/packages/pi-ai/src/providers/openai-responses.fast-mode.test.ts +73 -0
  54. package/packages/pi-ai/src/providers/openai-responses.ts +26 -3
  55. package/packages/pi-ai/src/providers/simple-options.ts +2 -0
  56. package/packages/pi-ai/src/types.ts +5 -0
  57. package/packages/pi-coding-agent/dist/core/keybindings.d.ts +1 -1
  58. package/packages/pi-coding-agent/dist/core/keybindings.d.ts.map +1 -1
  59. package/packages/pi-coding-agent/dist/core/keybindings.js +2 -0
  60. package/packages/pi-coding-agent/dist/core/keybindings.js.map +1 -1
  61. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  62. package/packages/pi-coding-agent/dist/core/sdk.js +4 -2
  63. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  64. package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.d.ts +2 -0
  65. package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.d.ts.map +1 -0
  66. package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.js +35 -0
  67. package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.js.map +1 -0
  68. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +12 -0
  69. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  70. package/packages/pi-coding-agent/dist/core/settings-manager.fast-mode.test.d.ts +2 -0
  71. package/packages/pi-coding-agent/dist/core/settings-manager.fast-mode.test.d.ts.map +1 -0
  72. package/packages/pi-coding-agent/dist/core/settings-manager.fast-mode.test.js +35 -0
  73. package/packages/pi-coding-agent/dist/core/settings-manager.fast-mode.test.js.map +1 -0
  74. package/packages/pi-coding-agent/dist/core/settings-manager.js +24 -0
  75. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  76. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  77. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  78. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  79. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  80. package/packages/pi-coding-agent/dist/core/system-prompt.js +6 -1
  81. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  82. package/packages/pi-coding-agent/dist/core/tool-priority.d.ts +4 -0
  83. package/packages/pi-coding-agent/dist/core/tool-priority.d.ts.map +1 -0
  84. package/packages/pi-coding-agent/dist/core/tool-priority.js +18 -0
  85. package/packages/pi-coding-agent/dist/core/tool-priority.js.map +1 -0
  86. package/packages/pi-coding-agent/dist/core/tool-priority.test.d.ts +2 -0
  87. package/packages/pi-coding-agent/dist/core/tool-priority.test.d.ts.map +1 -0
  88. package/packages/pi-coding-agent/dist/core/tool-priority.test.js +27 -0
  89. package/packages/pi-coding-agent/dist/core/tool-priority.test.js.map +1 -0
  90. package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts +5 -0
  91. package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts.map +1 -1
  92. package/packages/pi-coding-agent/dist/core/tools/edit-diff.js +21 -0
  93. package/packages/pi-coding-agent/dist/core/tools/edit-diff.js.map +1 -1
  94. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +16 -1
  95. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -1
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.d.ts +2 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.d.ts.map +1 -0
  98. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js +34 -0
  99. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js.map +1 -0
  100. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.d.ts +45 -0
  101. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.d.ts.map +1 -0
  102. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.js +314 -0
  103. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.js.map +1 -0
  104. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.test.d.ts +2 -0
  105. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.test.d.ts.map +1 -0
  106. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.test.js +122 -0
  107. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.test.js.map +1 -0
  108. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.d.ts +7 -5
  109. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.d.ts.map +1 -1
  110. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js +86 -28
  111. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js.map +1 -1
  112. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts +4 -0
  113. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  114. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +23 -10
  115. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  116. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +8 -0
  117. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  118. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +52 -6
  119. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +19 -0
  121. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  122. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +127 -14
  123. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  124. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts +14 -0
  125. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts.map +1 -0
  126. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js +93 -0
  127. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js.map +1 -0
  128. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.d.ts +2 -0
  129. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.d.ts.map +1 -0
  130. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js +328 -0
  131. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js.map +1 -0
  132. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  133. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +123 -0
  134. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  135. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js +1 -1
  136. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js.map +1 -1
  137. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  138. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +7 -0
  139. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  140. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +4 -0
  141. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
  142. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
  143. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +9 -1
  144. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  145. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +103 -23
  146. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  147. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.d.ts.map +1 -1
  148. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js +41 -0
  149. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js.map +1 -1
  150. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +4 -4
  151. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
  152. package/packages/pi-coding-agent/package.json +1 -1
  153. package/packages/pi-coding-agent/src/core/keybindings.ts +4 -1
  154. package/packages/pi-coding-agent/src/core/sdk.ts +4 -2
  155. package/packages/pi-coding-agent/src/core/settings-manager.collapse-tool-calls.test.ts +46 -0
  156. package/packages/pi-coding-agent/src/core/settings-manager.fast-mode.test.ts +46 -0
  157. package/packages/pi-coding-agent/src/core/settings-manager.ts +36 -0
  158. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  159. package/packages/pi-coding-agent/src/core/system-prompt.ts +6 -1
  160. package/packages/pi-coding-agent/src/core/tool-priority.test.ts +30 -0
  161. package/packages/pi-coding-agent/src/core/tool-priority.ts +17 -0
  162. package/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +20 -0
  163. package/packages/pi-coding-agent/src/core/tools/edit-diff.ts +26 -0
  164. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-summary-line.test.ts +41 -0
  165. package/packages/pi-coding-agent/src/modes/interactive/components/btw-overlay.test.ts +172 -0
  166. package/packages/pi-coding-agent/src/modes/interactive/components/btw-overlay.ts +402 -0
  167. package/packages/pi-coding-agent/src/modes/interactive/components/diff.ts +105 -28
  168. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +21 -6
  169. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +63 -6
  170. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +1262 -1138
  171. package/packages/pi-coding-agent/src/modes/interactive/components/tool-summary-line.ts +120 -0
  172. package/packages/pi-coding-agent/src/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.ts +396 -0
  173. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +530 -398
  174. package/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts +1 -1
  175. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +7 -0
  176. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +4 -0
  177. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +109 -23
  178. package/packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts +60 -1
  179. package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +4 -4
  180. package/packages/pi-tui/dist/components/editor.js +3 -3
  181. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  182. package/packages/pi-tui/src/components/editor.ts +3 -3
  183. package/pkg/dist/modes/interactive/theme/themes.js +4 -4
  184. package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
  185. package/pkg/package.json +1 -1
  186. package/src/resources/extensions/browser-tools/tools/codegen.ts +5 -5
  187. package/src/resources/extensions/browser-tools/tools/navigation.ts +118 -196
  188. package/src/resources/extensions/browser-tools/tools/network-mock.ts +114 -205
  189. package/src/resources/extensions/browser-tools/tools/pages.ts +183 -237
  190. package/src/resources/extensions/browser-tools/tools/refs.ts +193 -507
  191. package/src/resources/extensions/browser-tools/tools/session.ts +182 -321
  192. package/src/resources/extensions/browser-tools/tools/state-persistence.ts +94 -172
  193. package/src/resources/extensions/browser-tools/utils.ts +1 -1
  194. package/src/resources/extensions/cache-timer/index.ts +3 -2
  195. package/src/resources/extensions/slash-commands/extension-manifest.json +2 -2
  196. package/src/resources/extensions/slash-commands/fast.ts +89 -0
  197. package/src/resources/extensions/slash-commands/index.ts +2 -0
  198. package/src/resources/extensions/slash-commands/plan.ts +42 -12
  199. package/src/resources/extensions/subagent/background-job-manager.ts +28 -0
  200. package/src/resources/extensions/subagent/in-process-runner.ts +534 -0
  201. package/src/resources/extensions/subagent/index.ts +489 -799
  202. package/src/resources/extensions/subagent/legacy-runner.ts +607 -0
  203. package/src/resources/extensions/voice/index.ts +308 -238
  204. package/src/resources/extensions/voice/push-to-talk.ts +42 -0
  205. package/src/resources/extensions/voice/tests/push-to-talk.test.ts +109 -0
@@ -11,24 +11,23 @@
11
11
  *
12
12
  * Uses JSON mode to capture structured output from subagents.
13
13
  */
14
- import { spawn, execFileSync } from "node:child_process";
15
14
  import * as crypto from "node:crypto";
16
15
  import * as fs from "node:fs";
17
16
  import * as os from "node:os";
18
17
  import * as path from "node:path";
19
18
  import { StringEnum } from "@gsd/pi-ai";
20
- import { getAgentDir, getMarkdownTheme, } from "@gsd/pi-coding-agent";
19
+ import { getMarkdownTheme, } from "@gsd/pi-coding-agent";
21
20
  import { Container, Key, Markdown, Spacer, Text } from "@gsd/pi-tui";
22
21
  import { Type } from "@sinclair/typebox";
23
22
  import { formatTokenCount, shortcutDesc } from "../shared/mod.js";
24
23
  import { discoverAgents } from "./agents.js";
25
- import { buildSubagentProcessArgs, getBundledExtensionPathsFromEnv } from "./launch-helpers.js";
26
24
  import { createIsolation, mergeDeltaPatches, readIsolationMode, } from "./isolation.js";
27
25
  import { registerWorker, updateWorker } from "./worker-registry.js";
28
- import { handleSubagentPermissionRequest, isSubagentPermissionRequest } from "./approval-proxy.js";
29
26
  import { resolveConfiguredSubagentModel } from "./configured-model.js";
30
- import { normalizeSubagentModel, resolveSubagentModel, } from "./model-resolution.js";
27
+ import { resolveSubagentModel } from "./model-resolution.js";
31
28
  import { loadEffectivePreferences } from "../shared/preferences.js";
29
+ import { readBudgetSubagentModelFromSettings, runLegacySingleAgent, stopLegacySubagents, } from "./legacy-runner.js";
30
+ import { MAX_IN_PROCESS_SUBAGENT_DEPTH, startInProcessSingleAgent, } from "./in-process-runner.js";
32
31
  import { CmuxClient } from "../cmux/index.js";
33
32
  import { BackgroundJobManager } from "./background-job-manager.js";
34
33
  import { runSubagentInBackground } from "./background-runner.js";
@@ -38,11 +37,13 @@ const MAX_PARALLEL_TASKS = 8;
38
37
  const MAX_CONCURRENCY = 4;
39
38
  const COLLAPSED_ITEM_COUNT = 10;
40
39
  const DEFAULT_AWAIT_SUBAGENT_TIMEOUT_SECONDS = 120;
41
- const liveSubagentProcesses = new Set();
40
+ const USE_IN_PROCESS_SUBAGENT = process.env.LSD_SUBAGENT_IN_PROCESS !== "0";
42
41
  const agentSessionLinksById = new Map();
43
42
  const agentSessionIdsByParent = new Map();
44
43
  const parentSessionByChild = new Map();
45
44
  const liveRuntimeBySessionFile = new Map();
45
+ const inProcessSubagentDepthBySessionId = new Map();
46
+ const inProcessSubagentAncestryBySessionId = new Map();
46
47
  let agentSessionLinkCounter = 0;
47
48
  function listSessionFiles(sessionDir) {
48
49
  if (!fs.existsSync(sessionDir))
@@ -57,25 +58,6 @@ function listSessionFiles(sessionDir) {
57
58
  return [];
58
59
  }
59
60
  }
60
- function detectNewSubagentSessionFile(sessionDir, before, startedAt) {
61
- const after = listSessionFiles(sessionDir);
62
- const created = after.filter((file) => !before.has(file));
63
- const candidates = created.length > 0 ? created : after;
64
- const ranked = candidates
65
- .map((file) => {
66
- let mtime = 0;
67
- try {
68
- mtime = fs.statSync(file).mtimeMs;
69
- }
70
- catch {
71
- mtime = 0;
72
- }
73
- return { file, mtime };
74
- })
75
- .filter((entry) => entry.mtime >= startedAt - 5000)
76
- .sort((a, b) => b.mtime - a.mtime);
77
- return ranked[0]?.file;
78
- }
79
61
  function registerAgentSessionLink(link) {
80
62
  const now = Date.now();
81
63
  const id = `agent-${++agentSessionLinkCounter}`;
@@ -203,35 +185,7 @@ const AwaitSubagentParams = Type.Object({
203
185
  })),
204
186
  });
205
187
  export async function stopLiveSubagents() {
206
- const active = Array.from(liveSubagentProcesses);
207
- if (active.length === 0)
208
- return;
209
- for (const proc of active) {
210
- try {
211
- proc.kill("SIGTERM");
212
- }
213
- catch {
214
- /* ignore */
215
- }
216
- }
217
- await Promise.all(active.map((proc) => new Promise((resolve) => {
218
- const done = () => resolve();
219
- const timer = setTimeout(done, 500);
220
- proc.once("exit", () => {
221
- clearTimeout(timer);
222
- resolve();
223
- });
224
- })));
225
- for (const proc of active) {
226
- if (proc.exitCode === null) {
227
- try {
228
- proc.kill("SIGKILL");
229
- }
230
- catch {
231
- /* ignore */
232
- }
233
- }
234
- }
188
+ await stopLegacySubagents();
235
189
  }
236
190
  function formatBackgroundSubagentResults(jobs) {
237
191
  if (jobs.length === 0)
@@ -449,442 +403,6 @@ async function mapWithConcurrencyLimit(items, concurrency, fn) {
449
403
  await Promise.all(workers);
450
404
  return results;
451
405
  }
452
- function writePromptToTempFile(agentName, prompt) {
453
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
454
- const safeName = agentName.replace(/[^\w.-]+/g, "_");
455
- const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
456
- fs.writeFileSync(filePath, prompt, { encoding: "utf-8", mode: 0o600 });
457
- return { dir: tmpDir, filePath };
458
- }
459
- function readBudgetSubagentModelFromSettings() {
460
- try {
461
- const settingsPath = path.join(getAgentDir(), "settings.json");
462
- if (!fs.existsSync(settingsPath))
463
- return undefined;
464
- const raw = fs.readFileSync(settingsPath, "utf-8");
465
- const parsed = JSON.parse(raw);
466
- return typeof parsed.budgetSubagentModel === "string"
467
- ? normalizeSubagentModel(parsed.budgetSubagentModel)
468
- : undefined;
469
- }
470
- catch {
471
- return undefined;
472
- }
473
- }
474
- function resolveSubagentCliPath(defaultCwd) {
475
- const candidates = [process.env.GSD_BIN_PATH, process.env.LSD_BIN_PATH, process.argv[1]]
476
- .map((value) => value?.trim())
477
- .filter((value) => Boolean(value && value !== "undefined"));
478
- for (const candidate of candidates) {
479
- if (path.isAbsolute(candidate) && fs.existsSync(candidate))
480
- return candidate;
481
- }
482
- const cwdCandidates = [path.join(defaultCwd, "dist", "loader.js"), path.join(defaultCwd, "scripts", "dev-cli.js")];
483
- for (const candidate of cwdCandidates) {
484
- if (fs.existsSync(candidate))
485
- return candidate;
486
- }
487
- for (const binName of ["lsd", "gsd"]) {
488
- try {
489
- const resolved = execFileSync("which", [binName], { encoding: "utf-8" }).trim();
490
- if (resolved)
491
- return resolved;
492
- }
493
- catch {
494
- /* ignore */
495
- }
496
- }
497
- return null;
498
- }
499
- function processSubagentEventLine(line, currentResult, emitUpdate, proc, onSessionInfo, onEventType, onParsedEvent) {
500
- if (!line.trim())
501
- return false;
502
- let event;
503
- try {
504
- event = JSON.parse(line);
505
- }
506
- catch {
507
- return false;
508
- }
509
- const eventType = typeof event.type === "string" ? event.type : "unknown";
510
- onEventType?.(eventType);
511
- onParsedEvent?.(event);
512
- if (event.type === "subagent_session_info") {
513
- let changed = false;
514
- if (typeof event.sessionFile === "string" && event.sessionFile) {
515
- if (currentResult.sessionFile !== event.sessionFile)
516
- changed = true;
517
- currentResult.sessionFile = event.sessionFile;
518
- }
519
- if (typeof event.parentSessionFile === "string" && event.parentSessionFile) {
520
- if (currentResult.parentSessionFile !== event.parentSessionFile)
521
- changed = true;
522
- currentResult.parentSessionFile = event.parentSessionFile;
523
- }
524
- if (changed) {
525
- onSessionInfo?.({
526
- sessionFile: currentResult.sessionFile,
527
- parentSessionFile: currentResult.parentSessionFile,
528
- });
529
- }
530
- return false;
531
- }
532
- if (proc && isSubagentPermissionRequest(event)) {
533
- void handleSubagentPermissionRequest(event, proc);
534
- return false;
535
- }
536
- if ((event.type === "message_end" || event.type === "turn_end") && event.message) {
537
- const msg = event.message;
538
- currentResult.messages.push(msg);
539
- if (msg.role === "assistant") {
540
- currentResult.usage.turns++;
541
- const usage = msg.usage;
542
- if (usage) {
543
- currentResult.usage.input += usage.input || 0;
544
- currentResult.usage.output += usage.output || 0;
545
- currentResult.usage.cacheRead += usage.cacheRead || 0;
546
- currentResult.usage.cacheWrite += usage.cacheWrite || 0;
547
- currentResult.usage.cost += usage.cost?.total || 0;
548
- currentResult.usage.contextTokens = usage.totalTokens || 0;
549
- }
550
- if (msg.model && (!currentResult.model || msg.model.includes("/")))
551
- currentResult.model = msg.model;
552
- if (msg.stopReason)
553
- currentResult.stopReason = msg.stopReason;
554
- if (msg.errorMessage)
555
- currentResult.errorMessage = msg.errorMessage;
556
- }
557
- emitUpdate();
558
- }
559
- if (event.type === "tool_result_end" && event.message) {
560
- currentResult.messages.push(event.message);
561
- emitUpdate();
562
- }
563
- return event.type === "agent_end";
564
- }
565
- async function waitForFile(filePath, signal, timeoutMs = 30 * 60 * 1000) {
566
- const started = Date.now();
567
- while (Date.now() - started < timeoutMs) {
568
- if (signal?.aborted)
569
- return false;
570
- if (fs.existsSync(filePath))
571
- return true;
572
- await new Promise((resolve) => setTimeout(resolve, 150));
573
- }
574
- return false;
575
- }
576
- async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, modelOverride, parentModel, signal, onUpdate, makeDetails, parentSessionFile, attachableSession, onSessionInfo, onSubagentEvent, foregroundHooks) {
577
- const agent = agents.find((a) => a.name === agentName);
578
- if (!agent) {
579
- const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
580
- return {
581
- agent: agentName,
582
- agentSource: "unknown",
583
- task,
584
- exitCode: 1,
585
- messages: [],
586
- stderr: `Unknown agent: "${agentName}". Available agents: ${available}.`,
587
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
588
- step,
589
- };
590
- }
591
- let tmpPromptDir = null;
592
- let tmpPromptPath = null;
593
- const preferences = loadEffectivePreferences()?.preferences;
594
- const settingsBudgetModel = readBudgetSubagentModelFromSettings();
595
- const resolvedModel = resolveConfiguredSubagentModel(agent, preferences, settingsBudgetModel);
596
- const inferredModel = resolveSubagentModel({ name: agent.name, model: resolvedModel }, { overrideModel: modelOverride, parentModel });
597
- const currentResult = {
598
- agent: agentName,
599
- agentSource: agent.source,
600
- task,
601
- exitCode: 0,
602
- messages: [],
603
- stderr: "",
604
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
605
- model: inferredModel,
606
- step,
607
- parentSessionFile,
608
- };
609
- const emitUpdate = () => {
610
- if (onUpdate) {
611
- onUpdate({
612
- content: [{ type: "text", text: getFinalOutput(currentResult.messages) || "(running...)" }],
613
- details: makeDetails([currentResult]),
614
- });
615
- }
616
- };
617
- let wasAborted = false;
618
- let deferTempPromptCleanup = false;
619
- let tempPromptCleanupDone = false;
620
- const cleanupTempPromptFiles = () => {
621
- if (tempPromptCleanupDone)
622
- return;
623
- tempPromptCleanupDone = true;
624
- if (tmpPromptPath)
625
- try {
626
- fs.unlinkSync(tmpPromptPath);
627
- }
628
- catch {
629
- /* ignore */
630
- }
631
- if (tmpPromptDir)
632
- try {
633
- fs.rmdirSync(tmpPromptDir);
634
- }
635
- catch {
636
- /* ignore */
637
- }
638
- };
639
- try {
640
- if (agent.systemPrompt.trim()) {
641
- const tmp = writePromptToTempFile(agent.name, agent.systemPrompt);
642
- tmpPromptDir = tmp.dir;
643
- tmpPromptPath = tmp.filePath;
644
- }
645
- const effectiveCwd = cwd ?? defaultCwd;
646
- const subagentSessionDir = parentSessionFile ? path.dirname(parentSessionFile) : undefined;
647
- const sessionFilesBefore = attachableSession && subagentSessionDir
648
- ? new Set(listSessionFiles(subagentSessionDir))
649
- : undefined;
650
- const launchStartedAt = Date.now();
651
- const args = buildSubagentProcessArgs(agent, task, tmpPromptPath, inferredModel, {
652
- noSession: !attachableSession,
653
- parentSessionFile: parentSessionFile,
654
- mode: attachableSession ? "rpc" : "json",
655
- });
656
- const exitCode = await new Promise((resolve) => {
657
- const bundledPaths = getBundledExtensionPathsFromEnv();
658
- const extensionArgs = bundledPaths.flatMap((p) => ["--extension", p]);
659
- const cliPath = resolveSubagentCliPath(effectiveCwd);
660
- if (!cliPath) {
661
- currentResult.stderr += "Unable to resolve LSD/GSD CLI path for subagent launch.";
662
- resolve(1);
663
- return;
664
- }
665
- const proc = spawn(process.execPath, [cliPath, ...extensionArgs, ...args], { cwd: effectiveCwd, shell: false, stdio: ["pipe", "pipe", "pipe"] });
666
- // Keep stdin open so approval/classifier responses can be proxied back
667
- // into the child process. Closing it here can leave the subagent stuck
668
- // after its first turn when it requests permission for a tool call.
669
- liveSubagentProcesses.add(proc);
670
- let buffer = "";
671
- let completionSeen = false;
672
- let resolved = false;
673
- let foregroundReleased = false;
674
- let isBusy = false;
675
- let commandSeq = 0;
676
- const pendingCommandResponses = new Map();
677
- const procAbortController = new AbortController();
678
- let resolveBackgroundResult;
679
- let rejectBackgroundResult;
680
- const backgroundResultPromise = new Promise((resolveBg, rejectBg) => {
681
- resolveBackgroundResult = resolveBg;
682
- rejectBackgroundResult = rejectBg;
683
- });
684
- const sendRpcCommand = async (command) => {
685
- const id = `sa_cmd_${++commandSeq}`;
686
- if (!proc.stdin)
687
- throw new Error("Subagent RPC stdin is not available.");
688
- return new Promise((resolveCmd, rejectCmd) => {
689
- pendingCommandResponses.set(id, { resolve: resolveCmd, reject: rejectCmd });
690
- proc.stdin.write(JSON.stringify({ id, ...command }) + "\n");
691
- });
692
- };
693
- const finishForeground = (code) => {
694
- if (resolved)
695
- return;
696
- resolved = true;
697
- resolve(code);
698
- };
699
- const adoptToBackground = (jobId) => {
700
- if (resolved || foregroundReleased)
701
- return false;
702
- foregroundReleased = true;
703
- deferTempPromptCleanup = true;
704
- currentResult.backgroundJobId = jobId;
705
- finishForeground(0);
706
- return true;
707
- };
708
- backgroundResultPromise.finally(() => {
709
- if (deferTempPromptCleanup)
710
- cleanupTempPromptFiles();
711
- });
712
- foregroundHooks?.onStart?.({
713
- agentName,
714
- task,
715
- cwd: cwd ?? defaultCwd,
716
- parentSessionFile,
717
- abortController: procAbortController,
718
- resultPromise: backgroundResultPromise,
719
- adoptToBackground,
720
- sendPrompt: attachableSession
721
- ? async (text, images) => {
722
- await sendRpcCommand({ type: "prompt", message: text, images });
723
- }
724
- : undefined,
725
- sendSteer: attachableSession
726
- ? async (text, images) => {
727
- await sendRpcCommand({ type: "steer", message: text, images });
728
- }
729
- : undefined,
730
- sendFollowUp: attachableSession
731
- ? async (text, images) => {
732
- await sendRpcCommand({ type: "follow_up", message: text, images });
733
- }
734
- : undefined,
735
- isBusy: attachableSession ? () => isBusy : undefined,
736
- });
737
- proc.stdout.on("data", (data) => {
738
- buffer += data.toString();
739
- const lines = buffer.split("\n");
740
- buffer = lines.pop() || "";
741
- for (const line of lines) {
742
- const trimmed = line.trim();
743
- if (!trimmed)
744
- continue;
745
- if (attachableSession) {
746
- try {
747
- const parsed = JSON.parse(trimmed);
748
- if (parsed?.type === "response" && typeof parsed.id === "string" && pendingCommandResponses.has(parsed.id)) {
749
- const pending = pendingCommandResponses.get(parsed.id);
750
- pendingCommandResponses.delete(parsed.id);
751
- if (parsed.success === false) {
752
- pending.reject(new Error(typeof parsed.error === "string" ? parsed.error : "Subagent RPC command failed."));
753
- }
754
- else {
755
- pending.resolve(parsed.data);
756
- }
757
- continue;
758
- }
759
- }
760
- catch {
761
- // Fall through to generic event processing.
762
- }
763
- }
764
- if (processSubagentEventLine(trimmed, currentResult, emitUpdate, proc, onSessionInfo, (eventType) => {
765
- if (eventType === "agent_start")
766
- isBusy = true;
767
- if (eventType === "agent_end")
768
- isBusy = false;
769
- }, (event) => onSubagentEvent?.(event, currentResult))) {
770
- completionSeen = true;
771
- try {
772
- proc.kill("SIGTERM");
773
- }
774
- catch {
775
- /* ignore */
776
- }
777
- }
778
- }
779
- });
780
- proc.stderr.on("data", (data) => {
781
- currentResult.stderr += data.toString();
782
- });
783
- proc.on("close", (code) => {
784
- liveSubagentProcesses.delete(proc);
785
- if (buffer.trim()) {
786
- const completedOnFlush = processSubagentEventLine(buffer, currentResult, emitUpdate, proc, onSessionInfo, (eventType) => {
787
- if (eventType === "agent_start")
788
- isBusy = true;
789
- if (eventType === "agent_end")
790
- isBusy = false;
791
- }, (event) => onSubagentEvent?.(event, currentResult));
792
- completionSeen = completionSeen || completedOnFlush;
793
- }
794
- isBusy = false;
795
- for (const pending of pendingCommandResponses.values()) {
796
- pending.reject(new Error("Subagent process closed before command response."));
797
- }
798
- pendingCommandResponses.clear();
799
- const finalExitCode = completionSeen && (code === null || code === 143 || code === 15) ? 0 : (code ?? 0);
800
- currentResult.exitCode = finalExitCode;
801
- if (attachableSession && sessionFilesBefore && subagentSessionDir && !currentResult.sessionFile) {
802
- const detected = detectNewSubagentSessionFile(subagentSessionDir, sessionFilesBefore, launchStartedAt);
803
- if (detected)
804
- currentResult.sessionFile = detected;
805
- }
806
- resolveBackgroundResult?.({
807
- summary: getFinalOutput(currentResult.messages),
808
- stderr: currentResult.stderr,
809
- exitCode: finalExitCode,
810
- model: currentResult.model,
811
- sessionFile: currentResult.sessionFile,
812
- parentSessionFile: currentResult.parentSessionFile,
813
- });
814
- foregroundHooks?.onFinish?.();
815
- finishForeground(finalExitCode);
816
- });
817
- proc.on("error", (error) => {
818
- liveSubagentProcesses.delete(proc);
819
- isBusy = false;
820
- for (const pending of pendingCommandResponses.values()) {
821
- pending.reject(error instanceof Error ? error : new Error(String(error)));
822
- }
823
- pendingCommandResponses.clear();
824
- rejectBackgroundResult?.(error);
825
- foregroundHooks?.onFinish?.();
826
- finishForeground(1);
827
- });
828
- if (attachableSession) {
829
- void sendRpcCommand({ type: "prompt", message: task }).catch((error) => {
830
- currentResult.stderr += error instanceof Error ? error.message : String(error);
831
- try {
832
- proc.kill("SIGTERM");
833
- }
834
- catch {
835
- /* ignore */
836
- }
837
- });
838
- }
839
- const killProc = () => {
840
- // If the process has been adopted to the background (e.g. via Ctrl+B or
841
- // /agent attach_live), foregroundReleased is true and the process should
842
- // survive the parent session's abort — don't kill it.
843
- if (foregroundReleased)
844
- return;
845
- wasAborted = true;
846
- procAbortController.abort();
847
- proc.kill("SIGTERM");
848
- setTimeout(() => {
849
- if (!proc.killed)
850
- proc.kill("SIGKILL");
851
- }, 5000);
852
- };
853
- if (signal) {
854
- if (signal.aborted)
855
- killProc();
856
- else
857
- signal.addEventListener("abort", killProc, { once: true });
858
- }
859
- if (procAbortController.signal.aborted) {
860
- killProc();
861
- }
862
- else {
863
- procAbortController.signal.addEventListener("abort", () => {
864
- proc.kill("SIGTERM");
865
- setTimeout(() => {
866
- if (!proc.killed)
867
- proc.kill("SIGKILL");
868
- }, 5000);
869
- }, { once: true });
870
- }
871
- });
872
- currentResult.exitCode = exitCode;
873
- if (attachableSession && sessionFilesBefore && subagentSessionDir) {
874
- const detected = detectNewSubagentSessionFile(subagentSessionDir, sessionFilesBefore, launchStartedAt);
875
- if (detected) {
876
- currentResult.sessionFile = detected;
877
- }
878
- }
879
- if (wasAborted)
880
- throw new Error("Subagent was aborted");
881
- return currentResult;
882
- }
883
- finally {
884
- if (!deferTempPromptCleanup)
885
- cleanupTempPromptFiles();
886
- }
887
- }
888
406
  const TaskItem = Type.Object({
889
407
  agent: Type.String({ description: "Name of the agent to invoke" }),
890
408
  task: Type.String({ description: "Task to delegate to the agent" }),
@@ -1033,7 +551,6 @@ export default function (pi) {
1033
551
  "Do not spawn or delegate to another subagent with the same name as yourself.",
1034
552
  `If the user asks you to continue ${subagentName} work, do that work directly in this session.`,
1035
553
  taskNote,
1036
- "IMPORTANT: There is NO human available to answer questions in this session. Do NOT call ask_user_questions. Make all decisions autonomously based on the task and context.",
1037
554
  ].join("\n");
1038
555
  return {
1039
556
  systemPrompt: `${event.systemPrompt}\n\n${antiRecursion}\n\n${metadata.subagentSystemPrompt}`,
@@ -1041,9 +558,19 @@ export default function (pi) {
1041
558
  });
1042
559
  pi.on("input", async (event, ctx) => {
1043
560
  const sessionFile = ctx.sessionManager.getSessionFile();
1044
- if (!sessionFile)
1045
- return;
1046
- const runtime = liveRuntimeBySessionFile.get(sessionFile);
561
+ const runtimeFromSession = sessionFile ? liveRuntimeBySessionFile.get(sessionFile) : undefined;
562
+ const runtimeFromForeground = (!runtimeFromSession && activeForegroundSubagent && !activeForegroundSubagent.claimed &&
563
+ activeForegroundSubagent.sendPrompt && activeForegroundSubagent.sendSteer && activeForegroundSubagent.sendFollowUp && activeForegroundSubagent.isBusy &&
564
+ sessionFile && activeForegroundSubagent.parentSessionFile === sessionFile)
565
+ ? {
566
+ agentName: activeForegroundSubagent.agentName,
567
+ isBusy: activeForegroundSubagent.isBusy,
568
+ sendPrompt: activeForegroundSubagent.sendPrompt,
569
+ sendSteer: activeForegroundSubagent.sendSteer,
570
+ sendFollowUp: activeForegroundSubagent.sendFollowUp,
571
+ }
572
+ : undefined;
573
+ const runtime = runtimeFromSession ?? runtimeFromForeground;
1047
574
  if (!runtime)
1048
575
  return;
1049
576
  const text = event.text?.trim();
@@ -1087,6 +614,8 @@ export default function (pi) {
1087
614
  agentSessionIdsByParent.clear();
1088
615
  parentSessionByChild.clear();
1089
616
  liveRuntimeBySessionFile.clear();
617
+ inProcessSubagentDepthBySessionId.clear();
618
+ inProcessSubagentAncestryBySessionId.clear();
1090
619
  liveStreamBufferBySession.clear();
1091
620
  });
1092
621
  // /subagents command
@@ -1226,31 +755,6 @@ export default function (pi) {
1226
755
  ctx.ui.notify("Live runtime is no longer available for this subagent. It may have completed.", "warning");
1227
756
  return;
1228
757
  }
1229
- // Adopt the foreground subagent to background before switching sessions.
1230
- // switchSession calls abort() which would fire the tool signal and SIGTERM
1231
- // the running subagent process. Adopting to background detaches the process
1232
- // from the foreground abort chain so it survives the session switch.
1233
- const foreground = activeForegroundSubagent;
1234
- if (foreground && !foreground.claimed && bgManager) {
1235
- foreground.claimed = true;
1236
- try {
1237
- const jobId = bgManager.adoptRunning(foreground.agentName, foreground.task, foreground.cwd, foreground.abortController, foreground.resultPromise, {
1238
- parentSessionFile: foreground.parentSessionFile ?? ctx.sessionManager.getSessionFile(),
1239
- });
1240
- const released = foreground.adoptToBackground(jobId);
1241
- if (!released) {
1242
- foreground.claimed = false;
1243
- bgManager.cancel(jobId);
1244
- }
1245
- else {
1246
- activeForegroundSubagent = null;
1247
- ctx.ui.setStatus(foregroundSubagentStatusKey, undefined);
1248
- }
1249
- }
1250
- catch {
1251
- foreground.claimed = false;
1252
- }
1253
- }
1254
758
  const switched = await ctx.switchSession(target.sessionFile);
1255
759
  if (switched.cancelled) {
1256
760
  ctx.ui.notify("Session switch was cancelled.", "warning");
@@ -1390,9 +894,19 @@ export default function (pi) {
1390
894
  running.claimed = true;
1391
895
  let jobId;
1392
896
  try {
1393
- jobId = manager.adoptRunning(running.agentName, running.task, running.cwd, running.abortController, running.resultPromise, {
1394
- parentSessionFile: running.parentSessionFile ?? ctx.sessionManager.getSessionFile(),
1395
- });
897
+ if (running.handle) {
898
+ jobId = manager.adoptHandle(running.agentName, running.task, running.cwd, running.handle, running.resultPromise, {
899
+ parentSessionFile: running.parentSessionFile ?? ctx.sessionManager.getSessionFile(),
900
+ });
901
+ }
902
+ else if (running.abortController) {
903
+ jobId = manager.adoptRunning(running.agentName, running.task, running.cwd, running.abortController, running.resultPromise, {
904
+ parentSessionFile: running.parentSessionFile ?? ctx.sessionManager.getSessionFile(),
905
+ });
906
+ }
907
+ else {
908
+ throw new Error("Foreground subagent cannot be moved to background (missing runtime handle).");
909
+ }
1396
910
  }
1397
911
  catch (error) {
1398
912
  running.claimed = false;
@@ -1456,7 +970,7 @@ export default function (pi) {
1456
970
  "For broad review or audit requests, use scout only as a prep step; the parent model or a reviewer should make the final judgments.",
1457
971
  "Skip scout when the user already named the exact file/function to inspect or the task is obviously narrow.",
1458
972
  "Use parallel mode when tasks are independent and don't need each other's output.",
1459
- "Default to foreground (background: false) for single-mode subagents. Only set background: true when the user explicitly asks to run it in the background or to keep chatting while it runs.",
973
+ "Use background: true when the user wants to keep chatting while a long-running agent works in parallel.",
1460
974
  "If the user wants to wait for a background subagent result, use await_subagent.",
1461
975
  ],
1462
976
  parameters: SubagentParams,
@@ -1468,6 +982,14 @@ export default function (pi) {
1468
982
  const cmuxClient = CmuxClient.fromPreferences(loadEffectivePreferences()?.preferences);
1469
983
  const cmuxSplitsEnabled = cmuxClient.getConfig().splits;
1470
984
  const invokingSessionFile = ctx.sessionManager.getSessionFile();
985
+ const invokingSessionId = ctx.sessionManager.getSessionId();
986
+ const invokingMetadata = getCurrentSessionSubagentMetadata(invokingSessionFile);
987
+ const currentSubagentName = invokingMetadata?.subagentName;
988
+ const trackedAncestry = inProcessSubagentAncestryBySessionId.get(invokingSessionId);
989
+ const currentAncestry = trackedAncestry ?? (currentSubagentName ? [currentSubagentName] : []);
990
+ const inferredCurrentDepth = currentSubagentName ? Math.max(currentAncestry.length, 1) : 0;
991
+ const currentSubagentDepth = Math.max(inProcessSubagentDepthBySessionId.get(invokingSessionId) ?? 0, inferredCurrentDepth);
992
+ const nextSubagentDepth = currentSubagentDepth + 1;
1471
993
  // Resolve isolation mode
1472
994
  const isolationMode = readIsolationMode();
1473
995
  const useIsolation = Boolean(params.isolated) && isolationMode !== "none";
@@ -1481,6 +1003,30 @@ export default function (pi) {
1481
1003
  projectAgentsDir: discovery.projectAgentsDir,
1482
1004
  results,
1483
1005
  });
1006
+ const trackInProcessDepth = (started, depth, ancestry) => {
1007
+ const sessionId = started.handle.sessionId;
1008
+ if (!sessionId)
1009
+ return;
1010
+ inProcessSubagentDepthBySessionId.set(sessionId, depth);
1011
+ inProcessSubagentAncestryBySessionId.set(sessionId, ancestry);
1012
+ void started.resultPromise.finally(() => {
1013
+ inProcessSubagentDepthBySessionId.delete(sessionId);
1014
+ inProcessSubagentAncestryBySessionId.delete(sessionId);
1015
+ });
1016
+ };
1017
+ const buildChildAncestry = (childAgentName) => [...currentAncestry, childAgentName];
1018
+ const requestedAgentNames = hasSingle
1019
+ ? [params.agent]
1020
+ : hasChain
1021
+ ? (params.chain ?? []).map((step) => step.agent)
1022
+ : (params.tasks ?? []).map((task) => task.agent);
1023
+ if (currentSubagentName && requestedAgentNames.some((name) => name === currentSubagentName)) {
1024
+ return {
1025
+ content: [{ type: "text", text: `Subagent "${currentSubagentName}" cannot spawn another subagent with the same name as itself.` }],
1026
+ details: makeDetails(hasChain ? "chain" : hasTasks ? "parallel" : "single")([]),
1027
+ isError: true,
1028
+ };
1029
+ }
1484
1030
  if (modeCount !== 1) {
1485
1031
  const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
1486
1032
  return {
@@ -1500,6 +1046,20 @@ export default function (pi) {
1500
1046
  isError: true,
1501
1047
  };
1502
1048
  }
1049
+ if (params.background && currentSubagentDepth > 0) {
1050
+ return {
1051
+ content: [{ type: "text", text: "Nested background subagent launches are not supported yet. Run the nested subagent in foreground mode." }],
1052
+ details: makeDetails("single")([]),
1053
+ isError: true,
1054
+ };
1055
+ }
1056
+ if (USE_IN_PROCESS_SUBAGENT && nextSubagentDepth > MAX_IN_PROCESS_SUBAGENT_DEPTH) {
1057
+ return {
1058
+ content: [{ type: "text", text: `Max subagent depth (${MAX_IN_PROCESS_SUBAGENT_DEPTH}) exceeded.` }],
1059
+ details: makeDetails(hasChain ? "chain" : hasTasks ? "parallel" : "single")([]),
1060
+ isError: true,
1061
+ };
1062
+ }
1503
1063
  if ((agentScope === "project" || agentScope === "both") && confirmProjectAgents && ctx.hasUI) {
1504
1064
  const requestedAgentNames = new Set();
1505
1065
  if (params.chain)
@@ -1544,7 +1104,13 @@ export default function (pi) {
1544
1104
  }
1545
1105
  }
1546
1106
  : undefined;
1547
- const result = await runSingleAgent(ctx.cwd, agents, step.agent, taskWithContext, step.cwd, i + 1, step.model, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, signal, chainUpdate, makeDetails("chain"), invokingSessionFile, false, undefined, undefined);
1107
+ const result = USE_IN_PROCESS_SUBAGENT
1108
+ ? await (async () => {
1109
+ const started = await startInProcessSingleAgent(ctx.cwd, agents, step.agent, taskWithContext, step.cwd, i + 1, step.model, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, signal, chainUpdate, makeDetails("chain"), invokingSessionFile, nextSubagentDepth, invokingSessionId, currentAncestry);
1110
+ trackInProcessDepth(started, nextSubagentDepth, buildChildAncestry(step.agent));
1111
+ return started.resultPromise;
1112
+ })()
1113
+ : await runLegacySingleAgent(ctx.cwd, agents, step.agent, taskWithContext, step.cwd, i + 1, step.model, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, signal, chainUpdate, makeDetails("chain"), invokingSessionFile, false, undefined, undefined);
1548
1114
  results.push(result);
1549
1115
  const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
1550
1116
  if (isError) {
@@ -1606,12 +1172,23 @@ export default function (pi) {
1606
1172
  const gridSurfaces = [];
1607
1173
  const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (t, index) => {
1608
1174
  const workerId = registerWorker(t.agent, t.task, index, batchSize, batchId);
1609
- const runTask = () => runSingleAgent(ctx.cwd, agents, t.agent, t.task, t.cwd, undefined, t.model, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, signal, (partial) => {
1610
- if (partial.details?.results[0]) {
1611
- allResults[index] = partial.details.results[0];
1612
- emitParallelUpdate();
1613
- }
1614
- }, makeDetails("parallel"), invokingSessionFile, false, undefined);
1175
+ const runTask = () => USE_IN_PROCESS_SUBAGENT
1176
+ ? (async () => {
1177
+ const started = await startInProcessSingleAgent(ctx.cwd, agents, t.agent, t.task, t.cwd, undefined, t.model, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, signal, (partial) => {
1178
+ if (partial.details?.results[0]) {
1179
+ allResults[index] = partial.details.results[0];
1180
+ emitParallelUpdate();
1181
+ }
1182
+ }, makeDetails("parallel"), invokingSessionFile, nextSubagentDepth, invokingSessionId, currentAncestry);
1183
+ trackInProcessDepth(started, nextSubagentDepth, buildChildAncestry(t.agent));
1184
+ return started.resultPromise;
1185
+ })()
1186
+ : runLegacySingleAgent(ctx.cwd, agents, t.agent, t.task, t.cwd, undefined, t.model, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, signal, (partial) => {
1187
+ if (partial.details?.results[0]) {
1188
+ allResults[index] = partial.details.results[0];
1189
+ emitParallelUpdate();
1190
+ }
1191
+ }, makeDetails("parallel"), invokingSessionFile, false, undefined);
1615
1192
  let result = await runTask();
1616
1193
  // Auto-retry failed tasks (likely API rate limit or transient error)
1617
1194
  const isFailed = result.exitCode !== 0 || (result.messages.length === 0 && !signal?.aborted);
@@ -1668,11 +1245,164 @@ export default function (pi) {
1668
1245
  const bgInferredModel = resolveSubagentModel({ name: agentForBg.name, model: bgResolvedModelCfg }, { overrideModel: params.model, parentModel: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined });
1669
1246
  let jobId;
1670
1247
  try {
1671
- jobId = runSubagentInBackground(manager, agents, params.agent, params.task, params.cwd, params.model, { defaultCwd: ctx.cwd, model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, parentSessionFile: invokingSessionFile }, async (bgSignal) => {
1672
- let liveSessionFile;
1673
- let liveRuntime;
1674
- const result = await runSingleAgent(ctx.cwd, agents, params.agent, params.task, params.cwd, undefined, params.model, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, bgSignal, undefined, // no streaming updates for background jobs
1675
- makeDetails("single"), invokingSessionFile, true, (info) => {
1248
+ if (USE_IN_PROCESS_SUBAGENT) {
1249
+ const started = await startInProcessSingleAgent(ctx.cwd, agents, params.agent, params.task, params.cwd, undefined, params.model, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, undefined, undefined, makeDetails("single"), invokingSessionFile, nextSubagentDepth, invokingSessionId, currentAncestry);
1250
+ trackInProcessDepth(started, nextSubagentDepth, buildChildAncestry(params.agent));
1251
+ const effectiveCwd = params.cwd ?? ctx.cwd;
1252
+ jobId = manager.adoptHandle(params.agent, params.task, effectiveCwd, started.handle, started.resultPromise.then((result) => ({
1253
+ summary: (getFinalOutput(result.messages) || "(no output)").slice(0, 300),
1254
+ stderr: result.stderr,
1255
+ exitCode: result.exitCode,
1256
+ model: result.model,
1257
+ sessionFile: result.sessionFile,
1258
+ parentSessionFile: result.parentSessionFile,
1259
+ })), {
1260
+ parentSessionFile: invokingSessionFile,
1261
+ model: bgInferredModel,
1262
+ });
1263
+ }
1264
+ else {
1265
+ jobId = runSubagentInBackground(manager, agents, params.agent, params.task, params.cwd, params.model, { defaultCwd: ctx.cwd, model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, parentSessionFile: invokingSessionFile }, async (bgSignal) => {
1266
+ let liveSessionFile;
1267
+ let liveRuntime;
1268
+ const result = await runLegacySingleAgent(ctx.cwd, agents, params.agent, params.task, params.cwd, undefined, params.model, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, bgSignal, undefined, // no streaming updates for background jobs
1269
+ makeDetails("single"), invokingSessionFile, true, (info) => {
1270
+ if (!invokingSessionFile || !info.sessionFile)
1271
+ return;
1272
+ upsertAgentSessionLink(params.agent, params.task, invokingSessionFile, info.sessionFile, "running");
1273
+ liveSessionFile = info.sessionFile;
1274
+ if (liveRuntime) {
1275
+ liveRuntime.sessionFile = info.sessionFile;
1276
+ liveRuntime.parentSessionFile = info.parentSessionFile ?? invokingSessionFile;
1277
+ liveRuntimeBySessionFile.set(info.sessionFile, liveRuntime);
1278
+ }
1279
+ }, (event, partial) => {
1280
+ const sessionFile = partial.sessionFile;
1281
+ if (!sessionFile || activeSessionFileForUi !== sessionFile)
1282
+ return;
1283
+ if (event?.type === "message_update" && event.assistantMessageEvent?.type === "text_delta") {
1284
+ const delta = String(event.assistantMessageEvent.delta ?? "");
1285
+ if (delta)
1286
+ pushLiveStreamDelta(sessionFile, delta);
1287
+ }
1288
+ if (event?.type === "message_end") {
1289
+ flushLiveStream(sessionFile);
1290
+ }
1291
+ }, {
1292
+ onStart: (control) => {
1293
+ if (!control.sendPrompt || !control.sendSteer || !control.sendFollowUp || !control.isBusy)
1294
+ return;
1295
+ liveRuntime = {
1296
+ sessionFile: liveSessionFile,
1297
+ parentSessionFile: invokingSessionFile,
1298
+ agentName: params.agent,
1299
+ isBusy: control.isBusy,
1300
+ sendPrompt: control.sendPrompt,
1301
+ sendSteer: control.sendSteer,
1302
+ sendFollowUp: control.sendFollowUp,
1303
+ };
1304
+ if (liveSessionFile) {
1305
+ liveRuntimeBySessionFile.set(liveSessionFile, liveRuntime);
1306
+ }
1307
+ },
1308
+ onFinish: () => {
1309
+ if (liveSessionFile)
1310
+ liveRuntimeBySessionFile.delete(liveSessionFile);
1311
+ },
1312
+ });
1313
+ return {
1314
+ exitCode: result.exitCode,
1315
+ finalOutput: getFinalOutput(result.messages),
1316
+ stderr: result.stderr,
1317
+ model: result.model,
1318
+ sessionFile: result.sessionFile,
1319
+ parentSessionFile: result.parentSessionFile,
1320
+ };
1321
+ });
1322
+ }
1323
+ }
1324
+ catch (err) {
1325
+ return {
1326
+ content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
1327
+ details: makeDetails("single")([]),
1328
+ isError: true,
1329
+ };
1330
+ }
1331
+ const bgModelLine = bgInferredModel ? `\nModel: ${bgInferredModel}` : "";
1332
+ return {
1333
+ content: [{ type: "text", text: `Background subagent started. Job ID: **${jobId}**\nAgent: ${params.agent}${bgModelLine}\nUse \`await_subagent\` to wait, \`/subagents wait ${jobId}\` to block in the TUI, or \`/subagents cancel ${jobId}\` to stop it.` }],
1334
+ details: makeDetails("single")([]),
1335
+ };
1336
+ }
1337
+ // ── Foreground (blocking) mode ───────────────────────────────
1338
+ let isolation = null;
1339
+ let mergeResult;
1340
+ try {
1341
+ const effectiveCwd = params.cwd ?? ctx.cwd;
1342
+ if (useIsolation) {
1343
+ const taskId = crypto.randomUUID();
1344
+ isolation = await createIsolation(effectiveCwd, taskId, isolationMode);
1345
+ }
1346
+ let result;
1347
+ if (USE_IN_PROCESS_SUBAGENT && !isolation) {
1348
+ const started = await startInProcessSingleAgent(ctx.cwd, agents, params.agent, params.task, params.cwd, undefined, params.model, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, signal, onUpdate, makeDetails("single"), invokingSessionFile, nextSubagentDepth, invokingSessionId, currentAncestry);
1349
+ trackInProcessDepth(started, nextSubagentDepth, buildChildAncestry(params.agent));
1350
+ const effectiveRunCwd = params.cwd ?? ctx.cwd;
1351
+ let releaseToBackground;
1352
+ const movedToBackground = new Promise((resolve) => {
1353
+ releaseToBackground = resolve;
1354
+ });
1355
+ activeForegroundSubagent = {
1356
+ claimed: false,
1357
+ agentName: params.agent,
1358
+ task: params.task,
1359
+ cwd: effectiveRunCwd,
1360
+ parentSessionFile: invokingSessionFile,
1361
+ handle: started.handle,
1362
+ resultPromise: started.resultPromise.then((done) => ({
1363
+ summary: getFinalOutput(done.messages) || "(no output)",
1364
+ stderr: done.stderr,
1365
+ exitCode: done.exitCode,
1366
+ model: done.model,
1367
+ sessionFile: done.sessionFile,
1368
+ parentSessionFile: done.parentSessionFile,
1369
+ })),
1370
+ adoptToBackground: (jobId) => {
1371
+ if (!releaseToBackground)
1372
+ return false;
1373
+ releaseToBackground(jobId);
1374
+ releaseToBackground = undefined;
1375
+ return true;
1376
+ },
1377
+ sendPrompt: started.handle.prompt,
1378
+ sendSteer: started.handle.steer,
1379
+ sendFollowUp: started.handle.followUp,
1380
+ isBusy: started.handle.isBusy,
1381
+ };
1382
+ ctx.ui.setStatus(foregroundSubagentStatusKey, foregroundSubagentHint);
1383
+ const winner = await Promise.race([
1384
+ started.resultPromise.then((done) => ({ type: "done", done })),
1385
+ movedToBackground.then((jobId) => ({ type: "background", jobId })),
1386
+ ]);
1387
+ if (winner.type === "background") {
1388
+ activeForegroundSubagent = null;
1389
+ ctx.ui.setStatus(foregroundSubagentStatusKey, undefined);
1390
+ result = {
1391
+ ...started.currentResult,
1392
+ backgroundJobId: winner.jobId,
1393
+ };
1394
+ }
1395
+ else {
1396
+ activeForegroundSubagent = null;
1397
+ ctx.ui.setStatus(foregroundSubagentStatusKey, undefined);
1398
+ result = winner.done;
1399
+ }
1400
+ }
1401
+ else {
1402
+ let liveSessionFile;
1403
+ let liveRuntime;
1404
+ result = await runLegacySingleAgent(ctx.cwd, agents, params.agent, params.task, isolation ? isolation.workDir : params.cwd, undefined, params.model, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, signal, onUpdate, makeDetails("single"), invokingSessionFile, !isolation, !isolation
1405
+ ? (info) => {
1676
1406
  if (!invokingSessionFile || !info.sessionFile)
1677
1407
  return;
1678
1408
  upsertAgentSessionLink(params.agent, params.task, invokingSessionFile, info.sessionFile, "running");
@@ -1682,7 +1412,9 @@ export default function (pi) {
1682
1412
  liveRuntime.parentSessionFile = info.parentSessionFile ?? invokingSessionFile;
1683
1413
  liveRuntimeBySessionFile.set(info.sessionFile, liveRuntime);
1684
1414
  }
1685
- }, (event, partial) => {
1415
+ }
1416
+ : undefined, !isolation
1417
+ ? (event, partial) => {
1686
1418
  const sessionFile = partial.sessionFile;
1687
1419
  if (!sessionFile || activeSessionFileForUi !== sessionFile)
1688
1420
  return;
@@ -1694,8 +1426,12 @@ export default function (pi) {
1694
1426
  if (event?.type === "message_end") {
1695
1427
  flushLiveStream(sessionFile);
1696
1428
  }
1697
- }, {
1429
+ }
1430
+ : undefined, !isolation
1431
+ ? {
1698
1432
  onStart: (control) => {
1433
+ activeForegroundSubagent = { ...control, claimed: false };
1434
+ ctx.ui.setStatus(foregroundSubagentStatusKey, foregroundSubagentHint);
1699
1435
  if (!control.sendPrompt || !control.sendSteer || !control.sendFollowUp || !control.isBusy)
1700
1436
  return;
1701
1437
  liveRuntime = {
@@ -1707,103 +1443,19 @@ export default function (pi) {
1707
1443
  sendSteer: control.sendSteer,
1708
1444
  sendFollowUp: control.sendFollowUp,
1709
1445
  };
1710
- if (liveSessionFile) {
1446
+ if (liveSessionFile && liveRuntime) {
1711
1447
  liveRuntimeBySessionFile.set(liveSessionFile, liveRuntime);
1712
1448
  }
1713
1449
  },
1714
1450
  onFinish: () => {
1451
+ activeForegroundSubagent = null;
1452
+ ctx.ui.setStatus(foregroundSubagentStatusKey, undefined);
1715
1453
  if (liveSessionFile)
1716
1454
  liveRuntimeBySessionFile.delete(liveSessionFile);
1717
1455
  },
1718
- });
1719
- return {
1720
- exitCode: result.exitCode,
1721
- finalOutput: getFinalOutput(result.messages),
1722
- stderr: result.stderr,
1723
- model: result.model,
1724
- sessionFile: result.sessionFile,
1725
- parentSessionFile: result.parentSessionFile,
1726
- };
1727
- });
1728
- }
1729
- catch (err) {
1730
- return {
1731
- content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
1732
- details: makeDetails("single")([]),
1733
- isError: true,
1734
- };
1735
- }
1736
- const bgModelLine = bgInferredModel ? `\nModel: ${bgInferredModel}` : "";
1737
- return {
1738
- content: [{ type: "text", text: `Background subagent started. Job ID: **${jobId}**\nAgent: ${params.agent}${bgModelLine}\nUse \`await_subagent\` to wait, \`/subagents wait ${jobId}\` to block in the TUI, or \`/subagents cancel ${jobId}\` to stop it.` }],
1739
- details: makeDetails("single")([]),
1740
- };
1741
- }
1742
- // ── Foreground (blocking) mode ───────────────────────────────
1743
- let isolation = null;
1744
- let mergeResult;
1745
- try {
1746
- const effectiveCwd = params.cwd ?? ctx.cwd;
1747
- if (useIsolation) {
1748
- const taskId = crypto.randomUUID();
1749
- isolation = await createIsolation(effectiveCwd, taskId, isolationMode);
1750
- }
1751
- let liveSessionFile;
1752
- let liveRuntime;
1753
- const result = await runSingleAgent(ctx.cwd, agents, params.agent, params.task, isolation ? isolation.workDir : params.cwd, undefined, params.model, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, signal, onUpdate, makeDetails("single"), invokingSessionFile, !isolation, !isolation
1754
- ? (info) => {
1755
- if (!invokingSessionFile || !info.sessionFile)
1756
- return;
1757
- upsertAgentSessionLink(params.agent, params.task, invokingSessionFile, info.sessionFile, "running");
1758
- liveSessionFile = info.sessionFile;
1759
- if (liveRuntime) {
1760
- liveRuntime.sessionFile = info.sessionFile;
1761
- liveRuntime.parentSessionFile = info.parentSessionFile ?? invokingSessionFile;
1762
- liveRuntimeBySessionFile.set(info.sessionFile, liveRuntime);
1763
1456
  }
1764
- }
1765
- : undefined, !isolation
1766
- ? (event, partial) => {
1767
- const sessionFile = partial.sessionFile;
1768
- if (!sessionFile || activeSessionFileForUi !== sessionFile)
1769
- return;
1770
- if (event?.type === "message_update" && event.assistantMessageEvent?.type === "text_delta") {
1771
- const delta = String(event.assistantMessageEvent.delta ?? "");
1772
- if (delta)
1773
- pushLiveStreamDelta(sessionFile, delta);
1774
- }
1775
- if (event?.type === "message_end") {
1776
- flushLiveStream(sessionFile);
1777
- }
1778
- }
1779
- : undefined, !isolation
1780
- ? {
1781
- onStart: (control) => {
1782
- activeForegroundSubagent = { ...control, claimed: false };
1783
- ctx.ui.setStatus(foregroundSubagentStatusKey, foregroundSubagentHint);
1784
- if (!control.sendPrompt || !control.sendSteer || !control.sendFollowUp || !control.isBusy)
1785
- return;
1786
- liveRuntime = {
1787
- sessionFile: liveSessionFile,
1788
- parentSessionFile: invokingSessionFile,
1789
- agentName: params.agent,
1790
- isBusy: control.isBusy,
1791
- sendPrompt: control.sendPrompt,
1792
- sendSteer: control.sendSteer,
1793
- sendFollowUp: control.sendFollowUp,
1794
- };
1795
- if (liveSessionFile && liveRuntime) {
1796
- liveRuntimeBySessionFile.set(liveSessionFile, liveRuntime);
1797
- }
1798
- },
1799
- onFinish: () => {
1800
- activeForegroundSubagent = null;
1801
- ctx.ui.setStatus(foregroundSubagentStatusKey, undefined);
1802
- if (liveSessionFile)
1803
- liveRuntimeBySessionFile.delete(liveSessionFile);
1804
- },
1805
- }
1806
- : undefined);
1457
+ : undefined);
1458
+ }
1807
1459
  if (result.sessionFile && invokingSessionFile) {
1808
1460
  const existingParent = parentSessionByChild.get(result.sessionFile);
1809
1461
  if (!existingParent) {