@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
@@ -1,25 +1,38 @@
1
- import type { DesktopAction, DesktopCapabilities, DesktopCapture, DesktopSessionOptions } from "@oh-my-pi/pi-natives";
1
+ import type { DesktopCapabilities } from "@oh-my-pi/pi-natives";
2
2
  import { withTimeout } from "@oh-my-pi/pi-utils/async";
3
3
  import * as logger from "@oh-my-pi/pi-utils/logger";
4
+ import { Snowflake } from "@oh-my-pi/pi-utils/snowflake";
4
5
  import { workerHostEntry } from "@oh-my-pi/pi-utils/worker-host";
6
+ import type { ToolSession } from "../index";
5
7
  import { ToolAbortError, ToolError } from "../tool-errors";
6
8
  import {
7
9
  COMPUTER_WORKER_ARG,
8
- type ComputerWorkerError,
10
+ type ComputerRunOk,
11
+ type ComputerSessionSnapshot,
9
12
  type ComputerWorkerInbound,
10
13
  type ComputerWorkerOutbound,
14
+ type RunErrorPayload,
11
15
  } from "./protocol";
12
16
 
13
17
  const START_TIMEOUT_MS = 10_000;
14
18
  const CLOSE_TIMEOUT_MS = 1_500;
19
+ const GRACE_MS = 750;
15
20
  const SMOKE_TIMEOUT_MS = 5_000;
21
+ const RESTART_MESSAGE = "computer worker restarted; captures and ax refs were reset";
16
22
 
23
+ /** Runs desktop scripts and owns their persistent worker session. */
17
24
  export interface ComputerController {
18
- readonly capabilities: DesktopCapabilities | undefined;
19
- execute(actions: DesktopAction[], signal?: AbortSignal): Promise<DesktopCapture>;
25
+ run(
26
+ code: string,
27
+ timeoutMs: number,
28
+ snapshot: ComputerSessionSnapshot,
29
+ signal?: AbortSignal,
30
+ ): Promise<ComputerRunOk>;
31
+ capabilities(): Promise<DesktopCapabilities | undefined>;
20
32
  close(): Promise<void>;
21
33
  }
22
34
 
