@gajae-code/coding-agent 0.11.1 → 0.11.2

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 (404) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/types/cli/config-cli.d.ts +11 -1
  3. package/dist/types/cli/notify-cli.d.ts +1 -0
  4. package/dist/types/cli/session-picker.d.ts +2 -1
  5. package/dist/types/cli/setup-cli.d.ts +1 -1
  6. package/dist/types/commands/mcp-serve.d.ts +33 -0
  7. package/dist/types/commands/mcp.d.ts +1 -1
  8. package/dist/types/commands/notify.d.ts +0 -2
  9. package/dist/types/commands/sdk.d.ts +1 -1
  10. package/dist/types/config/atomic-yaml-patch.d.ts +18 -0
  11. package/dist/types/config/file-lock.d.ts +14 -4
  12. package/dist/types/config/keybindings.d.ts +42 -1
  13. package/dist/types/config/model-auth.d.ts +3 -0
  14. package/dist/types/config/model-bindings-applier.d.ts +13 -0
  15. package/dist/types/config/model-discovery-manager.d.ts +49 -0
  16. package/dist/types/config/model-equivalence.d.ts +20 -0
  17. package/dist/types/config/model-registry.d.ts +16 -15
  18. package/dist/types/config/model-resolver.d.ts +28 -18
  19. package/dist/types/config/models-config-schema.d.ts +11 -0
  20. package/dist/types/config/settings-schema.d.ts +56 -1
  21. package/dist/types/config/settings.d.ts +14 -11
  22. package/dist/types/coordinator-mcp/policy.d.ts +5 -0
  23. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +129 -0
  24. package/dist/types/coordinator-mcp/question-state.d.ts +356 -0
  25. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  26. package/dist/types/discovery/helpers.d.ts +0 -6
  27. package/dist/types/edit/index.d.ts +3 -0
  28. package/dist/types/eval/py/env.d.ts +9 -0
  29. package/dist/types/extensibility/extensions/loader.d.ts +2 -1
  30. package/dist/types/extensibility/extensions/runner.d.ts +2 -2
  31. package/dist/types/extensibility/extensions/types.d.ts +34 -3
  32. package/dist/types/extensibility/hooks/runner.d.ts +1 -1
  33. package/dist/types/extensibility/shared-events.d.ts +2 -0
  34. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  35. package/dist/types/gjc-runtime/deep-interview-state.d.ts +44 -0
  36. package/dist/types/gjc-runtime/goal-mode-request.d.ts +11 -1
  37. package/dist/types/gjc-runtime/launch-worktree.d.ts +4 -2
  38. package/dist/types/gjc-runtime/linux-proc.d.ts +29 -0
  39. package/dist/types/gjc-runtime/session-resolution.d.ts +2 -2
  40. package/dist/types/gjc-runtime/state-migrations.d.ts +1 -1
  41. package/dist/types/gjc-runtime/state-writer.d.ts +2 -1
  42. package/dist/types/gjc-runtime/team-launch.d.ts +59 -0
  43. package/dist/types/gjc-runtime/team-notify.d.ts +45 -0
  44. package/dist/types/gjc-runtime/team-runtime.d.ts +29 -121
  45. package/dist/types/gjc-runtime/team-store.d.ts +160 -0
  46. package/dist/types/gjc-runtime/team-workers.d.ts +78 -0
  47. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +8 -0
  48. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +13 -0
  49. package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +38 -0
  50. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -0
  51. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +23 -0
  52. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +55 -33
  53. package/dist/types/gjc-runtime/workflow-cli-common.d.ts +8 -0
  54. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -0
  55. package/dist/types/goals/continuation-timeout-guard.d.ts +22 -0
  56. package/dist/types/goals/runtime.d.ts +1 -0
  57. package/dist/types/goals/state.d.ts +8 -0
  58. package/dist/types/harness-control-plane/lifecycle-reconciliation.d.ts +17 -0
  59. package/dist/types/harness-control-plane/phase-rollup.d.ts +4 -7
  60. package/dist/types/harness-control-plane/receipt-ingest.d.ts +5 -0
  61. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  62. package/dist/types/hindsight/state.d.ts +11 -4
  63. package/dist/types/lsp/lspmux.d.ts +5 -3
  64. package/dist/types/lsp/path-trust.d.ts +1 -0
  65. package/dist/types/main.d.ts +3 -2
  66. package/dist/types/memories/storage.d.ts +1 -0
  67. package/dist/types/modes/action-registry.d.ts +29 -0
  68. package/dist/types/modes/components/chrome.d.ts +14 -0
  69. package/dist/types/modes/components/command-palette.d.ts +27 -0
  70. package/dist/types/modes/components/custom-provider-wizard.d.ts +1 -0
  71. package/dist/types/modes/components/index.d.ts +2 -1
  72. package/dist/types/modes/components/pet-selector.d.ts +1 -1
  73. package/dist/types/modes/components/plan-preview-overlay.d.ts +43 -0
  74. package/dist/types/modes/components/queue-mode-selector.d.ts +1 -1
  75. package/dist/types/modes/components/queue-pane.d.ts +12 -0
  76. package/dist/types/modes/components/read-tool-group.d.ts +6 -0
  77. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -4
  78. package/dist/types/modes/components/sessions-dashboard.d.ts +32 -0
  79. package/dist/types/modes/components/settings-selector.d.ts +1 -1
  80. package/dist/types/modes/components/show-images-selector.d.ts +1 -1
  81. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  82. package/dist/types/modes/components/status-line.d.ts +1 -94
  83. package/dist/types/modes/components/tasks-pane.d.ts +15 -0
  84. package/dist/types/modes/components/theme-selector.d.ts +1 -1
  85. package/dist/types/modes/components/tool-execution.d.ts +15 -0
  86. package/dist/types/modes/components/tool-status-header.d.ts +111 -0
  87. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +42 -0
  88. package/dist/types/modes/controllers/goal-mode-controller.d.ts +62 -0
  89. package/dist/types/modes/controllers/input-controller.d.ts +8 -1
  90. package/dist/types/modes/controllers/mode-gate.d.ts +8 -0
  91. package/dist/types/modes/controllers/plan-mode-controller.d.ts +65 -0
  92. package/dist/types/modes/controllers/selector-controller.d.ts +15 -3
  93. package/dist/types/modes/controllers/stt-controller.d.ts +26 -0
  94. package/dist/types/modes/interactive-mode.d.ts +19 -17
  95. package/dist/types/modes/jobs-observer.d.ts +2 -0
  96. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -11
  97. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +20 -2
  98. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +31 -0
  99. package/dist/types/modes/tasks-aggregator.d.ts +39 -0
  100. package/dist/types/modes/transcript-item-registry.d.ts +60 -0
  101. package/dist/types/modes/types.d.ts +20 -13
  102. package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
  103. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  104. package/dist/types/sdk/broker/broker.d.ts +78 -12
  105. package/dist/types/sdk/broker/lifecycle-codec.d.ts +4 -0
  106. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +15 -1
  107. package/dist/types/sdk/broker/lifecycle.d.ts +6 -2
  108. package/dist/types/sdk/broker/process-incarnation.d.ts +2 -0
  109. package/dist/types/sdk/broker/session-index.d.ts +0 -1
  110. package/dist/types/sdk/broker/state-version.d.ts +2 -0
  111. package/dist/types/sdk/bus/chat-effect-journal.d.ts +1 -0
  112. package/dist/types/sdk/bus/config-commands.d.ts +1 -1
  113. package/dist/types/sdk/bus/config.d.ts +11 -0
  114. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  115. package/dist/types/sdk/bus/html-format.d.ts +3 -2
  116. package/dist/types/sdk/bus/index.d.ts +27 -0
  117. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  118. package/dist/types/sdk/bus/notification-service.d.ts +20 -0
  119. package/dist/types/sdk/bus/rate-limit-pool.d.ts +13 -2
  120. package/dist/types/sdk/bus/recent-activity.d.ts +21 -12
  121. package/dist/types/sdk/bus/rich-document.d.ts +7 -0
  122. package/dist/types/sdk/bus/rich-render.d.ts +18 -41
  123. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
  124. package/dist/types/sdk/bus/telegram-daemon.d.ts +46 -3
  125. package/dist/types/sdk/bus/threaded-inbound.d.ts +1 -1
  126. package/dist/types/sdk/bus/threaded-render.d.ts +13 -0
  127. package/dist/types/sdk/bus/topic-registry.d.ts +24 -2
  128. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  129. package/dist/types/sdk/host/host.d.ts +2 -0
  130. package/dist/types/sdk/index.d.ts +1 -0
  131. package/dist/types/sdk/session-directory.d.ts +56 -0
  132. package/dist/types/session/agent-session.d.ts +82 -47
  133. package/dist/types/session/agent-storage.d.ts +7 -0
  134. package/dist/types/session/fallback-chain-controller.d.ts +10 -2
  135. package/dist/types/session/internal/managed-session-scope.d.ts +102 -0
  136. package/dist/types/session/internal/managed-session-storage.d.ts +38 -0
  137. package/dist/types/session/messages.d.ts +16 -0
  138. package/dist/types/session/session-manager-internal.d.ts +14 -0
  139. package/dist/types/session/session-manager.d.ts +82 -12
  140. package/dist/types/session/session-storage.d.ts +43 -3
  141. package/dist/types/session/volatile-context-pruning.d.ts +15 -0
  142. package/dist/types/skill-state/{deep-interview-mutation-guard.d.ts → workflow-mutation-guard.d.ts} +24 -8
  143. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -3
  144. package/dist/types/slash-commands/builtin-registry.d.ts +4 -1
  145. package/dist/types/system-prompt.d.ts +2 -11
  146. package/dist/types/task/executor.d.ts +3 -1
  147. package/dist/types/thinking.d.ts +7 -0
  148. package/dist/types/tool-discovery/tool-index.d.ts +1 -50
  149. package/dist/types/tools/ask.d.ts +64 -2
  150. package/dist/types/tools/bash.d.ts +3 -0
  151. package/dist/types/tools/browser/dead-tab-recovery.d.ts +24 -0
  152. package/dist/types/tools/browser/tab-supervisor.d.ts +21 -0
  153. package/dist/types/tools/cron.d.ts +2 -0
  154. package/dist/types/tools/fetch.d.ts +1 -0
  155. package/dist/types/tools/index.d.ts +30 -15
  156. package/dist/types/tools/output-meta.d.ts +1 -0
  157. package/dist/types/tools/read.d.ts +3 -0
  158. package/dist/types/tools/search-tool-bm25.d.ts +3 -4
  159. package/dist/types/utils/event-bus.d.ts +1 -1
  160. package/dist/types/web/search/index.d.ts +3 -1
  161. package/package.json +21 -9
  162. package/scripts/build-sdk-package-smoke.ts +34 -1
  163. package/scripts/generate-hotkeys-docs.ts +74 -0
  164. package/scripts/generate-sdk-operation-inventory.ts +64 -20
  165. package/scripts/verify-gjc-sdk-canonicalization.ts +182 -2
  166. package/src/async/job-manager.ts +49 -6
  167. package/src/cli/config-cli.ts +75 -2
  168. package/src/cli/notify-cli.ts +92 -26
  169. package/src/cli/session-picker.ts +20 -3
  170. package/src/cli/setup-cli.ts +1 -1
  171. package/src/cli.ts +1 -1
  172. package/src/commands/config.ts +1 -1
  173. package/src/commands/harness.ts +34 -106
  174. package/src/commands/launch.ts +6 -4
  175. package/src/commands/mcp-serve.ts +85 -8
  176. package/src/commands/mcp.ts +2 -2
  177. package/src/commands/notify.ts +8 -4
  178. package/src/commands/sdk.ts +61 -40
  179. package/src/commands/ultragoal.ts +4 -1
  180. package/src/config/atomic-yaml-patch.ts +80 -13
  181. package/src/config/file-lock.ts +105 -43
  182. package/src/config/keybindings.ts +64 -20
  183. package/src/config/model-auth.ts +9 -0
  184. package/src/config/model-bindings-applier.ts +139 -0
  185. package/src/config/model-discovery-manager.ts +224 -0
  186. package/src/config/model-equivalence.ts +66 -0
  187. package/src/config/model-registry.ts +169 -302
  188. package/src/config/model-resolver.ts +361 -369
  189. package/src/config/models-config-schema.ts +5 -4
  190. package/src/config/settings-schema.ts +159 -2
  191. package/src/config/settings.ts +458 -482
  192. package/src/coordinator-mcp/policy.ts +76 -11
  193. package/src/coordinator-mcp/question-gate-codec.ts +274 -0
  194. package/src/coordinator-mcp/question-state.ts +778 -0
  195. package/src/coordinator-mcp/server.ts +1702 -238
  196. package/src/debug/index.ts +1 -1
  197. package/src/defaults/gjc/skills/deep-interview/SKILL.md +13 -1
  198. package/src/defaults/gjc/skills/team/SKILL.md +16 -0
  199. package/src/discovery/helpers.ts +1 -50
  200. package/src/edit/index.ts +64 -0
  201. package/src/eval/py/env.ts +26 -0
  202. package/src/eval/py/kernel.ts +5 -3
  203. package/src/extensibility/extensions/loader.ts +9 -1
  204. package/src/extensibility/extensions/runner.ts +12 -3
  205. package/src/extensibility/extensions/types.ts +39 -1
  206. package/src/extensibility/gjc-plugins/installer.ts +16 -16
  207. package/src/extensibility/hooks/runner.ts +2 -2
  208. package/src/extensibility/runtime-skill-discovery.ts +33 -13
  209. package/src/extensibility/shared-events.ts +2 -0
  210. package/src/gjc-runtime/deep-interview-recorder.ts +63 -3
  211. package/src/gjc-runtime/deep-interview-runtime.ts +76 -27
  212. package/src/gjc-runtime/deep-interview-state.ts +317 -5
  213. package/src/gjc-runtime/goal-mode-request.ts +55 -8
  214. package/src/gjc-runtime/launch-tmux.ts +2 -5
  215. package/src/gjc-runtime/launch-worktree.ts +5 -5
  216. package/src/gjc-runtime/linux-proc.ts +71 -0
  217. package/src/gjc-runtime/ralplan-runtime.ts +74 -90
  218. package/src/gjc-runtime/session-resolution.ts +18 -3
  219. package/src/gjc-runtime/session-state-sidecar.ts +6 -107
  220. package/src/gjc-runtime/state-migrations.ts +1 -1
  221. package/src/gjc-runtime/state-runtime.ts +320 -249
  222. package/src/gjc-runtime/state-writer.ts +36 -40
  223. package/src/gjc-runtime/team-launch.ts +319 -0
  224. package/src/gjc-runtime/team-notify.ts +326 -0
  225. package/src/gjc-runtime/team-runtime.ts +1099 -1482
  226. package/src/gjc-runtime/team-store.ts +828 -0
  227. package/src/gjc-runtime/team-workers.ts +962 -0
  228. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +2 -11
  229. package/src/gjc-runtime/tmux-owner-isolation.ts +14 -9
  230. package/src/gjc-runtime/tmux-sessions.ts +4 -21
  231. package/src/gjc-runtime/ultragoal-change-set.ts +172 -0
  232. package/src/gjc-runtime/ultragoal-evidence.ts +874 -0
  233. package/src/gjc-runtime/ultragoal-guard.ts +29 -5
  234. package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
  235. package/src/gjc-runtime/ultragoal-runtime.ts +84 -1311
  236. package/src/gjc-runtime/workflow-cli-common.ts +30 -0
  237. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  238. package/src/gjc-runtime/workflow-manifest.ts +18 -0
  239. package/src/goals/continuation-timeout-guard.ts +79 -0
  240. package/src/goals/runtime.ts +7 -5
  241. package/src/goals/state.ts +18 -0
  242. package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
  243. package/src/harness-control-plane/phase-rollup.ts +4 -7
  244. package/src/harness-control-plane/receipt-ingest.ts +5 -0
  245. package/src/harness-control-plane/receipt-spool.ts +51 -7
  246. package/src/harness-control-plane/session-lease.ts +12 -130
  247. package/src/hindsight/backend.ts +26 -27
  248. package/src/hindsight/state.ts +101 -32
  249. package/src/hooks/skill-state.ts +17 -11
  250. package/src/internal-urls/docs-index.generated.ts +21 -18
  251. package/src/lsp/client.ts +1 -1
  252. package/src/lsp/config.ts +145 -26
  253. package/src/lsp/index.ts +1 -1
  254. package/src/lsp/lspmux.ts +39 -10
  255. package/src/lsp/path-trust.ts +81 -0
  256. package/src/main.ts +28 -8
  257. package/src/memories/index.ts +1 -0
  258. package/src/memories/storage.ts +6 -2
  259. package/src/modes/action-registry.ts +165 -0
  260. package/src/modes/components/chrome.ts +40 -0
  261. package/src/modes/components/command-palette.ts +168 -0
  262. package/src/modes/components/custom-editor.ts +1 -1
  263. package/src/modes/components/custom-provider-wizard.ts +51 -14
  264. package/src/modes/components/index.ts +2 -1
  265. package/src/modes/components/pet-selector.ts +11 -17
  266. package/src/modes/components/plan-preview-overlay.ts +295 -0
  267. package/src/modes/components/queue-mode-selector.ts +10 -27
  268. package/src/modes/components/queue-pane.ts +79 -0
  269. package/src/modes/components/read-tool-group.ts +13 -0
  270. package/src/modes/components/session-observer-overlay.ts +207 -772
  271. package/src/modes/components/sessions-dashboard.ts +181 -0
  272. package/src/modes/components/settings-selector.ts +1 -1
  273. package/src/modes/components/show-images-selector.ts +10 -24
  274. package/src/modes/components/status-line/types.ts +1 -1
  275. package/src/modes/components/status-line.ts +1 -969
  276. package/src/modes/components/tasks-pane.ts +73 -0
  277. package/src/modes/components/theme-selector.ts +11 -31
  278. package/src/modes/components/tool-execution.ts +22 -0
  279. package/src/modes/components/tool-status-header.ts +1056 -0
  280. package/src/modes/components/transcript-viewer-overlay.ts +384 -0
  281. package/src/modes/controllers/btw-controller.ts +1 -0
  282. package/src/modes/controllers/event-controller.ts +3 -2
  283. package/src/modes/controllers/extension-ui-controller.ts +24 -1
  284. package/src/modes/controllers/goal-mode-controller.ts +474 -0
  285. package/src/modes/controllers/input-controller.ts +764 -229
  286. package/src/modes/controllers/mode-gate.ts +24 -0
  287. package/src/modes/controllers/plan-mode-controller.ts +517 -0
  288. package/src/modes/controllers/selector-controller.ts +225 -10
  289. package/src/modes/controllers/stt-controller.ts +125 -0
  290. package/src/modes/interactive-mode.ts +289 -1150
  291. package/src/modes/jobs-observer.ts +3 -0
  292. package/src/modes/runtime-init.ts +8 -0
  293. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -106
  294. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +71 -16
  295. package/src/modes/shared/agent-wire/workflow-gate-types.ts +177 -0
  296. package/src/modes/tasks-aggregator.ts +199 -0
  297. package/src/modes/transcript-item-registry.ts +229 -0
  298. package/src/modes/types.ts +45 -18
  299. package/src/modes/utils/ui-helpers.ts +17 -9
  300. package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
  301. package/src/prompts/agent-fragments/restricted-bash.md +1 -0
  302. package/src/prompts/agents/architect.md +2 -8
  303. package/src/prompts/agents/critic.md +2 -8
  304. package/src/prompts/agents/executor.md +3 -0
  305. package/src/prompts/agents/planner.md +4 -12
  306. package/src/prompts/system/custom-system-prompt.md +1 -23
  307. package/src/prompts/system/plan-mode-approved.md +8 -0
  308. package/src/prompts/system/system-prompt.md +17 -96
  309. package/src/prompts/tools/bash.md +4 -4
  310. package/src/prompts/tools/browser.md +15 -52
  311. package/src/prompts/tools/find.md +2 -2
  312. package/src/prompts/tools/hashline.md +13 -55
  313. package/src/prompts/tools/read.md +3 -6
  314. package/src/prompts/tools/search-tool-bm25.md +0 -2
  315. package/src/prompts/tools/search.md +2 -4
  316. package/src/prompts/tools/skill-discovery.md +1 -2
  317. package/src/prompts/tools/task.md +1 -1
  318. package/src/runtime-mcp/manager.ts +1 -1
  319. package/src/sdk/broker/broker.ts +206 -78
  320. package/src/sdk/broker/discovery.ts +1 -1
  321. package/src/sdk/broker/ensure.ts +3 -1
  322. package/src/sdk/broker/lifecycle-codec.ts +11 -0
  323. package/src/sdk/broker/lifecycle-ledger.ts +371 -74
  324. package/src/sdk/broker/lifecycle.ts +1399 -142
  325. package/src/sdk/broker/process-incarnation.ts +5 -13
  326. package/src/sdk/broker/session-index.ts +145 -56
  327. package/src/sdk/broker/state-version.ts +21 -0
  328. package/src/sdk/broker/transport.ts +1 -1
  329. package/src/sdk/bus/chat-daemon-cli.ts +2 -0
  330. package/src/sdk/bus/chat-daemon-control.ts +8 -28
  331. package/src/sdk/bus/chat-effect-journal.ts +10 -2
  332. package/src/sdk/bus/config-commands.ts +5 -4
  333. package/src/sdk/bus/config.ts +98 -0
  334. package/src/sdk/bus/conversation-store.ts +5 -29
  335. package/src/sdk/bus/daemon-paths.ts +2 -0
  336. package/src/sdk/bus/html-format.ts +20 -3
  337. package/src/sdk/bus/index.ts +778 -93
  338. package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
  339. package/src/sdk/bus/natives-augment.d.ts +26 -1
  340. package/src/sdk/bus/notification-service.ts +168 -20
  341. package/src/sdk/bus/rate-limit-pool.ts +58 -14
  342. package/src/sdk/bus/recent-activity.ts +188 -74
  343. package/src/sdk/bus/rich-document.ts +258 -0
  344. package/src/sdk/bus/rich-render.ts +62 -70
  345. package/src/sdk/bus/telegram-daemon-cli.ts +15 -59
  346. package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
  347. package/src/sdk/bus/telegram-daemon.ts +1389 -237
  348. package/src/sdk/bus/threaded-inbound.ts +8 -3
  349. package/src/sdk/bus/threaded-render.ts +57 -1
  350. package/src/sdk/bus/topic-registry.ts +120 -13
  351. package/src/sdk/host/control/dispatch.ts +14 -2
  352. package/src/sdk/host/control/operations.ts +6 -1
  353. package/src/sdk/host/host.ts +31 -1
  354. package/src/sdk/host/query/handlers.ts +20 -22
  355. package/src/sdk/host/query/revision-store.ts +80 -57
  356. package/src/sdk/index.ts +1 -0
  357. package/src/sdk/protocol/operation-inventory.generated.json +943 -1907
  358. package/src/sdk/session-directory.ts +141 -0
  359. package/src/sdk/session.ts +458 -479
  360. package/src/session/agent-session.ts +1700 -583
  361. package/src/session/agent-storage.ts +18 -0
  362. package/src/session/cache-economics.ts +25 -8
  363. package/src/session/fallback-chain-controller.ts +53 -3
  364. package/src/session/internal/managed-session-scope.ts +2226 -0
  365. package/src/session/internal/managed-session-storage.ts +453 -0
  366. package/src/session/messages.ts +91 -2
  367. package/src/session/session-manager-internal.ts +33 -0
  368. package/src/session/session-manager.ts +1112 -333
  369. package/src/session/session-storage.ts +444 -39
  370. package/src/session/volatile-context-pruning.ts +78 -0
  371. package/src/setup/credential-auto-import.ts +2 -1
  372. package/src/setup/hermes/templates/operator-instructions.v1.md +1 -1
  373. package/src/setup/provider-onboarding.ts +36 -61
  374. package/src/setup/provider-presets.json +5 -0
  375. package/src/skill-state/active-state.ts +3 -13
  376. package/src/skill-state/{deep-interview-mutation-guard.ts → workflow-mutation-guard.ts} +47 -29
  377. package/src/skill-state/workflow-state-contract.ts +6 -41
  378. package/src/slash-commands/builtin-registry.ts +46 -11
  379. package/src/system-prompt.ts +37 -44
  380. package/src/task/agents.ts +19 -1
  381. package/src/task/executor.ts +90 -6
  382. package/src/task/index.ts +36 -21
  383. package/src/thinking.ts +21 -6
  384. package/src/tool-discovery/tool-index.ts +2 -134
  385. package/src/tools/ask.ts +157 -21
  386. package/src/tools/ast-edit.ts +1 -1
  387. package/src/tools/bash.ts +38 -0
  388. package/src/tools/browser/dead-tab-recovery.ts +78 -0
  389. package/src/tools/browser/tab-supervisor.ts +282 -36
  390. package/src/tools/computer.ts +10 -3
  391. package/src/tools/cron.ts +30 -16
  392. package/src/tools/eval.ts +1 -2
  393. package/src/tools/fetch.ts +11 -4
  394. package/src/tools/index.ts +76 -24
  395. package/src/tools/output-meta.ts +13 -5
  396. package/src/tools/read.ts +37 -1
  397. package/src/tools/resource-gc.ts +2 -2
  398. package/src/tools/search-tool-bm25.ts +11 -74
  399. package/src/utils/event-bus.ts +9 -4
  400. package/src/web/search/index.ts +3 -3
  401. package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
  402. package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
  403. package/src/discovery/plugin-dir-roots.ts +0 -28
  404. package/src/runtime-mcp/discoverable-tool-metadata.ts +0 -25
