@oh-my-pi/pi-coding-agent 17.0.8 → 17.1.0

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 (340) hide show
  1. package/CHANGELOG.md +87 -1
  2. package/README.md +1 -1
  3. package/dist/cli.js +5495 -5021
  4. package/dist/types/async/job-manager.d.ts +37 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/bench-cli.d.ts +41 -0
  7. package/dist/types/commands/bench.d.ts +15 -3
  8. package/dist/types/commands/launch.d.ts +4 -0
  9. package/dist/types/config/__tests__/model-registry.test.d.ts +1 -0
  10. package/dist/types/config/model-registry.d.ts +30 -1
  11. package/dist/types/config/models-config-schema.d.ts +18 -6
  12. package/dist/types/config/models-config.d.ts +14 -5
  13. package/dist/types/config/provider-globals.d.ts +2 -2
  14. package/dist/types/config/service-tier.d.ts +5 -1
  15. package/dist/types/config/settings-schema.d.ts +350 -50
  16. package/dist/types/config/settings.d.ts +7 -0
  17. package/dist/types/dap/client.d.ts +20 -0
  18. package/dist/types/exec/bash-executor.d.ts +38 -0
  19. package/dist/types/exec/direnv.d.ts +33 -0
  20. package/dist/types/export/html/args.d.ts +15 -0
  21. package/dist/types/export/html/index.d.ts +11 -26
  22. package/dist/types/export/html/web-palette.d.ts +55 -116
  23. package/dist/types/extensibility/extensions/loader.d.ts +3 -0
  24. package/dist/types/extensibility/extensions/types.d.ts +17 -2
  25. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -0
  26. package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +10 -0
  27. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +14 -1
  28. package/dist/types/internal-urls/registry-helpers.d.ts +11 -0
  29. package/dist/types/live/controller.d.ts +40 -0
  30. package/dist/types/live/protocol.d.ts +85 -0
  31. package/dist/types/live/protocol.test.d.ts +1 -0
  32. package/dist/types/live/transport.d.ts +48 -0
  33. package/dist/types/live/visualizer.d.ts +33 -0
  34. package/dist/types/lsp/client.d.ts +18 -1
  35. package/dist/types/mcp/config-writer.test.d.ts +1 -0
  36. package/dist/types/mcp/manager.d.ts +6 -2
  37. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  38. package/dist/types/mcp/render.d.ts +2 -2
  39. package/dist/types/mcp/smithery-auth.d.ts +1 -1
  40. package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
  41. package/dist/types/mcp/tool-bridge.d.ts +7 -3
  42. package/dist/types/mcp/types.d.ts +6 -0
  43. package/dist/types/memory-backend/tool-names.d.ts +2 -0
  44. package/dist/types/modes/components/login-dialog.d.ts +2 -2
  45. package/dist/types/modes/components/oauth-selector.d.ts +8 -0
  46. package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
  47. package/dist/types/modes/components/settings-defs.d.ts +8 -1
  48. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
  49. package/dist/types/modes/components/tool-execution.d.ts +9 -1
  50. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  51. package/dist/types/modes/controllers/event-controller.d.ts +6 -1
  52. package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
  53. package/dist/types/modes/controllers/mcp-command-controller.d.ts +3 -0
  54. package/dist/types/modes/interactive-mode.d.ts +5 -0
  55. package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
  56. package/dist/types/modes/rpc/rpc-frame.d.ts +16 -0
  57. package/dist/types/modes/rpc/rpc-messages.d.ts +26 -0
  58. package/dist/types/modes/rpc/rpc-types.d.ts +40 -0
  59. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
  60. package/dist/types/modes/setup-wizard/scenes/types.d.ts +9 -1
  61. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
  62. package/dist/types/modes/theme/theme.d.ts +1 -1
  63. package/dist/types/modes/types.d.ts +3 -1
  64. package/dist/types/modes/utils/context-usage.d.ts +12 -2
  65. package/dist/types/plan-mode/plan-files.d.ts +10 -0
  66. package/dist/types/registry/agent-lifecycle.d.ts +21 -9
  67. package/dist/types/registry/agent-registry.d.ts +11 -4
  68. package/dist/types/sdk.d.ts +15 -1
  69. package/dist/types/secrets/index.d.ts +15 -2
  70. package/dist/types/secrets/obfuscator.d.ts +113 -15
  71. package/dist/types/session/acp-permission-gate.d.ts +19 -0
  72. package/dist/types/session/agent-session-events.d.ts +84 -0
  73. package/dist/types/session/agent-session-types.d.ts +314 -0
  74. package/dist/types/session/agent-session.d.ts +120 -750
  75. package/dist/types/session/async-job-delivery.d.ts +28 -0
  76. package/dist/types/session/bash-runner.d.ts +75 -0
  77. package/dist/types/session/blob-store.d.ts +11 -1
  78. package/dist/types/session/blob-store.test.d.ts +1 -0
  79. package/dist/types/session/checkpoint-entries.d.ts +28 -0
  80. package/dist/types/session/eval-runner.d.ts +49 -0
  81. package/dist/types/session/irc-bridge.d.ts +46 -0
  82. package/dist/types/session/messages.d.ts +22 -0
  83. package/dist/types/session/model-controls.d.ts +180 -0
  84. package/dist/types/session/prewalk.d.ts +46 -0
  85. package/dist/types/session/queued-messages.d.ts +22 -0
  86. package/dist/types/session/redis-session-storage.d.ts +1 -0
  87. package/dist/types/session/retry-fallback-chains.d.ts +74 -0
  88. package/dist/types/session/role-models.d.ts +15 -0
  89. package/dist/types/session/session-advisors.d.ts +228 -0
  90. package/dist/types/session/session-entries.d.ts +8 -0
  91. package/dist/types/session/session-handoff.d.ts +70 -0
  92. package/dist/types/session/session-history-format.d.ts +6 -1
  93. package/dist/types/session/session-loader.d.ts +6 -4
  94. package/dist/types/session/session-maintenance.d.ts +246 -0
  95. package/dist/types/session/session-manager.d.ts +41 -0
  96. package/dist/types/session/session-memory.d.ts +56 -0
  97. package/dist/types/session/session-provider-boundary.d.ts +62 -0
  98. package/dist/types/session/session-stats.d.ts +48 -0
  99. package/dist/types/session/session-tools.d.ts +143 -0
  100. package/dist/types/session/session-workspace.d.ts +32 -0
  101. package/dist/types/session/stream-guards.d.ts +44 -0
  102. package/dist/types/session/todo-tracker.d.ts +56 -0
  103. package/dist/types/session/ttsr-coordinator.d.ts +51 -0
  104. package/dist/types/session/turn-recovery.d.ts +225 -0
  105. package/dist/types/system-prompt.d.ts +2 -0
  106. package/dist/types/task/executor.d.ts +4 -1
  107. package/dist/types/task/output-manager.d.ts +3 -1
  108. package/dist/types/task/types.d.ts +14 -0
  109. package/dist/types/tools/approval.d.ts +1 -0
  110. package/dist/types/tools/browser/launch.d.ts +5 -0
  111. package/dist/types/tools/hub/jobs.d.ts +1 -1
  112. package/dist/types/tools/image-gen.d.ts +4 -4
  113. package/dist/types/tools/image-providers.d.ts +38 -0
  114. package/dist/types/tools/index.d.ts +8 -0
  115. package/dist/types/tools/path-utils.d.ts +8 -0
  116. package/dist/types/tools/report-tool-issue.d.ts +24 -9
  117. package/dist/types/tools/todo.d.ts +10 -4
  118. package/dist/types/tools/xdev.d.ts +5 -1
  119. package/dist/types/tts/streaming-player.d.ts +14 -0
  120. package/dist/types/tts/vocalizer.d.ts +5 -0
  121. package/dist/types/utils/jj.d.ts +29 -0
  122. package/dist/types/utils/lang-from-path.d.ts +1 -0
  123. package/dist/types/utils/title-generator.d.ts +30 -0
  124. package/dist/types/vibe/runtime.d.ts +36 -8
  125. package/dist/types/web/search/index.d.ts +1 -1
  126. package/dist/types/web/search/provider.d.ts +15 -5
  127. package/dist/types/web/search/providers/firecrawl.d.ts +9 -0
  128. package/dist/types/web/search/types.d.ts +95 -1
  129. package/package.json +13 -12
  130. package/scripts/generate-share-viewer.ts +4 -6
  131. package/scripts/legacy-pi-virtual-module.ts +1 -1
  132. package/src/advisor/__tests__/advisor.test.ts +643 -0
  133. package/src/advisor/runtime.ts +144 -43
  134. package/src/async/job-manager.ts +90 -2
  135. package/src/cli/args.ts +2 -0
  136. package/src/cli/auth-gateway-cli.ts +18 -3
  137. package/src/cli/bench-cli.ts +395 -29
  138. package/src/cli/flag-tables.ts +3 -0
  139. package/src/cli/gc-cli.ts +2 -2
  140. package/src/cli/grievances-cli.ts +2 -2
  141. package/src/cli/usage-cli.ts +11 -2
  142. package/src/collab/host.ts +3 -2
  143. package/src/commands/bench.ts +18 -4
  144. package/src/commands/launch.ts +4 -0
  145. package/src/config/__tests__/model-registry.test.ts +147 -0
  146. package/src/config/model-registry.ts +50 -2
  147. package/src/config/model-resolver.ts +0 -1
  148. package/src/config/models-config-schema.ts +15 -4
  149. package/src/config/provider-globals.ts +9 -9
  150. package/src/config/service-tier.ts +26 -1
  151. package/src/config/settings-schema.ts +250 -54
  152. package/src/config/settings.ts +81 -2
  153. package/src/dap/client.ts +37 -4
  154. package/src/edit/index.ts +49 -12
  155. package/src/eval/py/executor.ts +103 -33
  156. package/src/exec/bash-executor.ts +89 -6
  157. package/src/exec/direnv.ts +145 -0
  158. package/src/export/html/args.ts +20 -0
  159. package/src/export/html/index.ts +45 -47
  160. package/src/export/html/template.css +19 -1
  161. package/src/export/html/template.html +6 -0
  162. package/src/export/html/template.js +21 -0
  163. package/src/export/html/tool-views.generated.js +31 -31
  164. package/src/export/html/web-palette.ts +116 -132
  165. package/src/export/share.ts +249 -49
  166. package/src/extensibility/extensions/loader.ts +29 -1
  167. package/src/extensibility/extensions/runner.ts +6 -0
  168. package/src/extensibility/extensions/types.ts +33 -2
  169. package/src/extensibility/legacy-pi-coding-agent-shim.ts +1 -0
  170. package/src/extensibility/legacy-pi-tui-shim.ts +10 -0
  171. package/src/extensibility/plugins/legacy-pi-compat.ts +870 -311
  172. package/src/hindsight/state.ts +64 -6
  173. package/src/internal-urls/local-protocol.ts +2 -1
  174. package/src/internal-urls/memory-protocol.ts +2 -2
  175. package/src/internal-urls/registry-helpers.ts +40 -0
  176. package/src/internal-urls/skill-protocol.ts +2 -2
  177. package/src/internal-urls/ssh-protocol.ts +2 -1
  178. package/src/internal-urls/vault-protocol.ts +2 -1
  179. package/src/live/audio-worklet.txt +59 -0
  180. package/src/live/browser-runtime.txt +221 -0
  181. package/src/live/controller.ts +464 -0
  182. package/src/live/prompts/agent-final-message.md +3 -0
  183. package/src/live/prompts/live-instructions.md +23 -0
  184. package/src/live/protocol.test.ts +140 -0
  185. package/src/live/protocol.ts +233 -0
  186. package/src/live/transport.ts +490 -0
  187. package/src/live/visualizer.ts +234 -0
  188. package/src/lsp/client.ts +52 -9
  189. package/src/lsp/clients/biome-client.ts +3 -4
  190. package/src/lsp/index.ts +44 -11
  191. package/src/main.ts +9 -1
  192. package/src/mcp/config-writer.test.ts +43 -0
  193. package/src/mcp/config-writer.ts +109 -82
  194. package/src/mcp/manager.ts +39 -8
  195. package/src/mcp/oauth-discovery.ts +10 -2
  196. package/src/mcp/render.ts +94 -35
  197. package/src/mcp/smithery-auth.test.ts +29 -0
  198. package/src/mcp/smithery-auth.ts +3 -2
  199. package/src/mcp/tool-bridge.ts +107 -11
  200. package/src/mcp/types.ts +7 -0
  201. package/src/memories/index.ts +40 -20
  202. package/src/memory-backend/tool-names.ts +2 -0
  203. package/src/mnemopi/backend.ts +24 -7
  204. package/src/modes/acp/acp-agent.ts +56 -10
  205. package/src/modes/acp/acp-event-mapper.ts +8 -1
  206. package/src/modes/components/agent-hub.ts +27 -10
  207. package/src/modes/components/login-dialog.ts +14 -4
  208. package/src/modes/components/oauth-selector.ts +24 -7
  209. package/src/modes/components/plan-review-overlay.ts +350 -35
  210. package/src/modes/components/settings-defs.ts +28 -3
  211. package/src/modes/components/settings-selector.ts +178 -6
  212. package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
  213. package/src/modes/components/status-line/component.ts +123 -2
  214. package/src/modes/components/tool-execution.test.ts +63 -2
  215. package/src/modes/components/tool-execution.ts +11 -2
  216. package/src/modes/controllers/command-controller.ts +70 -22
  217. package/src/modes/controllers/event-controller.ts +111 -16
  218. package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
  219. package/src/modes/controllers/extension-ui-controller.ts +20 -4
  220. package/src/modes/controllers/live-command-controller.ts +178 -0
  221. package/src/modes/controllers/mcp-command-controller.ts +80 -41
  222. package/src/modes/controllers/omfg-controller.ts +44 -40
  223. package/src/modes/controllers/selector-controller.ts +18 -10
  224. package/src/modes/controllers/session-focus-controller.ts +6 -1
  225. package/src/modes/interactive-mode.ts +160 -38
  226. package/src/modes/rpc/rpc-client.ts +94 -3
  227. package/src/modes/rpc/rpc-frame.ts +164 -4
  228. package/src/modes/rpc/rpc-messages.ts +127 -0
  229. package/src/modes/rpc/rpc-mode.ts +79 -7
  230. package/src/modes/rpc/rpc-types.ts +34 -2
  231. package/src/modes/runtime-init.ts +2 -0
  232. package/src/modes/setup-wizard/scenes/model.ts +5 -7
  233. package/src/modes/setup-wizard/scenes/providers.ts +3 -2
  234. package/src/modes/setup-wizard/scenes/sign-in.ts +8 -2
  235. package/src/modes/setup-wizard/scenes/theme.ts +13 -3
  236. package/src/modes/setup-wizard/scenes/types.ts +9 -1
  237. package/src/modes/setup-wizard/scenes/web-search.ts +23 -10
  238. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  239. package/src/modes/theme/theme.ts +1 -1
  240. package/src/modes/types.ts +3 -1
  241. package/src/modes/utils/context-usage.ts +23 -7
  242. package/src/modes/utils/ui-helpers.ts +10 -5
  243. package/src/plan-mode/plan-files.ts +40 -0
  244. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  245. package/src/prompts/bench/cache-prefix.md +3 -0
  246. package/src/prompts/bench/cache-suffix.md +1 -0
  247. package/src/prompts/goals/guided-goal-system.md +24 -3
  248. package/src/prompts/system/custom-system-prompt.md +1 -1
  249. package/src/prompts/system/project-prompt.md +8 -1
  250. package/src/prompts/system/subagent-async-pending.md +6 -0
  251. package/src/prompts/system/system-prompt.md +1 -1
  252. package/src/prompts/system/workflow-notice.md +48 -40
  253. package/src/prompts/system/xdev-mount-notice.md +4 -0
  254. package/src/prompts/tools/task.md +12 -2
  255. package/src/prompts/tools/todo.md +3 -1
  256. package/src/registry/agent-lifecycle.ts +116 -46
  257. package/src/registry/agent-registry.ts +35 -8
  258. package/src/registry/persisted-agents.ts +26 -2
  259. package/src/sdk.ts +255 -139
  260. package/src/secrets/index.ts +127 -4
  261. package/src/secrets/obfuscator.ts +2328 -105
  262. package/src/session/acp-permission-gate.ts +165 -0
  263. package/src/session/agent-session-error-log.test.ts +1 -1
  264. package/src/session/agent-session-events.ts +66 -0
  265. package/src/session/agent-session-types.ts +334 -0
  266. package/src/session/agent-session.ts +1910 -12213
  267. package/src/session/artifacts.ts +6 -5
  268. package/src/session/async-job-delivery.ts +74 -0
  269. package/src/session/bash-runner.ts +326 -0
  270. package/src/session/blob-store.test.ts +56 -0
  271. package/src/session/blob-store.ts +18 -2
  272. package/src/session/checkpoint-entries.ts +81 -0
  273. package/src/session/eval-runner.ts +212 -0
  274. package/src/session/indexed-session-storage.ts +9 -2
  275. package/src/session/irc-bridge.ts +203 -0
  276. package/src/session/messages.ts +263 -1
  277. package/src/session/model-controls.ts +714 -0
  278. package/src/session/prewalk.ts +252 -0
  279. package/src/session/queued-messages.ts +93 -0
  280. package/src/session/redis-session-storage.ts +51 -11
  281. package/src/session/retry-fallback-chains.ts +455 -0
  282. package/src/session/role-models.ts +85 -0
  283. package/src/session/session-advisors.ts +1679 -0
  284. package/src/session/session-context.test.ts +224 -1
  285. package/src/session/session-context.ts +47 -5
  286. package/src/session/session-entries.ts +8 -0
  287. package/src/session/session-handoff.ts +308 -0
  288. package/src/session/session-history-format.ts +20 -9
  289. package/src/session/session-loader.ts +10 -51
  290. package/src/session/session-maintenance.ts +2983 -0
  291. package/src/session/session-manager.ts +390 -30
  292. package/src/session/session-memory.ts +222 -0
  293. package/src/session/session-provider-boundary.ts +307 -0
  294. package/src/session/session-stats.ts +293 -0
  295. package/src/session/session-tools.ts +943 -0
  296. package/src/session/session-workspace.ts +53 -0
  297. package/src/session/stream-guards.ts +417 -0
  298. package/src/session/todo-tracker.ts +380 -0
  299. package/src/session/ttsr-coordinator.ts +496 -0
  300. package/src/session/turn-recovery.ts +1629 -0
  301. package/src/slash-commands/builtin-registry.ts +92 -11
  302. package/src/slash-commands/helpers/usage-report.ts +25 -4
  303. package/src/system-prompt.ts +41 -17
  304. package/src/task/executor.ts +288 -70
  305. package/src/task/index.ts +148 -111
  306. package/src/task/output-manager.ts +25 -3
  307. package/src/task/persisted-revive.ts +4 -3
  308. package/src/task/structured-subagent.ts +4 -1
  309. package/src/task/types.ts +16 -0
  310. package/src/tools/approval.ts +55 -10
  311. package/src/tools/bash-interactive.ts +26 -0
  312. package/src/tools/bash-skill-urls.ts +28 -2
  313. package/src/tools/bash.ts +339 -123
  314. package/src/tools/browser/launch.ts +9 -1
  315. package/src/tools/browser/tab-supervisor.ts +8 -5
  316. package/src/tools/browser.ts +94 -47
  317. package/src/tools/hub/index.ts +1 -1
  318. package/src/tools/hub/jobs.ts +67 -8
  319. package/src/tools/image-gen.ts +20 -28
  320. package/src/tools/image-providers.ts +50 -0
  321. package/src/tools/index.ts +12 -0
  322. package/src/tools/path-utils.ts +49 -2
  323. package/src/tools/read.ts +156 -88
  324. package/src/tools/renderers.ts +7 -1
  325. package/src/tools/report-tool-issue.ts +79 -28
  326. package/src/tools/todo.ts +101 -11
  327. package/src/tools/vibe.ts +1 -2
  328. package/src/tools/write.ts +51 -1
  329. package/src/tools/xdev.ts +67 -3
  330. package/src/tts/streaming-player.ts +62 -4
  331. package/src/tts/vocalizer.ts +18 -1
  332. package/src/utils/jj.ts +125 -4
  333. package/src/utils/lang-from-path.ts +7 -0
  334. package/src/utils/title-generator.ts +137 -1
  335. package/src/vibe/runtime.ts +852 -72
  336. package/src/web/search/index.ts +3 -6
  337. package/src/web/search/provider.ts +31 -16
  338. package/src/web/search/providers/firecrawl.ts +46 -13
  339. package/src/web/search/providers/xai.ts +7 -1
  340. package/src/web/search/types.ts +8 -1