35
+ /** Minimal Bun worker lifecycle surface used by the supervisor. */
23
36
  export interface ComputerWorkerHandle {
24
37
  send(message: ComputerWorkerInbound): void;
25
38
  onMessage(handler: (message: ComputerWorkerOutbound) => void): () => void;
@@ -27,6 +40,7 @@ export interface ComputerWorkerHandle {
27
40
  terminate(): Promise<void>;
28
41
  }
29
42
 
43
+ /** Startup and shutdown deadlines for a computer worker. */
30
44
  export interface ComputerSupervisorTimeouts {
31
45
  startMs: number;
32
46
  closeMs: number;
@@ -36,13 +50,22 @@ const DEFAULT_TIMEOUTS: ComputerSupervisorTimeouts = {
36
50
  startMs: START_TIMEOUT_MS,
37
51
  closeMs: CLOSE_TIMEOUT_MS,
38
52
  };
53
+
54
+ /** Dispatches a tool call requested from desktop JavaScript. */
55
+ export type ComputerSessionToolCaller = (
56
+ name: string,
57
+ args: unknown,
58
+ options: { session: ToolSession; signal?: AbortSignal; emitStatus?: () => void },
59
+ ) => Promise<unknown>;
60
+
61
+ /** Creates an isolated computer worker handle. */
39
62
  export type ComputerWorkerFactory = () => ComputerWorkerHandle;
40
63
 
41
- function workerError(error: ComputerWorkerError): Error {
42
- const result = new ToolError(error.message);
43
- result.name = error.name;
44
- if (error.stack) result.stack = error.stack;
45
- return result;
64
+ interface PendingRun {
65
+ resolve(value: ComputerRunOk): void;
66
+ reject(error: unknown): void;
67
+ signal?: AbortSignal;
68
+ toolCalls: Map<string, AbortController>;
46
69
  }
47
70
 
48
71
  function wrapWorker(worker: Worker): ComputerWorkerHandle {
@@ -56,10 +79,19 @@ function wrapWorker(worker: Worker): ComputerWorkerHandle {
56
79
  return () => worker.removeEventListener("message", listener);
57
80
  },
58
81
  onError(handler) {
59
- const listener = (event: ErrorEvent): void =>
82
+ const onError = (event: ErrorEvent): void =>
60
83
  handler(event.error instanceof Error ? event.error : new Error(event.message));
61
- worker.addEventListener("error", listener);
62
- return () => worker.removeEventListener("error", listener);
84
+ const onMessageError = (event: MessageEvent): void =>
85
+ handler(new Error(`Computer worker message error: ${String(event.data)}`));
86
+ const onClose = (): void => handler(new Error("Computer worker exited"));
87
+ worker.addEventListener("error", onError);
88
+ worker.addEventListener("messageerror", onMessageError);
89
+ worker.addEventListener("close", onClose);
90
+ return () => {
91
+ worker.removeEventListener("error", onError);
92
+ worker.removeEventListener("messageerror", onMessageError);
93
+ worker.removeEventListener("close", onClose);
94
+ };
63
95
  },
64
96
  async terminate() {
65
97
  worker.terminate();
@@ -67,6 +99,7 @@ function wrapWorker(worker: Worker): ComputerWorkerHandle {
67
99
  };
68
100
  }
69
101
 
102
+ /** Spawns the computer worker through the active CLI host when available. */
70
103
  export function spawnComputerWorker(): ComputerWorkerHandle {
71
104
  const hostEntry = workerHostEntry();
72
105
  const worker = hostEntry
@@ -75,133 +108,116 @@ export function spawnComputerWorker(): ComputerWorkerHandle {
75
108
  return wrapWorker(worker);
76
109
  }
77
110
 
78
- interface PendingRequest {
79
- resolve(capture: DesktopCapture): void;
80
- reject(error: unknown): void;
111
+ function errorFromPayload(payload: RunErrorPayload): Error {
112
+ const error = payload.isAbort
113
+ ? new ToolAbortError(payload.message)
114
+ : payload.isToolError
115
+ ? new ToolError(payload.message)
116
+ : new Error(payload.message);
117
+ error.name = payload.name;
118
+ if (payload.stack) error.stack = payload.stack;
119
+ return error;
120
+ }
121
+
122
+ function toErrorPayload(error: unknown): RunErrorPayload {
123
+ if (error instanceof Error) {
124
+ return {
125
+ name: error.name,
126
+ message: error.message,
127
+ stack: error.stack,
128
+ isAbort: error.name === "AbortError" || error.name === "ToolAbortError",
129
+ isToolError: error instanceof ToolError || error.name === "ToolError",
130
+ };
131
+ }
132
+ return { name: "Error", message: String(error), isAbort: false, isToolError: false };
81
133
  }
82
134
 
135
+ /** Supervises one lazy, crash-isolated computer worker per agent session. */
83
136
  export class ComputerSupervisor implements ComputerController {
137
+ readonly #session: ToolSession;
138
+ readonly #createWorker: ComputerWorkerFactory;
139
+ readonly #callSessionTool: ComputerSessionToolCaller;
140
+ readonly #timeouts: ComputerSupervisorTimeouts;
84
141
  #worker?: ComputerWorkerHandle;
85
142
  #startPromise?: Promise<void>;
86
- #capabilities?: DesktopCapabilities;
87
- #pending = new Map<string, PendingRequest>();
143
+ #startReject?: (error: unknown) => void;
144
+ #startResolve?: () => void;
145
+ #latestCapabilities?: DesktopCapabilities;
146
+ #pending = new Map<string, PendingRun>();
88
147
  #nextId = 0;
89
- #serial: Promise<void> = Promise.resolve();
90
148
  #closed = false;
91
149
  #unsubscribeMessage?: () => void;
92
150
  #unsubscribeError?: () => void;
93
151
 
94
152
  constructor(
95
- private readonly options: DesktopSessionOptions,
96
- private readonly createWorker: ComputerWorkerFactory = spawnComputerWorker,
97
- private readonly timeouts: ComputerSupervisorTimeouts = DEFAULT_TIMEOUTS,
98
- ) {}
99
-
100
- get capabilities(): DesktopCapabilities | undefined {
101
- return this.#capabilities;
153
+ session: ToolSession,
154
+ createWorker: ComputerWorkerFactory = spawnComputerWorker,
155
+ timeouts: ComputerSupervisorTimeouts = DEFAULT_TIMEOUTS,
156
+ callSessionTool: ComputerSessionToolCaller = async () => {
157
+ throw new ToolError("Computer session tool bridge is unavailable");
158
+ },
159
+ ) {
160
+ this.#session = session;
161
+ this.#createWorker = createWorker;
162
+ this.#timeouts = timeouts;
163
+ this.#callSessionTool = callSessionTool;
102
164
  }
103
165
 
104
- execute(actions: DesktopAction[], signal?: AbortSignal): Promise<DesktopCapture> {
105
- if (this.#closed) return Promise.reject(new ToolError("Computer session is closed"));
106
- const run = (): Promise<DesktopCapture> => this.#execute(actions, signal);
107
- const result = this.#serial.then(run, run);
108
- this.#serial = result.then(
109
- () => undefined,
110
- () => undefined,
111
- );
112
- return result;
166
+ async capabilities(): Promise<DesktopCapabilities | undefined> {
167
+ return this.#latestCapabilities;
113
168
  }
114
169
 
115
- async #execute(actions: DesktopAction[], signal?: AbortSignal): Promise<DesktopCapture> {
170
+ async run(
171
+ code: string,
172
+ timeoutMs: number,
173
+ snapshot: ComputerSessionSnapshot,
174
+ signal?: AbortSignal,
175
+ ): Promise<ComputerRunOk> {
116
176
  if (this.#closed) throw new ToolError("Computer session is closed");
117
177
  if (signal?.aborted) throw new ToolAbortError();
118
178
  await this.#start();
119
179
  if (signal?.aborted) throw new ToolAbortError();
180
+
120
181
  const id = `computer-${++this.#nextId}`;
121
- const request = Promise.withResolvers<DesktopCapture>();
122
- this.#pending.set(id, request);
123
- this.#worker!.send({ type: "execute", id, actions });
124
- if (!signal) return request.promise;
182
+ const { promise, resolve, reject } = Promise.withResolvers<ComputerRunOk>();
183
+ const pending: PendingRun = { resolve, reject, signal, toolCalls: new Map() };
184
+ this.#pending.set(id, pending);
185
+ const abort = (): void => {
186
+ this.#safeSend({ type: "abort", id });
187
+ for (const controller of pending.toolCalls.values()) controller.abort(signal?.reason);
188
+ };
189
+ if (signal?.aborted) abort();
190
+ else signal?.addEventListener("abort", abort, { once: true });
125
191
 
126
- const aborted = Promise.withResolvers<never>();
127
- const onAbort = (): void => aborted.reject(new ToolAbortError());
128
- signal.addEventListener("abort", onAbort, { once: true });
129
192
  try {
130
- return await Promise.race([request.promise, aborted.promise]);
131
- } catch (error) {
132
- if (error instanceof ToolAbortError) await this.#terminate(error);
133
- throw error;
193
+ this.#worker?.send({ type: "run", id, code, timeoutMs, session: snapshot });
194
+ return await this.#raceWithGrace(promise, timeoutMs);
134
195
  } finally {
135
- signal.removeEventListener("abort", onAbort);
196
+ signal?.removeEventListener("abort", abort);
197
+ this.#pending.delete(id);
136
198
  }
137
199
  }
138
200
 
139
201
  #start(): Promise<void> {
140
202
  if (this.#startPromise) return this.#startPromise;
141
- const ready = Promise.withResolvers<void>();
203
+ const started = Promise.withResolvers<void>();
204
+ this.#startReject = started.reject;
205
+ this.#startResolve = started.resolve;
142
206
  try {
143
- logger.debug("Starting native computer worker", {
144
- backend: this.options.backend,
145
- display: this.options.display,
146
- maxWidth: this.options.maxWidth,
147
- maxHeight: this.options.maxHeight,
148
- });
149
- const worker = this.createWorker();
207
+ logger.debug("Starting computer worker");
208
+ const worker = this.#createWorker();
150
209
  this.#worker = worker;
151
- this.#unsubscribeMessage = worker.onMessage(message => {
152
- if (message.type === "ready") {
153
- this.#capabilities = message.capabilities;
154
- logger.debug("Native computer worker ready", {
155
- backend: message.capabilities.backend,
156
- capturePermission: message.capabilities.capturePermission,
157
- inputPermission: message.capabilities.inputPermission,
158
- displayCount: message.capabilities.displayCount,
159
- });
160
- ready.resolve();
161
- return;
162
- }
163
- if (message.type === "result") {
164
- this.#capabilities = message.capabilities;
165
- const pending = this.#pending.get(message.id);
166
- this.#pending.delete(message.id);
167
- pending?.resolve(message.capture);
168
- logger.debug("Native computer capture completed", {
169
- requestId: message.id,
170
- backend: message.capture.backend,
171
- capturePermission: message.capture.capturePermission,
172
- inputPermission: message.capture.inputPermission,
173
- width: message.capture.width,
174
- height: message.capture.height,
175
- });
176
- return;
177
- }
178
- if (message.type === "error") {
179
- const error = workerError(message.error);
180
- logger.warn("Native computer worker request failed", {
181
- requestId: message.id,
182
- errorName: message.error.name,
183
- });
184
- if (message.id) {
185
- const pending = this.#pending.get(message.id);
186
- this.#pending.delete(message.id);
187
- pending?.reject(error);
188
- } else {
189
- ready.reject(error);
190
- }
191
- }
192
- });
210
+ this.#unsubscribeMessage = worker.onMessage(message => this.#handleMessage(message));
193
211
  this.#unsubscribeError = worker.onError(error => {
194
- ready.reject(error);
195
- void this.#terminate(error);
212
+ void this.#workerFailed(error);
196
213
  });
197
- worker.send({ type: "init", options: this.options });
198
214
  } catch (error) {
199
- ready.reject(error);
215
+ started.reject(error);
200
216
  }
201
217
  this.#startPromise = withTimeout(
202
- ready.promise,
203
- this.timeouts.startMs,
204
- "Timed out starting native computer worker",
218
+ started.promise,
219
+ this.#timeouts.startMs,
220
+ "Timed out starting computer worker",
205
221
  ).catch(async error => {
206
222
  await this.#terminate(error);
207
223
  throw error;
@@ -209,21 +225,110 @@ export class ComputerSupervisor implements ComputerController {
209
225
  return this.#startPromise;
210
226
  }
211
227
 
228
+ #handleMessage(message: ComputerWorkerOutbound): void {
229
+ if (message.type === "ready") {
230
+ this.#startResolve?.();
231
+ this.#startResolve = undefined;
232
+ this.#startReject = undefined;
233
+ return;
234
+ }
235
+ if (message.type === "result") {
236
+ const pending = this.#pending.get(message.id);
237
+ if (!pending) return;
238
+ this.#pending.delete(message.id);
239
+ if (message.ok) {
240
+ this.#latestCapabilities = message.payload.capabilities;
241
+ pending.resolve(message.payload);
242
+ } else {
243
+ pending.reject(errorFromPayload(message.error));
244
+ }
245
+ return;
246
+ }
247
+ if (message.type === "tool-call") {
248
+ void this.#dispatchToolCall(message);
249
+ }
250
+ }
251
+
252
+ async #dispatchToolCall(message: Extract<ComputerWorkerOutbound, { type: "tool-call" }>): Promise<void> {
253
+ const pending = this.#pending.get(message.runId);
254
+ if (!pending) {
255
+ this.#safeSend({
256
+ type: "tool-reply",
257
+ id: message.id,
258
+ reply: {
259
+ ok: false,
260
+ error: { name: "ToolError", message: "No active run for tool call", isToolError: true, isAbort: false },
261
+ },
262
+ });
263
+ return;
264
+ }
265
+ const controller = new AbortController();
266
+ pending.toolCalls.set(message.id, controller);
267
+ const onParentAbort = (): void => controller.abort(pending.signal?.reason);
268
+ if (pending.signal?.aborted) onParentAbort();
269
+ else pending.signal?.addEventListener("abort", onParentAbort, { once: true });
270
+ try {
271
+ const value = await this.#callSessionTool(message.name, message.args, {
272
+ session: this.#session,
273
+ signal: controller.signal,
274
+ emitStatus: () => {},
275
+ });
276
+ this.#safeSend({ type: "tool-reply", id: message.id, reply: { ok: true, value } });
277
+ } catch (error) {
278
+ this.#safeSend({ type: "tool-reply", id: message.id, reply: { ok: false, error: toErrorPayload(error) } });
279
+ } finally {
280
+ pending.toolCalls.delete(message.id);
281
+ pending.signal?.removeEventListener("abort", onParentAbort);
282
+ }
283
+ }
284
+
285
+ #safeSend(message: ComputerWorkerInbound): void {
286
+ try {
287
+ this.#worker?.send(message);
288
+ } catch (error) {
289
+ logger.debug("Computer worker send failed", {
290
+ error: error instanceof Error ? error.message : String(error),
291
+ });
292
+ }
293
+ }
294
+
295
+ async #raceWithGrace(promise: Promise<ComputerRunOk>, timeoutMs: number): Promise<ComputerRunOk> {
296
+ const timeoutSignal = AbortSignal.timeout(timeoutMs + GRACE_MS);
297
+ const timeout = Promise.withResolvers<never>();
298
+ const onTimeout = (): void => timeout.reject(new ToolError(RESTART_MESSAGE));
299
+ timeoutSignal.addEventListener("abort", onTimeout, { once: true });
300
+ try {
301
+ return await Promise.race([promise, timeout.promise]);
302
+ } catch (error) {
303
+ if (error instanceof ToolError && error.message === RESTART_MESSAGE) await this.#terminate(error);
304
+ throw error;
305
+ } finally {
306
+ timeoutSignal.removeEventListener("abort", onTimeout);
307
+ }
308
+ }
309
+
310
+ async #workerFailed(error: Error): Promise<void> {
311
+ logger.warn("Computer worker failed", { error: error.message });
312
+ await this.#terminate(error);
313
+ }
314
+
212
315
  async #terminate(reason: unknown): Promise<void> {
