vellum 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (361) hide show
  1. package/README.md +15 -2
  2. package/bun.lock +5 -2
  3. package/package.json +4 -2
  4. package/scripts/capture-x-graphql.ts +562 -0
  5. package/scripts/ipc/check-swift-decoder-drift.ts +2 -1
  6. package/scripts/test.sh +5 -0
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +161 -34
  8. package/src/__tests__/account-registry.test.ts +2 -1
  9. package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
  10. package/src/__tests__/app-bundler.test.ts +12 -33
  11. package/src/__tests__/asset-materialize-tool.test.ts +16 -15
  12. package/src/__tests__/asset-search-tool.test.ts +23 -22
  13. package/src/__tests__/attachments-store.test.ts +56 -127
  14. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
  15. package/src/__tests__/browser-skill-endstate.test.ts +5 -8
  16. package/src/__tests__/call-bridge.test.ts +385 -0
  17. package/src/__tests__/call-constants.test.ts +40 -0
  18. package/src/__tests__/call-orchestrator.test.ts +454 -0
  19. package/src/__tests__/call-recovery.test.ts +518 -0
  20. package/src/__tests__/call-routes-http.test.ts +459 -0
  21. package/src/__tests__/call-state-machine.test.ts +143 -0
  22. package/src/__tests__/call-state.test.ts +133 -0
  23. package/src/__tests__/call-store.test.ts +691 -0
  24. package/src/__tests__/cli-discover.test.ts +1 -1
  25. package/src/__tests__/commit-message-enrichment-service.test.ts +550 -0
  26. package/src/__tests__/compaction.benchmark.test.ts +176 -0
  27. package/src/__tests__/computer-use-tools.test.ts +250 -0
  28. package/src/__tests__/config-schema.test.ts +348 -3
  29. package/src/__tests__/conflict-store.test.ts +2 -1
  30. package/src/__tests__/contacts-tools.test.ts +331 -0
  31. package/src/__tests__/conversation-store.test.ts +30 -32
  32. package/src/__tests__/credential-security-invariants.test.ts +4 -0
  33. package/src/__tests__/date-context.test.ts +373 -0
  34. package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
  35. package/src/__tests__/doordash-session.test.ts +9 -0
  36. package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
  37. package/src/__tests__/followup-tools.test.ts +303 -0
  38. package/src/__tests__/handlers-twitter-config.test.ts +718 -0
  39. package/src/__tests__/intent-routing.test.ts +64 -57
  40. package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
  41. package/src/__tests__/ipc-snapshot.test.ts +96 -28
  42. package/src/__tests__/llm-usage-store.test.ts +3 -8
  43. package/src/__tests__/media-generate-image.test.ts +1 -1
  44. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  45. package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
  46. package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
  47. package/src/__tests__/playbook-tools.test.ts +342 -0
  48. package/src/__tests__/profile-compiler.test.ts +2 -1
  49. package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
  50. package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
  51. package/src/__tests__/recurrence-engine.test.ts +69 -0
  52. package/src/__tests__/recurrence-types.test.ts +71 -0
  53. package/src/__tests__/registry.test.ts +17 -10
  54. package/src/__tests__/relay-server.test.ts +633 -0
  55. package/src/__tests__/reminder-store.test.ts +6 -3
  56. package/src/__tests__/reminder.test.ts +43 -77
  57. package/src/__tests__/run-orchestrator-assistant-events.test.ts +222 -0
  58. package/src/__tests__/run-orchestrator.test.ts +7 -7
  59. package/src/__tests__/runtime-attachment-metadata.test.ts +19 -20
  60. package/src/__tests__/runtime-runs-http.test.ts +5 -23
  61. package/src/__tests__/runtime-runs.test.ts +11 -11
  62. package/src/__tests__/schedule-store.test.ts +482 -0
  63. package/src/__tests__/schedule-tools.test.ts +700 -0
  64. package/src/__tests__/scheduler-recurrence.test.ts +329 -0
  65. package/src/__tests__/server-history-render.test.ts +14 -13
  66. package/src/__tests__/session-error.test.ts +28 -0
  67. package/src/__tests__/session-init.benchmark.test.ts +462 -0
  68. package/src/__tests__/session-queue.test.ts +89 -16
  69. package/src/__tests__/session-runtime-assembly.test.ts +161 -0
  70. package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
  71. package/src/__tests__/signup-e2e.test.ts +2 -1
  72. package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
  73. package/src/__tests__/skill-script-runner.test.ts +159 -0
  74. package/src/__tests__/speaker-identification.test.ts +52 -0
  75. package/src/__tests__/subagent-manager-notify.test.ts +42 -10
  76. package/src/__tests__/subagent-tools.test.ts +141 -41
  77. package/src/__tests__/task-compiler.test.ts +2 -1
  78. package/src/__tests__/task-runner.test.ts +2 -1
  79. package/src/__tests__/task-scheduler.test.ts +2 -1
  80. package/src/__tests__/task-tools.test.ts +49 -56
  81. package/src/__tests__/tool-audit-listener.test.ts +1 -0
  82. package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
  83. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
  84. package/src/__tests__/tool-executor.test.ts +13 -17
  85. package/src/__tests__/turn-commit.test.ts +273 -2
  86. package/src/__tests__/twilio-provider.test.ts +143 -0
  87. package/src/__tests__/twilio-routes.test.ts +789 -0
  88. package/src/__tests__/twitter-auth-handler.test.ts +581 -0
  89. package/src/__tests__/view-image-tool.test.ts +217 -0
  90. package/src/__tests__/workspace-git-service.test.ts +403 -0
  91. package/src/__tests__/workspace-heartbeat-service.test.ts +141 -2
  92. package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
  93. package/src/bundler/app-bundler.ts +35 -14
  94. package/src/calls/call-bridge.ts +95 -0
  95. package/src/calls/call-constants.ts +48 -0
  96. package/src/calls/call-domain.ts +276 -0
  97. package/src/calls/call-orchestrator.ts +390 -0
  98. package/src/calls/call-recovery.ts +207 -0
  99. package/src/calls/call-state-machine.ts +68 -0
  100. package/src/calls/call-state.ts +64 -0
  101. package/src/calls/call-store.ts +416 -0
  102. package/src/calls/relay-server.ts +335 -0
  103. package/src/calls/speaker-identification.ts +213 -0
  104. package/src/calls/twilio-config.ts +34 -0
  105. package/src/calls/twilio-provider.ts +173 -0
  106. package/src/calls/twilio-routes.ts +250 -0
  107. package/src/calls/types.ts +37 -0
  108. package/src/calls/voice-provider.ts +14 -0
  109. package/src/cli/config-commands.ts +334 -0
  110. package/src/cli/core-commands.ts +776 -0
  111. package/src/cli/doordash.ts +256 -25
  112. package/src/cli/ipc-client.ts +82 -0
  113. package/src/cli/map.ts +246 -0
  114. package/src/cli/twitter.ts +575 -0
  115. package/src/cli.ts +7 -5
  116. package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
  117. package/src/commands/cc-command-registry.ts +209 -0
  118. package/src/config/bundled-skills/contacts/SKILL.md +39 -0
  119. package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
  120. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
  121. package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
  122. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
  123. package/src/config/bundled-skills/document/SKILL.md +18 -0
  124. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  125. package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
  126. package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
  127. package/src/config/bundled-skills/doordash/SKILL.md +163 -0
  128. package/src/config/bundled-skills/followups/SKILL.md +32 -0
  129. package/src/config/bundled-skills/followups/TOOLS.json +100 -0
  130. package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
  131. package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
  132. package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
  133. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  134. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -24
  135. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
  136. package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
  137. package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
  138. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
  139. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
  140. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
  141. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
  142. package/src/config/bundled-skills/reminder/SKILL.md +20 -0
  143. package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
  144. package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
  145. package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
  146. package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
  147. package/src/config/bundled-skills/schedule/SKILL.md +74 -0
  148. package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
  149. package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
  150. package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
  151. package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
  152. package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
  153. package/src/config/bundled-skills/subagent/SKILL.md +25 -0
  154. package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
  155. package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
  156. package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
  157. package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
  158. package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
  159. package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
  160. package/src/config/bundled-skills/tasks/SKILL.md +28 -0
  161. package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
  162. package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
  163. package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
  164. package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
  165. package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
  166. package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
  167. package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
  168. package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
  169. package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
  170. package/src/config/bundled-skills/twitter/SKILL.md +134 -0
  171. package/src/config/bundled-skills/watcher/SKILL.md +27 -0
  172. package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
  173. package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
  174. package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
  175. package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
  176. package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
  177. package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
  178. package/src/config/defaults.ts +44 -0
  179. package/src/config/loader.ts +4 -1
  180. package/src/config/schema.ts +218 -1
  181. package/src/config/system-prompt.ts +100 -6
  182. package/src/config/templates/IDENTITY.md +7 -0
  183. package/src/config/types.ts +5 -0
  184. package/src/contacts/contact-store.ts +4 -4
  185. package/src/daemon/assistant-attachments.ts +10 -0
  186. package/src/daemon/classifier.ts +3 -1
  187. package/src/daemon/computer-use-session.ts +3 -1
  188. package/src/daemon/date-context.ts +136 -0
  189. package/src/daemon/handlers/apps.ts +16 -1
  190. package/src/daemon/handlers/browser.ts +54 -0
  191. package/src/daemon/handlers/computer-use.ts +7 -1
  192. package/src/daemon/handlers/config.ts +192 -4
  193. package/src/daemon/handlers/diagnostics.ts +5 -1
  194. package/src/daemon/handlers/documents.ts +18 -29
  195. package/src/daemon/handlers/home-base.ts +5 -1
  196. package/src/daemon/handlers/index.ts +40 -271
  197. package/src/daemon/handlers/misc.ts +9 -1
  198. package/src/daemon/handlers/publish.ts +6 -1
  199. package/src/daemon/handlers/sessions.ts +65 -12
  200. package/src/daemon/handlers/shared.ts +36 -1
  201. package/src/daemon/handlers/signing.ts +37 -0
  202. package/src/daemon/handlers/skills.ts +20 -6
  203. package/src/daemon/handlers/subagents.ts +8 -3
  204. package/src/daemon/handlers/twitter-auth.ts +169 -0
  205. package/src/daemon/handlers/work-items.ts +495 -39
  206. package/src/daemon/ipc-contract-inventory.json +40 -4
  207. package/src/daemon/ipc-contract.ts +185 -37
  208. package/src/daemon/ipc-protocol.ts +7 -2
  209. package/src/daemon/lifecycle.ts +48 -5
  210. package/src/daemon/main.ts +10 -4
  211. package/src/daemon/ride-shotgun-handler.ts +74 -10
  212. package/src/daemon/server.ts +144 -29
  213. package/src/daemon/session-agent-loop.ts +887 -0
  214. package/src/daemon/session-attachments.ts +28 -5
  215. package/src/daemon/session-error.ts +24 -3
  216. package/src/daemon/session-lifecycle.ts +147 -0
  217. package/src/daemon/session-media-retry.ts +147 -0
  218. package/src/daemon/session-messaging.ts +145 -0
  219. package/src/daemon/session-notifiers.ts +164 -0
  220. package/src/daemon/session-process.ts +2 -2
  221. package/src/daemon/session-queue-manager.ts +1 -0
  222. package/src/daemon/session-runtime-assembly.ts +52 -0
  223. package/src/daemon/session-skill-tools.ts +124 -5
  224. package/src/daemon/session-slash.ts +3 -0
  225. package/src/daemon/session-surfaces.ts +77 -2
  226. package/src/daemon/session-tool-setup.ts +222 -2
  227. package/src/daemon/session-usage.ts +0 -2
  228. package/src/daemon/session.ts +114 -1365
  229. package/src/daemon/video-thumbnail.ts +60 -0
  230. package/src/doordash/client.ts +121 -27
  231. package/src/doordash/queries.ts +1 -2
  232. package/src/export/formatter.ts +3 -1
  233. package/src/followups/followup-store.ts +4 -2
  234. package/src/followups/types.ts +6 -0
  235. package/src/hooks/templates.ts +1 -1
  236. package/src/index.ts +32 -1151
  237. package/src/media/gemini-image-service.ts +1 -1
  238. package/src/memory/attachments-store.ts +28 -83
  239. package/src/memory/channel-delivery-store.ts +7 -21
  240. package/src/memory/clarification-resolver.ts +6 -5
  241. package/src/memory/contradiction-checker.ts +3 -2
  242. package/src/memory/conversation-key-store.ts +10 -29
  243. package/src/memory/conversation-store.ts +2 -1
  244. package/src/memory/db.ts +362 -2
  245. package/src/memory/entity-extractor.ts +6 -3
  246. package/src/memory/items-extractor.ts +5 -4
  247. package/src/memory/jobs-store.ts +3 -2
  248. package/src/memory/llm-usage-store.ts +1 -2
  249. package/src/memory/runs-store.ts +1 -2
  250. package/src/memory/schema.ts +65 -2
  251. package/src/messaging/style-analyzer.ts +3 -2
  252. package/src/messaging/thread-summarizer.ts +8 -12
  253. package/src/messaging/triage-engine.ts +4 -2
  254. package/src/providers/openrouter/client.ts +20 -0
  255. package/src/providers/registry.ts +8 -0
  256. package/src/runtime/http-server.ts +277 -25
  257. package/src/runtime/http-types.ts +0 -2
  258. package/src/runtime/routes/attachment-routes.ts +5 -6
  259. package/src/runtime/routes/call-routes.ts +140 -0
  260. package/src/runtime/routes/channel-routes.ts +12 -19
  261. package/src/runtime/routes/conversation-routes.ts +5 -9
  262. package/src/runtime/routes/run-routes.ts +4 -8
  263. package/src/runtime/run-orchestrator.ts +39 -6
  264. package/src/schedule/recurrence-engine.ts +138 -0
  265. package/src/schedule/recurrence-types.ts +67 -0
  266. package/src/schedule/schedule-store.ts +102 -57
  267. package/src/schedule/scheduler.ts +9 -6
  268. package/src/security/oauth2.ts +29 -4
  269. package/src/security/secret-allowlist.ts +46 -0
  270. package/src/skills/clawhub.ts +1 -1
  271. package/src/subagent/manager.ts +40 -8
  272. package/src/swarm/backend-claude-code.ts +64 -9
  273. package/src/swarm/worker-prompts.ts +2 -1
  274. package/src/tasks/SPEC.md +34 -28
  275. package/src/tasks/ephemeral-permissions.ts +16 -7
  276. package/src/tasks/task-compiler.ts +5 -4
  277. package/src/tasks/task-runner.ts +10 -5
  278. package/src/tasks/task-scheduler.ts +1 -1
  279. package/src/tasks/tool-sanitizer.ts +36 -0
  280. package/src/tools/assets/search.ts +4 -4
  281. package/src/tools/browser/api-map.ts +220 -0
  282. package/src/tools/browser/auto-navigate.ts +270 -0
  283. package/src/tools/browser/browser-execution.ts +2 -1
  284. package/src/tools/browser/browser-manager.ts +2 -2
  285. package/src/tools/browser/network-recorder.ts +5 -4
  286. package/src/tools/browser/x-auto-navigate.ts +207 -0
  287. package/src/tools/calls/call-end.ts +67 -0
  288. package/src/tools/calls/call-start.ts +73 -0
  289. package/src/tools/calls/call-status.ts +81 -0
  290. package/src/tools/claude-code/claude-code.ts +77 -11
  291. package/src/tools/contacts/contact-merge.ts +46 -78
  292. package/src/tools/contacts/contact-search.ts +35 -79
  293. package/src/tools/contacts/contact-upsert.ts +35 -108
  294. package/src/tools/credentials/vault.ts +21 -5
  295. package/src/tools/document/document-tool.ts +71 -144
  296. package/src/tools/executor.ts +129 -10
  297. package/src/tools/followups/followup_create.ts +46 -88
  298. package/src/tools/followups/followup_list.ts +34 -74
  299. package/src/tools/followups/followup_resolve.ts +31 -66
  300. package/src/tools/host-terminal/cli-discover.ts +2 -1
  301. package/src/tools/host-terminal/host-shell.ts +10 -0
  302. package/src/tools/memory/handlers.ts +5 -4
  303. package/src/tools/network/__tests__/web-search.test.ts +427 -0
  304. package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
  305. package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
  306. package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
  307. package/src/tools/network/web-fetch.ts +18 -6
  308. package/src/tools/playbooks/index.ts +4 -5
  309. package/src/tools/playbooks/playbook-create.ts +3 -47
  310. package/src/tools/playbooks/playbook-delete.ts +1 -25
  311. package/src/tools/playbooks/playbook-list.ts +1 -28
  312. package/src/tools/playbooks/playbook-update.ts +3 -51
  313. package/src/tools/registry.ts +2 -4
  314. package/src/tools/reminder/reminder.ts +5 -78
  315. package/src/tools/schedule/create.ts +69 -74
  316. package/src/tools/schedule/delete.ts +21 -47
  317. package/src/tools/schedule/list.ts +55 -74
  318. package/src/tools/schedule/update.ts +77 -84
  319. package/src/tools/subagent/abort.ts +29 -58
  320. package/src/tools/subagent/message.ts +30 -63
  321. package/src/tools/subagent/read.ts +53 -84
  322. package/src/tools/subagent/spawn.ts +43 -82
  323. package/src/tools/subagent/status.ts +42 -71
  324. package/src/tools/swarm/delegate.ts +2 -1
  325. package/src/tools/tasks/index.ts +8 -6
  326. package/src/tools/tasks/task-delete.ts +69 -56
  327. package/src/tools/tasks/task-list.ts +31 -52
  328. package/src/tools/tasks/task-run.ts +74 -102
  329. package/src/tools/tasks/task-save.ts +33 -65
  330. package/src/tools/tasks/work-item-enqueue.ts +192 -134
  331. package/src/tools/tasks/work-item-list.ts +33 -78
  332. package/src/tools/tasks/work-item-remove.ts +60 -0
  333. package/src/tools/tasks/work-item-update.ts +114 -0
  334. package/src/tools/terminal/backends/native.ts +3 -1
  335. package/src/tools/tool-manifest.ts +20 -74
  336. package/src/tools/types.ts +6 -0
  337. package/src/tools/ui-surface/definitions.ts +6 -1
  338. package/src/tools/watch/screen-watch.ts +3 -1
  339. package/src/tools/watcher/create.ts +52 -98
  340. package/src/tools/watcher/delete.ts +20 -46
  341. package/src/tools/watcher/digest.ts +36 -70
  342. package/src/tools/watcher/list.ts +49 -79
  343. package/src/tools/watcher/update.ts +45 -91
  344. package/src/twitter/client.ts +690 -0
  345. package/src/twitter/session.ts +91 -0
  346. package/src/usage/types.ts +0 -1
  347. package/src/util/truncate.ts +6 -0
  348. package/src/watcher/providers/slack.ts +2 -1
  349. package/src/watcher/watcher-store.ts +3 -2
  350. package/src/work-items/work-item-store.ts +236 -2
  351. package/src/workspace/commit-message-enrichment-service.ts +284 -0
  352. package/src/workspace/commit-message-provider.ts +95 -0
  353. package/src/workspace/git-service.ts +272 -52
  354. package/src/workspace/heartbeat-service.ts +70 -13
  355. package/src/workspace/provider-commit-message-generator.ts +242 -0
  356. package/src/workspace/turn-commit.ts +100 -51
  357. package/src/tools/contacts/index.ts +0 -4
  358. package/src/tools/document/index.ts +0 -5
  359. package/src/tools/followups/index.ts +0 -3
  360. package/src/tools/subagent/index.ts +0 -5
  361. /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