@@ -7,9 +7,24 @@ import type { ToolSession } from "../sdk";
7
7
  import { enforceInlineByteCap } from "../session/streaming-output";
8
8
  import { truncateForPrompt } from "./approval";
9
9
  import { resolveCmuxKind } from "./browser/cmux/rpc";
10
- import { acquireBrowser, type BrowserHandle, type BrowserKind, type BrowserKindTag } from "./browser/registry";
10
+ import {
11
+ acquireBrowser,
12
+ type BrowserHandle,
13
+ type BrowserKind,
14
+ type BrowserKindTag,
15
+ holdBrowser,
16
+ releaseBrowser,
17
+ } from "./browser/registry";
11
18
  import type { Observation, ScreenshotResult } from "./browser/tab-protocol";
12
- import { acquireTab, dropHeadlessTabs, getTab, releaseAllTabs, releaseTab, runInTab } from "./browser/tab-supervisor";
19
+ import {
20
+ type AcquireTabResult,
21
+ acquireTab,
22
+ dropHeadlessTabs,
23
+ getTab,
24
+ releaseAllTabs,
25
+ releaseTab,
26
+ runInTab,
27
+ } from "./browser/tab-supervisor";
13
28
  import type { OutputMeta } from "./output-meta";
14
29
  import { resolveToCwd } from "./path-utils";
