@oh-my-pi/pi-coding-agent 17.2.4 → 17.2.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 (319) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/{CHANGELOG-bpmhv26t.md → CHANGELOG-q2w43aw3.md} +51 -0
  3. package/dist/cli.js +4148 -3717
  4. package/dist/{template-8vdv6xb4.js → template-c2hyaytt.js} +8 -26
  5. package/dist/types/cli/args.d.ts +4 -1
  6. package/dist/types/cli/browser-relay-cli.d.ts +15 -0
  7. package/dist/types/cli/command-help.d.ts +99 -0
  8. package/dist/types/cli/help-extra.d.ts +2 -0
  9. package/dist/types/cli/thinking-levels.d.ts +5 -0
  10. package/dist/types/cli/update-cli.d.ts +13 -0
  11. package/dist/types/commands/browser-relay.d.ts +38 -0
  12. package/dist/types/commands/commit.d.ts +3 -0
  13. package/dist/types/commands/complete.d.ts +1 -1
  14. package/dist/types/commands/completions.d.ts +6 -0
  15. package/dist/types/commands/grep.d.ts +3 -0
  16. package/dist/types/commands/join.d.ts +4 -0
  17. package/dist/types/commands/launch-help.d.ts +171 -0
  18. package/dist/types/commands/launch.d.ts +5 -1
  19. package/dist/types/commands/models.d.ts +3 -0
  20. package/dist/types/commands/say.d.ts +10 -0
  21. package/dist/types/commands/worktree.d.ts +3 -0
  22. package/dist/types/config/keybindings.d.ts +5 -0
  23. package/dist/types/config/model-discovery.d.ts +2 -2
  24. package/dist/types/config/models-config-schema-bundle.d.ts +3 -0
  25. package/dist/types/config/models-config-schema.d.ts +2 -0
  26. package/dist/types/config/models-config.d.ts +1 -0
  27. package/dist/types/config/service-tier.d.ts +2 -0
  28. package/dist/types/config/settings-schema.d.ts +69 -21
  29. package/dist/types/cursor-bridge-tools.d.ts +1 -1
  30. package/dist/types/cursor.d.ts +7 -4
  31. package/dist/types/edit/index.d.ts +3 -3
  32. package/dist/types/edit/modes/replace.d.ts +22 -15
  33. package/dist/types/edit/renderer.d.ts +1 -1
  34. package/dist/types/eval/py/kernel.d.ts +3 -1
  35. package/dist/types/eval/py/spawn-options.d.ts +12 -16
  36. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +8 -1
  37. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
  38. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
  39. package/dist/types/launch/broker.d.ts +1 -1
  40. package/dist/types/launch/client.d.ts +6 -3
  41. package/dist/types/launch/ensure.d.ts +8 -0
  42. package/dist/types/launch/paths.d.ts +1 -1
  43. package/dist/types/launch/protocol.d.ts +2 -2
  44. package/dist/types/lsp/client.d.ts +2 -0
  45. package/dist/types/lsp/mux/daemon.d.ts +15 -0
  46. package/dist/types/lsp/mux/protocol.d.ts +57 -0
  47. package/dist/types/lsp/mux/server.d.ts +18 -0
  48. package/dist/types/lsp/types.d.ts +22 -2
  49. package/dist/types/main.d.ts +2 -0
  50. package/dist/types/mnemopi/embed-client.d.ts +1 -1
  51. package/dist/types/mnemopi/state.d.ts +0 -23
  52. package/dist/types/modes/components/assistant-message.d.ts +2 -0
  53. package/dist/types/modes/components/custom-editor.d.ts +2 -1
  54. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  55. package/dist/types/modes/components/stripped-tool-calls-placeholder.d.ts +12 -0
  56. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  57. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  58. package/dist/types/modes/interactive-mode.d.ts +1 -0
  59. package/dist/types/modes/types.d.ts +1 -0
  60. package/dist/types/modes/workflow.d.ts +2 -1
  61. package/dist/types/sdk.d.ts +3 -1
  62. package/dist/types/secrets/index.d.ts +1 -1
  63. package/dist/types/session/agent-session-types.d.ts +3 -1
  64. package/dist/types/session/indexed-session-storage.d.ts +1 -0
  65. package/dist/types/session/session-advisors.d.ts +1 -1
  66. package/dist/types/session/session-context.d.ts +2 -1
  67. package/dist/types/session/session-maintenance.d.ts +1 -0
  68. package/dist/types/session/session-manager.d.ts +16 -11
  69. package/dist/types/session/session-stats.d.ts +10 -0
  70. package/dist/types/session/session-storage.d.ts +15 -3
  71. package/dist/types/system-prompt.d.ts +4 -1
  72. package/dist/types/task/index.d.ts +2 -1
  73. package/dist/types/task/spawn-policy.d.ts +5 -0
  74. package/dist/types/thinking.d.ts +1 -7
  75. package/dist/types/tools/ast-grep.d.ts +1 -1
  76. package/dist/types/tools/browser/registry.d.ts +2 -1
  77. package/dist/types/tools/browser/relay/bridge.d.ts +35 -0
  78. package/dist/types/tools/browser/relay/daemon.d.ts +16 -0
  79. package/dist/types/tools/browser/relay/kind.d.ts +19 -0
  80. package/dist/types/tools/browser/relay/protocol.d.ts +97 -0
  81. package/dist/types/tools/browser/relay/server.d.ts +35 -0
  82. package/dist/types/tools/browser/render.d.ts +1 -0
  83. package/dist/types/tools/browser.d.ts +3 -0
  84. package/dist/types/tools/computer/exposure.d.ts +9 -7
  85. package/dist/types/tools/computer/protocol.d.ts +70 -17
  86. package/dist/types/tools/computer/supervisor.d.ts +23 -10
  87. package/dist/types/tools/computer/worker-entry.d.ts +1 -0
  88. package/dist/types/tools/computer/worker.d.ts +42 -7
  89. package/dist/types/tools/computer-renderer.d.ts +7 -5
  90. package/dist/types/tools/computer.d.ts +25 -44
  91. package/dist/types/tools/context.d.ts +3 -0
  92. package/dist/types/tools/glob.d.ts +1 -1
  93. package/dist/types/tools/grep.d.ts +1 -1
  94. package/dist/types/tools/render-utils.d.ts +2 -0
  95. package/dist/types/tools/{browser/run-cancellation.d.ts → run-scope.d.ts} +8 -8
  96. package/dist/types/tools/tool-timeouts.d.ts +5 -0
  97. package/dist/types/tools/xdev.d.ts +8 -0
  98. package/dist/types/utils/clipboard.d.ts +2 -2
  99. package/dist/types/web/kagi.d.ts +1 -0
  100. package/dist/types/web/search/index.d.ts +1 -1
  101. package/dist/types/web/search/providers/anthropic.d.ts +1 -0
  102. package/dist/types/web/search/providers/base.d.ts +2 -0
  103. package/dist/types/web/search/providers/brave.d.ts +1 -0
  104. package/dist/types/web/search/providers/browser-page.d.ts +1 -0
  105. package/dist/types/web/search/providers/codex.d.ts +1 -0
  106. package/dist/types/web/search/providers/exa.d.ts +1 -0
  107. package/dist/types/web/search/providers/firecrawl.d.ts +1 -0
  108. package/dist/types/web/search/providers/gemini.d.ts +1 -0
  109. package/dist/types/web/search/providers/jina.d.ts +1 -0
  110. package/dist/types/web/search/providers/kagi.d.ts +1 -0
  111. package/dist/types/web/search/providers/kimi.d.ts +1 -0
  112. package/dist/types/web/search/providers/parallel.d.ts +1 -0
  113. package/dist/types/web/search/providers/perplexity.d.ts +1 -0
  114. package/dist/types/web/search/providers/searxng.d.ts +1 -0
  115. package/dist/types/web/search/providers/tavily.d.ts +1 -0
  116. package/dist/types/web/search/providers/tinyfish.d.ts +1 -0
  117. package/dist/types/web/search/providers/utils.d.ts +6 -9
  118. package/dist/types/web/search/providers/zai.d.ts +1 -0
  119. package/dist/types/web/search/types.d.ts +4 -0
  120. package/package.json +12 -12
  121. package/src/cli/args.ts +7 -87
  122. package/src/cli/browser-relay-cli.ts +119 -0
  123. package/src/cli/command-help.ts +103 -0
  124. package/src/cli/completion-gen.ts +5 -5
  125. package/src/cli/flag-tables.ts +9 -0
  126. package/src/cli/help-extra.ts +89 -0
  127. package/src/cli/setup-cli.ts +13 -21
  128. package/src/cli/thinking-levels.ts +7 -0
  129. package/src/cli/update-cli.ts +26 -7
  130. package/src/cli-commands.ts +170 -33
  131. package/src/cli.ts +17 -5
  132. package/src/commands/acp.ts +3 -1
  133. package/src/commands/agents.ts +3 -2
  134. package/src/commands/auth-broker.ts +3 -2
  135. package/src/commands/auth-gateway.ts +3 -2
  136. package/src/commands/bench.ts +2 -3
  137. package/src/commands/browser-relay.ts +53 -0
  138. package/src/commands/cleanse.ts +2 -2
  139. package/src/commands/commit.ts +3 -2
  140. package/src/commands/complete.ts +2 -1
  141. package/src/commands/completions.ts +3 -2
  142. package/src/commands/config.ts +3 -2
  143. package/src/commands/dry-balance.ts +2 -2
  144. package/src/commands/gallery.ts +3 -2
  145. package/src/commands/gc.ts +3 -2
  146. package/src/commands/grep.ts +3 -2
  147. package/src/commands/grievances.ts +3 -2
  148. package/src/commands/install.ts +2 -2
  149. package/src/commands/join.ts +3 -2
  150. package/src/commands/launch-help.ts +116 -0
  151. package/src/commands/launch.ts +7 -191
  152. package/src/commands/models.ts +3 -2
  153. package/src/commands/plugin.ts +3 -2
  154. package/src/commands/read.ts +3 -2
  155. package/src/commands/say.ts +3 -2
  156. package/src/commands/setup.ts +3 -2
  157. package/src/commands/shell.ts +3 -2
  158. package/src/commands/ssh.ts +3 -2
  159. package/src/commands/stats.ts +3 -2
  160. package/src/commands/tiny-models.ts +2 -2
  161. package/src/commands/token.ts +2 -2
  162. package/src/commands/ttsr.ts +2 -2
  163. package/src/commands/update.ts +3 -2
  164. package/src/commands/usage.ts +3 -2
  165. package/src/commands/web-search.ts +3 -2
  166. package/src/commands/worktree.ts +3 -2
  167. package/src/config/keybindings.ts +5 -0
  168. package/src/config/model-discovery.ts +30 -13
  169. package/src/config/model-registry.ts +9 -5
  170. package/src/config/models-config-schema-bundle.ts +9 -0
  171. package/src/config/resolve-config-value.ts +2 -1
  172. package/src/config/service-tier.ts +5 -0
  173. package/src/config/settings-schema.ts +72 -20
  174. package/src/cursor-bridge-tools.ts +1 -1
  175. package/src/cursor.ts +10 -14
  176. package/src/edit/diff.ts +17 -8
  177. package/src/edit/index.ts +20 -7
  178. package/src/edit/modes/replace.ts +93 -40
  179. package/src/edit/renderer.ts +1 -1
  180. package/src/edit/streaming.ts +14 -17
  181. package/src/eval/py/kernel.ts +2 -1
  182. package/src/eval/py/spawn-options.ts +24 -29
  183. package/src/export/html/template.js +8 -26
  184. package/src/extensibility/extensions/wrapper.ts +1 -0
  185. package/src/extensibility/legacy-pi-ai-shim.ts +22 -1
  186. package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -6
  187. package/src/extensibility/plugins/legacy-pi-compat.ts +124 -57
  188. package/src/extensibility/skills.ts +11 -0
  189. package/src/extensibility/utils.ts +10 -2
  190. package/src/launch/broker.ts +1 -1
  191. package/src/launch/client.ts +32 -13
  192. package/src/launch/ensure.ts +73 -0
  193. package/src/launch/paths.ts +1 -1
  194. package/src/launch/protocol.ts +2 -2
  195. package/src/lsp/client.ts +23 -2
  196. package/src/lsp/index.ts +8 -0
  197. package/src/lsp/mux/daemon.ts +348 -0
  198. package/src/lsp/mux/protocol.ts +96 -0
  199. package/src/lsp/mux/server.ts +797 -0
  200. package/src/lsp/types.ts +28 -2
  201. package/src/main.ts +7 -2
  202. package/src/mcp/oauth-flow.ts +11 -1
  203. package/src/mnemopi/embed-client.ts +49 -2
  204. package/src/mnemopi/state.ts +57 -23
  205. package/src/modes/components/assistant-message.ts +11 -0
  206. package/src/modes/components/chat-transcript-builder.ts +4 -0
  207. package/src/modes/components/custom-editor.ts +9 -0
  208. package/src/modes/components/read-tool-group.ts +10 -0
  209. package/src/modes/components/status-line/component.ts +1 -1
  210. package/src/modes/components/stripped-tool-calls-placeholder.ts +35 -0
  211. package/src/modes/components/tool-execution.ts +8 -0
  212. package/src/modes/components/tree-selector.ts +46 -48
  213. package/src/modes/controllers/event-controller.ts +17 -0
  214. package/src/modes/controllers/input-controller.ts +39 -1
  215. package/src/modes/controllers/mcp-command-controller.ts +11 -5
  216. package/src/modes/controllers/selector-controller.ts +20 -0
  217. package/src/modes/interactive-mode.ts +2 -0
  218. package/src/modes/types.ts +1 -0
  219. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  220. package/src/modes/utils/interactive-context-helpers.ts +1 -0
  221. package/src/modes/utils/ui-helpers.ts +6 -10
  222. package/src/modes/workflow.ts +8 -2
  223. package/src/prompts/agents/init.md +1 -1
  224. package/src/prompts/system/plan-mode-active.md +2 -2
  225. package/src/prompts/system/system-prompt.md +26 -52
  226. package/src/prompts/system/workflow-notice.md +3 -3
  227. package/src/prompts/tools/ast-grep.md +1 -1
  228. package/src/prompts/tools/bash.md +1 -1
  229. package/src/prompts/tools/browser.md +1 -0
  230. package/src/prompts/tools/computer.md +21 -21
  231. package/src/prompts/tools/glob.md +1 -1
  232. package/src/prompts/tools/grep.md +1 -1
  233. package/src/prompts/tools/replace.md +5 -4
  234. package/src/prompts/tools/task.md +4 -4
  235. package/src/sdk.ts +34 -6
  236. package/src/secrets/index.ts +1 -1
  237. package/src/session/agent-session-types.ts +3 -1
  238. package/src/session/agent-session.ts +13 -1
  239. package/src/session/indexed-session-storage.ts +9 -0
  240. package/src/session/prewalk.ts +24 -2
  241. package/src/session/session-advisors.ts +2 -2
  242. package/src/session/session-context.ts +16 -12
  243. package/src/session/session-dump-format.ts +1 -1
  244. package/src/session/session-maintenance.ts +38 -4
  245. package/src/session/session-manager.ts +102 -67
  246. package/src/session/session-paths.ts +118 -55
  247. package/src/session/session-stats.ts +48 -3
  248. package/src/session/session-storage.ts +30 -22
  249. package/src/session/stream-guards.ts +1 -1
  250. package/src/slash-commands/builtin-registry.ts +27 -24
  251. package/src/system-prompt.ts +8 -3
  252. package/src/task/index.ts +23 -8
  253. package/src/task/spawn-policy.ts +14 -0
  254. package/src/thinking.ts +2 -8
  255. package/src/tools/ast-grep.ts +10 -4
  256. package/src/tools/browser/cmux/cmux-tab.ts +5 -10
  257. package/src/tools/browser/cmux/rpc.ts +2 -8
  258. package/src/tools/browser/registry.ts +53 -2
  259. package/src/tools/browser/relay/bridge.ts +945 -0
  260. package/src/tools/browser/relay/daemon.ts +117 -0
  261. package/src/tools/browser/relay/extension-assets/background.js.txt +242 -0
  262. package/src/tools/browser/relay/extension-assets/manifest.json.txt +14 -0
  263. package/src/tools/browser/relay/extension-assets/options.html.txt +53 -0
  264. package/src/tools/browser/relay/extension-assets/options.js.txt +23 -0
  265. package/src/tools/browser/relay/kind.ts +41 -0
  266. package/src/tools/browser/relay/protocol.ts +54 -0
  267. package/src/tools/browser/relay/server.ts +141 -0
  268. package/src/tools/browser/render.ts +4 -1
  269. package/src/tools/browser/shared-daemon.ts +7 -58
  270. package/src/tools/browser/tab-supervisor.ts +5 -4
  271. package/src/tools/browser/tab-worker.ts +33 -12
  272. package/src/tools/browser.ts +26 -1
  273. package/src/tools/computer/exposure.ts +11 -35
  274. package/src/tools/computer/protocol.ts +51 -10
  275. package/src/tools/computer/supervisor.ts +227 -124
  276. package/src/tools/computer/worker-entry.ts +11 -6
  277. package/src/tools/computer/worker.ts +695 -81
  278. package/src/tools/computer-renderer.ts +104 -65
  279. package/src/tools/computer.ts +134 -428
  280. package/src/tools/context.ts +3 -0
  281. package/src/tools/glob.ts +9 -2
  282. package/src/tools/grep.ts +12 -6
  283. package/src/tools/inspect-image.ts +32 -6
  284. package/src/tools/render-utils.ts +2 -0
  285. package/src/tools/{browser/run-cancellation.ts → run-scope.ts} +11 -11
  286. package/src/tools/tool-timeouts.ts +1 -0
  287. package/src/tools/xdev.ts +29 -2
  288. package/src/utils/clipboard.ts +40 -14
  289. package/src/utils/command-args.ts +26 -28
  290. package/src/utils/tool-choice.ts +0 -13
  291. package/src/web/kagi.ts +2 -1
  292. package/src/web/search/index.ts +18 -2
  293. package/src/web/search/providers/anthropic.ts +4 -1
  294. package/src/web/search/providers/base.ts +2 -0
  295. package/src/web/search/providers/brave.ts +3 -1
  296. package/src/web/search/providers/browser-page.ts +6 -4
  297. package/src/web/search/providers/codex.ts +4 -1
  298. package/src/web/search/providers/duckduckgo.ts +2 -1
  299. package/src/web/search/providers/ecosia.ts +2 -1
  300. package/src/web/search/providers/exa.ts +4 -2
  301. package/src/web/search/providers/firecrawl.ts +3 -1
  302. package/src/web/search/providers/gemini.ts +8 -2
  303. package/src/web/search/providers/google.ts +2 -1
  304. package/src/web/search/providers/jina.ts +12 -2
  305. package/src/web/search/providers/kagi.ts +3 -0
  306. package/src/web/search/providers/kimi.ts +5 -1
  307. package/src/web/search/providers/mojeek.ts +2 -1
  308. package/src/web/search/providers/parallel.ts +5 -1
  309. package/src/web/search/providers/perplexity.ts +6 -3
  310. package/src/web/search/providers/public.ts +1 -1
  311. package/src/web/search/providers/searxng.ts +11 -5
  312. package/src/web/search/providers/startpage.ts +10 -4
  313. package/src/web/search/providers/synthetic.ts +11 -5
  314. package/src/web/search/providers/tavily.ts +3 -1
  315. package/src/web/search/providers/tinyfish.ts +3 -1
  316. package/src/web/search/providers/utils.ts +12 -10
  317. package/src/web/search/providers/xai.ts +2 -2
  318. package/src/web/search/providers/zai.ts +18 -3
  319. package/src/web/search/types.ts +6 -0