@@ -2,21 +2,30 @@ import * as net from 'node:net';
2
2
  import type {
3
3
  WorkItemsListRequest,
4
4
  WorkItemGetRequest,
5
- WorkItemCreateRequest,
6
5
  WorkItemUpdateRequest,
7
6
  WorkItemCompleteRequest,
7
+ WorkItemDeleteRequest,
8
8
  WorkItemRunTaskRequest,
9
+ WorkItemOutputRequest,
10
+ WorkItemPreflightRequest,
11
+ WorkItemApprovePermissionsRequest,
12
+ WorkItemCancelRequest,
9
13
  } from '../ipc-protocol.js';
10
- import { log, type HandlerContext } from './shared.js';
14
+ import { log, defineHandlers, type HandlerContext } from './shared.js';
15
+ import { getSubagentManager } from '../../subagent/index.js';
11
16
  import {
12
- createWorkItem,
17
+ deleteWorkItem,
13
18
  getWorkItem,
14
19
  listWorkItems,
15
20
  updateWorkItem,
16
21
  type WorkItemStatus,
17
22
  } from '../../work-items/work-item-store.js';
18
- import { getTask } from '../../tasks/task-store.js';
23
+ import { getTask, getTaskRun } from '../../tasks/task-store.js';
19
24
  import { runTask } from '../../tasks/task-runner.js';