15
30
  import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
@@ -232,52 +247,84 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
232
247
  );
233
248
  }
234
249
 
235
- const browser = await untilAborted(signal, () =>
236
- acquireBrowser(kind, {
237
- cwd: this.session.cwd,
238
- viewport: params.viewport
239
- ? {
240
- width: params.viewport.width,
241
- height: params.viewport.height,
242
- deviceScaleFactor: params.viewport.scale,
243
- }
244
- : undefined,
245
- appArgs: params.app?.args,
246
- signal,
247
- }),
248
- );
250
+ // The requested timeout must cover the *entire* open — browser
251
+ // acquisition (CDP discovery/connect), queued tab acquisition, worker
252
+ // creation, and navigation — not only `acquireTab`. Compose one deadline
253
+ // from the caller signal and `params.timeout` and thread it through both
254
+ // stages so a stalled acquisition rejects at the requested boundary.
255
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
256
+ const openSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
257
+ try {
258
+ const browser = await untilAborted(openSignal, () =>
259
+ acquireBrowser(kind, {
260
+ cwd: this.session.cwd,
261
+ viewport: params.viewport
262
+ ? {
263
+ width: params.viewport.width,
264
+ height: params.viewport.height,
265
+ deviceScaleFactor: params.viewport.scale,
266
+ }
267
+ : undefined,
268
+ appArgs: params.app?.args,
269
+ signal: openSignal,
270
+ }),
271
+ );
272
+
273
+ // Hold one open-acquisition lease across the whole tab acquisition.
274
+ // A freshly-created browser sits in the registry at refCount 0 until a
275
+ // tab takes a hold; without this lease an abort/timeout mid-acquisition
276
+ // (or a sibling open of a different tab name on the same browser that
277
+ // fails) could dispose it out from under this operation. The lease is
278
+ // released exactly once — the success and failure paths are mutually
279
+ // exclusive — transferring ownership to the published tab on success or
280
+ // rolling the fresh browser back on failure.
281
+ holdBrowser(browser);
282
+ let result: AcquireTabResult;
283
+ try {
284
+ result = await untilAborted(openSignal, () =>
285
+ acquireTab(name, browser, {
286
+ url: params.url,
287
+ waitUntil: params.wait_until,
288
+ viewport: params.viewport
289
+ ? {
290
+ width: params.viewport.width,
291
+ height: params.viewport.height,
292
+ deviceScaleFactor: params.viewport.scale,
293
+ }
294
+ : undefined,
295
+ target: params.app?.target,
296
+ timeoutMs,
297
+ dialogs: params.dialogs,
298
+ signal: openSignal,
299
+ ownerSessionId: this.session.getSessionId?.() ?? undefined,
300
+ }),
301
+ );
302
+ } catch (error) {
303
+ await releaseBrowser(browser, { kill: false });
304
+ throw error;
305
+ }
306
+ await releaseBrowser(browser, { kill: false });
249
307
 
250
- const result = await untilAborted(signal, () =>
251
- acquireTab(name, browser, {
252
- url: params.url,
253
- waitUntil: params.wait_until,
254
- viewport: params.viewport
255
- ? {
256
- width: params.viewport.width,
257
- height: params.viewport.height,
258
- deviceScaleFactor: params.viewport.scale,
259
- }
260
- : undefined,
261
- target: params.app?.target,
262
- timeoutMs,
263
- dialogs: params.dialogs,
264
- signal,
265
- ownerSessionId: this.session.getSessionId?.() ?? undefined,
266
- }),
267
- );
268
- const tab = result.tab;
269
- const url = tab.info.url;
270
- const title = tab.info.title ?? "";
271
- details.url = url;
272
- details.viewport = tab.info.viewport;
273
- const verb = result.created ? "Opened" : "Reused";
274
- const lines = [
275
- `${verb} tab ${JSON.stringify(name)} on ${describeBrowser(browser)}`,
276
- `URL: ${url}`,
277
- title ? `Title: ${title}` : null,
278
- ].filter((l): l is string => typeof l === "string");
279
- details.result = lines.join("\n");
280
- return toolResult(details).text(lines.join("\n")).done();
308
+ const tab = result.tab;
309
+ const url = tab.info.url;
310
+ const title = tab.info.title ?? "";
311
+ details.url = url;
312
+ details.viewport = tab.info.viewport;
313
+ const verb = result.created ? "Opened" : "Reused";
314
+ const lines = [
315
+ `${verb} tab ${JSON.stringify(name)} on ${describeBrowser(browser)}`,
316
+ `URL: ${url}`,
317
+ title ? `Title: ${title}` : null,
318
+ ].filter((l): l is string => typeof l === "string");
319
+ details.result = lines.join("\n");
320
+ return toolResult(details).text(lines.join("\n")).done();
321
+ } catch (error) {
322
+ // Caller cancellation stays a ToolAbortError; the requested timeout
323
+ // becomes a timeout ToolError; anything else passes through unchanged.
324
+ if (signal?.aborted) throw error instanceof ToolAbortError ? error : new ToolAbortError();
325
+ if (timeoutSignal.aborted) throw new ToolError(`Browser open timed out after ${timeoutMs}ms`);
326
+ throw error;
327
+ }
281
328
  }
