@0xmaxma/claude-gateway 2.0.9 → 2.0.11

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/README.md +16 -1
  2. package/config.template.json +6 -4
  3. package/dist/agent/runner.d.ts +126 -11
  4. package/dist/agent/runner.d.ts.map +1 -1
  5. package/dist/agent/runner.js +102 -10
  6. package/dist/agent/runner.js.map +1 -1
  7. package/dist/agent/skill-learning/reviewer.d.ts.map +1 -1
  8. package/dist/agent/skill-learning/reviewer.js +1 -2
  9. package/dist/agent/skill-learning/reviewer.js.map +1 -1
  10. package/dist/api/browser-bindings-router.d.ts +6 -0
  11. package/dist/api/browser-bindings-router.d.ts.map +1 -0
  12. package/dist/api/browser-bindings-router.js +150 -0
  13. package/dist/api/browser-bindings-router.js.map +1 -0
  14. package/dist/api/capabilities.d.ts +53 -0
  15. package/dist/api/capabilities.d.ts.map +1 -0
  16. package/dist/api/capabilities.js +66 -0
  17. package/dist/api/capabilities.js.map +1 -0
  18. package/dist/api/connectors-router.js +1 -1
  19. package/dist/api/connectors-router.js.map +1 -1
  20. package/dist/api/gateway-router.d.ts.map +1 -1
  21. package/dist/api/gateway-router.js +38 -14
  22. package/dist/api/gateway-router.js.map +1 -1
  23. package/dist/api/gateway-version.d.ts +9 -0
  24. package/dist/api/gateway-version.d.ts.map +1 -0
  25. package/dist/api/gateway-version.js +57 -0
  26. package/dist/api/gateway-version.js.map +1 -0
  27. package/dist/api/jev-router.d.ts +8 -0
  28. package/dist/api/jev-router.d.ts.map +1 -0
  29. package/dist/api/jev-router.js +128 -0
  30. package/dist/api/jev-router.js.map +1 -0
  31. package/dist/api/router.d.ts.map +1 -1
  32. package/dist/api/router.js +53 -6
  33. package/dist/api/router.js.map +1 -1
  34. package/dist/api/voice-router.d.ts.map +1 -1
  35. package/dist/api/voice-router.js +42 -3
  36. package/dist/api/voice-router.js.map +1 -1
  37. package/dist/automation/browser-command.d.ts +79 -0
  38. package/dist/automation/browser-command.d.ts.map +1 -0
  39. package/dist/automation/browser-command.js +118 -0
  40. package/dist/automation/browser-command.js.map +1 -0
  41. package/dist/automation/browser-outcome.d.ts +3 -0
  42. package/dist/automation/browser-outcome.d.ts.map +1 -0
  43. package/dist/automation/browser-outcome.js +78 -0
  44. package/dist/automation/browser-outcome.js.map +1 -0
  45. package/dist/automation/browser-steps.d.ts +33 -0
  46. package/dist/automation/browser-steps.d.ts.map +1 -0
  47. package/dist/automation/browser-steps.js +293 -0
  48. package/dist/automation/browser-steps.js.map +1 -0
  49. package/dist/automation/browser-trace.d.ts +60 -0
  50. package/dist/automation/browser-trace.d.ts.map +1 -0
  51. package/dist/automation/browser-trace.js +24 -0
  52. package/dist/automation/browser-trace.js.map +1 -0
  53. package/dist/automation/browser-use.d.ts +210 -0
  54. package/dist/automation/browser-use.d.ts.map +1 -0
  55. package/dist/automation/browser-use.js +1271 -0
  56. package/dist/automation/browser-use.js.map +1 -0
  57. package/dist/automation/command-speech.d.ts +33 -0
  58. package/dist/automation/command-speech.d.ts.map +1 -0
  59. package/dist/automation/command-speech.js +117 -0
  60. package/dist/automation/command-speech.js.map +1 -0
  61. package/dist/automation/command-steps.d.ts +35 -0
  62. package/dist/automation/command-steps.d.ts.map +1 -0
  63. package/dist/automation/command-steps.js +155 -0
  64. package/dist/automation/command-steps.js.map +1 -0
  65. package/dist/automation/computer-choice-ids.d.ts +7 -0
  66. package/dist/automation/computer-choice-ids.d.ts.map +1 -0
  67. package/dist/automation/computer-choice-ids.js +24 -0
  68. package/dist/automation/computer-choice-ids.js.map +1 -0
  69. package/dist/automation/computer-command.d.ts +126 -0
  70. package/dist/automation/computer-command.d.ts.map +1 -0
  71. package/dist/automation/computer-command.js +313 -0
  72. package/dist/automation/computer-command.js.map +1 -0
  73. package/dist/automation/computer-outcome.d.ts +8 -0
  74. package/dist/automation/computer-outcome.d.ts.map +1 -0
  75. package/dist/automation/computer-outcome.js +82 -0
  76. package/dist/automation/computer-outcome.js.map +1 -0
  77. package/dist/automation/computer-policy.d.ts +84 -0
  78. package/dist/automation/computer-policy.d.ts.map +1 -0
  79. package/dist/automation/computer-policy.js +56 -0
  80. package/dist/automation/computer-policy.js.map +1 -0
  81. package/dist/automation/computer-safety.d.ts +27 -0
  82. package/dist/automation/computer-safety.d.ts.map +1 -0
  83. package/dist/automation/computer-safety.js +47 -0
  84. package/dist/automation/computer-safety.js.map +1 -0
  85. package/dist/automation/computer-steps.d.ts +20 -0
  86. package/dist/automation/computer-steps.d.ts.map +1 -0
  87. package/dist/automation/computer-steps.js +251 -0
  88. package/dist/automation/computer-steps.js.map +1 -0
  89. package/dist/automation/computer-use.d.ts +315 -0
  90. package/dist/automation/computer-use.d.ts.map +1 -0
  91. package/dist/automation/computer-use.js +864 -0
  92. package/dist/automation/computer-use.js.map +1 -0
  93. package/dist/automation/direct-command.d.ts +51 -0
  94. package/dist/automation/direct-command.d.ts.map +1 -0
  95. package/dist/automation/direct-command.js +152 -0
  96. package/dist/automation/direct-command.js.map +1 -0
  97. package/dist/automation/interrupt.d.ts +4 -0
  98. package/dist/automation/interrupt.d.ts.map +1 -0
  99. package/dist/automation/interrupt.js +26 -0
  100. package/dist/automation/interrupt.js.map +1 -0
  101. package/dist/build-provenance.json +4 -4
  102. package/dist/child-env-exclusions.d.ts +4 -0
  103. package/dist/child-env-exclusions.d.ts.map +1 -0
  104. package/dist/child-env-exclusions.js +23 -0
  105. package/dist/child-env-exclusions.js.map +1 -0
  106. package/dist/cli/startup-diagnostics.d.ts.map +1 -1
  107. package/dist/cli/startup-diagnostics.js +19 -0
  108. package/dist/cli/startup-diagnostics.js.map +1 -1
  109. package/dist/config/loader.d.ts.map +1 -1
  110. package/dist/config/loader.js +4 -0
  111. package/dist/config/loader.js.map +1 -1
  112. package/dist/config/reload-policy.d.ts.map +1 -1
  113. package/dist/config/reload-policy.js +2 -1
  114. package/dist/config/reload-policy.js.map +1 -1
  115. package/dist/jev/automatic-browser-bindings.d.ts +15 -0
  116. package/dist/jev/automatic-browser-bindings.d.ts.map +1 -0
  117. package/dist/jev/automatic-browser-bindings.js +115 -0
  118. package/dist/jev/automatic-browser-bindings.js.map +1 -0
  119. package/dist/jev/browser-connector.d.ts +29 -0
  120. package/dist/jev/browser-connector.d.ts.map +1 -0
  121. package/dist/jev/browser-connector.js +347 -0
  122. package/dist/jev/browser-connector.js.map +1 -0
  123. package/dist/jev/browser-consent.d.ts +6 -0
  124. package/dist/jev/browser-consent.d.ts.map +1 -0
  125. package/dist/jev/browser-consent.js +50 -0
  126. package/dist/jev/browser-consent.js.map +1 -0
  127. package/dist/jev/browser-contract.d.ts +215 -0
  128. package/dist/jev/browser-contract.d.ts.map +1 -0
  129. package/dist/jev/browser-contract.js +10 -0
  130. package/dist/jev/browser-contract.js.map +1 -0
  131. package/dist/jev/browser-evidence-summary.d.ts +73 -0
  132. package/dist/jev/browser-evidence-summary.d.ts.map +1 -0
  133. package/dist/jev/browser-evidence-summary.js +22 -0
  134. package/dist/jev/browser-evidence-summary.js.map +1 -0
  135. package/dist/jev/browser-module-import.d.ts +3 -0
  136. package/dist/jev/browser-module-import.d.ts.map +1 -0
  137. package/dist/jev/browser-module-import.js +6 -0
  138. package/dist/jev/browser-module-import.js.map +1 -0
  139. package/dist/jev/child-env.d.ts +11 -0
  140. package/dist/jev/child-env.d.ts.map +1 -0
  141. package/dist/jev/child-env.js +60 -0
  142. package/dist/jev/child-env.js.map +1 -0
  143. package/dist/jev/computer-connector.d.ts +34 -0
  144. package/dist/jev/computer-connector.d.ts.map +1 -0
  145. package/dist/jev/computer-connector.js +82 -0
  146. package/dist/jev/computer-connector.js.map +1 -0
  147. package/dist/jev/computer-inputs.d.ts +41 -0
  148. package/dist/jev/computer-inputs.d.ts.map +1 -0
  149. package/dist/jev/computer-inputs.js +7 -0
  150. package/dist/jev/computer-inputs.js.map +1 -0
  151. package/dist/jev/index.d.ts +6 -0
  152. package/dist/jev/index.d.ts.map +1 -0
  153. package/dist/jev/index.js +25 -0
  154. package/dist/jev/index.js.map +1 -0
  155. package/dist/jev/relay-grants.d.ts +9 -0
  156. package/dist/jev/relay-grants.d.ts.map +1 -0
  157. package/dist/jev/relay-grants.js +49 -0
  158. package/dist/jev/relay-grants.js.map +1 -0
  159. package/dist/jev/service.d.ts +27 -0
  160. package/dist/jev/service.d.ts.map +1 -0
  161. package/dist/jev/service.js +264 -0
  162. package/dist/jev/service.js.map +1 -0
  163. package/dist/jev/thinking-provider.d.ts +7 -0
  164. package/dist/jev/thinking-provider.d.ts.map +1 -0
  165. package/dist/jev/thinking-provider.js +25 -0
  166. package/dist/jev/thinking-provider.js.map +1 -0
  167. package/dist/jev/types.d.ts +124 -0
  168. package/dist/jev/types.d.ts.map +1 -0
  169. package/dist/jev/types.js +13 -0
  170. package/dist/jev/types.js.map +1 -0
  171. package/dist/jev/validation.d.ts +45 -0
  172. package/dist/jev/validation.d.ts.map +1 -0
  173. package/dist/jev/validation.js +179 -0
  174. package/dist/jev/validation.js.map +1 -0
  175. package/dist/load-dotenv.d.ts.map +1 -1
  176. package/dist/load-dotenv.js +5 -1
  177. package/dist/load-dotenv.js.map +1 -1
  178. package/dist/orchestration/agent-tool-schemas.d.ts.map +1 -1
  179. package/dist/orchestration/agent-tool-schemas.js +12 -7
  180. package/dist/orchestration/agent-tool-schemas.js.map +1 -1
  181. package/dist/orchestration/background-work.d.ts +1 -0
  182. package/dist/orchestration/background-work.d.ts.map +1 -1
  183. package/dist/orchestration/background-work.js +1 -0
  184. package/dist/orchestration/background-work.js.map +1 -1
  185. package/dist/orchestration/bridge.d.ts +4 -0
  186. package/dist/orchestration/bridge.d.ts.map +1 -1
  187. package/dist/orchestration/bridge.js +227 -41
  188. package/dist/orchestration/bridge.js.map +1 -1
  189. package/dist/orchestration/browser-fields.d.ts +25 -0
  190. package/dist/orchestration/browser-fields.d.ts.map +1 -0
  191. package/dist/orchestration/browser-fields.js +23 -0
  192. package/dist/orchestration/browser-fields.js.map +1 -0
  193. package/dist/orchestration/browser-routing.d.ts +1 -1
  194. package/dist/orchestration/browser-routing.d.ts.map +1 -1
  195. package/dist/orchestration/browser-routing.js +7 -3
  196. package/dist/orchestration/browser-routing.js.map +1 -1
  197. package/dist/orchestration/capabilities.d.ts +5 -2
  198. package/dist/orchestration/capabilities.d.ts.map +1 -1
  199. package/dist/orchestration/capabilities.js +24 -24
  200. package/dist/orchestration/capabilities.js.map +1 -1
  201. package/dist/orchestration/channel-controls.js +1 -1
  202. package/dist/orchestration/channel-controls.js.map +1 -1
  203. package/dist/orchestration/cli-skills.d.ts +1 -1
  204. package/dist/orchestration/cli-skills.d.ts.map +1 -1
  205. package/dist/orchestration/cli-skills.js +16 -10
  206. package/dist/orchestration/cli-skills.js.map +1 -1
  207. package/dist/orchestration/computer-end-screenshot.d.ts +11 -0
  208. package/dist/orchestration/computer-end-screenshot.d.ts.map +1 -0
  209. package/dist/orchestration/computer-end-screenshot.js +37 -0
  210. package/dist/orchestration/computer-end-screenshot.js.map +1 -0
  211. package/dist/orchestration/computer-prompt.d.ts +6 -0
  212. package/dist/orchestration/computer-prompt.d.ts.map +1 -0
  213. package/dist/orchestration/computer-prompt.js +24 -0
  214. package/dist/orchestration/computer-prompt.js.map +1 -0
  215. package/dist/orchestration/config.d.ts +17 -0
  216. package/dist/orchestration/config.d.ts.map +1 -1
  217. package/dist/orchestration/config.js +10 -4
  218. package/dist/orchestration/config.js.map +1 -1
  219. package/dist/orchestration/container-tool-schemas.d.ts +47 -3
  220. package/dist/orchestration/container-tool-schemas.d.ts.map +1 -1
  221. package/dist/orchestration/container-tool-schemas.js +41 -6
  222. package/dist/orchestration/container-tool-schemas.js.map +1 -1
  223. package/dist/orchestration/container.d.ts +4 -1
  224. package/dist/orchestration/container.d.ts.map +1 -1
  225. package/dist/orchestration/container.js +48 -29
  226. package/dist/orchestration/container.js.map +1 -1
  227. package/dist/orchestration/context-footprint.d.ts +1 -1
  228. package/dist/orchestration/context-footprint.d.ts.map +1 -1
  229. package/dist/orchestration/context-footprint.js +5 -5
  230. package/dist/orchestration/context-footprint.js.map +1 -1
  231. package/dist/orchestration/control-notification.d.ts +6 -0
  232. package/dist/orchestration/control-notification.d.ts.map +1 -0
  233. package/dist/orchestration/control-notification.js +23 -0
  234. package/dist/orchestration/control-notification.js.map +1 -0
  235. package/dist/orchestration/dashboard-reader-worker.js +2 -2
  236. package/dist/orchestration/dashboard-reader-worker.js.map +1 -1
  237. package/dist/orchestration/execution-observation.d.ts +3 -0
  238. package/dist/orchestration/execution-observation.d.ts.map +1 -1
  239. package/dist/orchestration/execution-observation.js.map +1 -1
  240. package/dist/orchestration/gateway-tasks/browser.d.ts +59 -0
  241. package/dist/orchestration/gateway-tasks/browser.d.ts.map +1 -0
  242. package/dist/orchestration/gateway-tasks/browser.js +426 -0
  243. package/dist/orchestration/gateway-tasks/browser.js.map +1 -0
  244. package/dist/orchestration/gateway-tasks/computer-context.d.ts +6 -0
  245. package/dist/orchestration/gateway-tasks/computer-context.d.ts.map +1 -0
  246. package/dist/orchestration/gateway-tasks/computer-context.js +19 -0
  247. package/dist/orchestration/gateway-tasks/computer-context.js.map +1 -0
  248. package/dist/orchestration/gateway-tasks/computer.d.ts +123 -0
  249. package/dist/orchestration/gateway-tasks/computer.d.ts.map +1 -0
  250. package/dist/orchestration/gateway-tasks/computer.js +519 -0
  251. package/dist/orchestration/gateway-tasks/computer.js.map +1 -0
  252. package/dist/orchestration/gateway-tasks/controller.d.ts +40 -6
  253. package/dist/orchestration/gateway-tasks/controller.d.ts.map +1 -1
  254. package/dist/orchestration/gateway-tasks/controller.js +118 -12
  255. package/dist/orchestration/gateway-tasks/controller.js.map +1 -1
  256. package/dist/orchestration/gateway-tasks/safemode.d.ts.map +1 -1
  257. package/dist/orchestration/gateway-tasks/safemode.js +2 -1
  258. package/dist/orchestration/gateway-tasks/safemode.js.map +1 -1
  259. package/dist/orchestration/jev-gateway.d.ts +25 -0
  260. package/dist/orchestration/jev-gateway.d.ts.map +1 -0
  261. package/dist/orchestration/jev-gateway.js +103 -0
  262. package/dist/orchestration/jev-gateway.js.map +1 -0
  263. package/dist/orchestration/jev-tool.d.ts +45 -0
  264. package/dist/orchestration/jev-tool.d.ts.map +1 -0
  265. package/dist/orchestration/jev-tool.js +10 -0
  266. package/dist/orchestration/jev-tool.js.map +1 -0
  267. package/dist/orchestration/live-execution-input.d.ts +26 -0
  268. package/dist/orchestration/live-execution-input.d.ts.map +1 -0
  269. package/dist/orchestration/live-execution-input.js +70 -0
  270. package/dist/orchestration/live-execution-input.js.map +1 -0
  271. package/dist/orchestration/notification-mailbox.d.ts.map +1 -1
  272. package/dist/orchestration/notification-mailbox.js +5 -2
  273. package/dist/orchestration/notification-mailbox.js.map +1 -1
  274. package/dist/orchestration/process-activity.d.ts +4 -2
  275. package/dist/orchestration/process-activity.d.ts.map +1 -1
  276. package/dist/orchestration/process-activity.js +31 -42
  277. package/dist/orchestration/process-activity.js.map +1 -1
  278. package/dist/orchestration/process-platform.d.ts +72 -0
  279. package/dist/orchestration/process-platform.d.ts.map +1 -0
  280. package/dist/orchestration/process-platform.js +396 -0
  281. package/dist/orchestration/process-platform.js.map +1 -0
  282. package/dist/orchestration/process-supervisor.d.ts +31 -7
  283. package/dist/orchestration/process-supervisor.d.ts.map +1 -1
  284. package/dist/orchestration/process-supervisor.js +89 -61
  285. package/dist/orchestration/process-supervisor.js.map +1 -1
  286. package/dist/orchestration/process-turn.d.ts +8 -0
  287. package/dist/orchestration/process-turn.d.ts.map +1 -1
  288. package/dist/orchestration/process-turn.js +97 -13
  289. package/dist/orchestration/process-turn.js.map +1 -1
  290. package/dist/orchestration/progress-review.d.ts +1 -1
  291. package/dist/orchestration/progress-review.d.ts.map +1 -1
  292. package/dist/orchestration/progress-review.js +5 -1
  293. package/dist/orchestration/progress-review.js.map +1 -1
  294. package/dist/orchestration/report-fidelity.d.ts +31 -0
  295. package/dist/orchestration/report-fidelity.d.ts.map +1 -0
  296. package/dist/orchestration/report-fidelity.js +34 -0
  297. package/dist/orchestration/report-fidelity.js.map +1 -0
  298. package/dist/orchestration/response-errors.d.ts.map +1 -1
  299. package/dist/orchestration/response-errors.js +1 -0
  300. package/dist/orchestration/response-errors.js.map +1 -1
  301. package/dist/orchestration/runtime.d.ts +51 -1
  302. package/dist/orchestration/runtime.d.ts.map +1 -1
  303. package/dist/orchestration/runtime.js +404 -33
  304. package/dist/orchestration/runtime.js.map +1 -1
  305. package/dist/orchestration/skill-catalog-budget.d.ts +6 -0
  306. package/dist/orchestration/skill-catalog-budget.d.ts.map +1 -0
  307. package/dist/orchestration/skill-catalog-budget.js +9 -0
  308. package/dist/orchestration/skill-catalog-budget.js.map +1 -0
  309. package/dist/orchestration/skills.d.ts +4 -2
  310. package/dist/orchestration/skills.d.ts.map +1 -1
  311. package/dist/orchestration/skills.js +44 -3
  312. package/dist/orchestration/skills.js.map +1 -1
  313. package/dist/orchestration/speech.d.ts +1 -1
  314. package/dist/orchestration/speech.d.ts.map +1 -1
  315. package/dist/orchestration/speech.js +1 -1
  316. package/dist/orchestration/store.d.ts +1 -0
  317. package/dist/orchestration/store.d.ts.map +1 -1
  318. package/dist/orchestration/store.js +2 -0
  319. package/dist/orchestration/store.js.map +1 -1
  320. package/dist/orchestration/task-controls.d.ts +78 -8
  321. package/dist/orchestration/task-controls.d.ts.map +1 -1
  322. package/dist/orchestration/task-controls.js +6 -1
  323. package/dist/orchestration/task-controls.js.map +1 -1
  324. package/dist/orchestration/task-questions.js +2 -2
  325. package/dist/orchestration/task-questions.js.map +1 -1
  326. package/dist/orchestration/tasks/automation-session.d.ts +25 -0
  327. package/dist/orchestration/tasks/automation-session.d.ts.map +1 -0
  328. package/dist/orchestration/tasks/automation-session.js +68 -0
  329. package/dist/orchestration/tasks/automation-session.js.map +1 -0
  330. package/dist/orchestration/tasks/driver.d.ts.map +1 -1
  331. package/dist/orchestration/tasks/driver.js +7 -5
  332. package/dist/orchestration/tasks/driver.js.map +1 -1
  333. package/dist/orchestration/tasks/failure.d.ts +6 -0
  334. package/dist/orchestration/tasks/failure.d.ts.map +1 -1
  335. package/dist/orchestration/tasks/failure.js +26 -2
  336. package/dist/orchestration/tasks/failure.js.map +1 -1
  337. package/dist/orchestration/tasks/scheduler.d.ts +1 -1
  338. package/dist/orchestration/tasks/scheduler.d.ts.map +1 -1
  339. package/dist/orchestration/tasks/scheduler.js +3 -1
  340. package/dist/orchestration/tasks/scheduler.js.map +1 -1
  341. package/dist/orchestration/tasks/service.d.ts +68 -7
  342. package/dist/orchestration/tasks/service.d.ts.map +1 -1
  343. package/dist/orchestration/tasks/service.js +647 -45
  344. package/dist/orchestration/tasks/service.js.map +1 -1
  345. package/dist/orchestration/types.d.ts +90 -3
  346. package/dist/orchestration/types.d.ts.map +1 -1
  347. package/dist/orchestration/types.js.map +1 -1
  348. package/dist/safemode/native.d.ts.map +1 -1
  349. package/dist/safemode/native.js +8 -3
  350. package/dist/safemode/native.js.map +1 -1
  351. package/dist/session/claude-bin.d.ts +29 -0
  352. package/dist/session/claude-bin.d.ts.map +1 -1
  353. package/dist/session/claude-bin.js +63 -12
  354. package/dist/session/claude-bin.js.map +1 -1
  355. package/dist/session/codex-extension-discovery.d.ts.map +1 -1
  356. package/dist/session/codex-extension-discovery.js +4 -2
  357. package/dist/session/codex-extension-discovery.js.map +1 -1
  358. package/dist/session/codex-native-mcp-client.d.ts.map +1 -1
  359. package/dist/session/codex-native-mcp-client.js +2 -1
  360. package/dist/session/codex-native-mcp-client.js.map +1 -1
  361. package/dist/session/codex-process.d.ts +4 -0
  362. package/dist/session/codex-process.d.ts.map +1 -1
  363. package/dist/session/codex-process.js +16 -6
  364. package/dist/session/codex-process.js.map +1 -1
  365. package/dist/session/compactor.d.ts.map +1 -1
  366. package/dist/session/compactor.js +1 -2
  367. package/dist/session/compactor.js.map +1 -1
  368. package/dist/session/process.d.ts +18 -0
  369. package/dist/session/process.d.ts.map +1 -1
  370. package/dist/session/process.js +175 -67
  371. package/dist/session/process.js.map +1 -1
  372. package/dist/session/runtime-profile.d.ts +18 -0
  373. package/dist/session/runtime-profile.d.ts.map +1 -1
  374. package/dist/session/runtime-profile.js +23 -0
  375. package/dist/session/runtime-profile.js.map +1 -1
  376. package/dist/session/worker-extensions.d.ts.map +1 -1
  377. package/dist/session/worker-extensions.js +4 -3
  378. package/dist/session/worker-extensions.js.map +1 -1
  379. package/dist/shell/claude-pty-shell.js +5 -4
  380. package/dist/shell/claude-pty-shell.js.map +1 -1
  381. package/dist/types.d.ts +5 -0
  382. package/dist/types.d.ts.map +1 -1
  383. package/dist/ui/dashboard-client.d.ts.map +1 -1
  384. package/dist/ui/dashboard-client.js +33 -3
  385. package/dist/ui/dashboard-client.js.map +1 -1
  386. package/dist/ui/web-ui.js +1 -1
  387. package/dist/utils/paths.d.ts +8 -0
  388. package/dist/utils/paths.d.ts.map +1 -1
  389. package/dist/utils/paths.js +11 -0
  390. package/dist/utils/paths.js.map +1 -1
  391. package/lib/automation/browser-thinking.cjs +9 -0
  392. package/lib/automation/browser-thinking.d.cts +2 -0
  393. package/lib/automation/computer-thinking.cjs +10 -0
  394. package/lib/automation/computer-thinking.d.cts +2 -0
  395. package/lib/automation/index.cjs +47 -0
  396. package/lib/automation/index.d.cts +3 -0
  397. package/lib/automation/reasoning-instructions.cjs +5 -0
  398. package/lib/automation/thinking.cjs +35 -0
  399. package/lib/automation/thinking.d.cts +2 -0
  400. package/mcp/capability-catalog.ts +2 -2
  401. package/mcp/server.ts +1 -1
  402. package/mcp/tools/tasks/module.ts +9 -0
  403. package/mcp/tools/telegram/receiver-server.ts +4 -2
  404. package/package.json +6 -4