25
+ import { getMessages } from '../../memory/conversation-store.js';
26
+ import { classifyRisk, check } from '../../permissions/checker.js';
27
+ import { truncate } from '../../util/truncate.js';
28
+ import { sanitizeToolList, getRegisteredToolNames, getToolDescription } from '../../tasks/tool-sanitizer.js';
20
29
 
21
30
  export function handleWorkItemsList(
22
31
  msg: WorkItemsListRequest,
@@ -36,34 +45,20 @@ export function handleWorkItemGet(
36
45
  ctx.send(socket, { type: 'work_item_get_response', item });
37
46
  }
38
47
 
39
- export function handleWorkItemCreate(
40
- msg: WorkItemCreateRequest,
41
- socket: net.Socket,
42
- ctx: HandlerContext,
43
- ): void {
44
- const task = getTask(msg.taskId);
45
- if (!task) {
46
- ctx.send(socket, { type: 'error', message: `Task not found: ${msg.taskId}` });
47
- return;
48
- }
49
- const item = createWorkItem({
50
- taskId: msg.taskId,
51
- title: msg.title ?? task.title,
52
- notes: msg.notes,
53
- priorityTier: msg.priorityTier,
54
- sortIndex: msg.sortIndex,
55
- });
56
- ctx.send(socket, { type: 'work_item_create_response', item });
57
-
58
- // Notify all connected clients so open Task Queue views refresh immediately
59
- broadcastWorkItemStatus(ctx, item.id);
60
- }
61
-
62
48
  export function handleWorkItemUpdate(
63
49
  msg: WorkItemUpdateRequest,
64
50
  socket: net.Socket,
65
51
  ctx: HandlerContext,
66
52
  ): void {
53
+ // Don't allow overwriting a cancelled status (e.g. from a late chat-completion observer)
54
+ if (msg.status !== undefined) {
55
+ const existing = getWorkItem(msg.id);
56
+ if (existing?.status === 'cancelled' && msg.status !== 'cancelled') {
57
+ ctx.send(socket, { type: 'work_item_update_response', item: existing });
58
+ return;
59
+ }
60
+ }
61
+
67
62
  const updates: Record<string, unknown> = {};
68
63
  if (msg.title !== undefined) updates.title = msg.title;
69
64
  if (msg.notes !== undefined) updates.notes = msg.notes;
@@ -78,6 +73,7 @@ export function handleWorkItemUpdate(
78
73
  // (e.g. priority/sort changes made by one client are reflected everywhere)
79
74
  if (item) {
80
75
  broadcastWorkItemStatus(ctx, item.id);
76
+ ctx.broadcast({ type: 'tasks_changed' });
81
77
  }
82
78
  }
83
79
 
@@ -86,6 +82,18 @@ export function handleWorkItemComplete(
86
82
  socket: net.Socket,
87
83
  ctx: HandlerContext,
88
84
  ): void {
85
+ // Only allow completion from the 'awaiting_review' state — this ensures
86
+ // items go through the full run lifecycle before being marked done.
87
+ const existing = getWorkItem(msg.id);
88
+ if (!existing) {
89
+ ctx.send(socket, { type: 'error', message: `Work item not found: ${msg.id}` });
90
+ return;
91
+ }
92
+ if (existing.status !== 'awaiting_review') {
93
+ ctx.send(socket, { type: 'error', message: `Cannot complete work item: status is '${existing.status}', expected 'awaiting_review'` });
94
+ return;
95
+ }
96
+
89
97
  const item = updateWorkItem(msg.id, { status: 'done' }) ?? null;
90
98
  ctx.send(socket, { type: 'work_item_update_response', item });
91
99
  if (item) {
@@ -102,9 +110,25 @@ export function handleWorkItemComplete(
102
110
  updatedAt: item.updatedAt,
103
111
  },
104
112
  });
113
+ ctx.broadcast({ type: 'tasks_changed' });
105
114
  }
106
115
  }