@@ -2,10 +2,17 @@ import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { getTerminalId } from "@oh-my-pi/pi-tui";
5
- import { getSessionsDir, getTerminalSessionsDir, isEnoent, logger, resolveEquivalentPath } from "@oh-my-pi/pi-utils";
5
+ import {
6
+ getSessionsDir,
7
+ getTerminalSessionsDir,
8
+ isEnoent,
9
+ isRecord,
10
+ logger,
11
+ resolveEquivalentPath,
12
+ } from "@oh-my-pi/pi-utils";
6
13
  import type { SessionStorage } from "./session-storage";
7
14
 
8
- const migratedSessionRoots = new Set<string>();
15
+ const SESSION_HEADER_PREFIX_BYTES = 4096;
9
16
 
10
17
  /**
11
18
  * Merge or rename a legacy session directory into its canonical target.
@@ -35,12 +42,16 @@ function encodeLegacyAbsoluteSessionDirName(cwd: string): string {
35
42
  return `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
36
43
  }
37
44
 
38
- function encodeRelativeSessionDirName(prefix: string, relative: string): string {
45
+ function encodeLegacyRelativeSessionDirName(prefix: string, relative: string): string {
39
46
  const encoded = relative.replace(/[/\\:]/g, "-");
40
47
  return encoded ? (prefix.endsWith("-") ? `${prefix}${encoded}` : `${prefix}-${encoded}`) : prefix;
41
48
  }
42
49
 
43
- function getDefaultSessionDirName(cwd: string): { encodedDirName: string; resolvedCwd: string } {
50
+ function getDefaultSessionDirName(cwd: string): {
51
+ encodedDirName: string;
52
+ legacyRelativeDirName: string | undefined;
53
+ resolvedCwd: string;
54
+ } {
44
55
  const resolvedCwd = path.resolve(cwd);
45
56
  const canonicalCwd = resolveEquivalentPath(resolvedCwd);
46
57
  const home = os.homedir();
@@ -49,72 +60,111 @@ function getDefaultSessionDirName(cwd: string): { encodedDirName: string; resolv
49
60
  const canonicalTempRoot = resolveEquivalentPath(tempRoot);
50
61
  const homeRelative = path.relative(canonicalHome, canonicalCwd);
51
62
  const tempRelative = path.relative(canonicalTempRoot, canonicalCwd);
52
- const encodedDirName =
53
- homeRelative === "" || (!homeRelative.startsWith("..") && !path.isAbsolute(homeRelative))
54
- ? encodeRelativeSessionDirName("-", homeRelative)
55
- : tempRelative === "" || (!tempRelative.startsWith("..") && !path.isAbsolute(tempRelative))
56
- ? encodeRelativeSessionDirName("-tmp", tempRelative)
57
- : encodeLegacyAbsoluteSessionDirName(canonicalCwd);
58
- return { encodedDirName, resolvedCwd };
59
- }
60
63
 
61
- /**
62
- * Migrate old `--<home-encoded>-*--` session dirs to the new `-*` format.
63
- * Runs once per sessions root on first access, best-effort.
64
- */
65
- function migrateHomeSessionDirs(sessionsRoot: string): void {
66
- if (migratedSessionRoots.has(sessionsRoot)) return;
67
- migratedSessionRoots.add(sessionsRoot);
64
+ let scope: "home" | "tmp" | "abs";
65
+ let legacyRelativeDirName: string | undefined;
66
+ if (homeRelative === "" || (!homeRelative.startsWith("..") && !path.isAbsolute(homeRelative))) {
67
+ scope = "home";
68
+ legacyRelativeDirName = encodeLegacyRelativeSessionDirName("-", homeRelative);
69
+ } else if (tempRelative === "" || (!tempRelative.startsWith("..") && !path.isAbsolute(tempRelative))) {
70
+ scope = "tmp";
71
+ legacyRelativeDirName = encodeLegacyRelativeSessionDirName("-tmp", tempRelative);
72
+ } else {
73
+ scope = "abs";
74
+ }
68
75
 
69
- const home = os.homedir();
70
- const homeEncoded = home.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-");
71
- const oldPrefix = `--${homeEncoded}-`;
72
- const oldExact = `--${homeEncoded}--`;
76
+ const normalized = canonicalCwd.replaceAll("\\", "/");
77
+ const readable = path
78
+ .basename(canonicalCwd)
79
+ .replace(/[^a-zA-Z0-9._-]+/g, "-")
80
+ .replace(/^-+|-+$/g, "")
81
+ .slice(-80);
82
+ const digest = Bun.SHA256.hash(normalized, "hex");
83
+ const encodedDirName = `${scope}-${readable || "project"}-${digest}`;
84
+ return { encodedDirName, legacyRelativeDirName, resolvedCwd };
85
+ }
73
86
 