282
329
 
283
330
  async #close(
@@ -283,7 +283,7 @@ export class HubTool implements AgentTool<typeof hubSchema, HubDetails> {
283
283
  if (!params.ids?.length) {
284
284
  return hubErrorResult('`ids` is required for op="cancel".', { op: "cancel", jobs: [] });
285
285
  }
286
- return executeCancel(this.session, manager, this.#ownerId(), params.ids);
286
+ return await executeCancel(this.session, manager, this.#ownerId(), params.ids);
287
287
  }
288
288
  case "jobs": {
289
289
  const manager = this.session.asyncJobManager;
@@ -12,6 +12,7 @@ import { settings } from "../../config/settings";
12
12
  import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
13
13
  import { shimmerEnabled, shimmerText } from "../../modes/theme/shimmer";
14
14
  import type { Theme } from "../../modes/theme/theme";
15
+ import { USER_INTERRUPT_LABEL } from "../../session/messages";
15
16
  import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../../tui";
16
17
  import type { ToolSession } from "..";
17
18
  import {
@@ -304,26 +305,38 @@ export function nothingToWaitForResult(session: ToolSession): AgentToolResult<Co
304
305
  }
305
306
 
306
307
  /** `cancel`: kill the named jobs; returns immediately with outcomes + snapshots. */
307
- export function executeCancel(
308
+ export async function executeCancel(
308
309
  session: ToolSession,
309
310
  manager: AsyncJobManager,
310
311
  ownerId: string | undefined,
311
312
  ids: string[],
312
- ): AgentToolResult<CoordinationDetails> {
313
+ ): Promise<AgentToolResult<CoordinationDetails>> {
313
314
  const ownerFilter = ownerId ? { ownerId } : undefined;
314
315
  const cancelOutcomes: CancelOutcome[] = [];
315
316
  for (const id of ids) {
316
317
  const existing = manager.getJob(id);
317
318
  if (!existing || (ownerId && existing.ownerId !== ownerId)) {
318
- cancelOutcomes.push({ id, status: "not_found", message: `Background job not found: ${id}` });
319
+ // No job by this id (or it belongs to another agent): a budget-aborted
320
+ // keep-alive subagent lives on as a jobless registration long after its
321
+ // job row is reaped, so let cancel reach the agent registration too.
322
+ cancelOutcomes.push(await cancelAgentRegistration(session, ownerId, id));
319
323
  continue;
320
324
  }
321
325
  if (existing.status !== "running") {
322
- cancelOutcomes.push({
323
- id,
324
- status: "already_completed",
325
- message: `Background job ${id} is already ${existing.status}.`,
326
- });
326
+ // The job row settled but may still be inside the retention window.
327
+ // The agent registration behind it (job id == agent id for task
328
+ // spawns) can outlive the row as an idle/parked zombie — try the
329
+ // registration kill before reporting the row as already done.
330
+ const regOutcome = await cancelAgentRegistration(session, ownerId, id);
331
+ cancelOutcomes.push(
332
+ regOutcome.status === "cancelled"
333
+ ? regOutcome
334
+ : {
335
+ id,
336
+ status: "already_completed",
337
+ message: `Background job ${id} is already ${existing.status}.`,
338
+ },
339
+ );
327
340
  continue;
328
341
  }
329
342
  const cancelled = manager.cancel(id, ownerFilter);
@@ -336,6 +349,52 @@ export function executeCancel(
336
349
  return buildJobResult(session, manager, "cancel", visibleJobs(manager, ids, ownerId), cancelOutcomes);
337
350
  }
338
351
 
352
+ /**
353
+ * Kill a non-job-backed agent registration named by `id`: abort any in-flight
354
+ * turn, then release it from the lifecycle (dispose session + unregister). This
355
+ * is the only kill path for a keep-alive subagent that was budget-aborted, went
356
+ * `idle`/`parked`, and outlived its job row — otherwise it is unstoppable short
357
+ * of a broker restart (issue #6315). Scoped to the caller's own descendants so
358
+ * cross-agent kills stay impossible; a bare test/SDK caller (no owner id) may
359
+ * target any sub. Never touches Main, the caller, or advisor transcripts.
360
+ */
361
+ async function cancelAgentRegistration(
362
+ session: ToolSession,
363
+ ownerId: string | undefined,
364
+ id: string,
365
+ ): Promise<CancelOutcome> {
366
+ const registry = session.agentRegistry;
367
+ const ref = registry?.get(id);
368
+ if (ref?.kind !== "sub") {
369
+ return { id, status: "not_found", message: `Background job not found: ${id}` };
370
+ }
371
+ if (id === ownerId) {
372
+ return { id, status: "not_found", message: `Cannot cancel yourself (${id}).` };
373
+ }
374
+ if (ownerId && ref.parentId !== ownerId) {
375
+ return { id, status: "not_found", message: `Agent ${id} was not spawned by you and cannot be cancelled.` };
376
+ }
377
+ const lifecycle = session.agentLifecycle?.();
378
+ try {
379
+ if (ref.status === "running" && ref.session) {
380
+ await ref.session.abort({ reason: USER_INTERRUPT_LABEL });
381
+ }
382
+ if (lifecycle) {
383
+ await lifecycle.release(id);
384
+ } else {
385
+ await ref.session?.dispose();
386
+ registry?.unregister(id);
387
+ }
388
+ } catch (error) {
389
+ return {
390
+ id,
391
+ status: "already_completed",
392
+ message: `Agent ${id} could not be fully cancelled: ${error instanceof Error ? error.message : String(error)}.`,
393
+ };
394
+ }
395
+ return { id, status: "cancelled", message: `Cancelled agent ${id} (killed session, dropped registration).` };
396
+ }
397
+
339
398
  /** `jobs`: read-only snapshot of every job plus the jobless running-agent roster. */
340
399
  export function executeJobsSnapshot(
341
400
  session: ToolSession,
@@ -27,6 +27,7 @@ import { settings } from "../config/settings";
27
27
  import type { CustomTool } from "../extensibility/custom-tools/types";
28
28
  import { ohMyPiXAIUserAgent, resolveXAIHttpCredentials } from "../lib/xai-http";
29
29
  import imageGenDescription from "../prompts/tools/image-gen.md" with { type: "text" };
30
+ import { AUTO_IMAGE_PROVIDER_ORDER, type ImageProvider, isImageProviderId } from "./image-providers";
30
31
  import { resolveReadPath } from "./path-utils";
31
32
 
32
33
  const DEFAULT_MODEL = "gemini-3-pro-image-preview";
@@ -44,7 +45,7 @@ const DEFAULT_ANTIGRAVITY_ENDPOINT_SANDBOX = "https://daily-cloudcode-pa.sandbox
44
45
  const IMAGE_SYSTEM_INSTRUCTION =
45
46
  "You are an AI image generator. Generate images based on user descriptions. Focus on creating high-quality, visually appealing images that match the user's request.";
46
47
 
47
- export type ImageProvider = "antigravity" | "gemini" | "openai" | "openai-codex" | "openrouter" | "xai";
48
+ export type { ImageProvider } from "./image-providers";
48
49
  export type ImageProviderPreference = ImageProvider | "auto";
49
50
 
50
51
  interface ImageApiKey {
@@ -57,17 +58,8 @@ interface ImageApiKey {
57
58
  const COMMON_IMAGE_ASPECT_RATIOS = ["1:1", "3:4", "4:3", "9:16", "16:9"] as const;
58
59
  const XAI_IMAGE_ASPECT_RATIOS = [...COMMON_IMAGE_ASPECT_RATIOS, "3:2", "2:3"] as const;
59
60
  const COMMON_IMAGE_ASPECT_RATIO_SET = new Set<string>(COMMON_IMAGE_ASPECT_RATIOS);
60
- const IMAGE_PROVIDER_CHOICES = [
61
- "auto",
62
- "antigravity",
63
- "gemini",
64
- "openai",
65
- "openai-codex",
66
- "openrouter",
67
- "xai",
68
- ] as const;
69
- const IMAGE_PROVIDER_PREFERENCES = new Set<string>(IMAGE_PROVIDER_CHOICES);
70
- const AUTO_IMAGE_PROVIDER_ORDER = ["openai", "openai-codex", "antigravity", "xai", "openrouter", "gemini"] as const;
61
+ const IMAGE_PROVIDER_REQUEST_CHOICES = ["auto", ...AUTO_IMAGE_PROVIDER_ORDER] as const;
62
+ const IMAGE_PROVIDER_PREFERENCES = new Set<string>(IMAGE_PROVIDER_REQUEST_CHOICES);
71
63
 
72
64
  const responseModalitySchema = type('"IMAGE" | "TEXT"');
73
65
 
@@ -81,8 +73,8 @@ const inputImageSchema = type({
81
73
  });
82
74
 
83
75
  const imageProviderSchema = type
84
- .enumerated(...IMAGE_PROVIDER_CHOICES)
85
- .describe("image provider for this request; overrides the providers.image setting (default: use the setting)");
76
+ .enumerated(...IMAGE_PROVIDER_REQUEST_CHOICES)
77
+ .describe("image provider for this request; overrides the providers.imageOrder setting (default: use the setting)");
86
78
 
87
79
  export const imageGenSchema = type({
88
80
  subject: type("string").describe("main subject"),
@@ -447,16 +439,16 @@ function extractOpenRouterImageUrls(message: OpenRouterMessage | undefined): str
447
439
  return urls;
448
440
  }
449
441
 
450
- /** Preferred provider set via settings (default: auto) */
451
- let preferredImageProvider: ImageProviderPreference = "auto";
442
+ /** Configured provider priority set via `providers.imageOrder` (default: none). */
443
+ let configuredImageProviderOrder: readonly ImageProvider[] = [];
452
444
 
453
445
  export function isImageProviderPreference(value: unknown): value is ImageProviderPreference {
454
446
  return typeof value === "string" && IMAGE_PROVIDER_PREFERENCES.has(value);
455
447
  }
456
448
 
457
- /** Set the preferred image provider from settings */
458
- export function setPreferredImageProvider(provider: ImageProviderPreference): void {
459
- preferredImageProvider = provider;
449
+ /** Set the configured image-provider priority from settings; invalid IDs are dropped. */
450
+ export function setImageProviderOrder(providers: readonly string[]): void {
451
+ configuredImageProviderOrder = providers.filter(isImageProviderId);
460
452
  }
461
453
  function assertImageAspectRatioSupported(provider: ImageProvider, aspectRatio: ImageGenParams["aspect_ratio"]): void {
462
454
  if (!aspectRatio || provider === "xai" || COMMON_IMAGE_ASPECT_RATIO_SET.has(aspectRatio)) {
@@ -624,19 +616,19 @@ function activeImageProvider(model: Model | undefined): Exclude<ImageProviderPre
624
616
  }
625
617
  }
626
618
 
627
- function imageProviderOrder(
628
- activeModel: Model | undefined,
629
- preference: ImageProviderPreference = preferredImageProvider,
630
- ): Array<Exclude<ImageProviderPreference, "auto">> {
631
- const providers: Array<Exclude<ImageProviderPreference, "auto">> = [];
632
- const added = new Set<Exclude<ImageProviderPreference, "auto">>();
633
- const add = (provider: Exclude<ImageProviderPreference, "auto"> | null): void => {
619
+ function imageProviderOrder(activeModel: Model | undefined, requested?: ImageProviderPreference): ImageProvider[] {
620
+ const providers: ImageProvider[] = [];
621
+ const added = new Set<ImageProvider>();
622
+ const add = (provider: ImageProvider | null): void => {
634
623
  if (!provider || added.has(provider)) return;
635
624
  added.add(provider);
636
625
  providers.push(provider);
637
626
  };
638
627
 
639
- if (preference !== "auto") add(preference);
628
+ // Per-request provider wins, then the configured priority list, then the
629
+ // active session's provider, then the built-in auto order.
630
+ if (requested !== undefined && requested !== "auto") add(requested);
631
+ for (const provider of configuredImageProviderOrder) add(provider);
640
632
  add(activeImageProvider(activeModel));
641
633
  for (const provider of AUTO_IMAGE_PROVIDER_ORDER) add(provider);
642
634
  return providers;
@@ -1109,7 +1101,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1109
1101
  async execute(_toolCallId, params, _onUpdate, ctx, signal) {
1110
1102
  return untilAborted(signal, async () => {
1111
1103
  const sessionId = ctx.sessionManager.getSessionId();
1112
- const providerOrder = imageProviderOrder(ctx.model, params.provider ?? preferredImageProvider);
1104
+ const providerOrder = imageProviderOrder(ctx.model, params.provider);
1113
1105
  const cwd = ctx.sessionManager.getCwd();
1114
1106
  const requestSignal = ptree.combineSignals(signal, IMAGE_TIMEOUT);
1115
1107
  const fetchImpl = ctx.fetch ?? fetch;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Image Generation Providers
3
+ *
4
+ * Leaf module (no runtime deps) shared by the image_gen tool, the settings
5
+ * schema, and settings migrations — mirrors `web/search/types.ts` so the
6
+ * provider list, auto order, and settings choices never drift apart.
7
+ */
8
+
9
+ /** Image generation backends, in settings/tool vocabulary. */
10
+ export type ImageProvider = "antigravity" | "gemini" | "openai" | "openai-codex" | "openrouter" | "xai";
11
+
12
+ /** Auto-resolution fallback order when no configured entry or session provider matches. */
13
+ export const AUTO_IMAGE_PROVIDER_ORDER: readonly ImageProvider[] = [
14
+ "openai",
15
+ "openai-codex",
16
+ "antigravity",
17
+ "xai",
18
+ "openrouter",
19
+ "gemini",
20
+ ];
21
+
22
+ /** Settings choices for `providers.imageOrder` (labels shared with the retired single-preference enum). */
23
+ export const IMAGE_PROVIDER_CHOICES = [
24
+ {
25
+ value: "openai",
26
+ label: "OpenAI",
27
+ description: "OPENAI_API_KEY (gpt-image-2) or active GPT model; falls back to a connected Codex subscription",
28
+ },
29
+ {
30
+ value: "openai-codex",
31
+ label: "OpenAI Codex (ChatGPT)",
32
+ description: "Uses a connected Codex / ChatGPT subscription — no OPENAI_API_KEY needed",
33
+ },
34
+ {
35
+ value: "antigravity",
36
+ label: "Antigravity",
37
+ description: "Requires google-antigravity OAuth",
38
+ },
39
+ {
40
+ value: "xai",
41
+ label: "xAI Grok Imagine",
42
+ description: "Requires xAI Grok OAuth or XAI_API_KEY",
43
+ },
44
+ { value: "gemini", label: "Gemini", description: "Requires GEMINI_API_KEY" },
45
+ { value: "openrouter", label: "OpenRouter", description: "Requires OPENROUTER_API_KEY" },
46
+ ] as const satisfies ReadonlyArray<{ value: ImageProvider; label: string; description: string }>;
47
+
48
+ export function isImageProviderId(value: unknown): value is ImageProvider {
49
+ return typeof value === "string" && AUTO_IMAGE_PROVIDER_ORDER.includes(value as ImageProvider);
50
+ }
@@ -20,11 +20,13 @@ import { LspTool } from "../lsp";
20
20
  import type { MCPManager } from "../mcp";
21
21
  import type { MnemopiSessionState } from "../mnemopi/state";
22
22
  import type { PlanModeState } from "../plan-mode/state";
23
+ import type { AgentLifecycleManager } from "../registry/agent-lifecycle";
23
24
  import type { AgentRegistry } from "../registry/agent-registry";
24
25
  import type { ArtifactManager } from "../session/artifacts";
25
26
  import type { ClientBridge } from "../session/client-bridge";
26
27
  import type { CustomMessage } from "../session/messages";
27
28
  import type { UsageStatistics } from "../session/session-entries";
29
+ import type { SessionManager } from "../session/session-manager";
28
30
  import type { ToolChoiceQueue } from "../session/tool-choice-queue";
29
31
  import { TaskTool } from "../task";
30
32
  import type { AgentOutputManager } from "../task/output-manager";
@@ -143,6 +145,8 @@ export interface DeferredDiagnosticsEntry {
143
145
  export interface ToolSession {
144
146
  /** Current working directory */
145
147
  cwd: string;
148
+ /** Additional workspace directories beyond cwd (multi-root), forwarded to subagents. */
149
+ additionalDirectories?: string[];
146
150
  /** Whether UI is available */
147
151
  hasUI: boolean;
148
152
  /**
@@ -214,6 +218,8 @@ export interface ToolSession {
214
218
  getEvalSessionId?: () => string | null;
215
219
  /** Get session file */
216
220
  getSessionFile: () => string | null;
221
+ /** Parent session journal used by tools that persist runtime lifecycle state. */
222
+ sessionManager?: Pick<SessionManager, "appendCustomEntry" | "ensureOnDisk" | "flush" | "getBranch" | "getEntries">;
217
223
  /** Get eval kernel owner ID for session-scoped retained-kernel cleanup. */
218
224
  getEvalKernelOwnerId?: () => string | null;
219
225
  /** Reject new eval work once session disposal has started. */
@@ -238,6 +244,8 @@ export interface ToolSession {
238
244
  xdevRegistry?: XdevRegistry;
239
245
  /** Agent registry for IRC routing across live sessions. */
240
246
  agentRegistry?: AgentRegistry;
247
+ /** Idle→parked→revive lifecycle owner; lets the hub kill a non-job-backed agent registration. Default: AgentLifecycleManager.global(). */
248
+ agentLifecycle?: () => AgentLifecycleManager;
241
249
  /** Get artifacts directory for artifact:// URLs */
242
250
  getArtifactsDir?: () => string | null;
243
251
  /** Get the ArtifactManager backing this session (shared across parent + subagents). */
@@ -510,6 +518,9 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
510
518
  if (!requestedTools.includes(name)) requestedTools.push(name);
511
519
  }
512
520
  }
521
+ if (session.settings.get("memory.backend") === "mnemopi" && !requestedTools.includes("memory_edit")) {
522
+ requestedTools.push("memory_edit");
523
+ }
513
524
  // Auto-learn tools are gated by `autolearn.enabled` but, like the memory
514
525
  // tools above, must also be force-included into an explicit requestedTools
515
526
  // list so a restricted top-level session whose controller/guidance is
@@ -553,6 +564,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
553
564
  if (name === "retain" || name === "recall" || name === "reflect") {
554
565
  return ["hindsight", "mnemopi"].includes(session.settings.get("memory.backend") ?? "");
555
566
  }
567
+ if (name === "memory_edit") return session.settings.get("memory.backend") === "mnemopi";
556
568
  if (name === "manage_skill") return session.settings.get("autolearn.enabled") && (session.taskDepth ?? 0) === 0;
557
569
  if (name === "learn") {
558
570
  return (
@@ -2,10 +2,11 @@ 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 * as url from "node:url";
5
- import { isEnoent, isEnotdir, stripWindowsExtendedLengthPathPrefix } from "@oh-my-pi/pi-utils";
5
+ import { glob } from "@oh-my-pi/pi-natives";
6
+ import { isEnoent, isEnotdir, stripWindowsExtendedLengthPathPrefix, untilAborted } from "@oh-my-pi/pi-utils";
6
7
  import type { Skill } from "../extensibility/skills";
7
8
  import { InternalUrlRouter, type LocalProtocolOptions } from "../internal-urls";
8
- import { ToolError } from "./tool-errors";
9
+ import { ToolAbortError, ToolError } from "./tool-errors";
9
10
 
10
11
  const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
11
12
  // A single line-range chunk: `N`, `N-M`, `N+K`, or open-ended `N-`. `..` is
@@ -1132,6 +1133,52 @@ export function resolveReadPath(filePath: string, cwd: string): string {
1132
1133
  return resolved;
1133
1134
  }
1134
1135
 
1136
+ const WORKSPACE_SUFFIX_TIMEOUT_MS = 5000;
1137
+
1138
+ function escapeGlobMetachars(value: string): string {
1139
+ return value.replace(/[*?[{]/g, "[$&]");
1140
+ }
1141
+
1142
+ /**
1143
+ * Find a unique workspace entry whose trailing path matches a missing authored path.
1144
+ * Returns `null` for no match, ambiguity, timeout, or scan failure.
1145
+ */
1146
+ export async function findUniqueWorkspaceSuffix(
1147
+ rawPath: string,
1148
+ cwd: string,
1149
+ signal?: AbortSignal,
1150
+ ): Promise<{ absolutePath: string; displayPath: string } | null> {
1151
+ const normalized = rawPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
1152
+ if (!normalized) return null;
1153
+
1154
+ const timeoutSignal = AbortSignal.timeout(WORKSPACE_SUFFIX_TIMEOUT_MS);
1155
+ const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
1156
+
1157
+ let matches: string[];
1158
+ try {
1159
+ const result = await untilAborted(combinedSignal, () =>
1160
+ glob({
1161
+ pattern: `**/${escapeGlobMetachars(normalized)}`,
1162
+ path: cwd,
1163
+ hidden: true,
1164
+ }),
1165
+ );
1166
+ matches = result.matches.map(match => match.path);
1167
+ } catch (error) {
1168
+ if (error instanceof Error && error.name === "AbortError") {
1169
+ if (!signal?.aborted) return null;
1170
+ throw new ToolAbortError();
1171
+ }
1172
+ return null;
1173
+ }
1174
+
1175
+ if (matches.length !== 1) return null;
1176
+ return {
1177
+ absolutePath: path.resolve(cwd, matches[0]),
1178
+ displayPath: matches[0],
1179
+ };
1180
+ }
1181
+
1135
1182
  // =============================================================================
1136
1183
  // Tool-scope resolution (search/ast tools)
1137
1184
  // =============================================================================