@@ -5,6 +5,13 @@ import type { ToolSession } from "../../sdk";
5
5
  import { expandPath } from "../path-utils";
6
6
  import { ToolAbortError, ToolError } from "../tool-errors";
7
7
  import { pickElectronTarget } from "./attach";
8
+ import {
9
+ consumeDeadTabRecovery,
10
+ discardDeadTabRecovery,
11
+ isDeadTabRecoveryLive,
12
+ peekDeadTabRecovery,
13
+ registerDeadTabRecovery,
14
+ } from "./dead-tab-recovery";
8
15
  import { type BrowserHandle, type BrowserKindTag, holdBrowser, releaseBrowser } from "./registry";
9
16
  import type {
10
17
  ReadyInfo,
@@ -55,6 +62,8 @@ export interface TabSession {
55
62
  pending: Map<string, PendingRun>;
56
63
  dialogPolicy?: DialogPolicy;
57
64
  kindTag: BrowserKindTag;
65
+ /** Immutable inputs needed to recreate this worker after an unexpected exit. */
66
+ recoveryOpts: AcquireTabOptions;
58
67
  /** Session that acquired this tab; used for session-scoped teardown (F13). */
59
68
  ownerId?: string;
60
69
  /** Unix-ms timestamp of the last acquire/run activity; drives GC idle + LRU ordering. */
@@ -73,6 +82,10 @@ export interface AcquireTabOptions {
73
82
  dialogs?: DialogPolicy;
74
83
  /** Owning session id so dispose can release only this session's tabs (F13). */
75
84
  ownerId?: string;
85
+ /** Internal recovery-only target id; makes a headless replacement attach, not open a page. */
86
+ recoveryTargetId?: string;
87
+ /** Recovery fence: replacement must not overwrite a tab installed after dead teardown. */
88
+ requireVacantName?: boolean;
76
89
  }
77
90
 
78
91
  export interface AcquireTabResult {
@@ -98,8 +111,20 @@ export interface ReleaseTabOptions {
98
111
  }
99
112
 
100
113
  const tabs = new Map<string, TabSession>();
114
+ const recoveringTabs = new Map<string, { ownerId: string | undefined; promise: Promise<TabSession> }>();
115
+ let afterWorkerInitializationForTest: ((name: string) => void) | undefined;
116
+ let workerFactoryForTest: (() => Promise<WorkerHandle>) | undefined;
117
+ let workerInitializerForTest:
118
+ | ((worker: WorkerHandle, payload: WorkerInitPayload, timeoutMs: number) => Promise<ReadyInfo>)
119
+ | undefined;
101
120
  const GRACE_MS = 750;
102
121
 
122
+ interface DeadTabDescriptor {
123
+ tab: TabSession;
124
+ browser: BrowserHandle;
125
+ opts: AcquireTabOptions;
126
+ }
127
+
103
128
  /**
104
129
  * Remaining time (ms) until an absolute teardown deadline, or +Infinity when no deadline
105
130
  * is set (the GC / session-teardown callers). A non-positive result means the shared
@@ -156,6 +181,46 @@ function beginRelease(tab: TabSession): boolean {
156
181
  return true;
157
182
  }
158
183
 
184
+ async function withTemporaryBrowserHold<T>(browser: BrowserHandle, work: () => Promise<T>): Promise<T> {
185
+ holdBrowser(browser);
186
+ try {
187
+ return await work();
188
+ } finally {
189
+ await releaseBrowser(browser, { kill: false });
190
+ }
191
+ }
192
+
193
+ /** Test-only: exercise the same hold-transfer primitive used by replacement recovery. */
194
+ export async function withTemporaryBrowserHoldForTest<T>(browser: BrowserHandle, work: () => Promise<T>): Promise<T> {
195
+ return await withTemporaryBrowserHold(browser, work);
196
+ }
197
+
198
+ /** Test-only: close exactly the target belonging to the supplied dead session. */
199
+ export async function closeOrphanTargetForTest(tab: TabSession): Promise<void> {
200
+ await closeOrphanTarget(tab);
201
+ }
202
+
203
+ function recoveryPromiseForOwner(name: string, ownerId: string | undefined): Promise<TabSession> | undefined {
204
+ const existing = recoveringTabs.get(name);
205
+ if (!existing || existing.ownerId !== ownerId) return undefined;
206
+ return existing.promise;
207
+ }
208
+
209
+ /** Test-only: install an in-flight recovery and observe owner isolation. */
210
+ export function __setRecoveringTabForTest(
211
+ name: string,
212
+ ownerId: string | undefined,
213
+ promise: Promise<TabSession>,
214
+ ): void {
215
+ recoveringTabs.set(name, { ownerId, promise });
216
+ }
217
+
218
+ export function recoveryPromiseForOwnerForTest(
219
+ name: string,
220
+ ownerId: string | undefined,
221
+ ): Promise<TabSession> | undefined {
222
+ return recoveryPromiseForOwner(name, ownerId);
223
+ }
159
224
  /** Read-only, GC-facing projection of a live tab. Never exposes the mutable `tabs` map. */
160
225
  export interface TabGcSnapshot {
161
226
  name: string;
@@ -193,14 +258,14 @@ export function listTabsForGc(): TabGcSnapshot[] {
193
258
  */
194
259
  export async function releaseTabIfGcEligible(name: string, policy: BrowserGcEligibilityPolicy): Promise<boolean> {
195
260
  const tab = tabs.get(name);
196
- if (!tab) return false;
197
- if (tab.releasing) return false;
198
- if (tab.state !== "alive") return false;
261
+ if (!tab || tab.releasing) return false;
262
+ if (tab.state === "dead") {
263
+ if (recoveringTabs.has(name) || isDeadTabRecoveryLive(name, policy.now())) return false;
264
+ return await releaseDeadTabForRecovery(name, tab, tab.ownerId);
265
+ }
199
266
  if (tab.pending.size !== 0) return false;
200
267
  if (tab.kindTag !== "headless" && tab.kindTag !== "spawned") return false;
201
268
  if (policy.now() - tab.lastUsedAt <= policy.idleMs) return false;
202
- // All predicates passed synchronously; releaseTab applies the shared begin-release guard
203
- // on the same microtask, so no other code runs between the check and the state transition.
204
269
  return await releaseTab(name, { kill: false });
205
270
  }
206
271
 
@@ -212,6 +277,36 @@ export function setTabForTest(tab: TabSession): void {
212
277
  /** Test-only: clear the tab registry between cases. */
213
278
  export function clearTabsForTest(): void {
214
279
  tabs.clear();
280
+ recoveringTabs.clear();
281
+ afterWorkerInitializationForTest = undefined;
282
+ workerFactoryForTest = undefined;
283
+ workerInitializerForTest = undefined;
284
+ }
285
+
286
+ export function __setAfterWorkerInitializationForTest(callback: ((name: string) => void) | undefined): void {
287
+ afterWorkerInitializationForTest = callback;
288
+ }
289
+ export function __setAcquireTabWorkerDepsForTest(
290
+ factory: (() => Promise<WorkerHandle>) | undefined,
291
+ initializer:
292
+ | ((worker: WorkerHandle, payload: WorkerInitPayload, timeoutMs: number) => Promise<ReadyInfo>)
293
+ | undefined,
294
+ ): void {
295
+ workerFactoryForTest = factory;
296
+ workerInitializerForTest = initializer;
297
+ }
298
+
299
+ async function initializeAcquireWorker(
300
+ worker: WorkerHandle,
301
+ payload: WorkerInitPayload,
302
+ timeoutMs: number,
303
+ ): Promise<ReadyInfo> {
304
+ return await (workerInitializerForTest?.(worker, payload, timeoutMs) ??
305
+ initializeTabWorker(worker, payload, timeoutMs));
306
+ }
307
+
308
+ async function spawnAcquireWorker(): Promise<WorkerHandle> {
309
+ return await (workerFactoryForTest?.() ?? spawnTabWorker());
215
310
  }
216
311
 
217
312
  export async function acquireTab(
@@ -220,6 +315,9 @@ export async function acquireTab(
220
315
  opts: AcquireTabOptions,
221
316
  ): Promise<AcquireTabResult> {
222
317
  const existing = tabs.get(name);
318
+ if (opts.requireVacantName && existing)
319
+ throw new ToolError(`Tab ${JSON.stringify(name)} was replaced during recovery.`);
320
+ let replacementHold = false;
223
321
  if (existing) {
224
322
  if (existing.browser === browser && existing.state === "alive") {
225
323
  if (opts.dialogs !== undefined && opts.dialogs !== existing.dialogPolicy) {
@@ -240,33 +338,50 @@ export async function acquireTab(
240
338
  return { tab: tabs.get(name)!, created: false };
241
339
  }
242
340
  } else {
243
- await releaseTab(name, { kill: false });
341
+ if (existing.browser === browser) {
342
+ holdBrowser(browser);
343
+ replacementHold = true;
344
+ }
345
+ try {
346
+ await releaseTab(name, { kill: false });
347
+ } catch (error) {
348
+ if (replacementHold) await releaseBrowser(browser, { kill: false });
349
+ throw error;
350
+ }
244
351
  }
245
352
  }
246
353
 
247
- const initPayload = await buildInitPayload(browser, opts);
248
- let worker = await spawnTabWorker();
354
+ let initPayload: WorkerInitPayload;
355
+ try {
356
+ initPayload = await buildInitPayload(browser, opts);
357
+ } catch (error) {
358
+ if (replacementHold) await releaseBrowser(browser, { kill: false });
359
+ throw error;
360
+ }
361
+ let worker = await spawnAcquireWorker();
249
362
  let info: ReadyInfo;
250
363
  try {
251
- info = await initializeTabWorker(worker, initPayload, opts.timeoutMs + GRACE_MS);
364
+ info = await initializeAcquireWorker(worker, initPayload, opts.timeoutMs + GRACE_MS);
252
365
  } catch (error) {
253
366
  // `BuildMessage`-class failures arrive asynchronously via the worker's `error` event,
254
367
  // after `spawnTabWorker`'s synchronous try/catch has already returned. Fall back to
255
368
  // the inline worker here so module-resolution failures don't poison every tab open.
256
369
  await worker.terminate().catch(() => undefined);
257
370
  if (worker.mode === "inline") {
258
- if (browser.refCount === 0) await releaseBrowser(browser, { kill: false });
371
+ if (replacementHold) await releaseBrowser(browser, { kill: false });
372
+ else if (browser.refCount === 0) await releaseBrowser(browser, { kill: false });
259
373
  throw error;
260
374
  }
261
375
  logger.warn("Tab worker init failed; retrying with inline tab worker (no sync-loop guard)", {
262
376
  error: error instanceof Error ? error.message : String(error),
263
377
  });
264
- worker = await spawnInlineWorker();
378
+ worker = await (workerFactoryForTest?.() ?? spawnInlineWorker());
265
379
  try {
266
- info = await initializeTabWorker(worker, initPayload, opts.timeoutMs + GRACE_MS);
380
+ info = await initializeAcquireWorker(worker, initPayload, opts.timeoutMs + GRACE_MS);
267
381
  } catch (inlineError) {
268
382
  await worker.terminate().catch(() => undefined);
269
- if (browser.refCount === 0) await releaseBrowser(browser, { kill: false });
383
+ if (replacementHold) await releaseBrowser(browser, { kill: false });
384
+ else if (browser.refCount === 0) await releaseBrowser(browser, { kill: false });
270
385
  const finalError = new ToolError(
271
386
  `Failed to start browser tab worker (inline fallback also failed): ${inlineError instanceof Error ? inlineError.message : String(inlineError)}`,
272
387
  );
@@ -275,7 +390,14 @@ export async function acquireTab(
275
390
  }
276
391
  }
277
392
 
393
+ afterWorkerInitializationForTest?.(name);
394
+ if (opts.requireVacantName && tabs.has(name)) {
395
+ await worker.terminate().catch(() => undefined);
396
+ if (replacementHold) await releaseBrowser(browser, { kill: false });
397
+ throw new ToolError(`Tab ${JSON.stringify(name)} was replaced during recovery.`);
398
+ }
278
399
  holdBrowser(browser);
400
+ if (replacementHold) await releaseBrowser(browser, { kill: false });
279
401
  const tab: TabSession = {
280
402
  name,
281
403
  browser,
@@ -288,8 +410,10 @@ export async function acquireTab(
288
410
  kindTag: browser.kind.kind,
289
411
  ownerId: opts.ownerId,
290
412
  lastUsedAt: Date.now(),
413
+ recoveryOpts: freezeRecoveryOptions(opts),
291
414
  };
292
415
  worker.onMessage(msg => handleTabMessage(tab, msg));
416
+ worker.onError(error => markTabDead(tab, error));
293
417
  tabs.set(name, tab);
294
418
  return { tab, created: true };
295
419
  }
@@ -307,8 +431,9 @@ async function runInTabWithSnapshot(
307
431
  opts: { code: string; timeoutMs: number; signal?: AbortSignal; session?: ToolSession },
308
432
  snapshot: SessionSnapshot,
309
433
  ): Promise<RunResultOk> {
310
- const tab = tabs.get(name);
311
- if (!tab || tab.state === "dead") throw new ToolError(`Tab ${JSON.stringify(name)} is not alive. Reopen it.`);
434
+ let tab = tabs.get(name);
435
+ if (tab?.state === "dead") tab = await recoverDeadTab(name, opts.session);
436
+ if (tab?.state !== "alive") throw new ToolError(`Tab ${JSON.stringify(name)} is not alive. Reopen it.`);
312
437
  if (tab.pending.size > 0) throw new ToolError(`Tab ${JSON.stringify(name)} is busy`);
313
438
  tab.lastUsedAt = Date.now();
314
439
  const id = Snowflake.next();
@@ -340,6 +465,82 @@ async function runInTabWithSnapshot(
340
465
  tab.pending.delete(id);
341
466
  }
342
467
  }
468
+ function markTabDead(tab: TabSession, error: Error): void {
469
+ if (tabs.get(tab.name) !== tab || tab.state !== "alive" || tab.releasing) return;
470
+ tab.state = "dead";
471
+ const closeError = new ToolError(`Browser tab worker stopped: ${error.message}`);
472
+ for (const pending of tab.pending.values()) pending.reject(closeError);
473
+ tab.pending.clear();
474
+ registerDeadTabRecovery(tab.name, tab.ownerId, Object.freeze({ tab, browser: tab.browser, opts: tab.recoveryOpts }));
475
+ }
476
+ function freezeRecoveryOptions(opts: AcquireTabOptions): AcquireTabOptions {
477
+ return Object.freeze({
478
+ ...opts,
479
+ signal: undefined,
480
+ viewport: opts.viewport ? Object.freeze({ ...opts.viewport }) : undefined,
481
+ });
482
+ }
483
+
484
+ async function recoverDeadTab(name: string, session: ToolSession | undefined): Promise<TabSession | undefined> {
485
+ const ownerId = session?.getSessionId?.() ?? undefined;
486
+ const existing = recoveryPromiseForOwner(name, ownerId);
487
+ if (existing) return await existing;
488
+ if (recoveringTabs.has(name)) return undefined;
489
+ const peeked = peekDeadTabRecovery<DeadTabDescriptor>(name, ownerId);
490
+ if (peeked.status !== "consumed" || !peeked.descriptor) return undefined;
491
+ const descriptor = peeked.descriptor;
492
+ const consumed = consumeDeadTabRecovery<DeadTabDescriptor>(name, ownerId);
493
+ if (consumed.status === "owner_mismatch") return undefined;
494
+ if (consumed.status === "expired_or_missing") {
495
+ await releaseDeadTabForRecovery(name, descriptor.tab, ownerId);
496
+ return undefined;
497
+ }
498
+ const recovery = withTemporaryBrowserHold(descriptor.browser, async (): Promise<TabSession> => {
499
+ if (!(await releaseDeadTabForRecovery(name, descriptor.tab, ownerId, true))) {
500
+ throw new ToolError(`Tab ${JSON.stringify(name)} is not alive. Reopen it.`);
501
+ }
502
+ try {
503
+ return (
504
+ await acquireTab(name, descriptor.browser, {
505
+ ...descriptor.opts,
506
+ recoveryTargetId: descriptor.tab.targetId,
507
+ requireVacantName: true,
508
+ })
509
+ ).tab;
510
+ } catch (error) {
511
+ if (descriptor.tab.kindTag === "headless") await closeOrphanTarget(descriptor.tab).catch(() => undefined);
512
+ throw error;
513
+ }
514
+ });
515
+ recoveringTabs.set(name, { ownerId, promise: recovery });
516
+ try {
517
+ return await recovery;
518
+ } finally {
519
+ if (recoveringTabs.get(name)?.promise === recovery) recoveringTabs.delete(name);
520
+ }
521
+ }
522
+
523
+ /**
524
+ * Claim and release exactly the dead tab that produced a recovery descriptor. Every
525
+ * predicate and the release election run synchronously before teardown can await.
526
+ */
527
+ export async function releaseDeadTabForRecovery(
528
+ name: string,
529
+ expected: TabSession,
530
+ ownerId: string | undefined,
531
+ preserveHeadlessTarget = false,
532
+ ): Promise<boolean> {
533
+ if (
534
+ tabs.get(name) !== expected ||
535
+ expected.state !== "dead" ||
536
+ expected.ownerId !== ownerId ||
537
+ !beginRelease(expected)
538
+ ) {
539
+ return false;
540
+ }
541
+ await releaseClaimedTab(expected, { kill: false }, false, preserveHeadlessTarget);
542
+ return true;
543
+ }
343
544
 
344
545
  export async function releaseTab(name: string, opts: ReleaseTabOptions = {}): Promise<boolean> {
345
546
  const tab = tabs.get(name);
@@ -351,9 +552,19 @@ export async function releaseTab(name: string, opts: ReleaseTabOptions = {}): Pr
351
552
  logger.debug("releaseTab: already releasing", { name });
352
553
  return false;
353
554
  }
354
- const wasAlive = tab.state === "alive";
555
+ await releaseClaimedTab(tab, opts, tab.state === "alive");
556
+ return true;
557
+ }
558
+
559
+ async function releaseClaimedTab(
560
+ tab: TabSession,
561
+ opts: ReleaseTabOptions,
562
+ wasAlive: boolean,
563
+ preserveHeadlessTarget = false,
564
+ ): Promise<void> {
355
565
  tab.state = "dead";
356
- const closeError = new ToolError(`Tab ${JSON.stringify(name)} was closed`);
566
+ discardDeadTabRecovery(tab.name);
567
+ const closeError = new ToolError(`Tab ${JSON.stringify(tab.name)} was closed`);
357
568
  for (const [id, pending] of tab.pending) {
358
569
  try {
359
570
  tab.worker.send({ type: "abort", id });
@@ -371,19 +582,29 @@ export async function releaseTab(name: string, opts: ReleaseTabOptions = {}): Pr
371
582
  forced = true;
372
583
  }
373
584
  }
374
- await awaitWithinBudget(tab.worker.terminate(), remainingBudget(deadlineAt), "worker.terminate");
375
- if (forced && tab.kindTag === "headless") {
376
- await awaitWithinBudget(closeOrphanTarget(tab), remainingBudget(deadlineAt), "closeOrphanTarget");
585
+ try {
586
+ await awaitWithinBudget(
587
+ Promise.resolve().then(() => tab.worker.terminate()),
588
+ remainingBudget(deadlineAt),
589
+ "worker.terminate",
590
+ );
591
+ if (!preserveHeadlessTarget && (forced || !wasAlive) && tab.kindTag === "headless") {
592
+ await awaitWithinBudget(
593
+ Promise.resolve().then(() => closeOrphanTarget(tab)),
594
+ remainingBudget(deadlineAt),
595
+ "closeOrphanTarget",
596
+ );
597
+ }
598
+ } finally {
599
+ await awaitWithinBudget(
600
+ Promise.resolve().then(() => releaseBrowser(tab.browser, { kill: opts.kill ?? false })),
601
+ remainingBudget(deadlineAt),
602
+ "releaseBrowser",
603
+ );
604
+ // Only delete if the map still holds THIS tab: a same-name reacquire during our async
605
+ // teardown may have installed a fresh tab that we must not evict.
606
+ if (tabs.get(tab.name) === tab) tabs.delete(tab.name);
377
607
  }
378
- await awaitWithinBudget(
379
- releaseBrowser(tab.browser, { kill: opts.kill ?? false }),
380
- remainingBudget(deadlineAt),
381
- "releaseBrowser",
382
- );
383
- // Only delete if the map still holds THIS tab: a same-name reacquire during our async
384
- // teardown may have installed a fresh tab that we must not evict.
385
- if (tabs.get(name) === tab) tabs.delete(name);
386
- return true;
387
608
  }
388
609
 
389
610
  export async function releaseAllTabs(opts: ReleaseTabOptions = {}): Promise<number> {
@@ -404,14 +625,20 @@ export async function releaseTabsForOwner(
404
625
  opts: ReleaseTabOptions = {},
405
626
  ): Promise<number> {
406
627
  if (!ownerId) return 0;
407
- const names = [...tabs.entries()].filter(([, tab]) => tab.ownerId === ownerId).map(([name]) => name);
628
+ const ownedTabs = [...tabs.values()].filter(tab => tab.ownerId === ownerId);
408
629
  let count = 0;
409
- for (const name of names) {
410
- if (await releaseTab(name, opts)) count++;
630
+ for (const tab of ownedTabs) {
631
+ if (await releaseOwnedTab(tab, ownerId, opts)) count++;
411
632
  }
412
633
  return count;
413
634
  }
414
635
 
636
+ async function releaseOwnedTab(expected: TabSession, ownerId: string, opts: ReleaseTabOptions): Promise<boolean> {
637
+ if (tabs.get(expected.name) !== expected || expected.ownerId !== ownerId || !beginRelease(expected)) return false;
638
+ await releaseClaimedTab(expected, opts, expected.state === "alive");
639
+ return true;
640
+ }
641
+
415
642
  export async function dropHeadlessTabs(): Promise<void> {
416
643
  const names = [...tabs.values()].filter(tab => tab.kindTag === "headless").map(tab => tab.name);
417
644
  for (const name of names) await releaseTab(name);
@@ -421,6 +648,15 @@ async function buildInitPayload(browser: BrowserHandle, opts: AcquireTabOptions)
421
648
  const safeDir = getPuppeteerDir();
422
649
  const browserWSEndpoint = browser.browser.wsEndpoint();
423
650
  if (!browserWSEndpoint) throw new ToolError("Browser websocket endpoint is unavailable");
651
+ if (opts.recoveryTargetId) {
652
+ return {
653
+ mode: "attach",
654
+ browserWSEndpoint,
655
+ safeDir,
656
+ targetId: opts.recoveryTargetId,
657
+ dialogs: opts.dialogs,
658
+ };
659
+ }
424
660
  if (browser.kind.kind === "headless") {
425
661
  return {
426
662
  mode: "headless",
@@ -444,6 +680,12 @@ async function buildInitPayload(browser: BrowserHandle, opts: AcquireTabOptions)
444
680
  dialogs: opts.dialogs,
445
681
  };
446
682
  }
683
+ export async function buildInitPayloadForTest(
684
+ browser: BrowserHandle,
685
+ opts: AcquireTabOptions,
686
+ ): Promise<WorkerInitPayload> {
687
+ return await buildInitPayload(browser, opts);
688
+ }
447
689
 
448
690
  function handleTabMessage(tab: TabSession, msg: WorkerOutbound): void {
449
691
  if (msg.type === "result") {
@@ -531,13 +773,17 @@ async function forceKillTab(name: string, reason: string): Promise<void> {
531
773
  if (!tab) return;
532
774
  if (!beginRelease(tab)) return;
533
775
  tab.state = "dead";
776
+ discardDeadTabRecovery(name);
534
777
  const error = new ToolError(reason);
535
778
  for (const pending of tab.pending.values()) pending.reject(error);
536
779
  tab.pending.clear();
537
- await tab.worker.terminate().catch(() => undefined);
538
- if (tab.kindTag === "headless") await closeOrphanTarget(tab);
539
- await releaseBrowser(tab.browser, { kill: false });
540
- if (tabs.get(name) === tab) tabs.delete(name);
780
+ try {
781
+ await tab.worker.terminate().catch(() => undefined);
782
+ if (tab.kindTag === "headless") await closeOrphanTarget(tab).catch(() => undefined);
783
+ } finally {
784
+ await releaseBrowser(tab.browser, { kill: false }).catch(() => undefined);
785
+ if (tabs.get(name) === tab) tabs.delete(name);
786
+ }
541
787
  }
542
788
 
543
789
  async function closeOrphanTarget(tab: TabSession): Promise<void> {
@@ -6,7 +6,7 @@ import type { ImageContent } from "@gajae-code/ai";
6
6
  import { prompt } from "@gajae-code/utils";
7
7
  import * as z from "zod/v4";
8
8
  import computerDescription from "../prompts/tools/computer.md" with { type: "text" };
9
- import { resizeImage } from "../utils/image-resize";
9
+ import { formatDimensionNote, resizeImage } from "../utils/image-resize";
10
10
  import { markScreenshotFallbackDirCreatedForGc } from "./computer-gc";
11
11
  import type { ToolSession } from "./index";
12
12
  import type { OutputMeta } from "./output-meta";
@@ -701,11 +701,16 @@ async function inlineImageContentFromNativeResult(
701
701
  details: ComputerToolDetails,
702
702
  session: ToolSession,
703
703
  ): Promise<ImageContent | undefined> {
704
+ // Anthropic rejects requests carrying more than 20 images when any image
705
+ // exceeds 2000px per dimension ("many-image requests"), so gating on bytes
706
+ // alone is not enough: an in-budget full-resolution capture can still brick
707
+ // the session once enough screenshots accumulate in history. Always route
708
+ // through resizeImage (its fast path returns already-small images
709
+ // untouched) and tell the model how to map coordinates back to the native
710
+ // screenshot frame when the inline image was scaled.
704
711
  const image = fullResolutionImageContentFromNativeResult(value);
705
712
  if (!image) return undefined;
706
713
  const maxBytes = getInlineScreenshotMaxBytes(session);
707
- const originalBytes = Buffer.byteLength(image.data, "base64");
708
- if (originalBytes <= maxBytes) return image;
709
714
 
710
715
  try {
711
716
  const resized = await resizeImage(image, {
@@ -715,6 +720,8 @@ async function inlineImageContentFromNativeResult(
715
720
  jpegQuality: COMPUTER_INLINE_SCREENSHOT_JPEG_QUALITY,
716
721
  });
717
722
  if (resized.buffer.length <= maxBytes) {
723
+ const note = formatDimensionNote(resized);
724
+ if (note) details.message = `${details.message} ${note}`;
718
725
  return { type: "image", data: resized.data, mimeType: resized.mimeType };
719
726
  }
720
727
  } catch {
package/src/tools/cron.ts CHANGED
@@ -58,6 +58,8 @@ export interface CronJobSnapshot {
58
58
  createdAt: number;
59
59
  expiresAt?: number;
60
60
  nextFireAt?: number;
61
+ /** True while this owner-scoped schedule's delivery is in progress. */
62
+ firing?: boolean;
61
63
  humanSchedule: string;
62
64
  ownerId?: string;
63
65
  }
@@ -444,7 +446,7 @@ function formatCronFireContent(snapshot: CronJobSnapshot): string {
444
446
  return `<task-notification>\nScheduled task ${snapshot.id} fired (${snapshot.humanSchedule}).\n\n${snapshot.prompt}\n</task-notification>`;
445
447
  }
446
448
 
447
- function deliverCronFire(record: CronScheduleRecord): void {
449
+ function deliverCronFire(record: CronScheduleRecord): Promise<void> | undefined {
448
450
  const content = formatCronFireContent(record.snapshot);
449
451
  const details = {
450
452
  id: record.snapshot.id,
@@ -455,16 +457,9 @@ function deliverCronFire(record: CronScheduleRecord): void {
455
457
  { customType: "cron-fire", content, display: false, attribution: "agent", details },
456
458
  { triggerTurn: true, deliverAs: "followUp" },
457
459
  );
458
- if (sendPromise) {
459
- void sendPromise.catch(error => {
460
- logger.warn("Cron fire delivery failed", {
461
- id: record.snapshot.id,
462
- error: error instanceof Error ? error.message : String(error),
463
- });
464
- });
465
- return;
466
- }
460
+ if (sendPromise) return sendPromise;
467
461
  record.session.steer?.({ customType: "cron-fire", content, details });
462
+ return undefined;
468
463
  }
469
464
 
470
465
  function scheduleRecord(ownerId: string | undefined, state: OwnerScheduleState, record: CronScheduleRecord): void {
@@ -509,16 +504,35 @@ function fireRecord(ownerId: string | undefined, state: OwnerScheduleState, id:
509
504
  deleteRecord(ownerId, id);
510
505
  return;
511
506
  }
507
+ record.snapshot.firing = true;
508
+ notifyCronChange();
509
+ const settle = () => {
510
+ if (record.disposed) return;
511
+ record.snapshot.firing = false;
512
+ notifyCronChange();
513
+ if (!record.snapshot.recurring) {
514
+ deleteRecord(ownerId, id);
515
+ return;
516
+ }
517
+ scheduleRecord(ownerId, state, record);
518
+ };
512
519
  try {
513
- deliverCronFire(record);
520
+ const delivery = deliverCronFire(record);
521
+ if (delivery) {
522
+ void delivery.then(settle, error => {
523
+ logger.warn("Cron fire delivery failed", {
524
+ id,
525
+ error: error instanceof Error ? error.message : String(error),
526
+ });
527
+ settle();
528
+ });
529
+ } else {
530
+ settle();
531
+ }
514
532
  } catch (error) {
515
533
  logger.warn("Cron fire delivery failed", { id, error: error instanceof Error ? error.message : String(error) });
534
+ settle();
516
535
  }
517
- if (!record.snapshot.recurring) {
518
- deleteRecord(ownerId, id);
519
- return;
520
- }
521
- scheduleRecord(ownerId, state, record);
522
536
  }
523
537
 
524
538
  const CRON_ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
package/src/tools/eval.ts CHANGED
@@ -183,8 +183,7 @@ function timeoutSecondsFromMs(timeoutMs: number): number {
183
183
  }
184
184
 
185
185
  async function resolveBackend(session: ToolSession, language: EvalLanguage): Promise<ResolvedBackend> {
186
- const allowPy = (session.settings.get("eval.py") as boolean | undefined) ?? true;
187
- const allowJs = (session.settings.get("eval.js") as boolean | undefined) ?? true;
186
+ const { python: allowPy, js: allowJs } = resolveEvalBackends(session);
188
187
 
189
188
  if (language === "python") {
190
189
  if (!allowPy) throw new ToolError("Python backend is disabled (eval.py = false).");
@@ -1292,20 +1292,20 @@ async function materializeReadUrlCacheEntry(
1292
1292
  session: ToolSession,
1293
1293
  entry: ReadUrlCacheEntry,
1294
1294
  ): Promise<ReadUrlCacheEntry | null> {
1295
+ if (entry.output.length > 0) return entry;
1295
1296
  if (entry.artifactId) {
1296
1297
  const artifactOutput = await readArtifactOutput(session, entry.artifactId);
1297
1298
  if (artifactOutput !== null) {
1298
1299
  return { ...entry, output: artifactOutput };
1299
1300
  }
1300
1301
  }
1301
-
1302
- return entry.output.length > 0 ? entry : null;
1302
+ return null;
1303
1303
  }
1304
1304
 
1305
1305
  async function persistReadUrlArtifact(session: ToolSession, output: string): Promise<string | undefined> {
1306
1306
  const { path: artifactPath, id } = (await session.allocateOutputArtifact?.("read")) ?? {};
1307
1307
  if (!artifactPath) return undefined;
1308
- await Bun.write(artifactPath, output);
1308
+ await Bun.write(artifactPath, wrapUntrustedContent(output));
1309
1309
  return id;
1310
1310
  }
1311
1311
 
@@ -1379,6 +1379,13 @@ export async function loadReadUrlCacheEntry(
1379
1379
  return fresh;
1380
1380
  }
1381
1381
 
1382
+ const UNTRUSTED_CONTENT_OPEN = "<untrusted-content>";
1383
+ const UNTRUSTED_CONTENT_CLOSE = "</untrusted-content>";
1384
+
1385
+ export function wrapUntrustedContent(content: string): string {
1386
+ return `${UNTRUSTED_CONTENT_OPEN}\n${content.replace(/<\/untrusted-content>/gi, "&lt;/untrusted-content>")}\n${UNTRUSTED_CONTENT_CLOSE}`;
1387
+ }
1388
+
1382
1389
  function buildUrlReadOutput(result: FetchRenderResult, content: string): string {
1383
1390
  let output = "";
1384
1391
  output += `URL: ${result.finalUrl}\n`;
@@ -1413,7 +1420,7 @@ export async function executeReadUrl(
1413
1420
  truncated: Boolean(cacheEntry.details.truncated || needsArtifact),
1414
1421
  };
1415
1422
 
1416
- const contentBlocks: Array<TextContent | ImageContent> = [{ type: "text", text: output }];
1423
+ const contentBlocks: Array<TextContent | ImageContent> = [{ type: "text", text: wrapUntrustedContent(output) }];
1417
1424
  if (cacheEntry.image) {
1418
1425
  contentBlocks.push({ type: "image", data: cacheEntry.image.data, mimeType: cacheEntry.image.mimeType });
1419
1426
  }