@vellumai/assistant 0.12.2 → 0.12.3-staging.1

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 (298) hide show
  1. package/.env.example +2 -0
  2. package/AGENTS.md +1 -1
  3. package/docs/architecture/integrations.md +7 -0
  4. package/docs/architecture/memory.md +17 -2
  5. package/docs/credential-execution-service.md +1 -1
  6. package/docs/desktop-browser-cli.md +7 -3
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  9. package/node_modules/@vellumai/environments/src/shell.test.ts +21 -0
  10. package/node_modules/@vellumai/environments/src/shell.ts +24 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  13. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +8 -2
  14. package/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  15. package/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  16. package/openapi.yaml +54 -5
  17. package/package.json +1 -1
  18. package/scripts/smoke-desktop-browser-cli.ts +1 -0
  19. package/scripts/sync-llm-catalog.ts +3 -0
  20. package/scripts/voice-ttft-spike.ts +2 -2
  21. package/src/__tests__/agent-loop.test.ts +259 -0
  22. package/src/__tests__/agent-wake-delegation-prompt.test.ts +64 -2
  23. package/src/__tests__/anthropic-provider.test.ts +55 -0
  24. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  25. package/src/__tests__/attachments-store.test.ts +22 -3
  26. package/src/__tests__/channel-approval.test.ts +9 -14
  27. package/src/__tests__/channel-reply-delivery.test.ts +56 -0
  28. package/src/__tests__/chat-credential-redaction.test.ts +23 -0
  29. package/src/__tests__/computer-use-screenshot-attachments.test.ts +411 -0
  30. package/src/__tests__/computer-use-screenshot-selection.test.ts +50 -0
  31. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  32. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  33. package/src/__tests__/conversation-agent-loop.test.ts +257 -6
  34. package/src/__tests__/conversation-attachments.test.ts +106 -0
  35. package/src/__tests__/conversation-fork-crud.test.ts +191 -0
  36. package/src/__tests__/conversation-rendered-delegation-state.test.ts +276 -0
  37. package/src/__tests__/credential-execution-client.test.ts +70 -53
  38. package/src/__tests__/db-conversation-tool-surface.test.ts +221 -0
  39. package/src/__tests__/events-tail-route.test.ts +33 -0
  40. package/src/__tests__/history-repair.test.ts +53 -0
  41. package/src/__tests__/inference-profile-session-handler.test.ts +26 -0
  42. package/src/__tests__/list-messages-tool-merge.test.ts +85 -1
  43. package/src/__tests__/llm-catalog-parity.test.ts +27 -4
  44. package/src/__tests__/oauth-apps-routes.test.ts +1 -0
  45. package/src/__tests__/oauth-commands-routes.test.ts +114 -101
  46. package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
  47. package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
  48. package/src/__tests__/oauth-providers-routes.test.ts +2 -0
  49. package/src/__tests__/persist-media-references.test.ts +50 -0
  50. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  51. package/src/__tests__/run-conversation-turn-persistence.test.ts +138 -1
  52. package/src/__tests__/schedule-routes.test.ts +20 -0
  53. package/src/__tests__/scheduler-result-notification.test.ts +23 -4
  54. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  55. package/src/__tests__/secret-routes-platform-proxy.test.ts +86 -31
  56. package/src/__tests__/secret-routes-scrub.test.ts +22 -27
  57. package/src/__tests__/secret-scanner.test.ts +20 -0
  58. package/src/__tests__/secure-keys-managed-failover.test.ts +8 -0
  59. package/src/__tests__/secure-keys.test.ts +7 -3
  60. package/src/__tests__/server-tool-pairing.test.ts +107 -0
  61. package/src/__tests__/skills.test.ts +5 -4
  62. package/src/__tests__/subagent-tool-gate-mode.test.ts +226 -0
  63. package/src/__tests__/terminal-tools.test.ts +8 -0
  64. package/src/__tests__/tool-result-follow-up.test.ts +99 -0
  65. package/src/__tests__/tool-result-metadata-plumbing.test.ts +63 -0
  66. package/src/__tests__/unicode.test.ts +36 -0
  67. package/src/agent/loop.ts +37 -12
  68. package/src/agent/tool-result-follow-up.ts +70 -0
  69. package/src/api/attachment-provenance.test.ts +68 -0
  70. package/src/api/computer-use-tool.test.ts +55 -0
  71. package/src/api/computer-use-tool.ts +27 -0
  72. package/src/api/events/assistant-outbound-attachment.ts +3 -0
  73. package/src/api/events/desktop-activity-changed.ts +10 -0
  74. package/src/api/events/question-request.ts +1 -0
  75. package/src/api/index.ts +11 -0
  76. package/src/api/responses/conversation-message.ts +3 -0
  77. package/src/approvals/approval-primitive.ts +5 -2
  78. package/src/approvals/scoped-approval-grants.ts +6 -2
  79. package/src/browser/virtual-desktop-target.ts +1 -2
  80. package/src/cli/commands/__tests__/cli-test-harness.ts +21 -3
  81. package/src/cli/commands/__tests__/plugins.test.ts +20 -1
  82. package/src/cli/commands/__tests__/schedules.test.ts +14 -0
  83. package/src/cli/commands/bash.help.ts +4 -3
  84. package/src/cli/commands/browser.help.ts +5 -1
  85. package/src/cli/commands/credentials.help.ts +3 -3
  86. package/src/cli/commands/oauth/index.help.ts +10 -0
  87. package/src/cli/commands/oauth/providers.ts +12 -0
  88. package/src/cli/commands/oauth/request.test.ts +179 -2
  89. package/src/cli/commands/oauth/request.ts +41 -10
  90. package/src/cli/commands/plugins.ts +12 -5
  91. package/src/cli/commands/schedules.ts +2 -0
  92. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  93. package/src/cli/lib/__tests__/install-from-github.test.ts +41 -0
  94. package/src/cli/lib/__tests__/local-plugin-upgrade.test.ts +133 -51
  95. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +8 -2
  96. package/src/cli/lib/bundled-marketplace.json +855 -0
  97. package/src/cli/lib/bundled-plugin-packages.json +783 -1
  98. package/src/cli/lib/inspect-plugin.ts +11 -4
  99. package/src/cli/lib/upgrade-plugin.ts +19 -11
  100. package/src/cli/output.ts +18 -4
  101. package/src/config/__tests__/default-provider.test.ts +4 -0
  102. package/src/config/bundled-skills/schedule/SKILL.md +6 -6
  103. package/src/config/loader.ts +9 -17
  104. package/src/config/platform-identity.ts +8 -8
  105. package/src/config/profile-text-generation.test.ts +51 -0
  106. package/src/config/profile-text-generation.ts +51 -0
  107. package/src/config/schemas/__tests__/memory-v3.test.ts +12 -0
  108. package/src/config/schemas/llm.ts +7 -2
  109. package/src/config/schemas/mcp.ts +5 -1
  110. package/src/config/schemas/memory-v3.ts +12 -0
  111. package/src/credential-execution/ces-runtime.ts +2 -2
  112. package/src/credential-execution/executable-discovery.ts +36 -134
  113. package/src/credential-execution/process-manager.test.ts +20 -24
  114. package/src/credential-execution/process-manager.ts +24 -19
  115. package/src/daemon/__tests__/conversation-tool-setup.test.ts +43 -0
  116. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +36 -0
  117. package/src/daemon/assistant-attachments.ts +20 -12
  118. package/src/daemon/chat-credential-redaction.ts +16 -1
  119. package/src/daemon/conversation-agent-loop-handlers.ts +74 -3
  120. package/src/daemon/conversation-agent-loop.ts +12 -0
  121. package/src/daemon/conversation-attachments.ts +93 -12
  122. package/src/daemon/conversation-tool-setup.ts +71 -3
  123. package/src/daemon/conversation-turn-finalize.ts +26 -13
  124. package/src/daemon/conversation.ts +72 -17
  125. package/src/daemon/daemon-control.ts +3 -7
  126. package/src/daemon/lifecycle.ts +4 -4
  127. package/src/daemon/mcp-reload-service.ts +1 -1
  128. package/src/daemon/message-types/shared.ts +1 -0
  129. package/src/daemon/orphan-reaper.ts +4 -3
  130. package/src/daemon/persist-media-references.ts +20 -3
  131. package/src/daemon/tool-setup-types.ts +6 -0
  132. package/src/daemon/wake-conversation-ops.ts +50 -15
  133. package/src/desktop/desktop-automation-lease.test.ts +230 -0
  134. package/src/desktop/desktop-automation-lease.ts +90 -6
  135. package/src/desktop/desktop-help.ts +15 -0
  136. package/src/desktop/desktop-stream-bridge.test.ts +65 -10
  137. package/src/desktop/desktop-stream-bridge.ts +3 -3
  138. package/src/desktop/desktop-wallpaper-renderer.ts +132 -0
  139. package/src/desktop/desktop-wallpaper-worker.ts +17 -0
  140. package/src/desktop/desktop-wallpaper.test.ts +48 -7
  141. package/src/desktop/desktop-wallpaper.ts +34 -102
  142. package/src/desktop/virtual-desktop-feature.ts +1 -1
  143. package/src/mcp/__tests__/manager-tool-caps.test.ts +111 -0
  144. package/src/mcp/__tests__/startup.test.ts +30 -7
  145. package/src/mcp/__tests__/tool-caps.test.ts +107 -0
  146. package/src/mcp/manager.ts +168 -101
  147. package/src/mcp/startup.ts +33 -13
  148. package/src/mcp/tool-caps.ts +176 -0
  149. package/src/messaging/provider-message-metadata.ts +3 -3
  150. package/src/notifications/__tests__/copy-composer.test.ts +70 -0
  151. package/src/notifications/copy-composer.ts +11 -3
  152. package/src/notifications/schedule-result-producer.ts +4 -6
  153. package/src/oauth/AGENTS.md +2 -0
  154. package/src/oauth/__tests__/identity-verifier.test.ts +36 -1
  155. package/src/oauth/identity-verifier.ts +24 -0
  156. package/src/oauth/oauth-store.ts +11 -1
  157. package/src/oauth/provider-serializer.ts +1 -0
  158. package/src/oauth/seed-providers.ts +12 -0
  159. package/src/permissions/confirmation-guardian-request.test.ts +16 -0
  160. package/src/permissions/confirmation-guardian-request.ts +2 -3
  161. package/src/permissions/question-prompter.test.ts +31 -0
  162. package/src/permissions/question-prompter.ts +2 -0
  163. package/src/persistence/attachments-store.ts +84 -44
  164. package/src/persistence/conversation-crud.ts +111 -20
  165. package/src/persistence/conversation-plugin-facade.ts +30 -0
  166. package/src/persistence/conversation-tool-surface.ts +135 -0
  167. package/src/persistence/conversation-types.test.ts +32 -0
  168. package/src/persistence/conversation-types.ts +26 -4
  169. package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +78 -0
  170. package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +29 -0
  171. package/src/persistence/migrations/379-oauth-providers-response-ok-field.test.ts +92 -0
  172. package/src/persistence/migrations/379-oauth-providers-response-ok-field.ts +32 -0
  173. package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.test.ts +90 -0
  174. package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.ts +34 -0
  175. package/src/persistence/schema/conversation-tool-surfaces.ts +32 -0
  176. package/src/persistence/schema/index.ts +1 -0
  177. package/src/persistence/schema/oauth.ts +1 -0
  178. package/src/persistence/steps.ts +24 -3
  179. package/src/plugin-api/conversation-turn.ts +31 -7
  180. package/src/plugin-api/index.ts +11 -1
  181. package/src/plugin-api/plugin-channel-turn-trust.test.ts +133 -0
  182. package/src/plugin-api/plugin-channel-turn-trust.ts +71 -0
  183. package/src/plugins/defaults/memory/AGENTS.md +39 -5
  184. package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +365 -0
  185. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +43 -0
  186. package/src/plugins/defaults/memory/__tests__/conversation-memory-purge.test.ts +1 -0
  187. package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +2 -0
  188. package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +17 -0
  189. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +74 -0
  190. package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +161 -0
  191. package/src/plugins/defaults/memory/__tests__/relocated-memory-test-rows.ts +10 -0
  192. package/src/plugins/defaults/memory/buffer-file.ts +354 -0
  193. package/src/plugins/defaults/memory/buffer-format.ts +40 -0
  194. package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -2
  195. package/src/plugins/defaults/memory/context-search/format.ts +2 -1
  196. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -1
  197. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +2 -1
  198. package/src/plugins/defaults/memory/conversation-memory-purge.ts +4 -0
  199. package/src/plugins/defaults/memory/graph/capability-seed.ts +1 -2
  200. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -42
  201. package/src/plugins/defaults/memory/host-utils.ts +0 -10
  202. package/src/plugins/defaults/memory/injectors.ts +4 -3
  203. package/src/plugins/defaults/memory/memory-retrospective-job.ts +72 -183
  204. package/src/plugins/defaults/memory/memory-run-evidence.ts +213 -0
  205. package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +1 -1
  206. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +407 -99
  207. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +10 -0
  208. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +107 -7
  209. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +307 -86
  210. package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +34 -0
  211. package/src/plugins/defaults/memory/substrate/page-index.ts +2 -1
  212. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +89 -49
  213. package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
  214. package/src/plugins/defaults/memory/tools.ts +1 -1
  215. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
  216. package/src/plugins/defaults/memory/v1/graph/extraction.ts +2 -1
  217. package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
  218. package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +5 -0
  219. package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +5 -2
  220. package/src/plugins/defaults/memory/v2/reranker.ts +2 -1
  221. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +81 -1
  222. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +87 -0
  223. package/src/plugins/defaults/memory/v3/__tests__/plugin-schema.test.ts +12 -0
  224. package/src/plugins/defaults/memory/v3/__tests__/pool-log-store.test.ts +183 -4
  225. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +103 -2
  226. package/src/plugins/defaults/memory/v3/card.ts +2 -1
  227. package/src/plugins/defaults/memory/v3/injector.ts +212 -178
  228. package/src/plugins/defaults/memory/v3/orchestrate.ts +112 -25
  229. package/src/plugins/defaults/memory/v3/plugin-schema.ts +54 -4
  230. package/src/plugins/defaults/memory/v3/pool-log-store.ts +253 -1
  231. package/src/plugins/defaults/memory/v3/pool-select.test.ts +35 -2
  232. package/src/plugins/defaults/memory/v3/pool-select.ts +31 -19
  233. package/src/plugins/defaults/memory/v3/sections.ts +2 -1
  234. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +41 -14
  235. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +4 -1
  236. package/src/plugins/defaults/tool-result-truncate/terminal.ts +1 -46
  237. package/src/prompts/__tests__/parallel-tasks-section.test.ts +25 -0
  238. package/src/prompts/delegation-gate.ts +57 -0
  239. package/src/prompts/system-prompt.ts +14 -32
  240. package/src/providers/inference/adapter-factory.ts +6 -0
  241. package/src/providers/jev/client.test.ts +260 -0
  242. package/src/providers/jev/client.ts +518 -0
  243. package/src/providers/model-catalog.ts +58 -3
  244. package/src/providers/server-tool-pairing.ts +16 -7
  245. package/src/runtime/AGENTS.md +2 -2
  246. package/src/runtime/__tests__/agent-wake.test.ts +94 -1
  247. package/src/runtime/agent-wake.ts +31 -4
  248. package/src/runtime/guardian-action-service.ts +2 -17
  249. package/src/runtime/guardian-reply-router.ts +1 -8
  250. package/src/runtime/http-server.ts +2 -2
  251. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +89 -0
  252. package/src/runtime/migrations/vbundle-import-policy.ts +33 -11
  253. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +24 -2
  254. package/src/runtime/routes/channel-route-shared.ts +1 -9
  255. package/src/runtime/routes/conversation-routes.ts +29 -4
  256. package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
  257. package/src/runtime/routes/desktop-setup-routes.ts +8 -5
  258. package/src/runtime/routes/guardian-approval-interception.ts +24 -0
  259. package/src/runtime/routes/inbound-message-handler.ts +2 -3
  260. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
  261. package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -4
  262. package/src/runtime/routes/inference-profile-session-handler.ts +11 -1
  263. package/src/runtime/routes/inference-profiles-routes.ts +9 -0
  264. package/src/runtime/routes/oauth-commands-routes.ts +57 -54
  265. package/src/runtime/routes/oauth-providers.ts +4 -0
  266. package/src/runtime/routes/oauth-request-hints.test.ts +246 -0
  267. package/src/runtime/routes/oauth-request-hints.ts +149 -0
  268. package/src/runtime/routes/schedule-routes.ts +2 -0
  269. package/src/runtime/routes/secret-routes.ts +99 -119
  270. package/src/schedule/__tests__/worker-mcp-bootstrap.test.ts +107 -0
  271. package/src/schedule/__tests__/worker-mcp-readiness.test.ts +173 -0
  272. package/src/schedule/__tests__/worker-mcp-tools.test.ts +13 -47
  273. package/src/schedule/run-script.ts +2 -2
  274. package/src/schedule/scheduler.ts +47 -11
  275. package/src/schedule/tool-surface-readiness.ts +65 -0
  276. package/src/schedule/worker-mcp.ts +85 -0
  277. package/src/schedule/worker.ts +9 -31
  278. package/src/security/secure-keys.ts +60 -48
  279. package/src/tools/ask-question/ask-question-tool.test.ts +167 -5
  280. package/src/tools/ask-question/ask-question-tool.ts +119 -23
  281. package/src/tools/browser/browser-execution.ts +7 -1
  282. package/src/tools/host-terminal/host-shell.ts +12 -6
  283. package/src/tools/shared/filesystem/file-ops-service.ts +1 -31
  284. package/src/tools/shared/shell-output.test.ts +10 -0
  285. package/src/tools/shared/shell-output.ts +14 -2
  286. package/src/tools/skills/sandbox-runner.ts +13 -2
  287. package/src/tools/skills/scaffold-managed.ts +2 -1
  288. package/src/tools/terminal/__tests__/safe-env.test.ts +33 -4
  289. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +0 -14
  290. package/src/tools/terminal/safe-env.ts +35 -24
  291. package/src/tools/terminal/sanitized-bash.ts +15 -2
  292. package/src/tools/terminal/shell-launch.test.ts +162 -0
  293. package/src/tools/terminal/shell.test.ts +29 -0
  294. package/src/tools/terminal/shell.ts +13 -7
  295. package/src/util/browser-human-verification.ts +14 -0
  296. package/src/util/host-process.test.ts +17 -1
  297. package/src/util/host-process.ts +24 -0
  298. package/src/util/unicode.ts +29 -0