107
116
 
117
+ export function handleWorkItemDelete(
118
+ msg: WorkItemDeleteRequest,
119
+ socket: net.Socket,
120
+ ctx: HandlerContext,
121
+ ): void {
122
+ const existing = getWorkItem(msg.id);
123
+ if (!existing) {
124
+ ctx.send(socket, { type: 'work_item_delete_response', id: msg.id, success: false });
125
+ return;
126
+ }
127
+ deleteWorkItem(msg.id);
128
+ ctx.send(socket, { type: 'work_item_delete_response', id: msg.id, success: true });
129
+ ctx.broadcast({ type: 'tasks_changed' });
130
+ }
131
+
108
132
  function broadcastWorkItemStatus(ctx: HandlerContext, id: string): void {
109
133
  const item = getWorkItem(id);
110
134
  if (item) {
@@ -124,6 +148,212 @@ function broadcastWorkItemStatus(ctx: HandlerContext, id: string): void {
124
148
  }
125
149
  }
126
150
 
151
+ /** Extract plain text from a message content string (handles JSON content block arrays). */
152
+ function extractTextFromContent(content: string): string {
153
+ try {
154
+ const parsed = JSON.parse(content);
155
+ if (Array.isArray(parsed)) {
156
+ return parsed
157
+ .filter((b: { type: string }) => b.type === 'text')
158
+ .map((b: { text: string }) => b.text)
159
+ .join('\n');
160
+ }
161
+ } catch {
162
+ // Plain text content — use as-is
163
+ }
164
+ return content;
165
+ }
166
+
167
+ /** Extract tool_result blocks from a user message's content. */
168
+ function extractToolResults(content: string): Array<{ tool_use_id: string; content: string; is_error?: boolean }> {
169
+ try {
170
+ const parsed = JSON.parse(content);
171
+ if (Array.isArray(parsed)) {
172
+ return parsed
173
+ .filter((b: { type: string }) => b.type === 'tool_result')
174
+ .map((b: { tool_use_id: string; content?: string | Array<{ type: string; text?: string }>; is_error?: boolean }) => {
175
+ let text = '';
176
+ if (typeof b.content === 'string') {
177
+ text = b.content;
178
+ } else if (Array.isArray(b.content)) {
179
+ text = b.content
180
+ .filter((c) => c.type === 'text' && c.text)
181
+ .map((c) => c.text!)
182
+ .join('\n');
183
+ }
184
+ return { tool_use_id: b.tool_use_id, content: text, is_error: b.is_error };
185
+ });
186
+ }
187
+ } catch {
188
+ // Not JSON — no tool_result blocks
189
+ }
190
+ return [];
191
+ }
192
+
193
+ /**
194
+ * Build highlights from tool outcomes in the conversation. Scans for
195
+ * tool_use (assistant) and tool_result (user) pairs, extracting concrete
196
+ * outcomes like errors, file paths, and URLs.
197
+ */
198
+ function extractToolHighlights(
199
+ msgs: Array<{ role: string; content: string }>,
200
+ maxHighlights: number,
201
+ ): string[] {
202
+ const highlights: string[] = [];
203
+
204
+ // Build a map of tool_use_id -> tool name from assistant messages
205
+ const toolNameById = new Map<string, string>();
206
+ for (const m of msgs) {
207
+ if (m.role !== 'assistant') continue;
208
+ try {
209
+ const parsed = JSON.parse(m.content);
210
+ if (Array.isArray(parsed)) {
211
+ for (const block of parsed) {
212
+ if (block.type === 'tool_use' && block.id && block.name) {
213
+ toolNameById.set(block.id, block.name);
214
+ }
215
+ }
216
+ }
217
+ } catch { /* skip */ }
218
+ }
219
+
220
+ // Scan tool_result messages in reverse order (most recent first)
221
+ for (let i = msgs.length - 1; i >= 0 && highlights.length < maxHighlights; i--) {
222
+ const m = msgs[i];
223
+ if (m.role !== 'user') continue;
224
+
225
+ const results = extractToolResults(m.content);
226
+ for (const result of results) {
227
+ if (highlights.length >= maxHighlights) break;
228
+
229
+ const toolName = toolNameById.get(result.tool_use_id) ?? 'tool';
230
+ const resultText = result.content.trim();
231
+
232
+ if (result.is_error) {
233
+ // Always surface errors
234
+ const errorSnippet = truncate(resultText, 200, '...');
235
+ highlights.push(`- ${toolName}: Error — ${errorSnippet}`);
236
+ } else if (resultText) {
237
+ // Extract notable signal from successful results: file paths, URLs, or
238
+ // a short summary of what happened
239
+ const firstLine = resultText.split('\n')[0].trim();
240
+ if (firstLine.length > 0 && firstLine.length <= 200) {
241
+ highlights.push(`- ${toolName}: ${firstLine}`);
242
+ } else if (firstLine.length > 200) {
243
+ highlights.push(`- ${toolName}: ${truncate(firstLine, 200, '...')}`);
244
+ }
245
+ }
246
+ }
247
+ }
248
+
249
+ return highlights;
250
+ }
251
+
252
+ export function handleWorkItemOutput(
253
+ msg: WorkItemOutputRequest,
254
+ socket: net.Socket,
255
+ ctx: HandlerContext,
256
+ ): void {
257
+ try {
258
+ const workItem = getWorkItem(msg.id);
259
+ if (!workItem) {
260
+ ctx.send(socket, { type: 'work_item_output_response', id: msg.id, success: false, error: 'Work item not found' });
261
+ return;
262
+ }
263
+
264
+ // Use the task run's conversationId as the authoritative source. This
265
+ // ensures we read from the actual run's conversation, not stale references
266
+ // on the work item.
267
+ let conversationId: string | null = null;
268
+ let completedAt: number | null = null;
269
+
270
+ if (workItem.lastRunId) {
271
+ const run = getTaskRun(workItem.lastRunId);
272
+ if (run) {
273
+ conversationId = run.conversationId;
274
+ completedAt = run.finishedAt != null ? Math.floor(run.finishedAt / 1000) : null;
275
+ }
276
+ }
277
+
278
+ // Fall back to the work item's stored conversationId if the run lookup
279
+ // didn't yield one (e.g. run record was deleted but work item still has
280
+ // the reference).
281
+ if (!conversationId) {
282
+ conversationId = workItem.lastRunConversationId;
283
+ }
284
+
285
+ if (!conversationId) {
286
+ ctx.send(socket, { type: 'work_item_output_response', id: msg.id, success: false, error: 'This task has not been run yet. No output is available.' });
287
+ return;
288
+ }
289
+
290
+ let summary = '';
291
+ let highlights: string[] = [];
292
+
293
+ const msgs = getMessages(conversationId);
294
+
295
+ // Find the last assistant message with text content (not tool calls).
296
+ // Skip messages that are purely about task management rather than
297
+ // reporting what the run actually did.
298
+ for (let i = msgs.length - 1; i >= 0; i--) {
299
+ const m = msgs[i];
300
+ if (m.role !== 'assistant') continue;
301
+
302
+ const text = extractTextFromContent(m.content);
303
+ if (!text.trim()) continue;
304
+
305
+ summary = truncate(text, 2000, '');
306
+
307
+ // Extract bullet points from the assistant's prose
308
+ const lines = text.split('\n');
309
+ for (const line of lines) {
310
+ const trimmed = line.trim();
311
+ if ((trimmed.startsWith('-') || trimmed.startsWith('*')) && trimmed.length > 2) {
312
+ highlights.push(trimmed);
313
+ if (highlights.length >= 5) break;
314
+ }
315
+ }
316
+ break;
317
+ }
318
+
319
+ // If we didn't get enough highlights from the assistant prose, supplement
320
+ // with concrete tool outcomes from the conversation.
321
+ if (highlights.length < 5) {
322
+ const toolHighlights = extractToolHighlights(msgs, 5 - highlights.length);
323
+ highlights = [...highlights, ...toolHighlights];
324
+ }
325
+
326
+ // If there's no assistant summary at all, synthesize one from tool results
327
+ // so the user still sees what happened.
328
+ if (!summary && msgs.length > 0) {
329
+ const toolHighlights = extractToolHighlights(msgs, 10);
330
+ if (toolHighlights.length > 0) {
331
+ summary = 'Task completed. Tool outcomes:\n' + toolHighlights.join('\n');
332
+ // Use the tool highlights as the main highlights too
333
+ highlights = toolHighlights.slice(0, 5);
334
+ }
335
+ }
336
+
337
+ ctx.send(socket, {
338
+ type: 'work_item_output_response',
339
+ id: msg.id,
340
+ success: true,
341
+ output: {
342
+ title: workItem.title,
343
+ status: workItem.lastRunStatus ?? workItem.status,
344
+ runId: workItem.lastRunId,
345
+ conversationId,
346
+ completedAt,
347
+ summary,
348
+ highlights,
349
+ },
350
+ });
351
+ } catch (err) {
352
+ log.error({ err, workItemId: msg.id }, 'handleWorkItemOutput failed');
353
+ ctx.send(socket, { type: 'work_item_output_response', id: msg.id, success: false, error: 'Failed to load task output' });
354
+ }
355
+ }
356
+
127
357
  export async function handleWorkItemRunTask(
128
358
  msg: WorkItemRunTaskRequest,
129
359
  socket: net.Socket,
@@ -131,10 +361,58 @@ export async function handleWorkItemRunTask(
131
361
  ): Promise<void> {
132
362
  const workItem = getWorkItem(msg.id);
133
363
  if (!workItem) {
134
- ctx.send(socket, { type: 'work_item_run_task_response', id: msg.id, lastRunId: '', success: false, error: 'Work item not found' });
364
+ ctx.send(socket, { type: 'work_item_run_task_response', id: msg.id, lastRunId: '', success: false, error: 'Work item not found', errorCode: 'not_found' });
365
+ return;
366
+ }
367
+
368
+ if (workItem.status === 'running') {
369
+ ctx.send(socket, { type: 'work_item_run_task_response', id: msg.id, lastRunId: workItem.lastRunId ?? '', success: false, error: 'Work item is already running', errorCode: 'already_running' });
135
370
  return;
136
371
  }
137
372
 
373
+ const NON_RUNNABLE_STATUSES: readonly string[] = ['archived'];
374
+ if (NON_RUNNABLE_STATUSES.includes(workItem.status)) {
375
+ ctx.send(socket, { type: 'work_item_run_task_response', id: msg.id, lastRunId: workItem.lastRunId ?? '', success: false, error: `Work item has status '${workItem.status}' and cannot be run`, errorCode: 'invalid_status' });
376
+ return;
377
+ }
378
+
379
+ const task = getTask(workItem.taskId);
380
+ if (!task) {
381
+ ctx.send(socket, { type: 'work_item_run_task_response', id: msg.id, lastRunId: '', success: false, error: `Associated task not found: ${workItem.taskId}`, errorCode: 'no_task' });
382
+ return;
383
+ }
384
+
385
+ // Compute required tools using the same resolution logic as preflight:
386
+ // work-item snapshot first, then task template, then all registered tools.
387
+ let requiredTools: string[];
388
+ if (workItem.requiredTools !== null && workItem.requiredTools !== undefined) {
389
+ requiredTools = sanitizeToolList(JSON.parse(workItem.requiredTools));
390
+ } else {
391
+ requiredTools = task.requiredTools
392
+ ? sanitizeToolList(JSON.parse(task.requiredTools))
393
+ : getRegisteredToolNames();
394
+ }
395
+
396
+ // Permission checkpoint: if the task requires tools, verify all have been approved.
397
+ // Empty required tools means no approvals needed.
398
+ let approvedTools: string[] | undefined;
399
+ if (requiredTools.length > 0) {
400
+ approvedTools = workItem.approvedTools ? JSON.parse(workItem.approvedTools) : undefined;
401
+ const approvedSet = new Set<string>(approvedTools ?? []);
402
+ const missingApprovals = requiredTools.filter((t) => !approvedSet.has(t));
403
+ if (missingApprovals.length > 0) {
404
+ ctx.send(socket, {
405
+ type: 'work_item_run_task_response',
406
+ id: msg.id,
407
+ lastRunId: '',
408
+ success: false,
409
+ error: 'Required tool permissions have not been approved. Run preflight first.',
410
+ errorCode: 'permission_required',
411
+ });
412
+ return;
413
+ }
414
+ }
415
+
138
416
  // Set status to running
139
417
  updateWorkItem(msg.id, { status: 'running' });
140
418
 
@@ -143,34 +421,212 @@ export async function handleWorkItemRunTask(
143
421
 
144
422
  // Broadcast the running state
145
423
  broadcastWorkItemStatus(ctx, msg.id);
424
+ ctx.broadcast({ type: 'tasks_changed' });
146
425
 
147
- // Execute task asynchronously — create a session and wire processMessage
426
+ // Execute task asynchronously — lazily create a session inside the callback
427
+ // using the conversationId provided by runTask, so the session references
428
+ // the conversation that was actually inserted into the database.
148
429
  try {
149
- const session = await ctx.getOrCreateSession(crypto.randomUUID());
430
+ let session: Awaited<ReturnType<typeof ctx.getOrCreateSession>> | null = null;
150
431
  const result = await runTask(
151
- { taskId: workItem.taskId, workingDir: process.cwd() },
152
- async (_conversationId, message) => {
432
+ { taskId: workItem.taskId, workingDir: process.cwd(), approvedTools },
433
+ async (conversationId, message, taskRunId) => {
434
+ if (!session) {
435
+ // Store conversationId on the work item immediately so the cancel
436
+ // handler can locate the session while the task is still running.
437
+ updateWorkItem(msg.id, { lastRunConversationId: conversationId });
438
+ session = await ctx.getOrCreateSession(conversationId);
439
+
440
+ // Notify clients so they can create a visible chat thread for this task run
441
+ ctx.broadcast({
442
+ type: 'task_run_thread_created',
443
+ conversationId,
444
+ workItemId: msg.id,
445
+ title: workItem.title,
446
+ });
447
+ // Wire the taskRunId so the executor can retrieve ephemeral permission rules
448
+ (session as unknown as { taskRunId?: string }).taskRunId = taskRunId;
449
+ // Prevent interactive clients from rebinding to this session mid-run
450
+ (session as unknown as { headlessLock: boolean }).headlessLock = true;
451
+ }
153
452
  await session.processMessage(message, [], (event) => {
154
453
  ctx.broadcast(event);
155
454
  });
156
455
  },
157
456
  );
158
457
 
159
- const finalStatus: WorkItemStatus = result.status === 'completed' ? 'awaiting_review' : 'failed';
160
- updateWorkItem(msg.id, {
161
- status: finalStatus,
162
- lastRunId: result.taskRunId,
163
- lastRunConversationId: result.conversationId,
164
- lastRunStatus: result.status,
165
- });
458
+ // Release the headless lock now that the task run is done
459
+ if (session) {
460
+ (session as unknown as { headlessLock: boolean }).headlessLock = false;
461
+ }
462
+
463
+ // Don't overwrite cancelled status — the cancel handler already set it
464
+ const current = getWorkItem(msg.id);
465
+ if (current?.status !== 'cancelled') {
466
+ const finalStatus: WorkItemStatus = result.status === 'completed' ? 'awaiting_review' : 'failed';
467
+ updateWorkItem(msg.id, {
468
+ status: finalStatus,
469
+ lastRunId: result.taskRunId,
470
+ lastRunConversationId: result.conversationId,
471
+ lastRunStatus: result.status,
472
+ });
473
+ }
166
474
 
167
475
  broadcastWorkItemStatus(ctx, msg.id);
476
+ ctx.broadcast({ type: 'tasks_changed' });
168
477
  } catch (err) {
478
+ // Release the headless lock on failure
479
+ if (session) {
480
+ (session as unknown as { headlessLock: boolean }).headlessLock = false;
481
+ }
169
482
  log.error({ err, workItemId: msg.id }, 'work_item_run_task failed');
170
483
  updateWorkItem(msg.id, {
171
484
  status: 'failed',
172
485
  lastRunStatus: 'failed',
173
486
  });
174
487
  broadcastWorkItemStatus(ctx, msg.id);
488
+ ctx.broadcast({ type: 'tasks_changed' });
489
+ }
490
+ }
491
+
492
+
493
+ export async function handleWorkItemPreflight(
494
+ msg: WorkItemPreflightRequest,
495
+ socket: net.Socket,
496
+ ctx: HandlerContext,
497
+ ): Promise<void> {
498
+ const workItem = getWorkItem(msg.id);
499
+ if (!workItem) {
500
+ ctx.send(socket, { type: 'work_item_preflight_response', id: msg.id, success: false, error: 'Work item not found' });
501
+ return;
502
+ }
503
+
504
+ // Compute required tools from the work-item snapshot first; only fall
505
+ // back to the task template (or all registered tools) when the
506
+ // snapshot is null.
507
+ let requiredTools: string[];
508
+ if (workItem.requiredTools !== null && workItem.requiredTools !== undefined) {
509
+ requiredTools = sanitizeToolList(JSON.parse(workItem.requiredTools));
510
+ } else {
511
+ const task = getTask(workItem.taskId);
512
+ if (!task) {
513
+ ctx.send(socket, { type: 'work_item_preflight_response', id: msg.id, success: false, error: `Associated task not found: ${workItem.taskId}` });
514
+ return;
515
+ }
516
+ requiredTools = task.requiredTools
517
+ ? sanitizeToolList(JSON.parse(task.requiredTools))
518
+ : getRegisteredToolNames();
519
+ }
520
+
521
+ // If the work item explicitly requires no tools, skip the dialog.
522
+ if (requiredTools.length === 0) {
523
+ ctx.send(socket, { type: 'work_item_preflight_response', id: msg.id, success: true, permissions: [] });
524
+ return;
525
+ }
526
+
527
+ // If some tools are already approved, only prompt for the missing ones.
528
+ // When all required tools are covered, skip the dialog entirely.
529
+ if (workItem.approvedTools) {
530
+ const approvedSet = new Set<string>(JSON.parse(workItem.approvedTools));
531
+ requiredTools = requiredTools.filter((t) => !approvedSet.has(t));
532
+ if (requiredTools.length === 0) {
533
+ ctx.send(socket, { type: 'work_item_preflight_response', id: msg.id, success: true, permissions: [] });
534
+ return;
535
+ }
536
+ }
537
+
538
+ const workingDir = process.cwd();
539
+ const permissions = await Promise.all(
540
+ requiredTools.map(async (tool) => {
541
+ const risk = await classifyRisk(tool, {}, workingDir);
542
+ const result = await check(tool, {}, workingDir);
543
+ return {
544
+ tool,
545
+ description: getToolDescription(tool),
546
+ riskLevel: risk.toLowerCase() as 'low' | 'medium' | 'high',
547
+ currentDecision: result.decision as 'allow' | 'deny' | 'prompt',
548
+ };
549
+ }),
550
+ );
551
+
552
+ ctx.send(socket, { type: 'work_item_preflight_response', id: msg.id, success: true, permissions });
553
+ }
554
+
555
+ export function handleWorkItemApprovePermissions(
556
+ msg: WorkItemApprovePermissionsRequest,
557
+ socket: net.Socket,
558
+ ctx: HandlerContext,
559
+ ): void {
560
+ const workItem = getWorkItem(msg.id);
561
+ if (!workItem) {
562
+ ctx.send(socket, { type: 'work_item_approve_permissions_response', id: msg.id, success: false, error: 'Work item not found' });
563
+ return;
564
+ }
565
+
566
+ // Merge newly approved tools with any previously approved ones so reruns
567
+ // that only need a subset of previously-approved tools don't require
568
+ // re-approval.
569
+ const existingApproved: string[] = workItem.approvedTools
570
+ ? JSON.parse(workItem.approvedTools)
571
+ : [];
572
+ const newApproved = sanitizeToolList(msg.approvedTools);
573
+ const merged = [...new Set([...existingApproved, ...newApproved])];
574
+
575
+ updateWorkItem(msg.id, {
576
+ approvedTools: JSON.stringify(sanitizeToolList(merged)),
577
+ approvalStatus: 'approved',
578
+ });
579
+
580
+ ctx.send(socket, { type: 'work_item_approve_permissions_response', id: msg.id, success: true });
581
+ }
582
+
583
+ export function handleWorkItemCancel(
584
+ msg: WorkItemCancelRequest,
585
+ socket: net.Socket,
586
+ ctx: HandlerContext,
587
+ ): void {
588
+ const workItem = getWorkItem(msg.id);
589
+ if (!workItem) {
590
+ ctx.send(socket, { type: 'work_item_cancel_response', id: msg.id, success: false, error: 'Work item not found' });
591
+ return;
592
+ }
593
+
594
+ if (workItem.status !== 'running') {
595
+ ctx.send(socket, { type: 'work_item_cancel_response', id: msg.id, success: false, error: `Work item is not running (status: ${workItem.status})` });
596
+ return;
175
597
  }
598
+
599
+ // Abort the session associated with this work item's current run
600
+ const conversationId = workItem.lastRunConversationId;
601
+ if (conversationId) {
602
+ const session = ctx.sessions.get(conversationId);
603
+ if (session) {
604
+ (session as unknown as { headlessLock: boolean }).headlessLock = false;
605
+ session.abort();
606
+ getSubagentManager().abortAllForParent(conversationId);
607
+ }
608
+ }
609
+
610
+ updateWorkItem(msg.id, {
611
+ status: 'cancelled',
612
+ lastRunStatus: 'cancelled',
613
+ });
614
+
615
+ ctx.send(socket, { type: 'work_item_cancel_response', id: msg.id, success: true });
616
+
617
+ broadcastWorkItemStatus(ctx, msg.id);
618
+ ctx.broadcast({ type: 'tasks_changed' });
176
619
  }
620
+
621
+ export const workItemHandlers = defineHandlers({
622
+ work_items_list: handleWorkItemsList,
623
+ work_item_get: handleWorkItemGet,
624
+ work_item_update: handleWorkItemUpdate,
625
+ work_item_complete: handleWorkItemComplete,
626
+ work_item_delete: handleWorkItemDelete,
627
+ work_item_run_task: handleWorkItemRunTask,
628
+ work_item_output: handleWorkItemOutput,
629
+ work_item_preflight: handleWorkItemPreflight,
630
+ work_item_approve_permissions: handleWorkItemApprovePermissions,
631
+ work_item_cancel: handleWorkItemCancel,
632
+ });