@@ -1,7 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TaskService = void 0;
4
+ exports.handoffCommandId = handoffCommandId;
4
5
  exports.taskIndexEntry = taskIndexEntry;
6
+ const control_notification_1 = require("../control-notification");
7
+ const browser_fields_1 = require("../browser-fields");
8
+ const computer_inputs_1 = require("../../jev/computer-inputs");
9
+ const automation_session_1 = require("./automation-session");
10
+ const computer_outcome_1 = require("../../automation/computer-outcome");
11
+ const browser_outcome_1 = require("../../automation/browser-outcome");
12
+ const direct_command_1 = require("../../automation/direct-command");
13
+ const browser_contract_1 = require("../../jev/browser-contract");
5
14
  const path_1 = require("path");
6
15
  const workflow_1 = require("../workflow");
7
16
  const timing_1 = require("./timing");
@@ -18,6 +27,13 @@ const types_1 = require("../types");
18
27
  /** How many finished tasks the per-turn index page keeps. Unfinished tasks are never dropped;
19
28
  * older finished ones stay reachable through task_status with an explicit task_id. */
20
29
  const INDEXED_FINISHED_TASKS = 20;
30
+ /** Direct Computer/Remote Browser commands that may wait behind a settling round. */
31
+ const COMMAND_QUEUE_MAX = 20;
32
+ /** Stable control ID for the agent's one command on a handed-off utterance. */
33
+ function handoffCommandId(inputId) {
34
+ const h = (0, crypto_1.createHash)('sha256').update('agent-handoff:' + inputId).digest('hex');
35
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
36
+ }
21
37
  /** One index row: identity, live state and how to fetch the rest — never a report body.
22
38
  * A finished task's progress, execution observation and workflow checkpoint/review history
23
39
  * are report bodies, so they are dropped here and read back via task_status when needed.
@@ -35,10 +51,36 @@ function taskIndexEntry(task) {
35
51
  cancellation: task.cancellation, replacedByTaskId: task.replacedByTaskId,
36
52
  workstreamId: task.workstreamId, continueTaskId: task.continueTaskId, continuationPolicy: task.continuationPolicy,
37
53
  resultAvailable: Boolean(task.result),
38
- gatewayTarget: task.gatewayTarget,
54
+ actionLog: task.actionLog,
55
+ computerConnection: task.computerConnection, automationController: task.automationController ?? "agent", gatewayTarget: task.gatewayTarget, automationSession: (0, automation_session_1.automationSession)(task),
39
56
  details: { tool: 'task_status', task_id: task.taskId },
40
57
  };
41
58
  }
59
+ const ACTION_LOG_MAX = 12;
60
+ const CORRECTION_HEAD = 'Latest user correction (apply first; supersedes conflicting earlier requirements):\n';
61
+ /** The round's own command for the log: a correction's latest text, never its preamble, and never typed field text. */
62
+ function loggedCommand(instructions) {
63
+ let command = instructions.startsWith(CORRECTION_HEAD) ? instructions.slice(CORRECTION_HEAD.length).split('\n\nEarlier requirements and corrections')[0] : instructions;
64
+ const typed = (0, direct_command_1.textCommand)(command);
65
+ if (typed)
66
+ command = command.replace(typed.text, '[text]');
67
+ return command;
68
+ }
69
+ const clip = (text, max) => { const chars = [...text.replace(/\s+/gu, ' ').trim()]; return chars.length > max ? chars.slice(0, max - 1).join('') + '…' : chars.join(''); };
70
+ /** Appends one round's own outcome line, so a later summary need not infer earlier rounds from the latest report. */
71
+ function recordAction(task, revision, command, outcome) {
72
+ let result = task.gatewayTarget?.adapter === 'computer' && outcome.computerReport ? (0, computer_outcome_1.computerOutcomeText)(outcome.computerReport)
73
+ : task.gatewayTarget?.adapter === 'browser' && outcome.browserReport ? (0, browser_outcome_1.browserOutcomeText)(outcome.browserReport) : undefined;
74
+ if (result === undefined)
75
+ return;
76
+ const last = outcome.computerReport?.lastAction;
77
+ // A step-run line counts steps; name the last completed action as well.
78
+ if (last && !last.blocked && !result.startsWith('Done:'))
79
+ result += ` Last completed action: ${(0, computer_outcome_1.computerActionText)(last)}.`;
80
+ if (outcome.type === 'unknown')
81
+ result = 'Outcome unknown; not replayed. ' + result;
82
+ task.actionLog = [...(task.actionLog ?? []).filter(entry => entry.revision !== revision), { revision, command: clip(loggedCommand(command), 80), result: clip(result, 240) }].slice(-ACTION_LOG_MAX);
83
+ }
42
84
  /** Task mutations use short durable transactions; admission probes are read-only. */