@@ -18,9 +18,10 @@
18
18
  * route captured calls back to the originating conversation's probe arrays.
19
19
  */
20
20
 
21
- import { beforeEach, describe, expect, mock, test } from "bun:test";
21
+ import { beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
22
22
 
23
23
  import type { DiskPressureStatus } from "../../daemon/disk-pressure-guard.js";
24
+ import { desktopAutomationLease } from "../../desktop/desktop-automation-lease.js";
24
25
 
25
26
  // ── Per-conversation capture registry ────────────────────────────────
26
27
  //
@@ -501,6 +502,13 @@ function makeWakeConversation(options: {
501
502
  `tools:${snapshotAllowedTools()?.join(",") ?? "all"}`,
502
503
  );
503
504
  },
505
+ // Mirrors Conversation.setPreactivatedSkillIds; the wake's tool-scope
506
+ // restore calls it, and a double without it throws inside the restore
507
+ // closure, leaving every field restored after it untouched.
508
+ preactivatedSkillIds: undefined as string[] | undefined,
509
+ setPreactivatedSkillIds(ids: string[] | undefined) {
510
+ this.preactivatedSkillIds = ids;
511
+ },
504
512
  get wakePersonaOverride() {
505
513
  return wakePersonaOverride;
506
514
  },
@@ -1357,6 +1365,50 @@ describe("wakeAgentForOpportunity", () => {
1357
1365
  expect(conversation.toolContextPin).toBeUndefined();
1358
1366
  });
1359
1367
 
1368
+ test("applies wireToolDefinitions and the delegation state alongside the allowlist and restores both after the wake", async () => {
1369
+ const replay = [
1370
+ { name: "remember", description: "Save", input_schema: {} },
1371
+ { name: "bell_jingle", description: "Ring", input_schema: {} },
1372
+ ];
1373
+ let replayDuringRun: unknown;
1374
+ let delegationDuringRun: unknown;
1375
+ const conversation = makeWakeConversation({
1376
+ runImpl: async (input) => {
1377
+ replayDuringRun = conversation.wireToolReplay;
1378
+ delegationDuringRun = conversation.delegateIndependentTasksReplay;
1379
+ return runResult([
1380
+ ...input,
1381
+ { role: "assistant", content: [{ type: "text", text: "Saved." }] },
1382
+ ]);
1383
+ },
1384
+ });
1385
+
1386
+ const result = await wakeAgentForOpportunity(
1387
+ {
1388
+ conversationId: conversation.conversationId,
1389
+ hint: "review for memories",
1390
+ source: "memory-retrospective",
1391
+ allowedTools: ["remember"],
1392
+ toolGateMode: "execution",
1393
+ wireToolDefinitions: replay,
1394
+ delegateIndependentTasks: true,
1395
+ },
1396
+ { resolveTarget: async () => conversation },
1397
+ );
1398
+
1399
+ expect(result).toEqual({ invoked: true, producedToolCalls: false });
1400
+ // The replay array is live on the conversation for the duration of the
1401
+ // run (the tool resolver returns it as the wire array), as is the
1402
+ // delegation-section state the source's prompt rendered (the prompt
1403
+ // build reads it)...
1404
+ expect(replayDuringRun).toEqual(replay);
1405
+ expect(delegationDuringRun).toBe(true);
1406
+ // ...and both are cleared alongside the allowlist + gate mode after the
1407
+ // wake.
1408
+ expect(conversation.wireToolReplay).toBeUndefined();
1409
+ expect(conversation.delegateIndependentTasksReplay).toBeUndefined();
1410
+ });
1411
+
1360
1412
  test("defaults to the wire gate mode when toolGateMode is absent", async () => {
1361
1413
  let gateModeDuringRun: string | undefined;
1362
1414
  const conversation = makeWakeConversation({
@@ -1517,6 +1569,47 @@ describe("wakeAgentForOpportunity", () => {
1517
1569
  expect(conversation.pushedMessages[2]).toEqual(followupAssistant);
1518
1570
  });
1519
1571
 
1572
+ for (const fails of [false, true]) {
1573
+ test(`releases desktop control before a ${fails ? "failed" : "completed"} wake drains the queue`, async () => {
1574
+ const conversation = makeWakeConversation({
1575
+ scriptedAssistant: {
1576
+ role: "assistant",
1577
+ content: [{ type: "text", text: "reply" }],
1578
+ },
1579
+ ...(fails
1580
+ ? {
1581
+ runImpl: async () => {
1582
+ throw new Error("wake failed");
1583
+ },
1584
+ }
1585
+ : {}),
1586
+ });
1587
+ const release = spyOn(desktopAutomationLease, "releaseForConversation");
1588
+ const setProcessing = conversation.setProcessing.bind(conversation);
1589
+ conversation.setProcessing = (processing) => {
1590
+ if (!processing) {
1591
+ expect(release).toHaveBeenCalledWith(conversation.conversationId);
1592
+ }
1593
+ setProcessing(processing);
1594
+ };
1595
+ try {
1596
+ const result = await wakeAgentForOpportunity(
1597
+ {
1598
+ conversationId: conversation.conversationId,
1599
+ hint: "x",
1600
+ source: "unit-test",
1601
+ },
1602
+ { resolveTarget: async () => conversation },
1603
+ );
1604
+ expect(result.invoked).toBe(!fails);
1605
+ expect(release).toHaveBeenCalledTimes(1);
1606
+ expect(conversation.drainQueueCalls).toBe(1);
1607
+ } finally {
1608
+ release.mockRestore();
1609
+ }
1610
+ });
1611
+ }
1612
+
1520
1613
  test("marks processing true during the run and false afterwards", async () => {
1521
1614
  const conversation = makeWakeConversation({
1522
1615
  baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
@@ -107,6 +107,7 @@ import {
107
107
  persistWakeTriggerMessage,
108
108
  scopeWakeAllowedTools,
109
109
  } from "../daemon/wake-conversation-ops.js";
110
+ import { desktopAutomationLease } from "../desktop/desktop-automation-lease.js";
110
111
  import {
111
112
  recordCompactionEndBestEffort,
112
113
  recordCompactionStartBestEffort,
@@ -118,7 +119,7 @@ import {
118
119
  setAgentLoopExitReasonOnLatestLog,
119
120
  } from "../persistence/llm-request-log-store.js";
120
121
  import type { SystemPromptPersonaOverride } from "../prompts/system-prompt.js";
121
- import type { Message } from "../providers/types.js";
122
+ import type { Message, ToolDefinition } from "../providers/types.js";
122
123
  import {
123
124
  type UntrustedContentSource,
124
125
  wrapUntrustedContent,
@@ -350,6 +351,27 @@ export interface WakeOptions {
350
351
  * skill-management authoring tools callable directly.
351
352
  */
352
353
  preactivateSkillIds?: readonly string[];
354
+ /**
355
+ * Tool definitions to send verbatim as the wake's wire tool array, in place
356
+ * of the ones the conversation would resolve for itself. Used by fork-based
357
+ * memory retrospectives to replay the SOURCE conversation's recorded surface
358
+ * (`getRecordedConversationToolSurface`) so the provider prompt-cache prefix matches
359
+ * the source's live turns byte for byte. Definitions only: what may execute
360
+ * is still decided by `allowedTools` and the turn's own active set. Applied
361
+ * and restored alongside `allowedTools`; ignored when `allowedTools` is
362
+ * absent.
363
+ */
364
+ wireToolDefinitions?: readonly ToolDefinition[];
365
+ /**
366
+ * Whether the source's recorded surface rendered the system prompt's
367
+ * parallel-delegation section, replayed into the wake's prompt so that
368
+ * tier of the provider cache prefix matches the source's too (the wake's
369
+ * own scope cannot spawn, so deriving it would render the section off where
370
+ * an interactive source rendered it on). Prompt only: a spawn attempt is
371
+ * still rejected at execution. Applied and restored alongside
372
+ * `allowedTools`; ignored when `allowedTools` is absent.
373
+ */
374
+ delegateIndependentTasks?: boolean;
353
375
  /**
354
376
  * Explicit persona/channel slugs for the wake's system-prompt build,
355
377
  * applied to the conversation for the duration of the run and restored
@@ -1410,6 +1432,7 @@ export async function wakeAgentForOpportunity(
1410
1432
  * is one function rather than a rebuild bolted onto either half.
1411
1433
  */
1412
1434
  const restoreWakeTurnScope = (): void => {
1435
+ desktopAutomationLease.releaseForConversation(conversationId);
1413
1436
  restoreWakeAllowedTools();
1414
1437
  clearWakePersonaOverride();
1415
1438
  syncWakeLoopSystemPrompt();
@@ -1422,9 +1445,13 @@ export async function wakeAgentForOpportunity(
1422
1445
  restoreWakeToolScope = scopeWakeAllowedTools(
1423
1446
  conversation,
1424
1447
  new Set(opts.allowedTools),
1425
- opts.toolGateMode,
1426
- opts.toolContextPin,
1427
- opts.preactivateSkillIds,
1448
+ {
1449
+ gateMode: opts.toolGateMode,
1450
+ toolContextPin: opts.toolContextPin,
1451
+ preactivateSkillIds: opts.preactivateSkillIds,
1452
+ wireToolDefinitions: opts.wireToolDefinitions,
1453
+ delegateIndependentTasks: opts.delegateIndependentTasks,
1454
+ },
1428
1455
  );
1429
1456
  return true;
1430
1457
  } catch (err) {
@@ -16,21 +16,6 @@ import {
16
16
  isApprovalAction,
17
17
  } from "./channel-approval-types.js";
18
18
 
19
- // ---------------------------------------------------------------------------
20
- // Constants
21
- // ---------------------------------------------------------------------------
22
-
23
- /**
24
- * Legacy actions that map to canonical ones during client rollout.
25
- * All temporal/persistent approval variants collapse to approve_once.
26
- * Keep until all clients are updated and no in-flight buttons remain.
27
- */
28
- const LEGACY_ACTION_MAP: Record<string, string> = {
29
- approve_10m: "approve_once",
30
- approve_conversation: "approve_once",
31
- approve_always: "approve_once",
32
- };
33
-
34
19
  // ---------------------------------------------------------------------------
35
20
  // Types
36
21
  // ---------------------------------------------------------------------------
@@ -98,8 +83,8 @@ export async function processGuardianDecision(
98
83
  ): Promise<ProcessGuardianDecisionResult> {
99
84
  const { requestId, conversationId, channel, actorContext } = params;
100
85
 
101
- // 1. Canonicalize legacy actions, then validate
102
- const action = LEGACY_ACTION_MAP[params.action] ?? params.action;
86
+ // 1. Validate the action
87
+ const action = params.action;
103
88
  if (!isApprovalAction(action)) {
104
89
  return {
105
90
  ok: false,
@@ -153,12 +153,6 @@ export interface GuardianReplyResult {
153
153
  // Callback data parser — format: "apr:<requestId>:<action>"
154
154
  // ---------------------------------------------------------------------------
155
155
 
156
- const LEGACY_CALLBACK_MAP: Record<string, string> = {
157
- approve_10m: "approve_once",
158
- approve_conversation: "approve_once",
159
- approve_always: "approve_once",
160
- };
161
-
162
156
  interface ParsedCallback {
163
157
  requestId: string;
164
158
  action: ApprovalAction;
@@ -170,8 +164,7 @@ function parseCallbackAction(data: string): ParsedCallback | null {
170
164
  return null;
171
165
  }
172
166
  const requestId = parts[1];
173
- const rawAction = parts.slice(2).join(":");
174
- const action = LEGACY_CALLBACK_MAP[rawAction] ?? rawAction;
167
+ const action = parts.slice(2).join(":");
175
168
  if (!requestId || !isApprovalAction(action)) {
176
169
  return null;
177
170
  }
@@ -254,9 +254,9 @@ type AllWebSocketData =
254
254
 
255
255
  function virtualDesktopEnabled(): boolean {
256
256
  try {
257
- return isVirtualDesktopEnabled(getConfig());
257
+ return isVirtualDesktopEnabled();
258
258
  } catch (err) {
259
- log.warn({ err }, "Failed to read config for desktop stream gate");
259
+ log.warn({ err }, "Failed to check desktop stream gate");
260
260
  return false;
261
261
  }
262
262
  }
@@ -15,6 +15,7 @@ import {
15
15
  isConfigArchivePath,
16
16
  isCredentialMetadataArchivePath,
17
17
  isLegacyPersonaArchivePath,
18
+ isLocalDevRuntimeVersion,
18
19
  isWorkspaceNamespacedArchivePath,
19
20
  LEGACY_USER_MD_ARCHIVE_PATH,
20
21
  partitionWorkspacePreserveSkipDirs,
@@ -257,4 +258,92 @@ describe("evaluateRuntimeCompatibility", () => {
257
258
  ),
258
259
  ).toEqual({ ok: true });
259
260
  });
261
+
262
+ // A `vel up` minikube assistant image is stamped
263
+ // `<pkg.version>-local.<ts>.<sha>` from the checkout's package.json,
264
+ // which only moves on release cuts, so the minimum bound cannot be
265
+ // decided from it; the explicit maximum ceiling still applies.
266
+ test("local dev runtime build skips the minimum bound", () => {
267
+ expect(
268
+ evaluateRuntimeCompatibility(
269
+ { min_runtime_version: "0.12.2-staging.4", max_runtime_version: null },
270
+ "0.12.1-local.20260916144827.2f409056e6",
271
+ ),
272
+ ).toEqual({ ok: true });
273
+ });
274
+
275
+ test("local dev runtime build within an explicit range passes", () => {
276
+ expect(
277
+ evaluateRuntimeCompatibility(
278
+ { min_runtime_version: "0.12.2", max_runtime_version: "0.12.5" },
279
+ "0.12.1-local.20260916144827.2f409056e6",
280
+ ),
281
+ ).toEqual({ ok: true });
282
+ });
283
+
284
+ test("local dev runtime build above max still fails", () => {
285
+ const compat = {
286
+ min_runtime_version: "0.7.0",
287
+ max_runtime_version: "0.7.5",
288
+ };
289
+ const runtime = "0.12.1-local.20260916144827.2f409056e6";
290
+ expect(evaluateRuntimeCompatibility(compat, runtime)).toEqual({
291
+ ok: false,
292
+ reason: "version_incompatible",
293
+ bundle_compat: compat,
294
+ runtime_version: runtime,
295
+ });
296
+ });
297
+
298
+ test("local dev runtime build with unparsable max fails open", () => {
299
+ expect(
300
+ evaluateRuntimeCompatibility(
301
+ { min_runtime_version: "0.7.0", max_runtime_version: "garbage" },
302
+ "0.12.1-local.20260916144827.2f409056e6",
303
+ ),
304
+ ).toEqual({ ok: true });
305
+ });
306
+
307
+ test("other prerelease builds are still gated", () => {
308
+ const compat = {
309
+ min_runtime_version: "0.12.2-staging.4",
310
+ max_runtime_version: null,
311
+ };
312
+ for (const runtime of [
313
+ "0.12.1-dev.202609151913.a151da3",
314
+ "0.12.1-staging.9",
315
+ "0.12.1",
316
+ ]) {
317
+ expect(evaluateRuntimeCompatibility(compat, runtime)).toEqual({
318
+ ok: false,
319
+ reason: "version_incompatible",
320
+ bundle_compat: compat,
321
+ runtime_version: runtime,
322
+ });
323
+ }
324
+ });
325
+ });
326
+
327
+ describe("isLocalDevRuntimeVersion", () => {
328
+ test("matches the local hatch / minikube version stamp", () => {
329
+ expect(
330
+ isLocalDevRuntimeVersion("0.12.1-local.20260916144827.2f409056e6"),
331
+ ).toBe(true);
332
+ expect(isLocalDevRuntimeVersion("0.0.0-local.1789584535000")).toBe(true);
333
+ });
334
+
335
+ test("rejects release, dev and staging builds and lookalikes", () => {
336
+ for (const version of [
337
+ "0.12.1",
338
+ "0.12.1-dev.202609151913.a151da3",
339
+ "0.12.2-staging.4",
340
+ "0.0.0-dev",
341
+ "0.0.0-legacy",
342
+ "0.12.1-localhost.1",
343
+ "local.0.12.1",
344
+ "",
345
+ ]) {
346
+ expect(isLocalDevRuntimeVersion(version)).toBe(false);
347
+ }
348
+ });
260
349
  });
@@ -172,6 +172,20 @@ export function formatRuntimeCompatibilityMessage(
172
172
  return `Cannot import: bundle requires runtime ${range}, but this runtime is ${runtimeVersion}. Update your runtime before importing.`;
173
173
  }
174
174
 
175
+ /**
176
+ * Whether `version` names a runtime built from a source checkout: the
177
+ * `<pkg.version>-local.<timestamp>.<sha>` `APP_VERSION` that the platform
178
+ * repo's `vel up` stamps on the assistant images it builds for its local
179
+ * minikube cluster. The triple is the checkout's package.json version,
180
+ * which only moves on release cuts, so it says nothing about whether the
181
+ * checkout is newer or older than a release. A CLI hatch from a checkout
182
+ * without an explicit `APP_VERSION` runs as the plain package version and
183
+ * is not covered.
184
+ */
185
+ export function isLocalDevRuntimeVersion(version: string): boolean {
186
+ return /^\d+\.\d+\.\d+-local\./.test(version);
187
+ }
188
+
175
189
  export function evaluateRuntimeCompatibility(
176
190
  compat: RuntimeCompatibility,
177
191
  runtimeVersion: string,
@@ -179,17 +193,25 @@ export function evaluateRuntimeCompatibility(
179
193
  if (compat.min_runtime_version === LEGACY_RUNTIME_VERSION_SENTINEL) {
180
194
  return { ok: true };
181
195
  }
182
- const minCmp = compareSemver(runtimeVersion, compat.min_runtime_version);
183
- if (minCmp === null) {
184
- return { ok: true };
185
- }
186
- if (minCmp < 0) {
187
- return {
188
- ok: false,
189
- reason: "version_incompatible",
190
- bundle_compat: compat,
191
- runtime_version: runtimeVersion,
192
- };
196
+ // A local dev build skips the minimum bound, like an unparsable version
197
+ // does below: its triple is not comparable to release versions (see
198
+ // isLocalDevRuntimeVersion), so "runtime older than the bundle" cannot be
199
+ // decided from it. The maximum bound still applies: it is an explicit
200
+ // ceiling the bundle's producer recorded, and a local triple above it is
201
+ // as good a signal as any release's.
202
+ if (!isLocalDevRuntimeVersion(runtimeVersion)) {
203
+ const minCmp = compareSemver(runtimeVersion, compat.min_runtime_version);
204
+ if (minCmp === null) {
205
+ return { ok: true };
206
+ }
207
+ if (minCmp < 0) {
208
+ return {
209
+ ok: false,
210
+ reason: "version_incompatible",
211
+ bundle_compat: compat,
212
+ runtime_version: runtimeVersion,
213
+ };
214
+ }
193
215
  }
194
216
  if (compat.max_runtime_version !== null) {
195
217
  const maxCmp = compareSemver(runtimeVersion, compat.max_runtime_version);
@@ -123,8 +123,7 @@ function seedVellumConnection(): void {
123
123
 
124
124
  function persistedProfiles(): Record<string, unknown> {
125
125
  const llm = loadRawConfig().llm as
126
- | { profiles?: Record<string, unknown> }
127
- | undefined;
126
+ { profiles?: Record<string, unknown> } | undefined;
128
127
  return llm?.profiles ?? {};
129
128
  }
130
129
 
@@ -923,6 +922,29 @@ describe("PUT inference/active-profile validation", () => {
923
922
  ).rejects.toThrow(/disabled/);
924
923
  });
925
924
 
925
+ test("rejects a profile whose catalog model does not produce chat text", async () => {
926
+ setConfig("llm", {
927
+ profiles: {
928
+ jev: {
929
+ source: "user",
930
+ provider: "jev",
931
+ model: "jev-latest",
932
+ status: "active",
933
+ },
934
+ },
935
+ });
936
+ const promise = call("inference_profiles_set_active", {
937
+ body: { name: "jev" },
938
+ });
939
+ await expect(promise).rejects.toBeInstanceOf(BadRequestError);
940
+ await expect(promise).rejects.toThrow(
941
+ /structured answers rather than chat text/,
942
+ );
943
+ expect(
944
+ (loadRawConfig().llm as { activeProfile?: string }).activeProfile,
945
+ ).toBeUndefined();
946
+ });
947
+
926
948
  test("rejects a profile that cannot serve requests — no escape hatch", async () => {
927
949
  setConfig("llm", {
928
950
  profiles: {
@@ -29,13 +29,6 @@ export function requiredDecisionKeywords(
29
29
  // Callback data parser — format: "apr:<requestId>:<action>"
30
30
  // ---------------------------------------------------------------------------
31
31
 
32
- /** Map legacy callback actions to canonical ones for in-flight buttons. */
33
- const LEGACY_CALLBACK_MAP: Record<string, string> = {
34
- approve_10m: "approve_once",
35
- approve_conversation: "approve_once",
36
- approve_always: "approve_once",
37
- };
38
-
39
32
  export function parseCallbackData(
40
33
  data: string,
41
34
  sourceChannel?: string,
@@ -45,8 +38,7 @@ export function parseCallbackData(
45
38
  return null;
46
39
  }
47
40
  const requestId = parts[1];
48
- const rawAction = parts.slice(2).join(":");
49
- const action = LEGACY_CALLBACK_MAP[rawAction] ?? rawAction;
41
+ const action = parts.slice(2).join(":");
50
42
  if (!requestId || !isApprovalAction(action)) {
51
43
  return null;
52
44
  }
@@ -113,7 +113,7 @@ import { recordOnboardingEvent } from "../../onboarding/onboarding-events-store.
113
113
  import {
114
114
  classifyKind,
115
115
  getAttachmentById,
116
- getAttachmentMetadataForMessage,
116
+ getAttachmentMetadataForMessages,
117
117
  getAttachmentsByIds,
118
118
  resolveAttachmentsForPersist,
119
119
  } from "../../persistence/attachments-store.js";
@@ -132,6 +132,7 @@ import {
132
132
  isSuppressedQueuedMessage,
133
133
  isSystemCardMetadata,
134
134
  type MessageRow,
135
+ parseMessageMetadata,
135
136
  recordConversationPersistedSeq,
136
137
  setConversationInferenceProfile,
137
138
  } from "../../persistence/conversation-crud.js";
@@ -141,6 +142,7 @@ import {
141
142
  } from "../../persistence/conversation-key-store.js";
142
143
  import { searchConversations } from "../../persistence/conversation-queries.js";
143
144
  import {
145
+ computerUseScreenshotAttachmentIdsFromMetadata,
144
146
  isNoResponseMetadata,
145
147
  messageMetadataIsAmbientSightKeep,
146
148
  } from "../../persistence/conversation-types.js";
@@ -1180,9 +1182,29 @@ export async function handleListMessages({
1180
1182
  let msgAttachments: RuntimeAttachmentMetadata[] = [];
1181
1183
  if (m.id) {
1182
1184
  const idsToQuery = [m.id, ...mergedMessageIds];
1183
- const linked = idsToQuery.flatMap((id) =>
1184
- getAttachmentMetadataForMessage(id),
1185
- );
1185
+ const linkedRows = getAttachmentMetadataForMessages(idsToQuery);
1186
+ const computerUseScreenshotIdsByMessage = new Map<
1187
+ string,
1188
+ Set<string>
1189
+ >();
1190
+ const linked = linkedRows.map((row) => {
1191
+ let screenshotIds = computerUseScreenshotIdsByMessage.get(
1192
+ row.messageId,
1193
+ );
1194
+ if (!screenshotIds) {
1195
+ screenshotIds = new Set(
1196
+ computerUseScreenshotAttachmentIdsFromMetadata(
1197
+ parseMessageMetadata(row.messageMetadata),
1198
+ ),
1199
+ );
1200
+ computerUseScreenshotIdsByMessage.set(row.messageId, screenshotIds);
1201
+ }
1202
+ return {
1203
+ ...row.attachment,
1204
+ computerUseScreenshot:
1205
+ screenshotIds.has(row.attachment.id) || undefined,
1206
+ };
1207
+ });
1186
1208
  if (linked.length > 0) {
1187
1209
  msgAttachments = await Promise.all(
1188
1210
  linked.map(async (a) => {
@@ -1221,6 +1243,9 @@ export async function handleListMessages({
1221
1243
  ? { thumbnailData: a.thumbnailBase64 }
1222
1244
  : {}),
1223
1245
  fileBacked: true,
1246
+ ...(a.computerUseScreenshot
1247
+ ? { computerUseScreenshot: true }
1248
+ : {}),
1224
1249
  };
1225
1250
  }),
1226
1251
  );
@@ -79,8 +79,8 @@ describe("desktop setup route feature gate", () => {
79
79
  setOverridesForTesting({ "assistant-desktop": true });
80
80
  expect(route.handler({})).toEqual(
81
81
  route.method === "GET"
82
- ? { state: "required" }
83
- : { state: "installing", stage: "packages" },
82
+ ? { state: "required", automationActive: false }
83
+ : { state: "installing", stage: "packages", automationActive: false },
84
84
  );
85
85
  expect(route.method === "GET" ? status : start).toHaveBeenCalledTimes(1);
86
86
  expect(route.method === "GET" ? start : status).not.toHaveBeenCalled();
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
 
3
- import { getConfig } from "../../config/loader.js";
3
+ import { desktopAutomationLease } from "../../desktop/desktop-automation-lease.js";
4
4
  import { desktopDependencyInstaller } from "../../desktop/desktop-dependencies.js";
5
5
  import { isVirtualDesktopEnabled } from "../../desktop/virtual-desktop-feature.js";
6
6
  import { GATEWAY_PRINCIPALS } from "../auth/route-policy.js";
@@ -9,6 +9,7 @@ import type { RouteDefinition } from "./types.js";
9
9
 
10
10
  const statusSchema = z.object({
11
11
  state: z.enum(["required", "installing", "ready", "failed", "unsupported"]),
12
+ automationActive: z.boolean().optional(),
12
13
  stage: z.enum(["packages", "chrome", "checking"]).optional(),
13
14
  });
14
15
 
@@ -19,14 +20,16 @@ export const ROUTES: RouteDefinition[] = ["GET", "POST"].map((method) => ({
19
20
  method,
20
21
  policy: { requiredScopes: [], allowedPrincipalTypes: GATEWAY_PRINCIPALS },
21
22
  handler: () => {
22
- if (!isVirtualDesktopEnabled(getConfig())) {
23
+ if (!isVirtualDesktopEnabled()) {
23
24
  throw new NotFoundError(
24
25
  "Virtual desktop is available only on enabled platform-hosted assistants",
25
26
  );
26
27
  }
27
- return method === "GET"
28
- ? desktopDependencyInstaller.getStatus()
29
- : desktopDependencyInstaller.start();
28
+ const status =
29
+ method === "GET"
30
+ ? desktopDependencyInstaller.getStatus()
31
+ : desktopDependencyInstaller.start();
32
+ return { ...status, automationActive: desktopAutomationLease.isActive };
30
33
  },
31
34
  summary:
32
35
  method === "GET"
@@ -299,6 +299,30 @@ export async function handleApprovalInterception(
299
299
 
300
300
  return { handled: true, type: "stale_ignored" };
301
301
  }
302
+
303
+ // An approval callback that names no approval action is a button press
304
+ // the daemon does not serve (an id outside the decision vocabulary, or an
305
+ // answer token aimed at a card this rail does not resolve). It is never
306
+ // text: the channel normalizers copy callback data into `content`, so
307
+ // letting it fall through would hand the raw callback to the
308
+ // conversational and natural-language parsers, which can read a token
309
+ // like `approve_always` as an approval. Consume it as a stale button.
310
+ if (callbackData.startsWith("apr:")) {
311
+ log.warn(
312
+ { conversationId, callbackData },
313
+ "Approval callback carries no approval action, ignoring stale button press",
314
+ );
315
+ if (approvalMessageId) {
316
+ editStaleApprovalMessage({
317
+ replyCallbackUrl,
318
+ chatId: conversationExternalId,
319
+ messageId: approvalMessageId,
320
+ assistantId,
321
+ conversationId,
322
+ });
323
+ }
324
+ return { handled: true, type: "stale_ignored" };
325
+ }
302
326
  }
303
327
 
304
328
  // ── Conversational approval engine for plain-text messages ──
@@ -1531,8 +1531,7 @@ export async function handleChannelInbound({
1531
1531
  sourceMetadata.account.length > 0
1532
1532
  ? sourceMetadata.account
1533
1533
  : undefined;
1534
- const slackBotMentioned =
1535
- sourceChannel === "slack" && sourceMetadata?.slackBotMentioned === true;
1534
+ const botMentioned = sourceMetadata?.botMentioned === true;
1536
1535
 
1537
1536
  // ── DM cold-start backfill ──
1538
1537
  // First time a Slack DM without thread_ts lands in a conversation that
@@ -1625,7 +1624,7 @@ export async function handleChannelInbound({
1625
1624
  approvalCopyGenerator,
1626
1625
  chatType: sourceChatType,
1627
1626
  clientTimezone: inboundClientTimezone,
1628
- slackBotMentioned,
1627
+ botMentioned,
1629
1628
  slackInbound,
1630
1629
  channelInbound,
1631
1630
  });
@@ -1169,7 +1169,7 @@ describe("channel activity timing", () => {
1169
1169
  externalChatId: channelId,
1170
1170
  trustCtx,
1171
1171
  metadataHints: [],
1172
- slackBotMentioned: true,
1172
+ botMentioned: true,
1173
1173
  replyCallbackUrl: `https://example.test/deliver/slack?channel=${channelId}&threadTs=${threadTs}`,
1174
1174
  });
1175
1175