74
- let entries: string[];
87
+ function readSessionCwd(sessionFile: string, buffer: Buffer): string | undefined {
88
+ let descriptor: number | undefined;
75
89
  try {
76
- entries = fs.readdirSync(sessionsRoot);
90
+ descriptor = fs.openSync(sessionFile, "r");
91
+ const bytesRead = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
92
+ const prefix = buffer.toString("utf8", 0, bytesRead);
93
+ for (const line of prefix.split(/\r?\n/, 3)) {
94
+ try {
95
+ const record: unknown = JSON.parse(line);
96
+ if (isRecord(record) && record.type === "session" && typeof record.cwd === "string") {
97
+ return record.cwd;
98
+ }
99
+ } catch {
100
+ // Ignore title slots or truncated/corrupt headers.
101
+ }
102
+ }
77
103
  } catch {
78
- return;
104
+ // Best-effort migration leaves unreadable sessions in the current bucket.
105
+ } finally {
106
+ if (descriptor !== undefined) fs.closeSync(descriptor);
79
107
  }
108
+ return undefined;
109
+ }
80
110
 
111
+ function moveSessionBundle(sourceDir: string, targetDir: string, sessionName: string, entries: string[]): void {
112
+ fs.mkdirSync(targetDir, { recursive: true });
113
+ const artifactsName = path.basename(sessionName, ".jsonl");
81
114
  for (const entry of entries) {
82
- let remainder: string;
83
- if (entry === oldExact) {
84
- remainder = "";
85
- } else if (entry.startsWith(oldPrefix) && entry.endsWith("--")) {
86
- remainder = entry.slice(oldPrefix.length, -2);
115
+ if (entry !== sessionName && entry !== artifactsName && !entry.startsWith(`${sessionName}.`)) continue;
116
+ const source = path.join(sourceDir, entry);
117
+ if (!fs.existsSync(source)) continue;
118
+ const target = path.join(targetDir, entry);
119
+ const existing = fs.statSync(target, { throwIfNoEntry: false });
120
+ if (!existing) {
121
+ fs.renameSync(source, target);
122
+ } else if (existing.isDirectory() && fs.statSync(source).isDirectory()) {
123
+ migrateSessionDirPath(source, target);
87
124
  } else {
88
- continue;
89
- }
90
-
91
- const newName = remainder ? `-${remainder}` : "-";
92
- const oldPath = path.join(sessionsRoot, entry);
93
- const newPath = path.join(sessionsRoot, newName);
94
-
95
- try {
96
- migrateSessionDirPath(oldPath, newPath);
97
- } catch {
98
- // Best effort
125
+ fs.rmSync(source, { recursive: true, force: true });
99
126
  }
100
127
  }
101
128
  }
102
129
 
103
- function migrateLegacyAbsoluteSessionDir(cwd: string, sessionDir: string, sessionsRoot: string): void {
104
- const legacyDir = path.join(sessionsRoot, encodeLegacyAbsoluteSessionDirName(cwd));
105
- if (legacyDir === sessionDir || !fs.existsSync(legacyDir)) return;
106
-
107
- try {
108
- migrateSessionDirPath(legacyDir, sessionDir);
109
- } catch {
110
- // Best effort
130
+ function rerouteCollidingSessions(
131
+ cwd: string,
132
+ legacyDir: string,
133
+ sessionsRoot: string,
134
+ kind: "relative" | "absolute",
135
+ ): void {
136
+ const entries = fs.readdirSync(legacyDir);
137
+ const currentCwd = resolveEquivalentPath(path.resolve(cwd));
138
+ const buffer = Buffer.allocUnsafe(SESSION_HEADER_PREFIX_BYTES);
139
+ for (const entry of entries) {
140
+ if (!entry.endsWith(".jsonl")) continue;
141
+ const recordedCwd = readSessionCwd(path.join(legacyDir, entry), buffer);
142
+ if (!recordedCwd) continue;
143
+ const recordedCanonical = resolveEquivalentPath(path.resolve(recordedCwd));
144
+ if (
145
+ recordedCanonical === currentCwd ||
146
+ !fs.statSync(recordedCanonical, { throwIfNoEntry: false })?.isDirectory()
147
+ ) {
148
+ continue;
149
+ }
150
+ const recordedNames = getDefaultSessionDirName(recordedCanonical);
151
+ const recordedLegacyName =
152
+ kind === "relative"
153
+ ? recordedNames.legacyRelativeDirName
154
+ : encodeLegacyAbsoluteSessionDirName(recordedCanonical);
155
+ if (recordedLegacyName !== path.basename(legacyDir)) continue;
156
+ moveSessionBundle(legacyDir, path.join(sessionsRoot, recordedNames.encodedDirName), entry, entries);
111
157
  }
112
158
  }
113
159
 
114
160
  export function resolveManagedSessionRoot(sessionDir: string, cwd: string): string | undefined {
115
161
  const currentDirName = path.basename(sessionDir);
116
- const { encodedDirName } = getDefaultSessionDirName(cwd);
117
- if (currentDirName !== encodedDirName && currentDirName !== encodeLegacyAbsoluteSessionDirName(cwd)) {
162
+ const { encodedDirName, legacyRelativeDirName } = getDefaultSessionDirName(cwd);
163
+ if (
164
+ currentDirName !== encodedDirName &&
165
+ currentDirName !== legacyRelativeDirName &&
166
+ currentDirName !== encodeLegacyAbsoluteSessionDirName(cwd)
167
+ ) {
118
168
  return undefined;
119
169
  }
120
170
  return path.dirname(sessionDir);
@@ -130,10 +180,23 @@ export function computeDefaultSessionDir(
130
180
  storage: SessionStorage,
131
181
  sessionsRoot: string = getSessionsDir(),
132
182
  ): string {
133
- const { encodedDirName, resolvedCwd } = getDefaultSessionDirName(cwd);
134
- migrateHomeSessionDirs(sessionsRoot);
183
+ const { encodedDirName, legacyRelativeDirName, resolvedCwd } = getDefaultSessionDirName(cwd);
135
184
  const sessionDir = path.join(sessionsRoot, encodedDirName);
136
- migrateLegacyAbsoluteSessionDir(resolvedCwd, sessionDir, sessionsRoot);
185
+ const legacyDirs: Array<{ kind: "relative" | "absolute"; name: string | undefined }> = [
186
+ { kind: "relative", name: legacyRelativeDirName },
187
+ { kind: "absolute", name: encodeLegacyAbsoluteSessionDirName(resolvedCwd) },
188
+ ];
189
+ for (const legacy of legacyDirs) {
190
+ if (!legacy.name) continue;
191
+ const legacyDir = path.join(sessionsRoot, legacy.name);
192
+ if (legacyDir === sessionDir || !fs.existsSync(legacyDir)) continue;
193
+ try {
194
+ rerouteCollidingSessions(resolvedCwd, legacyDir, sessionsRoot, legacy.kind);
195
+ migrateSessionDirPath(legacyDir, sessionDir);
196
+ } catch {
197
+ // Best effort
198
+ }
199
+ }
137
200
  storage.ensureDirSync(sessionDir);
138
201
  return sessionDir;
139
202
  }
@@ -34,6 +34,11 @@ export interface SessionStatsTrackerHost {
34
34
  sessionId(): string;
35
35
  }
36
36
 
37
+ function correctedPromptTokens(assistant: AssistantMessage): number {
38
+ const providerPromptTokens = assistant.contextSnapshot?.promptTokens ?? calculatePromptTokens(assistant.usage);
39
+ return Math.max(0, providerPromptTokens - (assistant.contextSnapshot?.historyRewriteTokensRemoved ?? 0));
40
+ }
41
+
37
42
  /** Computes session totals and tracks the in-flight context estimate. */
38
43
  export class SessionStatsTracker {
39
44
  readonly #host: SessionStatsTrackerHost;
@@ -160,8 +165,7 @@ export class SessionStatsTracker {
160
165
  const useAnchor =
161
166
  anchorAssistant !== undefined && anchorIndex !== -1 && (!pending || anchorIndex >= pending.cutoffCount);
162
167
  if (useAnchor && anchorAssistant) {
163
- const promptTokens =
164
- anchorAssistant.contextSnapshot?.promptTokens ?? calculatePromptTokens(anchorAssistant.usage);
168
+ const promptTokens = correctedPromptTokens(anchorAssistant);
165
169
  const nonMessageTokens =
166
170
  anchorAssistant.contextSnapshot?.nonMessageTokens ?? computeNonMessageTokens(this.#host.session);
167
171
  anchored = true;
@@ -199,7 +203,7 @@ export class SessionStatsTracker {
199
203
  ) {
200
204
  continue;
201
205
  }
202
- const promptTokens = message.contextSnapshot?.promptTokens ?? calculatePromptTokens(message.usage);
206
+ const promptTokens = correctedPromptTokens(message);
203
207
  const nonMessageTokens =
204
208
  message.contextSnapshot?.nonMessageTokens ?? computeNonMessageTokens(this.#host.session);
205
209
  let tailTokens = 0;
@@ -256,6 +260,47 @@ export class SessionStatsTracker {
256
260
  return this.#pendingContextSnapshot?.nonMessageTokens;
257
261
  }
258
262
 
263
+ /**
264
+ * Apply an estimated prompt-prefix reduction to the current provider anchor.
265
+ *
266
+ * History after the anchor is estimated live by {@link getContextBreakdown};
267
+ * callers must pass only savings from entries already included in the
268
+ * anchor's provider-reported prompt. Persisting the correction on the
269
+ * assistant snapshot keeps reloads accurate, and the next successful
270
+ * assistant response naturally replaces it with a fresh provider anchor.
271
+ */
272
+ recordAnchoredHistoryRewrite(tokensRemoved: number): void {
273
+ if (!Number.isFinite(tokensRemoved) || tokensRemoved <= 0) return;
274
+
275
+ const branchEntries = this.#host.sessionManager.getBranch();
276
+ const latestCompaction = getLatestCompactionEntry(branchEntries);
277
+ const compactionIndex = latestCompaction ? branchEntries.lastIndexOf(latestCompaction) : -1;
278
+ for (let index = branchEntries.length - 1; index > compactionIndex; index--) {
279
+ const entry = branchEntries[index];
280
+ if (entry.type !== "message" || entry.message.role !== "assistant") continue;
281
+ const assistant = entry.message;
282
+ if (
283
+ assistant.stopReason === "aborted" ||
284
+ assistant.stopReason === "error" ||
285
+ !assistant.usage ||
286
+ !hasContextTokenUsage(assistant.usage)
287
+ ) {
288
+ continue;
289
+ }
290
+
291
+ if (!assistant.contextSnapshot) {
292
+ assistant.contextSnapshot = {
293
+ promptTokens: calculatePromptTokens(assistant.usage),
294
+ nonMessageTokens: computeNonMessageTokens(this.#host.session),
295
+ };
296
+ }
297
+ const snapshot = assistant.contextSnapshot;
298
+ snapshot.historyRewriteTokensRemoved = (snapshot.historyRewriteTokensRemoved ?? 0) + Math.floor(tokensRemoved);
299
+ this.#contextUsageRevision++;
300
+ return;
301
+ }
302
+ }
303
+
259
304
  /** Sets or clears the in-flight context snapshot. */
260
305
  setPendingSnapshot(snapshot: PendingContextSnapshot | undefined): void {
261
306
  this.#pendingContextSnapshot = snapshot;
@@ -14,13 +14,25 @@ export interface SessionStorageStat {
14
14
 
15
15
  export interface SessionStorageWriter {
16
16
  /**
17
- * Append one newline-terminated line. File storage batches same-turn appends
18
- * until its microtask boundary; memory storage updates in-body; indexed
19
- * backends queue in call order.
17
+ * Append one newline-terminated line.
18
+ *
19
+ * File and memory storage apply the line synchronously before the returned
20
+ * promise settles, so a software crash after `append` returns (or after a
21
+ * fire-and-forget call begins) still sees the entry on disk / in body. No
22
+ * `fsync` — power loss may still drop the last page. Indexed backends update
23
+ * the local index immediately and queue the remote publish in call order.
20
24
  *
21
25
  * `line` MUST include the trailing newline.
22
26
  */
23
27
  append(line: string): Promise<void>;
28
+ /**
29
+ * Synchronous append when the backend can apply the line before return.
30
+ * File and memory implement this so {@link SessionManager} can latch the
31
+ * first write failure before the appending call returns (surfaced by a later
32
+ * flushSync/close/next append — the turn loop does not throw from append).
33
+ * Indexed backends update the local index immediately and queue remote I/O.
34
+ */
35
+ appendSync?(line: string): void;
24
36
  /** Resolve once all queued appends complete. No fsync. */
25
37
  flush(): Promise<void>;
26
38
  /** Drain synchronously flushable queued work when the backend supports it. No fsync. */
@@ -93,8 +105,6 @@ class FileSessionStorageWriter implements SessionStorageWriter {
93
105
  #closed = false;
94
106
  #error: Error | undefined;
95
107
  #onError: ((err: Error) => void) | undefined;
96
- #pending = "";
97
- #flushScheduled = false;
98
108
 
99
109
  constructor(fpath: string, options?: { flags?: "a" | "w"; onError?: (err: Error) => void }) {
100
110
  this.#onError = options?.onError;
@@ -129,34 +139,29 @@ class FileSessionStorageWriter implements SessionStorageWriter {
129
139
  }
130
140
  }
131
141
 
132
- #flushPendingNow(): void {
133
- this.#flushScheduled = false;
134
- if (this.#pending.length === 0) return;
135
- const pending = this.#pending;
136
- this.#pending = "";
142
+ appendSync(line: string): void {
143
+ if (this.#closed) throw new Error("Writer closed");
144
+ if (this.#error) throw this.#error;
145
+ // Write in-body so software crash after the call still sees the entry.
146
+ // Microtask batching used to leave completed transcript lines only in
147
+ // memory until the next event-loop turn; process crash then lost every
148
+ // post-checkpoint event. flush/flushSync remain no-op drains (no fsync).
137
149
  try {
138
- this.#writeNow(pending);
150
+ this.#writeNow(line);
139
151
  } catch (err) {
140
- this.#recordError(err);
152
+ throw this.#recordError(err);
141
153
  }
142
154
  }
143
155
 
144
156
  async append(line: string): Promise<void> {
145
- if (this.#closed) throw new Error("Writer closed");
146
- if (this.#error) throw this.#error;
147
- this.#pending += line;
148
- if (this.#flushScheduled) return;
149
- this.#flushScheduled = true;
150
- queueMicrotask(() => this.#flushPendingNow());
157
+ this.appendSync(line);
151
158
  }
152
159
 
153
160
  async flush(): Promise<void> {
154
- this.#flushPendingNow();
155
161
  if (this.#error) throw this.#error;
156
162
  }
157
163
 
158
164
  flushSync(): void {
159
- this.#flushPendingNow();
160
165
  if (this.#error) throw this.#error;
161
166
  }
162
167
 
@@ -166,7 +171,6 @@ class FileSessionStorageWriter implements SessionStorageWriter {
166
171
 
167
172
  async close(): Promise<void> {
168
173
  if (this.#closed) return;
169
- this.#flushPendingNow();
170
174
  this.#closed = true;
171
175
  // Unregister from finalization - we're closing properly
172
176
  writerRegistry.unregister(this);
@@ -487,7 +491,7 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
487
491
  return error;
488
492
  }
489
493
 
490
- async append(line: string): Promise<void> {
494
+ appendSync(line: string): void {
491
495
  if (this.#closed) throw new Error("Writer closed");
492
496
  if (this.#error) throw this.#error;
493
497
  try {
@@ -498,6 +502,10 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
498
502
  }
499
503
  }
500
504
 
505
+ async append(line: string): Promise<void> {
506
+ this.appendSync(line);
507
+ }
508
+
501
509
  async flush(): Promise<void> {
502
510
  if (this.#error) throw this.#error;
503
511
  }
@@ -164,7 +164,7 @@ export class StreamingEditGuard {
164
164
  const toolCall = messageContent[contentIndex] as ToolCall;
165
165
  if (toolCall.name !== "edit") return undefined;
166
166
  const args = toolCall.arguments;
167
- if (!args || typeof args !== "object" || Array.isArray(args) || "old_text" in args || "new_text" in args) {
167
+ if (!args || typeof args !== "object" || Array.isArray(args) || "old_string" in args || "new_string" in args) {
168
168
  return undefined;
169
169
  }
170
170
  const filePath = typeof args.path === "string" ? args.path : undefined;
@@ -102,39 +102,42 @@ function formatFastModeStatus(session: AgentSession): string {
102
102
  }
103
103
 
104
104
  /** Detailed, session-effective `/computer status` diagnostics. */
105
- function formatComputerUseStatus(session: AgentSession): string {
105
+ async function formatComputerUseStatus(session: AgentSession): Promise<string> {
106
106
  const enabled = session.settings.get("computer.enabled");
107
107
  const active = session.getEnabledToolNames().includes("computer");
108
108
  const model = session.model;
109
109
  const modelName = model ? formatModelString(model) : "none";
110
- const exposure = !enabled || !active ? "not exposed" : computerExposureMode(model);
111
- const toolState = active ? "active" : enabled ? "unavailable" : "inactive";
110
+ const exposure = !enabled
111
+ ? "not exposed (disabled)"
112
+ : !active
113
+ ? "not exposed (tool inactive)"
114
+ : computerExposureMode(model);
112
115
  const configured = {
113
- backend: session.settings.get("computer.backend"),
114
116
  display: session.settings.get("computer.display"),
115
117
  maxWidth: session.settings.get("computer.maxWidth"),
116
118
  maxHeight: session.settings.get("computer.maxHeight"),
117
119
  };
118
- const computerTool = session.getToolByName("computer") as Pick<ComputerTool, "effectiveConfiguration"> | undefined;
119
- const effective = computerTool?.effectiveConfiguration ?? configured;
120
- const configurationChanged =
121
- effective.backend !== configured.backend ||
122
- effective.display !== configured.display ||
123
- effective.maxWidth !== configured.maxWidth ||
124
- effective.maxHeight !== configured.maxHeight;
120
+ const computerTool = active
121
+ ? (session.getToolByName("computer") as Pick<ComputerTool, "capabilities"> | undefined)
122
+ : undefined;
123
+ const capabilities = await computerTool?.capabilities();
124
+ const capabilityStatus = capabilities
125
+ ? [
126
+ `backend=${capabilities.backend}${capabilities.displayServer ? `/${capabilities.displayServer}` : ""}`,
127
+ `capture=${capabilities.capture} (${capabilities.capturePermission})`,
128
+ `input=${capabilities.input} (${capabilities.inputPermission})`,
129
+ `ax=${capabilities.ax} (${capabilities.axPermission})`,
130
+ `backgroundWindowInput=${capabilities.backgroundWindowInput}`,
131
+ `deliveryModes=${capabilities.deliveryModes.join(",") || "none"}`,
132
+ ].join(", ")
133
+ : "session not started";
125
134
  return [
126
135
  `Computer use: ${enabled ? "enabled" : "disabled"}`,
127
- `tool: ${toolState}`,
128
- `backend: ${effective.backend}`,
129
- `display: ${effective.display}`,
130
- `capture: ${effective.maxWidth}×${effective.maxHeight}`,
131
- ...(configurationChanged
132
- ? [
133
- `next-session settings: backend=${configured.backend}, display=${configured.display}, capture=${configured.maxWidth}×${configured.maxHeight}`,
134
- ]
135
- : []),
136
- `model: ${modelName}`,
136
+ `tool: ${active ? "active" : "inactive"}`,
137
137
  `exposure: ${exposure}`,
138
+ `model: ${modelName}`,
139
+ `configured: display=${configured.display}, maxWidth=${configured.maxWidth}, maxHeight=${configured.maxHeight}`,
140
+ `capabilities: ${capabilityStatus}`,
138
141
  ].join(" · ");
139
142
  }
140
143
 
@@ -151,7 +154,7 @@ async function applyComputerUseToggle(session: AgentSession, enable: boolean): P
151
154
  }
152
155
  session.settings.override("computer.enabled", enable);
153
156
  return enable
154
- ? `Computer use enabled for this session. ${formatComputerUseStatus(session)}`
157
+ ? `Computer use enabled for this session. ${await formatComputerUseStatus(session)}`
155
158
  : "Computer use disabled for this session.";
156
159
  }
157
160
 
@@ -673,7 +676,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
673
676
  handle: async (command, runtime) => {
674
677
  const arg = command.args.trim().toLowerCase();
675
678
  if (arg === "status") {
676
- await runtime.output(formatComputerUseStatus(runtime.session));
679
+ await runtime.output(await formatComputerUseStatus(runtime.session));
677
680
  return commandConsumed();
678
681
  }
679
682
  if (!arg || arg === "toggle" || arg === "on" || arg === "off") {
@@ -686,7 +689,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
686
689
  handleTui: async (command, runtime) => {
687
690
  const arg = command.args.trim().toLowerCase();
688
691
  if (arg === "status") {
689
- runtime.ctx.showStatus(formatComputerUseStatus(runtime.ctx.session));
692
+ runtime.ctx.showStatus(await formatComputerUseStatus(runtime.ctx.session));
690
693
  runtime.ctx.editor.setText("");
691
694
  return;
692
695
  }
@@ -498,7 +498,8 @@ export interface BuildSystemPromptOptions {
498
498
  /**
499
499
  * Whether provider-native tool calling is active (no owned/in-band syntax).
500
500
  * When true and `inlineToolDescriptors` is false, the inventory renders as a
501
- * compact tool-name list; otherwise it renders full `# Tool:` sections. Default: true
501
+ * compact tool-name list; otherwise it renders the full Harmony-style
502
+ * `namespace functions { … }` catalog. Default: true
502
503
  */
503
504
  nativeTools?: boolean;
504
505
  /** Skills settings for discovery. */
@@ -525,6 +526,9 @@ export interface BuildSystemPromptOptions {
525
526
  taskMaxConcurrency?: number;
526
527
  /** Whether IRC-backed parallel coordination can be included in delegation policy. */
527
528
  taskIrcEnabled?: boolean;
529
+ /** Whether the read-only `scout` subagent is spawnable (not disabled, allowed by spawn policy). Defaults to true. */
530
+ scoutAvailable?: boolean;
531
+
528
532
  /** Rules with alwaysApply=true — their full content is injected into the prompt. */
529
533
  alwaysApplyRules?: AlwaysApplyRule[];
530
534
  /** Whether secret obfuscation is active. When true, explains the redaction format in the prompt. */
@@ -599,6 +603,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
599
603
  taskIrcEnabled = false,
600
604
  secretsEnabled = false,
601
605
  workspaceTree: providedWorkspaceTree,
606
+ scoutAvailable = true,
602
607
  memoryRootEnabled = false,
603
608
  securityEnabled = false,
604
609
  model,
@@ -793,7 +798,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
793
798
 
794
799
  // List mode shows a compact tool-name list; it only applies when descriptors
795
800
  // stay in provider-native tool schemas AND native tool calling is active.
796
- // Otherwise render full `# Tool:` sections inline in the system prompt.
801
+ // Otherwise render the full functions-namespace catalog in the system prompt.
797
802
  const toolListMode = !inlineToolDescriptors && nativeTools;
798
803
  // Build tool descriptions for system prompt rendering.
799
804
  const toolPromptNames = new Map<string, string>(toolNames.map(name => [name, tools?.get(name)?.wireName ?? name]));
@@ -826,7 +831,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
826
831
  examples: meta?.examples,
827
832
  };
828
833
  }),
829
- model ?? "",
830
834
  );
831
835
 
832
836
  // Filter skills for the rendered system prompt:
@@ -879,6 +883,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
879
883
  eagerTasksAlways,
880
884
  taskBatch,
881
885
  MAX_CONCURRENCY: normalizeConcurrencyLimit(taskMaxConcurrency),
886
+ scoutAvailable,
882
887
  taskIrcEnabled,
883
888
  secretsEnabled,
884
889
  hasMemoryRoot: memoryRootEnabled,
package/src/task/index.ts CHANGED
@@ -27,7 +27,7 @@ import { TASK_EFFORTS, type TaskEffort } from "../thinking";
27
27
  import { truncateForPrompt } from "../tools/approval";
28
28
  import { isIrcEnabled } from "../tools/hub";
29
29
  import { formatBytes, formatDuration } from "../tools/render-utils";
30
- import { resolveSpawnPolicy } from "./spawn-policy";
30
+ import { isScoutSpawnable, resolveSpawnPolicy } from "./spawn-policy";
31
31
  import {
32
32
  type AgentDefinition,
33
33
  type AgentProgress,
@@ -188,8 +188,10 @@ function renderDescription(options: TaskDescriptionOptions): string {
188
188
  readOnly: isReadOnlyAgent(agent),
189
189
  blocking: agent.blocking === true,
190
190
  }));
191
+ const scoutAvailable = isScoutSpawnable(options.disabledAgents, options.parentSpawns);
191
192
  return prompt.render(taskDescriptionTemplate, {
192
193
  agents: renderedAgents,
194
+ scoutAvailable,
193
195
  spawningDisabled,
194
196
  defaultAgent: spawnPolicy.defaultAgent,
195
197
  isolationEnabled: options.isolationEnabled,
@@ -398,15 +400,19 @@ const GENERIC_SPAWN_AGENTS: ReadonlySet<string> = new Set(["task", "sonic"]);
398
400
  * (DepthCapacity: it currently has the `task` tool). `agentNames` are the
399
401
  * per-item resolved agent types. Returns undefined when no nudge applies.
400
402
  */
401
- export function buildSpecializationAdvisory(agentNames: string[], depthCapacity: boolean): string | undefined {
403
+ export function buildSpecializationAdvisory(
404
+ agentNames: string[],
405
+ depthCapacity: boolean,
406
+ scoutAvailable = true,
407
+ ): string | undefined {
402
408
  if (!depthCapacity) return undefined;
403
409
  const generics = agentNames.filter(name => GENERIC_SPAWN_AGENTS.has(name));
404
410
  if (generics.length < 2) return undefined;
405
- return (
406
- `Tip: this call spawned ${generics.length} generic \`${generics[0]}\` workers. ` +
407
- `Check the agent list for a closer specialist type — e.g. read-only research belongs on ` +
408
- `\`agent: "scout"\`, which runs on a faster model.`
409
- );
411
+ const specialist = scoutAvailable
412
+ ? `Check the agent list for a closer specialist type — e.g. read-only research belongs on ` +
413
+ `\`agent: "scout"\`, which runs on a faster model.`
414
+ : `Check the agent list for a closer specialist type.`;
415
+ return `Tip: this call spawned ${generics.length} generic \`${generics[0]}\` workers. ${specialist}`;
410
416
  }
411
417
 
412
418
  /**
@@ -443,10 +449,11 @@ export function composeSpawnAdvisory(args: {
443
449
  depthCapacity: boolean;
444
450
  ircEnabled: boolean;
445
451
  willRunAsync: boolean;
452
+ scoutAvailable?: boolean;
446
453
  }): string | undefined {
447
454
  return (
448
455
  [
449
- buildSpecializationAdvisory(args.agents, args.depthCapacity),
456
+ buildSpecializationAdvisory(args.agents, args.depthCapacity, args.scoutAvailable),
450
457
  args.willRunAsync ? buildCoordinationAdvisory(args.items, args.depthCapacity, args.ircEnabled) : undefined,
451
458
  ]
452
459
  .filter(Boolean)
@@ -751,6 +758,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
751
758
  depthCapacity,
752
759
  ircEnabled,
753
760
  willRunAsync: false,
761
+ scoutAvailable: isScoutSpawnable(
762
+ this.session.settings.get("task.disabledAgents") as string[] | undefined,
763
+ this.session.getSessionSpawns?.() ?? "*",
764
+ ),
754
765
  });
755
766
  const result = await this.#executeSyncFanout(
756
767
  toolCallId,
@@ -784,6 +795,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
784
795
  depthCapacity,
785
796
  ircEnabled,
786
797
  willRunAsync: asyncItems.length > 0,
798
+ scoutAvailable: isScoutSpawnable(
799
+ this.session.settings.get("task.disabledAgents") as string[] | undefined,
800
+ this.session.getSessionSpawns?.() ?? "*",
801
+ ),
787
802
  });
788
803
  // Returns a fresh result (copied content array, copied text part) rather
789
804
  // than mutating the caller's — task results are short-lived here, but an
@@ -56,3 +56,17 @@ export function resolveSpawnPolicy(parentSpawns: string | boolean | null | undef
56
56
  allowedPromptText: allowedAgents.map(agent => `\`${agent}\``).join(", "),
57
57
  };
58
58
  }
59
+
60
+ /**
61
+ * Whether the `scout` agent is spawnable in a session: not disabled via
62
+ * `task.disabledAgents`, and permitted by the session spawn policy.
63
+ */
64
+ export function isScoutSpawnable(
65
+ disabledAgents: readonly string[] | undefined,
66
+ spawns: string | boolean | null | undefined,
67
+ ): boolean {
68
+ if (disabledAgents?.includes("scout")) return false;
69
+ const policy = resolveSpawnPolicy(spawns);
70
+ if (!policy.enabled) return false;
71
+ return policy.allowedAgents === null || policy.allowedAgents.includes("scout");
72
+ }