43
85
  class TaskService {
44
86
  constructor(store, config, defaultProjectRoot = '') {
@@ -87,7 +129,7 @@ class TaskService {
87
129
  if (!input || !JSON.parse(String(decision.input_ids_json)).includes(context.inputId))
88
130
  throw new types_1.OrchestrationError('ACCESS_DENIED');
89
131
  if (execute && !context.execute)
90
- throw new types_1.OrchestrationError('EXECUTION_DENIED');
132
+ throw new types_1.OrchestrationError('EXECUTION_DENIED', `EXECUTION_DENIED: This is an internal report/non-executing turn (execute capability is off for this turn), so it cannot commit a task ${kind === 'answer' ? 'answer' : kind === 'spawn' ? 'creation' : 'change'} here. This is a turn-scoped permission boundary — not a system outage, and not proof that a prior answer was rejected or lost: any task command already committed on an executing turn stays durably saved and reaches the worker when it resumes. Do not retry this mutation on this turn or report the service as unavailable; reconcile the worker's request against the committed answer/receipt, and surface only a genuinely new decision to the user on a normal turn.`);
91
133
  // Recovery may re-run inference with new tool-call IDs or rephrased args.
92
134
  // Only replay committed receipts for this original input; new mutations
93
135
  // require a new user input rather than guessing whether effects happened.
@@ -124,7 +166,7 @@ class TaskService {
124
166
  delete task.skill;
125
167
  return [this.withRecentTools(task)];
126
168
  }
127
- return this.store.all(`SELECT snapshot_json FROM tasks WHERE conversation_id=? AND (state NOT IN ('completed','failed','cancelled') OR id IN
169
+ return this.store.all(`SELECT snapshot_json FROM tasks WHERE conversation_id=? AND (state NOT IN ('completed','failed','cancelled') OR (json_extract(snapshot_json,'$.automationSession.status') IN ('idle','blocked') AND json_extract(snapshot_json,'$.automationSession.idleSince')+json_extract(snapshot_json,'$.automationSession.idleTimeoutMs')>${Date.now()}) OR id IN
128
170
  (SELECT id FROM tasks WHERE conversation_id=? AND state IN ('completed','failed','cancelled') ORDER BY created_at DESC,id DESC LIMIT 100))
129
171
  ORDER BY CASE WHEN state IN ('completed','failed','cancelled') THEN 1 ELSE 0 END,created_at DESC,id DESC`, conversationId, conversationId).map(row => {
130
172
  const task = JSON.parse(String(row.snapshot_json));
@@ -155,11 +197,12 @@ class TaskService {
155
197
  }
156
198
  /** Bounded recent-task page: every unfinished task, plus only the newest finished ones. */
157
199
  indexPage(conversationId) {
158
- return this.store.all(`SELECT snapshot_json FROM tasks WHERE conversation_id=? AND (state NOT IN ('completed','failed','cancelled') OR id IN
200
+ return this.store.all(`SELECT snapshot_json FROM tasks WHERE conversation_id=? AND (state NOT IN ('completed','failed','cancelled') OR (json_extract(snapshot_json,'$.automationSession.status') IN ('idle','blocked') AND json_extract(snapshot_json,'$.automationSession.idleSince')+json_extract(snapshot_json,'$.automationSession.idleTimeoutMs')>${Date.now()}) OR id IN
159
201
  (SELECT id FROM tasks WHERE conversation_id=? AND state IN ('completed','failed','cancelled') ORDER BY created_at DESC,id DESC LIMIT ${INDEXED_FINISHED_TASKS}))
160
202
  ORDER BY CASE WHEN state IN ('completed','failed','cancelled') THEN 1 ELSE 0 END,created_at DESC,id DESC`, conversationId, conversationId).map(row => JSON.parse(String(row.snapshot_json)));
161
203
  }
162
204
  withRecentTools(task) {
205
+ task.automationSession = (0, automation_session_1.automationSession)(task);
163
206
  task.recentTools = this.store.all("SELECT payload_json,occurred_at FROM conversation_events WHERE conversation_id=? AND type='tool.activity' AND json_extract(payload_json,'$.task_id')=? ORDER BY seq DESC LIMIT 8", task.conversationId, task.taskId).map(row => {
164
207
  const event = JSON.parse(String(row.payload_json)).payload;
165
208
  return { name: String(event.name ?? 'unknown'), description: typeof event.input?.description === 'string' ? (0, failure_1.taskFailure)(new Error(event.input.description)).message.slice(0, 512) : undefined, type: String(event.type), isError: event.is_error, occurredAt: Number(row.occurred_at) };
@@ -171,9 +214,18 @@ class TaskService {
171
214
  .map(row => String(row.path).slice(0, 2048)),
172
215
  currentFilesystemVerified: false,
173
216
  };
174
- return task;
217
+ // Direct corrections bypass the parent inference turn. Hydrated status and
218
+ // notifications must carry the current goal so verification cannot use stale memory.
219
+ return ['browser', 'computer'].includes(task.gatewayTarget?.adapter ?? '')
220
+ ? { ...task, currentInstructions: this.revision(task.taskId, task.revision).instructions }
221
+ : task;
175
222
  }
176
223
  spawn(context, command) {
224
+ if (command.computerInputs !== undefined) {
225
+ command.computerInputs = computer_inputs_1.ComputerInputs.parse(command.computerInputs);
226
+ if (command.gatewayTarget?.adapter !== 'computer')
227
+ throw new types_1.OrchestrationError('INVALID_INPUT');
228
+ }
177
229
  if (command.workingDirectory !== undefined) {
178
230
  if (this.config.tasks.workspaceMode !== 'host')
179
231
  throw new types_1.OrchestrationError('WORKING_DIRECTORY_HOST_ONLY');
@@ -184,6 +236,9 @@ class TaskService {
184
236
  (0, store_1.boundedText)(command.continueTaskId, 128);
185
237
  if (command.continuationPolicy !== undefined && (!command.continueTaskId || !['after_success', 'after_terminal'].includes(command.continuationPolicy)))
186
238
  throw new types_1.OrchestrationError('INVALID_INPUT');
239
+ const prepared = (0, browser_fields_1.preparedBrowserAnswers)(command.browserFields, context.inputId);
240
+ if (command.browserFields !== undefined && command.gatewayTarget?.adapter !== 'browser')
241
+ throw new types_1.OrchestrationError('INVALID_BROWSER_FIELDS');
187
242
  (0, store_1.boundedText)(command.title, 512);
188
243
  (0, store_1.boundedText)(command.instructions);
189
244
  (0, store_1.boundedText)(command.targetProfile, 128);
@@ -197,6 +252,24 @@ class TaskService {
197
252
  throw new types_1.OrchestrationError('INVALID_GATEWAY_TARGET');
198
253
  return this.command(context, 'spawn', command, true, () => {
199
254
  const conversation = this.store.get('SELECT * FROM conversations WHERE id=?', context.conversationId);
255
+ let replaced;
256
+ if (['browser', 'computer'].includes(command.gatewayTarget?.adapter ?? '')) {
257
+ const existing = this.store.all("SELECT snapshot_json FROM tasks WHERE conversation_id=? AND json_extract(snapshot_json,'$.gatewayTarget.adapter')=? AND json_extract(snapshot_json,'$.gatewayTarget.sessionId')=?", context.conversationId, command.gatewayTarget.adapter, command.gatewayTarget.sessionId).map(row => JSON.parse(String(row.snapshot_json))).find(task => task.ownerPrincipalId === context.principalId && (0, automation_session_1.automationSession)(task)?.status !== 'closed');
258
+ // A computer session that never acted (failed before or while waiting
259
+ // for access) holds nothing worth continuing; a new spawn replaces it.
260
+ const neverActed = (existing?.gatewayTarget?.adapter === 'computer' && (existing.computerReport?.steps ?? 0) === 0 && !existing.computerReport?.trace?.some(event => event.outcome === 'unknown') &&
261
+ ((existing.state === 'failed' && !existing.activeAttemptId) || (existing.state === 'running' && existing.computerReport?.status === 'waiting_access'))) ||
262
+ // A Remote Browser session that failed before any tab action likewise holds nothing.
263
+ (existing?.gatewayTarget?.adapter === 'browser' && existing.state === 'failed' && !existing.activeAttemptId && (existing.browserReport?.steps ?? 0) === 0 &&
264
+ existing.browserReport?.lastAction?.outcome !== 'unknown' && !existing.browserReport?.lastConfirmedAction);
265
+ if (existing && neverActed)
266
+ replaced = existing;
267
+ else if (existing) {
268
+ const report = existing.computerReport ?? existing.browserReport;
269
+ const cause = `state ${existing.state}${report?.reason ? `, last result ${report.reason}` : ''}${existing.failure?.code ? ` (${existing.failure.code})` : ''}, controlled by the ${existing.automationController === 'user' ? 'user' : 'agent'}`;
270
+ throw new types_1.OrchestrationError('AUTOMATION_SESSION_EXISTS', `This device session already has task ${existing.taskId} at revision ${existing.revision}: ${cause}. This is not a permission problem. Inspect it with task_status and use task_update when_ready for the next goal on the same task${existing.automationController === 'user' ? ' (the user controls it; wait for them or ask them to hand control back)' : ''}, or task_cancel it to end that session. Do not open another tab or replay completed work.`);
271
+ }
272
+ }
200
273
  const available = new Set();
201
274
  for (const input of this.store.all('SELECT id,attachment_refs_json FROM conversation_inputs WHERE conversation_id=?', context.conversationId)) {
202
275
  available.add(String(input.id));
@@ -231,8 +304,13 @@ class TaskService {
231
304
  task.continuationPolicy = command.continuationPolicy ?? 'after_success';
232
305
  task.latestProgress = { source: 'runtime', observedAt: now, text: `Queued after task ${prior.taskId} (${task.continuationPolicy}).` };
233
306
  }
234
- if (command.gatewayTarget)
307
+ if (command.gatewayTarget) {
235
308
  task.gatewayTarget = command.gatewayTarget;
309
+ if (["browser", "computer"].includes(command.gatewayTarget.adapter)) {
310
+ task.automationSession = { status: "active", idleTimeoutMs: this.config.tasks.automationIdleTimeoutMs };
311
+ task.automationController = "user";
312
+ }
313
+ }
236
314
  if (command.skill)
237
315
  task.skill = command.skill;
238
316
  if (context.model && !command.gatewayTarget)
@@ -241,30 +319,104 @@ class TaskService {
241
319
  if (projectRoot && !command.gatewayTarget)
242
320
  task.resourceProfile = { projectRoot, mode: this.config.tasks.workspaceMode };
243
321
  this.store.run('INSERT INTO tasks VALUES(?,?,?,?,?,?,?,?,?)', task.taskId, task.conversationId, task.state, 1, 1, null, JSON.stringify(task), now, now);
244
- const revision = { taskId: task.taskId, revision: 1, instructions: command.instructions,
245
- contextRefs: command.contextRefs ?? [], mode: 'when_ready', originatingInputId: context.inputId };
322
+ const revision = { requestBrowserConsent: command.gatewayTarget?.adapter === 'browser', taskId: task.taskId, revision: 1, instructions: command.instructions, computerInputs: command.computerInputs,
323
+ answers: prepared, contextRefs: command.contextRefs ?? [], mode: 'when_ready', originatingInputId: context.inputId };
246
324
  this.store.run('INSERT INTO task_revisions VALUES(?,?,?)', task.taskId, 1, JSON.stringify(revision));
247
325
  this.store.appendEvent(task.conversationId, 'task.created', task, task.taskId);
248
326
  this.store.enqueue('schedule', `schedule:${task.taskId}:1`, { taskId: task.taskId });
327
+ if (replaced) {
328
+ this.cancelOwned(context.conversationId, replaced.taskId, task.taskId);
329
+ this.store.appendEvent(task.conversationId, 'task.session_replaced', { taskId: replaced.taskId, replacedByTaskId: task.taskId }, replaced.taskId);
330
+ }
249
331
  return task;
250
332
  });
251
333
  }
252
- update(context, taskId, expectedRevision, instruction, mode) {
334
+ update(context, taskId, expectedRevision, instruction, mode, browserFields, computerInputs, startUrl) {
335
+ let browserUrl;
336
+ if (startUrl !== undefined) {
337
+ if (!context.execute)
338
+ throw new types_1.OrchestrationError('EXECUTION_DENIED');
339
+ try {
340
+ if (typeof startUrl !== 'string' || startUrl.length > 8192)
341
+ throw Error();
342
+ const u = new URL(startUrl);
343
+ if (!['http:', 'https:'].includes(u.protocol) || u.username || u.password)
344
+ throw Error();
345
+ browserUrl = u.href;
346
+ }
347
+ catch {
348
+ throw new types_1.OrchestrationError('INVALID_BROWSER_START_URL');
349
+ }
350
+ }
253
351
  (0, store_1.boundedText)(instruction);
352
+ if (computerInputs !== undefined)
353
+ computerInputs = computer_inputs_1.ComputerInputs.parse(computerInputs);
254
354
  if (!['when_ready', 'interrupt_and_resume'].includes(mode))
255
355
  throw new types_1.OrchestrationError('INVALID_INPUT');
256
- return this.command(context, 'update', { taskId, expectedRevision, instruction, mode }, context.execute, () => {
356
+ const prepared = (0, browser_fields_1.preparedBrowserAnswers)(browserFields, context.inputId);
357
+ return this.command(context, 'update', { taskId, expectedRevision, instruction, mode, browserFields, computerInputs, startUrl }, context.execute, () => {
257
358
  const task = this.owned(taskId, context.conversationId), version = task.stateVersion;
258
- if (types_1.TERMINAL_TASK_STATES.has(task.state))
359
+ if (browserUrl !== undefined && task.gatewayTarget?.adapter !== 'browser')
360
+ throw new types_1.OrchestrationError('INVALID_BROWSER_START_URL');
361
+ if (computerInputs !== undefined && task.gatewayTarget?.adapter !== 'computer')
362
+ throw new types_1.OrchestrationError('INVALID_INPUT');
363
+ if ((0, automation_session_1.automationSession)(task)?.status === 'closed')
364
+ throw new types_1.OrchestrationError('AUTOMATION_SESSION_CLOSED', 'This automation session was ended or expired. Ask for a new authorized session; never replay its previous action.');
365
+ const completionReview = task.gatewayTarget?.adapter === 'browser' && task.state === 'needs_reconciliation' &&
366
+ Boolean(task.activeAttemptId) && task.browserReport?.status === 'needs_verification' &&
367
+ ['COMPLETION_CANDIDATE', 'VERIFICATION_FAILED'].includes(task.browserReport.reason) &&
368
+ !task.browserReport.providerFailure && task.browserReport.lastAction?.outcome !== 'unknown';
369
+ if (task.automationController === 'user') {
370
+ // Jev gave up on this utterance and handed it to the agent: exactly one
371
+ // command, as the user's own next direct command (user keeps control).
372
+ const handoff = this.agentHandoff(context, task, expectedRevision, mode, browserFields, computerInputs, startUrl);
373
+ if (handoff)
374
+ return this.applyDirectControl(context.conversationId, taskId, task, { id: handoffCommandId(context.inputId), action: 'revise', text: instruction }, undefined, context.inputId);
375
+ throw new types_1.OrchestrationError('USER_CONTROLS_AUTOMATION');
376
+ }
377
+ const agentControl = !context.execute &&
378
+ ['browser', 'computer'].includes(task.gatewayTarget?.adapter ?? '') && task.capabilities.execute && task.ownerPrincipalId === context.principalId &&
379
+ task.state === 'waiting_input' && !task.activeAttemptId && !task.pendingQuestion && task.executionControl?.phase !== 'paused' &&
380
+ ['COMMAND_WAITING_INPUT', 'THINKING_WAITING_INPUT', 'COMPLETION_CANDIDATE', 'VERIFICATION_FAILED'].includes(task.browserReport?.reason ?? task.computerReport?.reason ?? '') &&
381
+ !task.browserReport?.providerFailure && task.browserReport?.lastAction?.outcome !== 'unknown' &&
382
+ // Device disconnect/reconnect changes stateVersion without changing the
383
+ // settled action. Authorize this decision against its immutable round,
384
+ // while the current ownership, controller and idle guards still apply.
385
+ Boolean(this.store.get(`SELECT n.id FROM notifications n JOIN tasks t ON t.id=n.task_id
386
+ WHERE n.task_id=? AND n.decision_id=? AND n.status='assigned' AND ${control_notification_1.CURRENT_CONTROL_ROUND_SQL} LIMIT 1`, taskId, context.decisionId));
387
+ // A provider failure before the first action can be replanned by the
388
+ // assigned parent. Never extend this to post-action or uncertain outcomes.
389
+ const preActionProviderFailure = task.computerReport?.status === 'blocked' &&
390
+ task.computerReport.steps === 0 &&
391
+ /^THINKING_HTTP_(429|502|503|504)$/.test(task.computerReport.reason) &&
392
+ !task.computerReport.trace?.some(event => event.outcome === 'unknown');
393
+ const computerRecovery = !context.execute && task.gatewayTarget?.adapter === 'computer' &&
394
+ task.capabilities.execute && task.ownerPrincipalId === context.principalId &&
395
+ task.state === 'failed' && !task.activeAttemptId && task.executionControl?.phase !== 'paused' &&
396
+ (preActionProviderFailure || ['COMPLETION_CANDIDATE', 'VERIFICATION_FAILED', 'NO_SUPPORTED_ACTION', 'COMPUTER_TEXT_UNGROUNDED'].includes(task.computerReport?.reason ?? '')) &&
397
+ Boolean(this.store.get("SELECT id FROM notifications WHERE task_id=? AND decision_id=? AND status='assigned' AND task_state_version=?", taskId, context.decisionId, task.stateVersion));
398
+ const browserRecovery = agentControl || computerRecovery || !context.execute && task.gatewayTarget?.adapter === 'browser' &&
399
+ task.capabilities.execute && task.ownerPrincipalId === context.principalId &&
400
+ !task.browserReport?.providerFailure && task.browserReport?.lastAction?.outcome !== 'unknown' &&
401
+ (completionReview || (task.state === 'failed' && !task.activeAttemptId &&
402
+ ['LOW_OPERATION_CONFIDENCE', 'LOW_TARGET_CONFIDENCE', 'STALE_RETRY_BUDGET', 'TARGET_OBSCURED', 'PAGE_CONTENT_UNAVAILABLE', 'MODEL_BLOCKED', 'OBSERVATION_TRUNCATED', 'NO_PROGRESS', 'NO_SUPPORTED_ACTION', 'TEXT_BUDGET'].includes(task.browserReport?.reason ?? ''))) &&
403
+ Boolean(this.store.get("SELECT id FROM notifications WHERE task_id=? AND decision_id=? AND status='assigned' AND task_state_version=?", taskId, context.decisionId, task.stateVersion));
404
+ if (types_1.TERMINAL_TASK_STATES.has(task.state) && !(task.gatewayTarget && (context.execute || browserRecovery) && ['completed', 'failed'].includes(task.state) && !task.activeAttemptId))
259
405
  throw new types_1.OrchestrationError('TASK_TERMINAL');
260
- if (['cancel_requested', 'recovering', 'needs_reconciliation', 'interrupting'].includes(task.state))
406
+ if (['cancel_requested', 'recovering', 'needs_reconciliation', 'interrupting'].includes(task.state) && !(completionReview && (context.execute || browserRecovery)))
261
407
  throw new types_1.OrchestrationError('STATE_CONFLICT');
262
408
  if (task.revision !== expectedRevision)
263
409
  throw new types_1.OrchestrationError('REVISION_CONFLICT');
264
- if (task.gatewayTarget && task.state !== 'queued')
265
- throw new types_1.OrchestrationError('GATEWAY_TASK_ALREADY_SENT', 'This request was already sent. Queue a follow-up task with continue_task_id instead.');
410
+ if (task.gatewayTarget && (task.ownerPrincipalId !== context.principalId || (!context.execute && !browserRecovery)))
411
+ throw new types_1.OrchestrationError('EXECUTION_DENIED');
412
+ if (task.gatewayTarget && mode !== 'when_ready')
413
+ throw new types_1.OrchestrationError('INVALID_INPUT', 'Use when_ready for Gateway-managed tasks; the current request must settle before revised instructions run.');
414
+ if (browserFields !== undefined && task.gatewayTarget?.adapter !== 'browser')
415
+ throw new types_1.OrchestrationError('INVALID_BROWSER_FIELDS');
266
416
  const priorRevision = this.revision(taskId, expectedRevision);
267
- if (!context.execute) {
417
+ if (browserRecovery && !agentControl && ((priorRevision.browserRecoveryCount ?? 0) >= 3 || priorRevision.guidance === instruction))
418
+ throw new types_1.OrchestrationError('BROWSER_RECOVERY_EXHAUSTED', 'Inspect the evidence and explain the unresolved blocker; do not repeat the same plan.');
419
+ if (!context.execute && !browserRecovery) {
268
420
  // A worker progress report increments stateVersion without revoking this
269
421
  // alert. Bind advice to the immutable alert and attempt, not that counter.
270
422
  const assigned = this.store.get(`SELECT n.id FROM notifications n JOIN conversation_events e
@@ -278,14 +430,35 @@ class TaskService {
278
430
  if (!assigned || alreadyAdvised || !task.supervision || task.state !== 'running' || !task.capabilities.execute || mode !== 'when_ready')
279
431
  throw new types_1.OrchestrationError('EXECUTION_DENIED');
280
432
  }
433
+ if (completionReview) {
434
+ const attempt = this.store.attempt(task.activeAttemptId);
435
+ attempt.state = 'ended';
436
+ this.store.saveAttempt(attempt);
437
+ this.pool.release(task.taskId, false);
438
+ task.activeAttemptId = undefined;
439
+ task.state = 'failed';
440
+ }
281
441
  task.revision++;
282
- const revision = { taskId, revision: task.revision, instructions: instruction,
283
- contextRefs: priorRevision.contextRefs, mode, originatingInputId: context.inputId,
284
- ...(!context.execute ? { instructions: priorRevision.instructions, answers: priorRevision.answers, originatingInputId: priorRevision.originatingInputId, guidance: instruction, guidanceBasis: { attemptId: task.activeAttemptId, workflowVersion: task.workflow?.version ?? 0, progressAt: task.latestProgress?.observedAt ?? 0 } } : {}) };
442
+ const revision = { ...(browserUrl ? { browserNavigation: { url: browserUrl, revision: task.revision } } : {}), requestBrowserConsent: context.execute && task.gatewayTarget?.adapter === 'browser', taskId, revision: task.revision, instructions: instruction, computerInputs: context.execute || computerRecovery || agentControl ? computerInputs : priorRevision.computerInputs,
443
+ answers: prepared, contextRefs: priorRevision.contextRefs, mode, originatingInputId: context.inputId,
444
+ ...(!context.execute && !computerRecovery && !agentControl ? { instructions: priorRevision.instructions, answers: prepared ?? priorRevision.answers, originatingInputId: priorRevision.originatingInputId, guidance: instruction, guidanceBasis: { attemptId: task.activeAttemptId, workflowVersion: task.workflow?.version ?? 0, progressAt: task.latestProgress?.observedAt ?? 0 } } : {}),
445
+ ...(browserRecovery && !agentControl ? { browserRecoveryCount: (priorRevision.browserRecoveryCount ?? 0) + 1, guidanceBasis: undefined } : {}) };
285
446
  this.store.run('INSERT INTO task_revisions VALUES(?,?,?)', taskId, task.revision, JSON.stringify(revision));
447
+ if (task.gatewayTarget && ['completed', 'failed'].includes(task.state)) {
448
+ task.state = 'queued';
449
+ delete task.gatewayDispatch;
450
+ delete task.executionControl;
451
+ delete task.failure;
452
+ delete task.result;
453
+ delete task.browserReport;
454
+ delete task.computerReport;
455
+ delete task.latestProgress;
456
+ if (context.execute)
457
+ task.initiatingInputId = context.inputId;
458
+ }
286
459
  if (task.state === 'waiting_input') {
287
460
  task.pendingQuestion = undefined;
288
- task.state = task.activeAttemptId ? 'interrupting' : 'queued';
461
+ task.state = task.activeAttemptId ? (task.gatewayTarget ? 'running' : 'interrupting') : 'queued';
289
462
  }
290
463
  else if (mode === 'interrupt_and_resume' && task.activeAttemptId)
291
464
  task.state = 'interrupting';
@@ -295,7 +468,152 @@ class TaskService {
295
468
  return task;
296
469
  }, taskId);
297
470
  }
471
+ /** Direct authenticated user control: no inference turn, no second task. */
472
+ controlByUser(conversationId, principalId, taskId, command) {
473
+ return this.store.transaction(() => {
474
+ this.store.assertMember(conversationId, principalId);
475
+ const task = this.owned(taskId, conversationId);
476
+ if (task.ownerPrincipalId !== principalId)
477
+ throw new types_1.OrchestrationError('ACCESS_DENIED');
478
+ if (!/^[0-9a-f-]{36}$/i.test(command.id) || !['pause', 'revise', 'resume', 'agent', 'user'].includes(command.action) || !Number.isSafeInteger(command.expectedRevision))
479
+ throw new types_1.OrchestrationError('INVALID_INPUT');
480
+ const id = 'user-control:' + command.id, hash = (0, store_1.payloadHash)({ taskId, ...command });
481
+ const prior = this.store.get('SELECT * FROM task_commands WHERE action_id=?', id);
482
+ if (prior) {
483
+ if (prior.principal_id !== principalId || prior.conversation_id !== conversationId)
484
+ throw new types_1.OrchestrationError('ACCESS_DENIED');
485
+ if (prior.payload_hash !== hash)
486
+ throw new types_1.OrchestrationError('IDEMPOTENCY_CONFLICT');
487
+ return task;
488
+ }
489
+ if (!['browser', 'computer'].includes(task.gatewayTarget?.adapter ?? '') || !task.capabilities.execute)
490
+ throw new types_1.OrchestrationError('EXECUTION_DENIED');
491
+ if ((0, automation_session_1.automationSession)(task)?.status === 'closed')
492
+ throw new types_1.OrchestrationError('AUTOMATION_SESSION_CLOSED');
493
+ if (task.revision !== command.expectedRevision)
494
+ throw new types_1.OrchestrationError('REVISION_CONFLICT');
495
+ if (command.action === 'agent' || command.action === 'user') {
496
+ if (command.text !== undefined)
497
+ throw new types_1.OrchestrationError('INVALID_INPUT');
498
+ task.automationController = command.action;
499
+ // The agent drives now: the user's queued direct commands must not run under it.
500
+ if (command.action === 'agent' && task.queuedCommands?.length) {
501
+ const queue = task.queuedCommands;
502
+ delete task.queuedCommands;
503
+ task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: `Control handed to the agent. ${queue.length} queued command${queue.length === 1 ? ' was' : 's were'} not sent: ${queue.map(c => JSON.stringify(c.text.slice(0, 60))).join(', ')}.`.slice(0, 4096) };
504
+ this.store.appendEvent(conversationId, 'task.command_queue_dropped', { taskId, count: queue.length, reason: 'agent_control' }, taskId);
505
+ }
506
+ this.store.saveTask(task, task.stateVersion);
507
+ this.store.run('INSERT INTO task_commands VALUES(?,?,?,?,?,?,?,?,?)', id, taskId, conversationId, principalId, null, 'user_control', hash, JSON.stringify(task), Date.now());
508
+ if (command.action === 'agent' && task.state === 'waiting_input' && !task.activeAttemptId && !task.pendingQuestion && task.executionControl?.phase !== 'paused')
509
+ this.notify(task);
510
+ return task;
511
+ }
512
+ return this.applyDirectControl(conversationId, taskId, task, { id: command.id, action: command.action, text: command.text }, task => this.store.run('INSERT INTO task_commands VALUES(?,?,?,?,?,?,?,?,?)', id, taskId, conversationId, principalId, null, 'user_control', hash, JSON.stringify(task), Date.now()));
513
+ });
514
+ }
515
+ /**
516
+ * The agent turn for a handed-off utterance (needs_agent AGENT_HANDOFF receipt
517
+ * on this input and task) may send one command: same task, the receipt's
518
+ * revision (no newer user command), the round settled and waiting, no fields,
519
+ * inputs or navigation, and never twice for the same utterance.
520
+ */
521
+ agentHandoff(context, task, expectedRevision, mode, browserFields, computerInputs, startUrl) {
522
+ const receipt = this.liveReceipt(context.inputId);
523
+ if (receipt?.code !== 'AGENT_HANDOFF' || receipt.status !== 'needs_agent' || receipt.taskId !== task.taskId)
524
+ return false;
525
+ if (!context.execute || task.ownerPrincipalId !== context.principalId)
526
+ throw new types_1.OrchestrationError('EXECUTION_DENIED');
527
+ if (this.store.get("SELECT 1 FROM task_revisions WHERE task_id=? AND json_extract(payload_json,'$.agentHandoffInputId')=?", task.taskId, context.inputId))
528
+ throw new types_1.OrchestrationError('AGENT_HANDOFF_USED', 'The one command for this handed-off utterance was already sent. Report its outcome; do not send another.');
529
+ if (mode !== 'when_ready' || browserFields !== undefined || computerInputs !== undefined || startUrl !== undefined)
530
+ throw new types_1.OrchestrationError('INVALID_INPUT', 'A handed-off command is one plain next command: mode=when_ready, no fields, inputs or start_url.');
531
+ if (expectedRevision !== receipt.revision || task.revision !== receipt.revision)
532
+ throw new types_1.OrchestrationError('REVISION_CONFLICT', 'The user has given a newer command since this utterance; do not act on it.');
533
+ if (task.activeAttemptId || !((0, automation_session_1.pausedForCommand)(task) || (0, automation_session_1.stoppedForCommand)(task)))
534
+ throw new types_1.OrchestrationError('STATE_CONFLICT');
535
+ return true;
536
+ }
537
+ /** The latest live-control receipt of an input (live-execution-input.ts liveControlReceipt). */
538
+ liveReceipt(inputId) {
539
+ const row = this.store.get("SELECT payload_json FROM conversation_events WHERE type='input.execution_control' AND json_extract(payload_json,'$.payload.inputId')=? ORDER BY seq DESC LIMIT 1", inputId);
540
+ return row ? JSON.parse(String(row.payload_json)).payload : undefined;
541
+ }
542
+ /** The owner's pause/revise/resume, or (agentHandoffInputId) the agent's single command for a handed-off utterance. Runs inside the caller's transaction. */
543
+ applyDirectControl(conversationId, taskId, task, command, record, agentHandoffInputId) {
544
+ task.automationController = 'user';
545
+ const paused = (0, automation_session_1.pausedForCommand)(task);
546
+ const recoverable = command.action === 'revise' && (0, automation_session_1.stoppedForCommand)(task);
547
+ if (!['queued', 'starting', 'running', 'interrupting'].includes(task.state) && !paused && !recoverable)
548
+ throw new types_1.OrchestrationError('STATE_CONFLICT');
549
+ // Only the owner's own pause (settled or still settling) leaves interrupted work. A settled
550
+ // round waiting for the next command has nothing to resume: re-running it would replay it.
551
+ if (command.action === 'resume' && !(task.executionControl?.action === 'pause' && ['pending', 'paused'].includes(task.executionControl.phase)))
552
+ throw new types_1.OrchestrationError('NOTHING_TO_RESUME', 'Nothing to resume: the last command already finished. Give the next command instead.');
553
+ if (command.action === 'revise')
554
+ (0, store_1.boundedText)(command.text ?? '', 4000);
555
+ else if (command.text !== undefined)
556
+ throw new types_1.OrchestrationError('INVALID_INPUT');
557
+ const previous = this.revision(taskId, task.revision);
558
+ // Never drop queued words silently: say which commands will not run.
559
+ const unsent = command.action === 'pause' ? task.queuedCommands ?? [] : [];
560
+ const dropped = unsent.length ? ` ${unsent.length} queued command${unsent.length === 1 ? ' was' : 's were'} not sent: ${unsent.map(c => JSON.stringify(c.text.slice(0, 60))).join(', ')}.` : '';
561
+ if (unsent.length)
562
+ this.store.appendEvent(conversationId, 'task.command_queue_dropped', { taskId, count: unsent.length, reason: 'paused' }, taskId);
563
+ if (command.action === 'pause')
564
+ delete task.queuedCommands;
565
+ // A user's own command is still settling: the next one waits its turn,
566
+ // verbatim, instead of superseding it or being folded into a correction.
567
+ if (command.action === 'revise' && this.queuesDirectCommand(task, previous)) {
568
+ const queue = task.queuedCommands ?? [];
569
+ if (queue.length >= COMMAND_QUEUE_MAX)
570
+ throw new types_1.OrchestrationError('COMMAND_QUEUE_FULL', 'Too many commands are waiting. Wait for the current ones to finish or pause.');
571
+ task.queuedCommands = [...queue, { id: command.id, text: command.text, at: Date.now() }];
572
+ task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: `Queued: ${JSON.stringify(command.text.slice(0, 120))} runs after the current command (${task.queuedCommands.length} waiting).` };
573
+ this.store.saveTask(task, task.stateVersion);
574
+ this.store.appendEvent(conversationId, 'task.command_queued', { taskId, commandId: command.id, position: task.queuedCommands.length }, taskId);
575
+ record?.(task);
576
+ return task;
577
+ }
578
+ const priorAnswers = previous.answers?.map(a => ({ field: a.browserFieldLabel ?? a.computerFieldLabel, text: a.text }));
579
+ const nextCommand = command.action === 'revise' && (paused || recoverable);
580
+ const instructions = nextCommand ? command.text : command.action === 'revise' ? CORRECTION_HEAD + command.text + '\n\nEarlier requirements and corrections, newest first. Keep only requirements compatible with the latest correction; do not perform superseded actions:\n' + previous.instructions + (priorAnswers?.length ? '\n\nEarlier user answers (subject to the latest correction):\n' + JSON.stringify(priorAnswers) : '') : previous.instructions;
581
+ (0, store_1.boundedText)(instructions, task.gatewayTarget?.adapter === 'browser' ? 8000 : 16000);
582
+ task.revision++;
583
+ if (recoverable && !paused) {
584
+ delete task.computerReport;
585
+ delete task.failure;
586
+ delete task.browserReport;
587
+ delete task.gatewayDispatch;
588
+ }
589
+ // The owner's new command supersedes any pending question (saveTask closes its record).
590
+ if (command.action === 'revise')
591
+ delete task.pendingQuestion;
592
+ this.store.run('INSERT INTO task_revisions VALUES(?,?,?)', taskId, task.revision, JSON.stringify({ ...previous, requestBrowserConsent: command.action !== 'pause', revision: task.revision, instructions, mode: 'interrupt_and_resume', computerInputs: command.action === 'revise' ? undefined : previous.computerInputs, answers: command.action === 'revise' ? undefined : previous.answers, browserRecoveryCount: 0, guidance: undefined, guidanceBasis: undefined, ...(command.action === 'revise' ? { directCommand: true } : {}), agentHandoffInputId }));
593
+ task.executionControl = { id: command.id, action: command.action, revision: task.revision, phase: task.activeAttemptId ? 'pending' : command.action === 'pause' ? 'paused' : 'pending', requestedAt: Date.now(), ...(agentHandoffInputId ? { agentHandoff: true } : {}) };
594
+ task.state = task.activeAttemptId ? 'interrupting' : command.action === 'pause' ? 'waiting_input' : 'queued';
595
+ task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: ((task.activeAttemptId ? 'Control accepted; settling the current action before applying it.' : command.action === 'pause' ? 'Automation paused.' : 'Control accepted; resuming from a fresh observation.') + dropped).slice(0, 4096) };
596
+ this.store.saveTask(task, task.stateVersion);
597
+ this.store.appendEvent(conversationId, 'task.control_accepted', { taskId, control: task.executionControl }, taskId);
598
+ record?.(task);
599
+ return task;
600
+ }
601
+ /** A user's direct command is still settling, so a new one joins the FIFO queue. */
602
+ queuesDirectCommand(task, previous) {
603
+ return ['browser', 'computer'].includes(task.gatewayTarget?.adapter ?? '') && previous.directCommand === true && (['queued', 'starting', 'running'].includes(task.state) || (task.state === 'interrupting' && task.executionControl?.action === 'revise'));
604
+ }
605
+ /** Speech while a direct command settles is the next command, not an interruption:
606
+ * pausing first would fold it into a correction of the running command. */
607
+ voicePauseApplies(taskId) {
608
+ const task = this.store.task(taskId);
609
+ if (!task)
610
+ return false;
611
+ return !(task.automationController === 'user' && this.queuesDirectCommand(task, this.revision(taskId, task.revision)));
612
+ }
298
613
  cancel(context, taskId, replacedByTaskId) {
614
+ // A handed-off utterance allows one task_update only; it never ends the user's session.
615
+ if (this.liveReceipt(context.inputId)?.code === 'AGENT_HANDOFF')
616
+ throw new types_1.OrchestrationError('AGENT_HANDOFF_ONE_COMMAND', 'This input may only become one task_update mode=when_ready on the handed-off task.');
299
617
  return this.command(context, 'cancel', { taskId, ...(replacedByTaskId ? { replacedByTaskId } : {}) }, false, () => this.cancelOwned(context.conversationId, taskId, replacedByTaskId), taskId);
300
618
  }
301
619
  /** Authenticated user control bypasses inference, but retains conversation ownership and task fencing. */
@@ -314,23 +632,38 @@ class TaskService {
314
632
  throw new types_1.OrchestrationError('INVALID_REPLACEMENT');
315
633
  this.owned(replacedByTaskId, conversationId);
316
634
  }
317
- if (types_1.TERMINAL_TASK_STATES.has(task.state))
635
+ if (types_1.TERMINAL_TASK_STATES.has(task.state) && !(task.gatewayTarget?.adapter === 'computer' && task.gatewayDispatch && (0, automation_session_1.automationSession)(task)?.status !== 'closed')) {
636
+ const session = (0, automation_session_1.automationSession)(task);
637
+ if (session && session.status !== 'closed') {
638
+ task.automationSession = { ...session, status: 'closed', closedAt: Date.now(), closedReason: requestedBy };
639
+ this.store.saveTask(task, task.stateVersion);
640
+ }
318
641
  return task;
642
+ }
319
643
  if (task.state === 'cancel_requested') {
320
- if (replacedByTaskId) {
644
+ // A user's explicit stop wins; an agent cancel must not erase the stall evidence.
645
+ const overridesStall = task.cancellation?.requestedBy === 'supervisor' && requestedBy === 'user';
646
+ if (overridesStall)
647
+ task.cancellation = { requestedBy, requestedAt: Date.now() };
648
+ if (replacedByTaskId)
321
649
  task.replacedByTaskId = replacedByTaskId;
650
+ if (replacedByTaskId || overridesStall)
322
651
  this.store.saveTask(task, task.stateVersion);
323
- }
324
652
  return task;
325
653
  }
326
654
  if (replacedByTaskId)
327
655
  task.replacedByTaskId = replacedByTaskId;
328
656
  const version = task.stateVersion;
329
- task.cancellation = { requestedBy, requestedAt: Date.now() };
330
- task.state = task.activeAttemptId ? 'cancel_requested' : 'cancelled';
657
+ // Retrying an unconfirmed stall stop ("Retry cleanup") finishes the supervisor's
658
+ // stop; it must not turn that stall into a user/agent cancellation.
659
+ const retriesStall = task.state === 'needs_reconciliation' && task.cancellation?.requestedBy === 'supervisor';
660
+ if (!retriesStall)
661
+ task.cancellation = { requestedBy, requestedAt: Date.now() };
662
+ delete task.queuedCommands;
663
+ task.state = task.activeAttemptId || (task.gatewayTarget?.adapter === 'computer' && task.gatewayDispatch) ? 'cancel_requested' : 'cancelled';
331
664
  task.pendingQuestion = undefined;
332
665
  delete task.failure;
333
- task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: task.activeAttemptId ? `Cancellation requested by ${requestedBy}; verifying that task execution has stopped.` : `Cancelled by ${requestedBy}. Existing files and prior effects are retained.` };
666
+ task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: task.state === 'cancel_requested' ? 'Disconnecting; waiting for execution and access to stop.' : 'Session ended. Existing files and prior effects are retained.' };
334
667
  this.store.saveTask(task, version);
335
668
  if (task.state === 'cancelled')
336
669
  this.notify(task);
@@ -338,24 +671,41 @@ class TaskService {
338
671
  this.store.enqueue('interrupt', `cancel:${taskId}:${task.stateVersion}`, { taskId });
339
672
  return task;
340
673
  }
674
+ /** A settled computer round still owns device access until explicit revocation. */
675
+ finishIdleComputerCleanup(taskId, requestId) {
676
+ this.store.transaction(() => {
677
+ const task = this.store.task(taskId);
678
+ if (!task || task.state !== 'cancel_requested' || task.activeAttemptId || task.gatewayTarget?.adapter !== 'computer' || task.gatewayDispatch?.requestId !== requestId)
679
+ throw new types_1.OrchestrationError('STALE_ATTEMPT');
680
+ task.state = 'cancelled';
681
+ task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: 'Computer Use disconnected.' };
682
+ this.store.saveTask(task, task.stateVersion);
683
+ this.notify(task);
684
+ });
685
+ }
341
686
  /** Scheduler-only cleanup acknowledgment; never authorizes new worker writes. */
342
687
  finishCleanup(attemptId, generation, stopped) {
343
688
  this.store.transaction(() => {
344
689
  const attempt = this.store.attempt(attemptId), task = attempt && this.store.task(attempt.taskId);
345
690
  if (!attempt || !task || attempt.generation !== generation || task.activeAttemptId !== attemptId || task.state !== 'cancel_requested')
346
691
  throw new types_1.OrchestrationError('STALE_ATTEMPT');
347
- const message = stopped ? `Cancelled by ${task.cancellation?.requestedBy ?? 'agent'}. Task execution stopped; existing files and prior effects are retained.` : 'Cannot verify that this task has stopped. Retry cleanup; no unrelated processes were stopped.';
348
- task.state = stopped ? 'cancelled' : 'needs_reconciliation';
692
+ const stalled = this.stalledFailure(task, stopped);
693
+ const message = stalled ? stalled.message : stopped ? `Cancelled by ${task.cancellation?.requestedBy ?? 'agent'}. Task execution stopped; existing files and prior effects are retained.` : 'Cannot verify that this task has stopped. Retry cleanup; no unrelated processes were stopped.';
694
+ task.state = stopped ? (stalled ? 'failed' : 'cancelled') : 'needs_reconciliation';
349
695
  task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: message };
350
696
  if (stopped) {
351
697
  attempt.state = 'ended';
352
698
  task.activeAttemptId = undefined;
353
699
  delete task.failure;
700
+ if (stalled) {
701
+ attempt.failure = stalled;
702
+ task.failure = stalled;
703
+ }
354
704
  this.pool.release(task.taskId, false);
355
705
  }
356
706
  else {
357
707
  attempt.state = 'unknown';
358
- task.failure = { code: 'CLEANUP_UNCONFIRMED', message, observedAt: Date.now() };
708
+ task.failure = stalled ?? { code: 'CLEANUP_UNCONFIRMED', message, observedAt: Date.now() };
359
709
  }
360
710
  this.store.saveAttempt(attempt);
361
711
  this.store.saveTask(task, task.stateVersion);
@@ -363,9 +713,35 @@ class TaskService {
363
713
  this.notify(task);
364
714
  });
365
715
  }
366
- answer(context, taskId, questionId, answer) {
716
+ answer(context, taskId, questionId, answer, browserFields, computerInputs) {
367
717
  (0, store_1.boundedText)(answer);
368
- return this.command(context, 'answer', { taskId, questionId, answer }, true, () => this.answerOwned(context.conversationId, taskId, questionId, answer, context.inputId), taskId);
718
+ if (computerInputs !== undefined)
719
+ computerInputs = computer_inputs_1.ComputerInputs.parse(computerInputs);
720
+ const prepared = (0, browser_fields_1.preparedBrowserAnswers)(browserFields, context.inputId);
721
+ return this.command(context, 'answer', { taskId, questionId, answer, browserFields, computerInputs }, context.execute, () => {
722
+ if (!context.execute) {
723
+ const task = this.owned(taskId, context.conversationId);
724
+ this.assertQuestion(task, questionId);
725
+ // A notification can fill missing browser text in its own authorized task,
726
+ // never approve a new operation, answer an unrelated task, or grant consent.
727
+ const assigned = this.store.get(`SELECT n.id FROM notifications n JOIN conversation_events e
728
+ ON e.conversation_id=n.conversation_id AND e.type='task.state_changed'
729
+ AND json_extract(e.payload_json,'$.payload.taskId')=n.task_id
730
+ AND json_extract(e.payload_json,'$.payload.stateVersion')=n.task_state_version
731
+ WHERE n.task_id=? AND n.conversation_id=? AND n.decision_id=? AND n.status='assigned'
732
+ AND json_extract(e.payload_json,'$.payload.pendingQuestion.questionId')=? LIMIT 1`, taskId, context.conversationId, context.decisionId, questionId);
733
+ const reviewed = context.questionReviewIds?.includes(questionId) && this.store.get('SELECT question_id FROM task_questions WHERE question_id=? AND task_id=? AND closed=0 AND revision=?', questionId, taskId, task.revision);
734
+ const missingBrowser = task.gatewayTarget?.adapter === 'browser' && task.browserReport?.reason === 'FIELD_TEXT_REQUIRED' && task.browserReport.fieldRequest?.reason === 'missing' && Boolean(task.browserReport.fieldRequest.label);
735
+ const missingComputer = task.gatewayTarget?.adapter === 'computer' && task.computerReport?.reason === 'FIELD_TEXT_REQUIRED' && task.computerReport.fieldRequest?.reason === 'missing' && Boolean(task.computerReport.fieldRequest.label);
736
+ if ((!assigned && !reviewed) || task.ownerPrincipalId !== context.principalId || !task.capabilities.execute || (!missingBrowser && !missingComputer))
737
+ throw new types_1.OrchestrationError('EXECUTION_DENIED', `EXECUTION_DENIED: This is an internal report/non-executing turn (execute capability is off for this turn), so it cannot commit a task answer here. This is a turn-scoped permission boundary — not a system outage, and not proof that a prior answer was rejected or lost: any task command already committed on an executing turn stays durably saved and reaches the worker when it resumes. Do not retry this mutation on this turn or report the service as unavailable; reconcile the worker's request against the committed answer/receipt, and surface only a genuinely new decision to the user on a normal turn.`);
738
+ }
739
+ if (browserFields !== undefined && this.owned(taskId, context.conversationId).gatewayTarget?.adapter !== 'browser')
740
+ throw new types_1.OrchestrationError('INVALID_BROWSER_FIELDS');
741
+ if (computerInputs !== undefined && this.owned(taskId, context.conversationId).gatewayTarget?.adapter !== 'computer')
742
+ throw new types_1.OrchestrationError('INVALID_INPUT');
743
+ return this.answerOwned(context.conversationId, taskId, questionId, answer, context.inputId, prepared, computerInputs);
744
+ }, taskId);
369
745
  }
370
746
  /** A scoped authenticated reply is user authorization, without a fabricated model decision. */
371
747
  answerByUser(conversationId, principalId, taskId, questionId, answer, acceptedInputId) {
@@ -426,18 +802,24 @@ class TaskService {
426
802
  if (task.state !== 'waiting_input' || task.pendingQuestion?.questionId !== questionId || task.pendingQuestion.revision !== task.revision)
427
803
  throw new types_1.OrchestrationError('STALE_QUESTION');
428
804
  }
429
- answerOwned(conversationId, taskId, questionId, answer, inputId) {
805
+ answerOwned(conversationId, taskId, questionId, answer, inputId, prepared, computerInputs) {
430
806
  const task = this.owned(taskId, conversationId), version = task.stateVersion;
431
807
  this.assertQuestion(task, questionId);
432
808
  // Persist early answers, while fencing scheduling until the old attempt really ends.
433
809
  const previous = this.revision(taskId, task.revision);
810
+ const browserFieldLabel = task.gatewayTarget?.adapter === 'browser' && task.browserReport?.reason === 'FIELD_TEXT_REQUIRED' && task.browserReport.fieldRequest?.reason === 'missing' ? task.browserReport.fieldRequest.label : undefined;
811
+ if (browserFieldLabel && answer.length > 2000)
812
+ throw new types_1.OrchestrationError('BROWSER_FIELD_VALUE_TOO_LONG');
813
+ const computerField = task.gatewayTarget?.adapter === 'computer' && task.computerReport?.fieldRequest?.reason === 'missing' ? task.computerReport.fieldRequest : undefined;
814
+ if (computerField && answer.length > 2000)
815
+ throw new types_1.OrchestrationError('COMPUTER_FIELD_VALUE_TOO_LONG');
434
816
  task.revision++;
435
- this.store.run('INSERT INTO task_revisions VALUES(?,?,?)', taskId, task.revision, JSON.stringify({ ...previous, revision: task.revision,
436
- answers: [...(previous.answers ?? []), { questionId, text: answer, inputId }], originatingInputId: inputId }));
817
+ this.store.run('INSERT INTO task_revisions VALUES(?,?,?)', taskId, task.revision, JSON.stringify({ ...previous, ...(computerInputs !== undefined ? { computerInputs } : {}), revision: task.revision,
818
+ answers: [...(previous.answers ?? []), ...(prepared ?? []), { questionId, text: answer, inputId, ...(browserFieldLabel ? { browserFieldLabel } : {}), ...(computerField ? { computerFieldLabel: computerField.label, computerApplication: computerField.application, computerWindowTitle: computerField.windowTitle, computerFieldRole: computerField.role } : {}) }], originatingInputId: inputId }));
437
819
  task.pendingQuestion = undefined;
438
- task.state = task.activeAttemptId ? 'interrupting' : 'queued';
820
+ task.state = task.activeAttemptId ? (task.gatewayTarget ? 'running' : 'interrupting') : 'queued';
439
821
  this.store.saveTask(task, version);
440
- this.store.enqueue(task.activeAttemptId ? 'interrupt' : 'schedule', `answer:${questionId}`, { taskId });
822
+ this.store.enqueue(task.activeAttemptId && !task.gatewayTarget ? 'interrupt' : 'schedule', `answer:${questionId}`, { taskId });
441
823
  if (this.store.get("SELECT name FROM sqlite_master WHERE type='table' AND name='conversation_intake'")) {
442
824
  const input = this.store.get('SELECT principal_id,binding_id,input_seq FROM conversation_inputs WHERE id=? AND conversation_id=?', inputId, conversationId);
443
825
  this.store.run(`DELETE FROM conversation_intake WHERE conversation_id=? AND principal_id=? AND binding_id=?
@@ -522,6 +904,8 @@ class TaskService {
522
904
  attempt.processIdentity = identity;
523
905
  task.state = 'running';
524
906
  task.appliedRevision = attempt.revision;
907
+ if (task.executionControl?.revision === attempt.revision)
908
+ task.executionControl.phase = 'applied';
525
909
  this.store.saveAttempt(attempt);
526
910
  this.store.saveTask(task, task.stateVersion);
527
911
  this.store.appendEvent(task.conversationId, 'task.revision_applied', { revision: attempt.revision }, task.taskId);
@@ -565,16 +949,51 @@ class TaskService {
565
949
  this.store.run('UPDATE tasks SET snapshot_json=? WHERE id=? AND active_attempt_id=?', JSON.stringify(task), task.taskId, attemptId);
566
950
  this.store.appendEvent(task.conversationId, 'task.execution', observation, task.taskId);
567
951
  const attempt = this.store.attempt(attemptId);
568
- if (observation.observedAt - (attempt.startedAt ?? attempt.processIdentity?.startedAt ?? task.createdAt) >= this.config.tasks.progressStaleMs) {
569
- this.supervise(task, 'stale_progress', 'A scheduled progress update is due. Report completed steps, the current step, and any concrete dependency being awaited. Use substantive evidence; process/tool activity alone does not prove progress. Do not volunteer reassurance that the task is not stuck or is really running.', observation.observedAt);
952
+ const startedAt = attempt.startedAt ?? attempt.processIdentity?.startedAt ?? task.createdAt;
953
+ // Reviews alone never end a task (#557): past the limit without a new
954
+ // worker report, stop the attempt through the cancellation path.
955
+ const reportedAt = Math.max(startedAt, task.latestProgress?.source === 'worker' ? task.latestProgress.observedAt : 0);
956
+ const limit = this.config.tasks.progressStaleLimitMs;
957
+ if (limit && task.state === 'running' && observation.observedAt - reportedAt >= limit) {
958
+ this.stopStalled(task, observation.observedAt - reportedAt, observation);
959
+ return;
960
+ }
961
+ if (observation.observedAt - startedAt >= this.config.tasks.progressStaleMs) {
962
+ if (observation.resultSeenAt !== undefined) {
963
+ this.supervise(task, 'result_seen_not_terminal', `The worker returned a final result ${Math.max(0, Math.round((observation.observedAt - observation.resultSeenAt) / 1000))}s ago, but ${observation.pendingBackground ?? 0} native background task(s) have not reported completion, so the task is not terminal yet. The result is accepted automatically once the background grace period passes without activity. Report what is still being awaited; stop background work that is no longer needed.`, observation.observedAt);
964
+ }
965
+ else {
966
+ this.supervise(task, 'stale_progress', 'A scheduled progress update is due. Report completed steps, the current step, and any concrete dependency being awaited. Use substantive evidence; process/tool activity alone does not prove progress. Do not volunteer reassurance that the task is not stuck or is really running.', observation.observedAt);
967
+ }
570
968
  }
571
969
  });
572
970
  }
971
+ stopStalled(task, quietMs, observation) {
972
+ const minutes = Math.round(quietMs / 60000);
973
+ const reason = `No new worker progress report for ${minutes} minute(s), past tasks.progressStaleLimitMs (${Math.round(this.config.tasks.progressStaleLimitMs / 60000)} minute(s)).` +
974
+ (observation.resultSeenAt !== undefined ? ` A final result was received but ${observation.pendingBackground ?? 0} native background task(s) never reported completion.` : '');
975
+ task.cancellation = { requestedBy: 'supervisor', requestedAt: observation.observedAt, reason };
976
+ task.state = 'cancel_requested';
977
+ task.latestProgress = { source: 'runtime', observedAt: observation.observedAt, text: `Stopping: ${reason}` };
978
+ this.store.saveTask(task, task.stateVersion);
979
+ this.store.appendEvent(task.conversationId, 'task.progress_stalled', { taskId: task.taskId, attemptId: observation.attemptId, quietMs, limitMs: this.config.tasks.progressStaleLimitMs, resultSeenAt: observation.resultSeenAt, pendingBackground: observation.pendingBackground }, task.taskId);
980
+ this.store.enqueue('interrupt', `stalled:${task.taskId}:${task.stateVersion}`, { taskId: task.taskId });
981
+ }
982
+ /** A supervisor stop is a failure with its evidence, not a user/agent cancellation.
983
+ * An unconfirmed stop keeps the cleanup code every other unconfirmed stop uses. */
984
+ stalledFailure(task, stopped = true) {
985
+ if (task.cancellation?.requestedBy !== 'supervisor')
986
+ return undefined;
987
+ const reason = task.cancellation.reason ?? 'No new worker progress report within tasks.progressStaleLimitMs.';
988
+ return stopped
989
+ ? { code: 'PROGRESS_STALLED', message: `${reason} Stopped the attempt; existing files and prior effects are retained. Inspect them before retrying.`, observedAt: task.cancellation.requestedAt }
990
+ : { code: 'CLEANUP_UNCONFIRMED', message: `${reason} Cannot verify that the attempt stopped. Retry cleanup; no unrelated processes were stopped.`, observedAt: Date.now() };
991
+ }
573
992
  supervise(task, reason, message, now = Date.now()) {
574
993
  // Keep internal inspection frequent even when user-facing reports are quiet.
575
994
  // The agent independently decides whether this review merits a message.
576
995
  const since = task.supervision?.observedAt ?? (task.activeAttemptId ? this.store.attempt(task.activeAttemptId)?.startedAt : undefined) ?? task.createdAt;
577
- if ((task.supervision || reason === 'stale_progress') && now - since < this.config.tasks.progressNotifyCooldownMs)
996
+ if ((task.supervision || reason !== 'repeated_tools') && now - since < this.config.tasks.progressNotifyCooldownMs)
578
997
  return;
579
998
  task.supervision = { id: (0, crypto_1.randomUUID)(), reason, observedAt: now, message };
580
999
  this.store.saveTask(task, task.stateVersion);
@@ -690,7 +1109,7 @@ class TaskService {
690
1109
  return result;
691
1110
  });
692
1111
  }
693
- requestInput(attemptId, generation, question, actionId) {
1112
+ requestInput(attemptId, generation, question, actionId, computerReport) {
694
1113
  (0, store_1.boundedText)(question, 4096);
695
1114
  return this.store.transaction(() => {
696
1115
  const { task, attempt } = this.active(attemptId, generation);
@@ -699,6 +1118,8 @@ class TaskService {
699
1118
  return receipt.prior;
700
1119
  if (task.state !== 'running' || task.revision !== attempt.revision)
701
1120
  throw new types_1.OrchestrationError('SUPERSEDED_QUESTION');
1121
+ if (computerReport && task.gatewayTarget?.adapter === 'computer')
1122
+ task.computerReport = computerReport;
702
1123
  task.state = 'waiting_input';
703
1124
  task.pendingQuestion = { questionId: (0, crypto_1.randomUUID)(), text: question, revision: attempt.revision };
704
1125
  this.store.saveTask(task, task.stateVersion);
@@ -721,9 +1142,16 @@ class TaskService {
721
1142
  }
722
1143
  return this.store.transaction(() => {
723
1144
  const { task, attempt } = this.active(attemptId, generation);
1145
+ if (task.gatewayTarget?.adapter === 'computer' && outcome.computerReport)
1146
+ task.computerReport = outcome.computerReport;
1147
+ if (task.gatewayTarget?.adapter === 'browser' && outcome.browserReport)
1148
+ task.browserReport = outcome.browserReport;
1149
+ if (outcome.computerReport || outcome.browserReport)
1150
+ recordAction(task, attempt.revision, this.revision(task.taskId, attempt.revision).instructions, outcome);
724
1151
  // A structured unresolved blocker is not successful task completion.
725
- if (outcome.type === 'paused' && !(task.state === 'waiting_input' && task.pendingQuestion) &&
726
- task.state !== 'interrupting' && task.state !== 'cancel_requested')
1152
+ const waitingForCommand = outcome.type === 'paused' && ['browser', 'computer'].includes(task.gatewayTarget?.adapter ?? '') && ['THINKING_WAITING_INPUT', 'COMMAND_WAITING_INPUT', 'COMPLETION_CANDIDATE', 'VERIFICATION_FAILED'].includes(outcome.computerReport?.reason ?? outcome.browserReport?.reason ?? '');
1153
+ if (outcome.type === 'paused' && !waitingForCommand && !(task.state === 'waiting_input' && task.pendingQuestion) &&
1154
+ task.state !== 'interrupting' && task.state !== 'cancel_requested' && !(task.gatewayTarget && task.revision > attempt.revision))
727
1155
  throw new types_1.OrchestrationError('STATE_CONFLICT');
728
1156
  // Keep the full final report, and preserve cancellation / new revisions / questions.
729
1157
  const blocked = outcome.type === 'completed' && task.workflow?.attemptId === attemptId &&
@@ -733,14 +1161,17 @@ class TaskService {
733
1161
  task.result = outcome.result;
734
1162
  outcome = { type: 'failed', failure: { code: 'WORKER_BLOCKED', message: task.workflow.checkpoint.findings.filter(f => f.status === 'open').map(f => f.summary).join('\n').slice(0, 4096), observedAt: Date.now() } };
735
1163
  }
1164
+ const stalled = task.state === 'cancel_requested' ? this.stalledFailure(task, outcome.type !== 'unknown') : undefined;
736
1165
  if (outcome.type !== 'completed' && outcome.type !== 'paused') {
737
- attempt.failure = outcome.failure ?? (0, failure_1.taskFailure)(undefined, outcome.type === 'stopped' ? 'WORKER_STOPPED' : 'WORKER_FAILED');
1166
+ attempt.failure = stalled ?? outcome.failure ?? (0, failure_1.taskFailure)(undefined, outcome.type === 'stopped' ? 'WORKER_STOPPED' : 'WORKER_FAILED');
738
1167
  task.failure = attempt.failure;
739
1168
  }
740
1169
  else {
741
1170
  delete task.failure;
742
1171
  }
743
1172
  if (outcome.type === 'unknown') {
1173
+ if (task.executionControl?.phase === 'pending')
1174
+ task.executionControl.phase = 'blocked';
744
1175
  attempt.state = 'unknown';
745
1176
  task.state = 'needs_reconciliation';
746
1177
  }
@@ -750,51 +1181,222 @@ class TaskService {
750
1181
  if (outcome.type === 'completed')
751
1182
  attempt.result = outcome.result;
752
1183
  if (task.state === 'cancel_requested') {
753
- task.state = outcome.type === 'completed' && task.revision === attempt.revision ? 'completed' : 'cancelled';
1184
+ task.state = outcome.type === 'completed' && task.revision === attempt.revision ? 'completed' : stalled ? 'failed' : 'cancelled';
754
1185
  }
755
1186
  else if (task.state === 'waiting_input' && task.pendingQuestion) { /* retain question; execution slot now free */ }
1187
+ else if (task.executionControl?.phase === 'pending' && task.executionControl.action === 'pause') {
1188
+ task.state = 'waiting_input';
1189
+ task.executionControl.phase = 'paused';
1190
+ delete task.failure;
1191
+ }
756
1192
  else if (task.state === 'interrupting' || task.revision > attempt.revision)
757
1193
  task.state = 'queued';
1194
+ else if (waitingForCommand) {
1195
+ task.state = 'waiting_input';
1196
+ task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: task.gatewayTarget?.adapter === 'computer' && outcome.computerReport ? (0, computer_outcome_1.computerOutcomeText)(outcome.computerReport) : task.gatewayTarget?.adapter === 'browser' && outcome.browserReport ? (0, browser_outcome_1.browserOutcomeText)(outcome.browserReport) : 'Waiting for your next command.' };
1197
+ }
758
1198
  else
759
1199
  task.state = outcome.type === 'completed' ? 'completed' : 'failed';
760
1200
  if (task.state === 'completed' && outcome.type === 'completed')
761
1201
  task.result = outcome.result;
762
1202
  }
1203
+ // A paused or superseded-revision result skips the failure branch above;
1204
+ // a stalled stop still ends failed, so it must carry the stall reason.
1205
+ if (task.state === 'failed' && stalled) {
1206
+ attempt.failure = stalled;
1207
+ task.failure = stalled;
1208
+ task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: stalled.message };
1209
+ }
1210
+ if (task.queuedCommands?.length)
1211
+ this.nextQueuedCommand(task, waitingForCommand || outcome.type === 'completed');
763
1212
  if (task.state === 'cancelled') {
764
1213
  delete task.failure;
765
1214
  task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: `Cancelled by ${task.cancellation?.requestedBy ?? 'agent'}. Existing files and prior effects are retained.` };
766
1215
  }
1216
+ if (outcome.type === 'completed' && outcome.result.unresolvedBackground) {
1217
+ this.store.appendEvent(task.conversationId, 'task.background_unresolved', { attemptId, ...outcome.result.unresolvedBackground }, task.taskId);
1218
+ }
767
1219
  if (outcome.type !== 'unknown')
768
1220
  this.pool.release(task.taskId, outcome.type === 'completed' || outcome.type === 'paused');
769
1221
  this.store.saveAttempt(attempt);
770
1222
  this.store.saveTask(task, task.stateVersion);
771
1223
  if (task.state === 'queued')
772
1224
  this.store.enqueue('schedule', `schedule:${task.taskId}:${task.stateVersion}`, { taskId: task.taskId });
773
- else if (types_1.TERMINAL_TASK_STATES.has(task.state) || task.state === 'needs_reconciliation')
1225
+ else if (types_1.TERMINAL_TASK_STATES.has(task.state) || task.state === 'needs_reconciliation' || (waitingForCommand && task.state === 'waiting_input' && task.automationController !== 'user' && task.executionControl?.phase !== 'paused'))
774
1226
  this.notify(task);
775
1227
  return task;
776
1228
  });
777
1229
  }
1230
+ /** Delivers the oldest queued direct command after a settled round, or drops
1231
+ * the queue when the round did not settle cleanly; queued text is never replayed later. */
1232
+ nextQueuedCommand(task, settled) {
1233
+ const queue = task.queuedCommands ?? [];
1234
+ if (!settled || !['waiting_input', 'completed'].includes(task.state) || task.pendingQuestion || task.executionControl?.phase === 'paused' || (0, automation_session_1.automationSession)(task)?.status === 'closed') {
1235
+ if (types_1.TERMINAL_TASK_STATES.has(task.state) || task.state === 'needs_reconciliation' || task.state === 'waiting_input') {
1236
+ const dropped = `${queue.length} queued command${queue.length === 1 ? ' was' : 's were'} not sent because the previous command did not finish normally: ${queue.map(c => JSON.stringify(c.text.slice(0, 60))).join(', ')}.`;
1237
+ task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: [task.latestProgress?.text, dropped].filter(Boolean).join('\n').slice(0, 4096) };
1238
+ delete task.queuedCommands;
1239
+ }
1240
+ return;
1241
+ }
1242
+ const [next, ...rest] = queue;
1243
+ const previous = this.revision(task.taskId, task.revision);
1244
+ task.revision++;
1245
+ this.store.run('INSERT INTO task_revisions VALUES(?,?,?)', task.taskId, task.revision, JSON.stringify({ ...previous, requestBrowserConsent: true, revision: task.revision, instructions: next.text, mode: 'interrupt_and_resume', computerInputs: undefined, answers: undefined, browserRecoveryCount: 0, guidance: undefined, guidanceBasis: undefined, directCommand: true }));
1246
+ task.queuedCommands = rest.length ? rest : undefined;
1247
+ if (!task.queuedCommands)
1248
+ delete task.queuedCommands;
1249
+ if (task.state === 'completed') {
1250
+ delete task.result;
1251
+ delete task.computerReport;
1252
+ delete task.browserReport;
1253
+ delete task.gatewayDispatch;
1254
+ }
1255
+ task.executionControl = { id: next.id, action: 'revise', revision: task.revision, phase: 'pending', requestedAt: Date.now() };
1256
+ task.state = 'queued';
1257
+ const done = task.latestProgress?.text;
1258
+ task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: [done, `Running queued command: ${JSON.stringify(next.text.slice(0, 120))}${rest.length ? ` (${rest.length} more waiting)` : ''}.`].filter(Boolean).join('\n').slice(0, 4096) };
1259
+ }
778
1260
  notify(task) {
779
1261
  const input = this.store.get('SELECT binding_id FROM conversation_inputs WHERE id=?', task.initiatingInputId);
780
1262
  const notificationId = (0, crypto_1.randomUUID)();
781
1263
  this.store.run(`INSERT INTO notifications(id,conversation_id,task_id,task_state_version,originating_binding_id) VALUES(?,?,?,?,?) ON CONFLICT(task_id,task_state_version) DO NOTHING`, notificationId, task.conversationId, task.taskId, task.stateVersion, input.binding_id);
782
1264
  this.store.enqueue('notification', `notification:${task.taskId}:${task.stateVersion}`, { conversationId: task.conversationId, taskId: task.taskId, notificationId });
783
1265
  }
1266
+ /** Parent independently checks a fresh, scoped browser observation. Never clears an uncertain mutation. */
1267
+ verifyBrowser(context, taskId, revision, requestId, evidenceId, evidence, check) {
1268
+ (0, store_1.boundedText)(evidence, 4096);
1269
+ (0, store_1.boundedText)(requestId, 256);
1270
+ (0, store_1.boundedText)(evidenceId, 128);
1271
+ return this.command(context, 'verify_browser', { taskId, revision, requestId, evidenceId, evidence }, context.execute, () => {
1272
+ const task = this.owned(taskId, context.conversationId);
1273
+ if (task.ownerPrincipalId !== context.principalId || task.gatewayTarget?.adapter !== 'browser' || !['needs_reconciliation', 'failed'].includes(task.state) || task.revision !== revision || task.gatewayDispatch?.requestId !== requestId || !task.browserReport || !(0, browser_contract_1.parentVerifiableBrowserResult)(task.browserReport))
1274
+ throw new types_1.OrchestrationError('BROWSER_VERIFICATION_UNAVAILABLE');
1275
+ if (!task.capabilities.execute || (!context.execute && !this.store.get("SELECT id FROM notifications WHERE task_id=? AND decision_id=? AND status='assigned' AND task_state_version=?", taskId, context.decisionId, task.stateVersion)))
1276
+ throw new types_1.OrchestrationError('EXECUTION_DENIED');
1277
+ check();
1278
+ const attemptId = task.activeAttemptId ?? this.store.get("SELECT id FROM task_attempts WHERE task_id=? AND revision=? AND state='ended' ORDER BY generation DESC LIMIT 1", taskId, revision)?.id;
1279
+ const attempt = attemptId ? this.store.attempt(String(attemptId)) : undefined;
1280
+ if (!attempt)
1281
+ throw new types_1.OrchestrationError('BROWSER_VERIFICATION_UNAVAILABLE');
1282
+ attempt.state = 'ended';
1283
+ task.activeAttemptId = undefined;
1284
+ task.state = task.revision > attempt.revision ? 'queued' : 'completed';
1285
+ delete task.failure;
1286
+ delete attempt.failure;
1287
+ task.browserReport.status = 'succeeded';
1288
+ task.browserReport.reason = 'PARENT_VERIFIED';
1289
+ task.browserReport.verification = { source: 'parent', evidence, at: Date.now() };
1290
+ task.result = { summary: 'Parent verified browser result: ' + evidence, artifactIds: [] };
1291
+ attempt.result = task.result;
1292
+ this.pool.release(task.taskId, true);
1293
+ this.store.saveAttempt(attempt);
1294
+ this.store.saveTask(task, task.stateVersion);
1295
+ this.store.appendEvent(task.conversationId, 'browser.parent_verified', { requestId, evidenceId, evidence }, taskId);
1296
+ if (task.state === 'queued')
1297
+ this.store.enqueue('schedule', `schedule:${task.taskId}:${task.stateVersion}`, { taskId: task.taskId });
1298
+ else
1299
+ this.notify(task);
1300
+ return task;
1301
+ }, taskId);
1302
+ }
1303
+ verifyComputer(context, taskId, revision, requestId, evidenceId, evidence, check) {
1304
+ (0, store_1.boundedText)(evidence, 4096);
1305
+ (0, store_1.boundedText)(requestId, 256);
1306
+ (0, store_1.boundedText)(evidenceId, 128);
1307
+ return this.command(context, 'verify_computer', { taskId, revision, requestId, evidenceId, evidence }, context.execute, () => {
1308
+ const task = this.owned(taskId, context.conversationId);
1309
+ if (task.ownerPrincipalId !== context.principalId || task.gatewayTarget?.adapter !== 'computer' || !['failed', 'waiting_input'].includes(task.state) || task.revision !== revision || task.gatewayDispatch?.requestId !== requestId || !task.computerReport || task.computerReport.status !== 'needs_verification')
1310
+ throw new types_1.OrchestrationError('COMPUTER_VERIFICATION_UNAVAILABLE');
1311
+ if (!task.capabilities.execute || (!context.execute && !this.store.get("SELECT id FROM notifications WHERE task_id=? AND decision_id=? AND status='assigned' AND task_state_version=?", taskId, context.decisionId, task.stateVersion)))
1312
+ throw new types_1.OrchestrationError('EXECUTION_DENIED');
1313
+ check();
1314
+ const attemptId = task.activeAttemptId ?? this.store.get("SELECT id FROM task_attempts WHERE task_id=? AND revision=? AND state='ended' ORDER BY generation DESC LIMIT 1", taskId, revision)?.id;
1315
+ const attempt = attemptId ? this.store.attempt(String(attemptId)) : undefined;
1316
+ if (!attempt)
1317
+ throw new types_1.OrchestrationError('COMPUTER_VERIFICATION_UNAVAILABLE');
1318
+ attempt.state = 'ended';
1319
+ task.activeAttemptId = undefined;
1320
+ task.state = task.revision > attempt.revision ? 'queued' : 'completed';
1321
+ delete task.failure;
1322
+ delete attempt.failure;
1323
+ task.computerReport.status = 'succeeded';
1324
+ task.computerReport.reason = 'PARENT_VERIFIED';
1325
+ task.result = { summary: 'Parent verified computer result: ' + evidence, artifactIds: [] };
1326
+ attempt.result = task.result;
1327
+ this.pool.release(task.taskId, true);
1328
+ this.store.saveAttempt(attempt);
1329
+ this.store.saveTask(task, task.stateVersion);
1330
+ this.store.appendEvent(task.conversationId, 'computer.parent_verified', { requestId, evidenceId, evidence }, taskId);
1331
+ if (task.state === 'queued')
1332
+ this.store.enqueue('schedule', `schedule:${task.taskId}:${task.stateVersion}`, { taskId: task.taskId });
1333
+ else
1334
+ this.notify(task);
1335
+ return task;
1336
+ }, taskId);
1337
+ }
1338
+ /** User-requested continuation from fresh evidence, never replay the old dispatch. */
1339
+ reconcileBrowser(context, taskId, revision, requestId, evidenceId, instructions, check) {
1340
+ (0, store_1.boundedText)(instructions, 8000);
1341
+ (0, store_1.boundedText)(requestId, 256);
1342
+ (0, store_1.boundedText)(evidenceId, 128);
1343
+ return this.command(context, 'reconcile_browser', { taskId, revision, requestId, evidenceId, instructions }, true, () => {
1344
+ const task = this.owned(taskId, context.conversationId);
1345
+ if ((0, automation_session_1.automationSession)(task)?.status === 'closed')
1346
+ throw new types_1.OrchestrationError('AUTOMATION_SESSION_CLOSED');
1347
+ if (!context.execute || !task.capabilities.execute || task.ownerPrincipalId !== context.principalId)
1348
+ throw new types_1.OrchestrationError('EXECUTION_DENIED');
1349
+ if (task.gatewayTarget?.adapter !== 'browser' || task.state !== 'needs_reconciliation' || !task.activeAttemptId || task.revision !== revision || task.gatewayDispatch?.requestId !== requestId)
1350
+ throw new types_1.OrchestrationError('STATE_CONFLICT');
1351
+ const resolution = check(), attempt = this.store.attempt(task.activeAttemptId);
1352
+ attempt.state = 'ended';
1353
+ this.store.saveAttempt(attempt);
1354
+ this.pool.release(taskId, false);
1355
+ const previous = this.revision(taskId, revision);
1356
+ task.revision++;
1357
+ task.activeAttemptId = undefined;
1358
+ task.state = 'queued';
1359
+ delete task.failure;
1360
+ delete task.browserReport;
1361
+ delete task.gatewayDispatch;
1362
+ delete task.pendingQuestion;
1363
+ task.executionControl = undefined;
1364
+ this.store.run('INSERT INTO task_revisions VALUES(?,?,?)', taskId, task.revision, JSON.stringify({ ...previous, requestBrowserConsent: true, revision: task.revision, instructions, mode: 'when_ready', originatingInputId: context.inputId, answers: undefined, guidance: undefined, guidanceBasis: undefined, browserRecoveryCount: 0 }));
1365
+ this.store.saveTask(task, task.stateVersion);
1366
+ this.store.appendEvent(task.conversationId, 'browser.continuation_authorized', { requestId, evidenceId, resolution, inputId: context.inputId }, taskId);
1367
+ this.store.enqueue('schedule', `schedule:${task.taskId}:${task.stateVersion}`, { taskId });
1368
+ return task;
1369
+ }, taskId);
1370
+ }
784
1371
  /** Offline operator workflow only; not exposed through conversation tools.
785
1372
  * Caller holds the instance lock and has checked liveness/side effects. */
786
1373
  reconcile(taskId, state, evidence) {
787
1374
  (0, store_1.boundedText)(evidence, 4096);
788
- if (!['queued', 'failed', 'cancelled'].includes(state))
1375
+ if (!['queued', 'failed', 'cancelled', 'waiting_input'].includes(state))
789
1376
  throw new types_1.OrchestrationError('INVALID_RECONCILIATION');
790
1377
  return this.store.transaction(() => {
791
1378
  const task = this.store.task(taskId);
792
1379
  if (!task || task.state !== 'needs_reconciliation' || !task.activeAttemptId)
793
1380
  throw new types_1.OrchestrationError('RECONCILIATION_NOT_REQUIRED');
1381
+ if (['queued', 'waiting_input'].includes(state) && (0, automation_session_1.automationSession)(task)?.status === 'closed')
1382
+ throw new types_1.OrchestrationError('AUTOMATION_SESSION_CLOSED');
794
1383
  const attempt = this.store.attempt(task.activeAttemptId);
795
1384
  attempt.state = 'ended';
796
1385
  task.activeAttemptId = undefined;
797
1386
  task.state = state;
1387
+ if (state === 'cancelled')
1388
+ task.failure = undefined;
1389
+ if (state === 'queued' && task.gatewayTarget) {
1390
+ task.gatewayDispatch = undefined;
1391
+ task.failure = undefined;
1392
+ task.computerReport = undefined;
1393
+ }
1394
+ if (state === 'waiting_input') {
1395
+ task.failure = undefined;
1396
+ task.computerReport = { steps: 0, ...task.computerReport, status: 'needs_input', reason: 'COMMAND_WAITING_INPUT' };
1397
+ if (task.automationSession)
1398
+ task.automationSession = { ...task.automationSession, status: 'idle', idleSince: Date.now() };
1399
+ }
798
1400
  this.pool.release(taskId, false);
799
1401
  task.pendingQuestion = undefined;
800
1402
  task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: `Operator reconciliation: ${evidence}` };