213
- logger.debug("Terminating native computer worker", {
214
- reason: reason instanceof Error ? reason.name : typeof reason,
215
- });
216
316
  const worker = this.#worker;
217
317
  this.#worker = undefined;
218
318
  this.#startPromise = undefined;
219
- this.#capabilities = undefined;
319
+ this.#startReject?.(reason);
320
+ this.#startReject = undefined;
321
+ this.#startResolve = undefined;
220
322
  this.#unsubscribeMessage?.();
221
323
  this.#unsubscribeMessage = undefined;
222
324
  this.#unsubscribeError?.();
223
325
  this.#unsubscribeError = undefined;
224
- for (const pending of this.#pending.values()) pending.reject(reason);
326
+ for (const pending of this.#pending.values()) {
327
+ for (const controller of pending.toolCalls.values()) controller.abort(reason);
328
+ pending.reject(reason);
329
+ }
225
330
  this.#pending.clear();
226
- await worker?.terminate();
331
+ await worker?.terminate().catch(() => undefined);
227
332
  }
228
333
 
229
334
  async close(): Promise<void> {
@@ -237,7 +342,7 @@ export class ComputerSupervisor implements ComputerController {
237
342
  });
238
343
  try {
239
344
  worker.send({ type: "close" });
240
- await withTimeout(closed.promise, this.timeouts.closeMs, "Timed out closing native computer worker");
345
+ await withTimeout(closed.promise, this.#timeouts.closeMs, "Timed out closing computer worker");
241
346
  } catch {
242
347
  // Forced termination below is the bounded close fallback.
243
348
  } finally {
@@ -249,6 +354,7 @@ export class ComputerSupervisor implements ComputerController {
249
354
 
250
355
  const ownedSupervisors = new Map<string, Set<ComputerController>>();
251
356
 
357
+ /** Registers a controller for owner-scoped session cleanup. */
252
358
  export function registerComputerController(ownerId: string | undefined, controller: ComputerController): () => void {
253
359
  if (!ownerId) return () => {};
254
360
  const controllers = ownedSupervisors.get(ownerId) ?? new Set<ComputerController>();
@@ -260,6 +366,7 @@ export function registerComputerController(ownerId: string | undefined, controll
260
366
  };
261
367
  }
262
368
 
369
+ /** Closes every computer session owned by an agent session. */
263
370
  export async function releaseComputerSessionsForOwner(ownerId: string | undefined): Promise<void> {
264
371
  if (!ownerId) return;
265
372
  const controllers = ownedSupervisors.get(ownerId);
@@ -268,24 +375,20 @@ export async function releaseComputerSessionsForOwner(ownerId: string | undefine
268
375
  await Promise.allSettled(Array.from(controllers, controller => controller.close()));
269
376
  }
270
377
 
378
+ /** Verifies computer worker startup, messaging, and bounded shutdown. */
271
379
  export async function smokeTestComputerWorker(
272
380
  timeoutMs = SMOKE_TIMEOUT_MS,
273
381
  createWorker: ComputerWorkerFactory = spawnComputerWorker,
274
382
  ): Promise<void> {
275
383
  const worker = createWorker();
276
- const exchange = async (
277
- message: ComputerWorkerInbound,
278
- expected: ComputerWorkerOutbound["type"],
279
- failureMessage: string,
280
- ): Promise<void> => {
384
+ const waitFor = async (expected: ComputerWorkerOutbound["type"], failureMessage: string): Promise<void> => {
281
385
  const response = Promise.withResolvers<void>();
282
386
  const unsubscribeMessage = worker.onMessage(received => {
283
387
  if (received.type === expected) response.resolve();
284
- else if (received.type === "error") response.reject(workerError(received.error));
388
+ else if (received.type === "result" && !received.ok) response.reject(errorFromPayload(received.error));
285
389
  });
286
390
  const unsubscribeError = worker.onError(error => response.reject(error));
287
391
  try {
288
- worker.send(message);
289
392
  await withTimeout(response.promise, timeoutMs, failureMessage);
290
393
  } finally {
291
394
  unsubscribeMessage();
@@ -294,12 +397,12 @@ export async function smokeTestComputerWorker(
294
397
  };
295
398
 
296
399
  try {
297
- await exchange(
298
- { type: "init", options: { backend: "auto", display: "all", maxWidth: 1920, maxHeight: 1200 } },
299
- "ready",
300
- "Computer worker smoke initialization timed out",
301
- );
302
- await exchange({ type: "close" }, "closed", "Computer worker smoke close timed out");
400
+ const pong = waitFor("pong", "Computer worker smoke ping timed out");
401
+ worker.send({ type: "ping", id: `computer-smoke-${Snowflake.next()}` });
402
+ await pong;
403
+ const closed = waitFor("closed", "Computer worker smoke close timed out");
404
+ worker.send({ type: "close" });
405
+ await closed;
303
406
  } finally {
304
407
  await worker.terminate();
305
408
  }
@@ -3,9 +3,12 @@ import { consumeWorkerInbox, isWorkerHostSelector } from "@oh-my-pi/pi-utils/wor
3
3
  import type { ComputerWorkerInbound, ComputerWorkerTransport } from "./protocol";
4
4
  import { ComputerWorkerCore } from "./worker";
5
5
 
6
- export function startComputerWorker(): void {
7
- if (!parentPort) throw new Error("computer-worker-entry: missing parentPort");
6
+ let started = false;
8
7
 
8
+ /** Starts the computer worker once when running inside a Bun worker thread. */
9
+ export function startComputerWorker(): void {
10
+ if (started || !parentPort) return;
11
+ started = true;
9
12
  const port = parentPort;
10
13
  const inbox = consumeWorkerInbox();
11
14
  const transport: ComputerWorkerTransport = {
@@ -26,9 +29,11 @@ export function startComputerWorker(): void {
26
29
  new ComputerWorkerCore(transport);
27
30
  }
28
31
 
29
- // Bun workers report `import.meta.main === false`. The source fallback still
30
- // enters this file directly, while packaged CLI workers carry the selector and
31
- // start the named entry only after installing its inbox.
32
- if (!Bun.isMainThread && !process.argv.some(isWorkerHostSelector) && import.meta.path === Bun.main) {
32
+ // Direct-source fallback: loaded as a worker's entry module outside a CLI
33
+ // host there is no selector argv, so start immediately. When any CLI-host
34
+ // worker re-enters cli.ts (which imports this module statically), the
35
+ // selector guard defers to the host's dispatch — an unguarded auto-start
36
+ // would hijack the message port of every other worker kind.
37
+ if (!Bun.argv.some(isWorkerHostSelector)) {
33
38
  startComputerWorker();
34
39
  }