@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
@@ -15,6 +15,13 @@ import type { ToolContext } from "../types.js";
15
15
  // `mock.module` is hoisted by bun before any static import of the tool
16
16
  // runs, so the import below sees the stubbed prompter even though
17
17
  // `askQuestionTool` captures the symbol at module-eval time.
18
+ const finishDesktopHelp = mock(async (_resume: boolean) => {});
19
+ const prepareDesktopHelp = mock(
20
+ async (_context: ToolContext) => finishDesktopHelp,
21
+ );
22
+ let promptError: Error | undefined;
23
+ mock.module("../../desktop/desktop-help.js", () => ({ prepareDesktopHelp }));
24
+
18
25
  const calls: QuestionPromptParams[] = [];
19
26
  let nextResult: QuestionPromptResult = {
20
27
  entries: [{ questionId: "q1", decision: "skipped" }],
@@ -28,6 +35,9 @@ mock.module("../../permissions/question-prompter.js", () => ({
28
35
  QuestionPrompter: class {
29
36
  async prompt(params: QuestionPromptParams): Promise<QuestionPromptOutcome> {
30
37
  calls.push(params);
38
+ if (promptError) {
39
+ throw promptError;
40
+ }
31
41
  // Mirror the real prompter: it mints the request id and assigns the
32
42
  // per-question `q1..qN` ids, then returns them alongside the resolution.
33
43
  return {
@@ -61,6 +71,9 @@ function makeContext(overrides: Partial<ToolContext> = {}): ToolContext {
61
71
  // `setNextResult()` before invoking `askQuestionTool.execute(...)`.
62
72
  beforeEach(() => {
63
73
  calls.length = 0;
74
+ prepareDesktopHelp.mockClear();
75
+ finishDesktopHelp.mockClear();
76
+ promptError = undefined;
64
77
  nextResult = {
65
78
  entries: [{ questionId: "q1", decision: "skipped" }],
66
79
  overall: "completed",
@@ -100,7 +113,7 @@ describe("askQuestionTool definition", () => {
100
113
  expect(def.description).toContain("up to 5");
101
114
  expect(def.description).toContain("Skip button");
102
115
 
103
- const schema = def.input_schema as {
116
+ const schema = def.input_schema as unknown as {
104
117
  properties: Record<
105
118
  string,
106
119
  {
@@ -670,7 +683,7 @@ describe("AskQuestionTool batched input", () => {
670
683
  });
671
684
 
672
685
  describe("askQuestionTool definition (batched schema)", () => {
673
- test("exposes `questions[]` shape, requires it, and drops the flat fields", () => {
686
+ test("exposes batched questions and a desktop help alternative", () => {
674
687
  const def = askQuestionTool;
675
688
  const schema = def.input_schema as unknown as {
676
689
  properties: Record<
@@ -708,9 +721,7 @@ describe("askQuestionTool definition (batched schema)", () => {
708
721
 
709
722
  expect(questions?.items?.required).toEqual(["question", "options"]);
710
723
 
711
- // `questions` is the only top-level input now.
712
- expect(schema.required).toEqual(["questions"]);
713
- expect(Object.keys(schema.properties)).toEqual(["questions"]);
724
+ expect(schema.properties.desktopHelp?.type).toBe("object");
714
725
 
715
726
  // The legacy flat fields are gone.
716
727
  expect(schema.properties.question).toBeUndefined();
@@ -825,3 +836,154 @@ describe("answered-question record", () => {
825
836
  ).toBeUndefined();
826
837
  });
827
838
  });
839
+
840
+ describe("virtual desktop help", () => {
841
+ test("reserves the desktop before waiting and resumes control after Done", async () => {
842
+ setNextResult(singleCompleted({ decision: "option", optionId: "done" }));
843
+ const result = await askQuestionTool.execute(
844
+ {
845
+ desktopHelp: {
846
+ message: "Please complete the CAPTCHA.",
847
+ doneLabel: "Done",
848
+ skipLabel: "Skip",
849
+ },
850
+ },
851
+ makeContext(),
852
+ );
853
+ expect(prepareDesktopHelp).toHaveBeenCalledTimes(1);
854
+ expect(finishDesktopHelp).toHaveBeenCalledWith(true);
855
+ expect(calls[0]?.questions[0]?.presentation).toBe("virtual_desktop");
856
+ expect(result.content).toContain("fresh browser snapshot");
857
+ expect(result.answeredQuestion?.responses[0]).toEqual({
858
+ questionId: "q1",
859
+ decision: "option",
860
+ optionId: "done",
861
+ });
862
+ });
863
+
864
+ test.each([
865
+ { decision: "skipped" as const },
866
+ { decision: "option" as const, optionId: "skip" },
867
+ ])("does not claim the obstacle was solved after Skip: %j", async (entry) => {
868
+ setNextResult(singleCompleted(entry));
869
+ const result = await askQuestionTool.execute(
870
+ {
871
+ desktopHelp: {
872
+ message: "Please complete the CAPTCHA.",
873
+ doneLabel: "Done",
874
+ skipLabel: "Skip",
875
+ },
876
+ },
877
+ makeContext(),
878
+ );
879
+ expect(finishDesktopHelp).toHaveBeenCalledWith(false);
880
+ expect(result.content).toContain("obstacle may still be present");
881
+ expect(result.isError).toBe(false);
882
+ });
883
+
884
+ test("does not wait for a user in a background turn", async () => {
885
+ await askQuestionTool.execute(
886
+ {
887
+ desktopHelp: {
888
+ message: "Please complete the CAPTCHA.",
889
+ doneLabel: "Done",
890
+ skipLabel: "Skip",
891
+ },
892
+ },
893
+ makeContext({ isInteractive: false }),
894
+ );
895
+ expect(prepareDesktopHelp).not.toHaveBeenCalled();
896
+ expect(calls).toHaveLength(0);
897
+ });
898
+
899
+ test("rejects mixing desktop help with a question batch", async () => {
900
+ const result = await askQuestionTool.execute(
901
+ {
902
+ ...validInput,
903
+ desktopHelp: {
904
+ message: "Please complete the CAPTCHA.",
905
+ doneLabel: "Done",
906
+ skipLabel: "Skip",
907
+ },
908
+ },
909
+ makeContext(),
910
+ );
911
+ expect(result.isError).toBe(true);
912
+ expect(calls).toHaveLength(0);
913
+ });
914
+ });
915
+
916
+ test("desktop help preserves model-localized fallback labels in history", async () => {
917
+ setNextResult(singleCompleted({ decision: "option", optionId: "done" }));
918
+ const result = await askQuestionTool.execute(
919
+ {
920
+ desktopHelp: {
921
+ message: "Completa la verificación.",
922
+ doneLabel: "Listo",
923
+ skipLabel: "Omitir",
924
+ },
925
+ },
926
+ makeContext(),
927
+ );
928
+ expect(calls[0]?.questions[0]?.options).toEqual([
929
+ { id: "done", label: "Listo" },
930
+ { id: "skip", label: "Omitir" },
931
+ ]);
932
+ expect(result.answeredQuestion?.questions[0]?.options).toEqual(
933
+ calls[0]?.questions[0]?.options,
934
+ );
935
+ });
936
+
937
+ test("desktop help does not park on a guardian channel without desktop controls", async () => {
938
+ const result = await askQuestionTool.execute(
939
+ {
940
+ desktopHelp: {
941
+ message: "Please sign in.",
942
+ doneLabel: "Done",
943
+ skipLabel: "Skip",
944
+ },
945
+ },
946
+ makeContext({
947
+ supportsDynamicUi: false,
948
+ supportsGuardianQuestionCards: true,
949
+ }),
950
+ );
951
+ expect(result.isError).toBe(true);
952
+ expect(result.content).toContain("continue in the Vellum app");
953
+ expect(prepareDesktopHelp).not.toHaveBeenCalled();
954
+ expect(calls).toHaveLength(0);
955
+ });
956
+
957
+ for (const overall of ["closed", "timed_out", "aborted"] as const) {
958
+ test(`desktop reservation ends when the question is ${overall}`, async () => {
959
+ setNextResult({ overall, entries: [] });
960
+ await askQuestionTool.execute(
961
+ {
962
+ desktopHelp: {
963
+ message: "Complete verification.",
964
+ doneLabel: "Done",
965
+ skipLabel: "Skip",
966
+ },
967
+ },
968
+ makeContext(),
969
+ );
970
+ expect(finishDesktopHelp).toHaveBeenCalledWith(false);
971
+ });
972
+ }
973
+
974
+ test("desktop reservation ends when presenting the question throws", async () => {
975
+ promptError = new Error("delivery failed");
976
+ await expect(
977
+ askQuestionTool.execute(
978
+ {
979
+ desktopHelp: {
980
+ message: "Complete verification.",
981
+ doneLabel: "Done",
982
+ skipLabel: "Skip",
983
+ },
984
+ },
985
+ makeContext(),
986
+ ),
987
+ ).rejects.toThrow("delivery failed");
988
+ expect(finishDesktopHelp).toHaveBeenCalledWith(false);
989
+ });
@@ -7,8 +7,10 @@ import type {
7
7
  import {
8
8
  QuestionPrompter,
9
9
  type QuestionPromptOutcome,
10
+ type QuestionPromptParamsEntry,
10
11
  } from "../../permissions/question-prompter.js";
11
12
  import { RiskLevel } from "../../permissions/types.js";
13
+ import { DESKTOP_HELP_GUIDANCE } from "../../util/browser-human-verification.js";
12
14
  import {
13
15
  invalidToolInputResult,
14
16
  toToolInputSchema,
@@ -74,17 +76,43 @@ const MAX_QUESTIONS_PER_BATCH = 5;
74
76
  // Callers pass a (possibly single-element) batch of questions. `execute()`
75
77
  // forwards them straight to the prompter. Loose so injected fields (e.g.
76
78
  // `activity`) never fail validation.
77
- export const askQuestionInputSchema = z.looseObject({
78
- questions: z
79
- .array(SingleQuestionSchema)
80
- .min(1)
81
- .max(MAX_QUESTIONS_PER_BATCH, {
82
- message: `At most ${MAX_QUESTIONS_PER_BATCH} questions per batch; split into multiple turns if you need more.`,
83
- })
84
- .describe(
85
- `1–${MAX_QUESTIONS_PER_BATCH} clarifying questions to ask in a single turn. Use a batch when several independent ambiguities block progress; ask one at a time when they're sequentially dependent. Past ${MAX_QUESTIONS_PER_BATCH} questions you should be implementing, not asking.`,
86
- ),
87
- });
79
+ export const askQuestionInputSchema = z
80
+ .looseObject({
81
+ desktopHelp: z
82
+ .object({
83
+ message: z
84
+ .string()
85
+ .min(1)
86
+ .describe(
87
+ "Use one short sentence to explain the needed human action, in the user's language.",
88
+ ),
89
+ doneLabel: z
90
+ .string()
91
+ .min(1)
92
+ .describe("The label for Done in the user's language."),
93
+ skipLabel: z
94
+ .string()
95
+ .min(1)
96
+ .describe("The label for Skip in the user's language."),
97
+ })
98
+ .describe(
99
+ "Immediately request human interaction for any CAPTCHA or bot-detection challenge, including sliders and press-and-hold checks. Also use for native dialogs requiring user interaction. For logins, use saved credentials or securely prompt for missing credentials first. Explain what the user should do. Shows a live preview with Step In, Done and Skip. Pass this instead of questions.",
100
+ )
101
+ .optional(),
102
+ questions: z
103
+ .array(SingleQuestionSchema)
104
+ .min(1)
105
+ .max(MAX_QUESTIONS_PER_BATCH, {
106
+ message: `At most ${MAX_QUESTIONS_PER_BATCH} questions per batch; split into multiple turns if you need more.`,
107
+ })
108
+ .describe(
109
+ `1–${MAX_QUESTIONS_PER_BATCH} clarifying questions to ask in a single turn. Use a batch when several independent ambiguities block progress; ask one at a time when they're sequentially dependent. Past ${MAX_QUESTIONS_PER_BATCH} questions you should be implementing, not asking.`,
110
+ )
111
+ .optional(),
112
+ })
113
+ .refine((input) => Boolean(input.questions) !== Boolean(input.desktopHelp), {
114
+ message: "Provide either questions or desktopHelp, not both.",
115
+ });
88
116
 
89
117
  export type SingleQuestion = z.infer<typeof SingleQuestionSchema>;
90
118
  export type AskQuestionInput = z.infer<typeof askQuestionInputSchema>;
@@ -92,6 +120,10 @@ export type AskQuestionInput = z.infer<typeof askQuestionInputSchema>;
92
120
  // ── Tool description ────────────────────────────────────────────────
93
121
 
94
122
  const DESCRIPTION = [
123
+ DESKTOP_HELP_GUIDANCE,
124
+ "For logins, use saved credentials first. Securely collect missing credentials",
125
+ "with assistant credentials prompt, then fill the login form yourself.",
126
+ "",
95
127
  "Use this tool whenever a request is ambiguous and can be resolved",
96
128
  "by 2–4 plausible interpretations or discrete choices. Prefer it over",
97
129
  "plain-text clarification — structured options are faster to answer and",
@@ -223,7 +255,10 @@ export const askQuestionTool = {
223
255
  category: "interaction",
224
256
  executionTarget: "sandbox",
225
257
  defaultRiskLevel: RiskLevel.Low,
226
- input_schema: toToolInputSchema(askQuestionInputSchema),
258
+ input_schema: {
259
+ ...toToolInputSchema(askQuestionInputSchema),
260
+ oneOf: [{ required: ["questions"] }, { required: ["desktopHelp"] }],
261
+ },
227
262
 
228
263
  async execute(
229
264
  input: Record<string, unknown>,
@@ -234,7 +269,19 @@ export const askQuestionTool = {
234
269
  return invalidToolInputResult("ask_question", parsed.error);
235
270
  }
236
271
 
237
- const questions: SingleQuestion[] = parsed.data.questions;
272
+ const { desktopHelp } = parsed.data;
273
+ const questions: QuestionPromptParamsEntry[] = desktopHelp
274
+ ? [
275
+ {
276
+ question: desktopHelp.message,
277
+ options: [
278
+ { id: "done", label: desktopHelp.doneLabel },
279
+ { id: "skip", label: desktopHelp.skipLabel },
280
+ ],
281
+ presentation: "virtual_desktop",
282
+ },
283
+ ]
284
+ : parsed.data.questions!;
238
285
 
239
286
  // No interactive user is present to answer (scheduled/headless/background
240
287
  // turn). Don't park the turn on a prompt no one can resolve — proceed with
@@ -243,12 +290,21 @@ export const askQuestionTool = {
243
290
  // model asks anyway, so it doesn't wait out the full response timeout.
244
291
  if (context.isInteractive === false) {
245
292
  return {
246
- content:
247
- "No interactive user is present to answer; proceeding with reasonable defaults.",
293
+ content: desktopHelp
294
+ ? "No interactive user is present to help in the virtual desktop. The obstacle remains unresolved."
295
+ : "No interactive user is present to answer; proceeding with reasonable defaults.",
248
296
  isError: false,
249
297
  };
250
298
  }
251
299
 
300
+ if (desktopHelp && context.supportsDynamicUi === false) {
301
+ return {
302
+ content:
303
+ "This channel cannot show the virtual desktop preview or Step In. Ask the user to continue in the Vellum app to complete this step. The obstacle remains unresolved.",
304
+ isError: true,
305
+ };
306
+ }
307
+
252
308
  // Channel turns (no dynamic UI) park only when the question can reach the
253
309
  // user as a guardian-request card with tappable options: a single-question
254
310
  // batch, asked by the guardian, on a channel whose notification adapter
@@ -277,13 +333,35 @@ export const askQuestionTool = {
277
333
  };
278
334
  }
279
335
 
336
+ let finishDesktopHelp: ((resume: boolean) => Promise<void>) | undefined;
337
+ if (desktopHelp) {
338
+ const { prepareDesktopHelp } =
339
+ await import("../../desktop/desktop-help.js");
340
+ const prepared = await prepareDesktopHelp(context);
341
+ if (typeof prepared !== "function") {
342
+ return prepared;
343
+ }
344
+ finishDesktopHelp = prepared;
345
+ }
346
+
280
347
  const prompter = new QuestionPrompter();
281
- const result = await prompter.prompt({
282
- conversationId: context.conversationId,
283
- questions,
284
- toolUseId: context.toolUseId,
285
- signal: context.signal,
286
- });
348
+ let result: QuestionPromptOutcome;
349
+ let resumeDesktop = false;
350
+ try {
351
+ result = await prompter.prompt({
352
+ conversationId: context.conversationId,
353
+ questions,
354
+ toolUseId: context.toolUseId,
355
+ signal: context.signal,
356
+ });
357
+ const answer = result.entries[0];
358
+ resumeDesktop =
359
+ result.overall === "completed" &&
360
+ (answer?.decision === "free_text" ||
361
+ (answer?.decision === "option" && answer.optionId === "done"));
362
+ } finally {
363
+ await finishDesktopHelp?.(resumeDesktop);
364
+ }
287
365
 
288
366
  // Format the aggregated transcript. Each line is keyed by the original
289
367
  // question text (not the daemon-assigned id) — the LLM never sees those
@@ -308,8 +386,26 @@ export const askQuestionTool = {
308
386
  const answeredQuestion = toAnsweredQuestion(result);
309
387
 
310
388
  switch (result.overall) {
311
- case "completed":
312
- return { content: lines.join("\n"), isError: false, answeredQuestion };
389
+ case "completed": {
390
+ let content = lines.join("\n");
391
+ if (desktopHelp) {
392
+ const entry = result.entries[0];
393
+ if (entry?.decision === "free_text") {
394
+ content +=
395
+ "\nTake a fresh browser snapshot before continuing; do not assume the obstacle was resolved.";
396
+ } else if (
397
+ entry?.decision === "option" &&
398
+ entry.optionId === "done"
399
+ ) {
400
+ content =
401
+ "The user finished interacting with the virtual desktop. Take a fresh browser snapshot and verify the result before continuing.";
402
+ } else {
403
+ content =
404
+ "The user skipped helping in the virtual desktop. The obstacle may still be present. Use another approach or explain what remains blocked.";
405
+ }
406
+ }
407
+ return { content, isError: false, answeredQuestion };
408
+ }
313
409
  case "closed": {
314
410
  const summary =
315
411
  "User closed the question card without answering. All questions skipped.";
@@ -4,6 +4,10 @@ import { getConfig } from "../../config/loader.js";
4
4
  import { HostBrowserProxy } from "../../daemon/host-browser-proxy.js";
5
5
  import type { ImageContent } from "../../providers/types.js";
6
6
  import { wrapUntrustedContent } from "../../security/untrusted-content.js";
7
+ import {
8
+ DESKTOP_HELP_GUIDANCE,
9
+ HUMAN_VERIFICATION_GUIDANCE,
10
+ } from "../../util/browser-human-verification.js";
7
11
  import { getLogger } from "../../util/logger.js";
8
12
  import { truncate } from "../../util/truncate.js";
9
13
  import { safeStringSlice } from "../../util/unicode.js";
@@ -1319,7 +1323,9 @@ export async function executeBrowserNavigate(
1319
1323
  "⚠️ CAPTCHA/Cloudflare verification detected on this page.",
1320
1324
  );
1321
1325
  lines.push(
1322
- "This challenge requires human verification. Surface this clearly: the page cannot be accessed until the verification is solved manually.",
1326
+ context.cdpClient
1327
+ ? DESKTOP_HELP_GUIDANCE
1328
+ : HUMAN_VERIFICATION_GUIDANCE,
1323
1329
  );
1324
1330
  if (cdp.kind === "local") {
1325
1331
  lines.push("");
@@ -23,8 +23,10 @@ import { conversationRevealNonce } from "../../runtime/reveal-nonce.js";
23
23
  import { redactSecrets } from "../../security/secret-scanner.js";
24
24
  import {
25
25
  buildShellInvocation,
26
+ buildShellSpawnFlags,
26
27
  prependUniquePathEntries,
27
28
  terminateProcessTree,
29
+ watchShellProcessStart,
28
30
  } from "../../util/host-process.js";
29
31
  import { getLogger } from "../../util/logger.js";
30
32
  import type { CompletedBackgroundTool } from "../background-tool-registry.js";
@@ -455,9 +457,9 @@ export const hostShellTool = {
455
457
  cwd: workingDir,
456
458
  env: hostEnv,
457
459
  stdio: ["ignore", "pipe", "pipe"],
458
- detached: true,
459
- windowsHide: true,
460
+ ...buildShellSpawnFlags(),
460
461
  });
462
+ const launch = watchShellProcessStart(child);
461
463
 
462
464
  const collector = attachBoundedStdio(child);
463
465
  let timedOut = false;
@@ -483,7 +485,9 @@ export const hostShellTool = {
483
485
  }
484
486
  completed = true;
485
487
  clearTimeout(timer);
486
- const result = collector.format(code, timedOut, timeoutSec);
488
+ const result = collector.format(code, timedOut, timeoutSec, {
489
+ started: launch.didStart(),
490
+ });
487
491
  // Cancel takes precedence over the SIGKILL-induced error result.
488
492
  const status = aborted
489
493
  ? "cancelled"
@@ -629,9 +633,9 @@ export const hostShellTool = {
629
633
  cwd: workingDir,
630
634
  env: hostEnv,
631
635
  stdio: ["ignore", "pipe", "pipe"],
632
- detached: true,
633
- windowsHide: true,
636
+ ...buildShellSpawnFlags(),
634
637
  });
638
+ const launch = watchShellProcessStart(child);
635
639
  const collector = attachBoundedStdio(child, {
636
640
  onOutput: context.onOutput,
637
641
  });
@@ -657,7 +661,9 @@ export const hostShellTool = {
657
661
  clearTimeout(timer);
658
662
  context.signal?.removeEventListener("abort", onAbort);
659
663
 
660
- const result = collector.format(code, timedOut, timeoutSec);
664
+ const result = collector.format(code, timedOut, timeoutSec, {
665
+ started: launch.didStart(),
666
+ });
661
667
 
662
668
  resolve({
663
669
  content: result.content,
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
4
4
  import { minimatch } from "minimatch";
5
5
 
6
6
  import { ensureDir, pathExists } from "../../../util/fs.js";
7
+ import { surrogateSafeWindow } from "../../../util/unicode.js";
7
8
  import { isAbortLikeError } from "../abort.js";
8
9
  import { applyEdit } from "./edit-engine.js";
9
10
  import * as Err from "./errors.js";
@@ -102,37 +103,6 @@ function truncationNotice(
102
103
  return `\n\n[Truncated: characters ${start}-${end} of ${totalChars}. Read on with start_index=${end}.]`;
103
104
  }
104
105
 
105
- const isHighSurrogate = (code: number): boolean =>
106
- code >= 0xd800 && code <= 0xdbff;
107
- const isLowSurrogate = (code: number): boolean =>
108
- code >= 0xdc00 && code <= 0xdfff;
109
-
110
- /**
111
- * Character window that never splits a surrogate pair. A split leaves a lone
112
- * half at each edge, and each encodes to U+FFFD, so the character is lost from
113
- * both this window and the next one paged in after it.
114
- */
115
- export function surrogateSafeWindow(
116
- total: number,
117
- charCodeAt: (index: number) => number,
118
- requestedStart: number,
119
- maxChars: number,
120
- ): { start: number; end: number } {
121
- let start = Math.max(0, Math.min(requestedStart, total));
122
- if (start > 0 && start < total && isLowSurrogate(charCodeAt(start))) {
123
- start -= 1;
124
- }
125
-
126
- let end = Math.min(total, start + maxChars);
127
- if (end > start && end < total && isHighSurrogate(charCodeAt(end - 1))) {
128
- // Backing off would empty a one-character window, which stalls paging on
129
- // the same offset, so take the whole pair instead.
130
- end = end - 1 > start ? end - 1 : Math.min(total, end + 1);
131
- }
132
-
133
- return { start, end };
134
- }
135
-
136
106
  export class FileSystemOps {
137
107
  private policy: PathPolicy;
138
108
  private sizeLimit: number | undefined;
@@ -7,6 +7,7 @@ import {
7
7
  formatShellOutput,
8
8
  MAX_OUTPUT_LENGTH,
9
9
  OUTPUT_TRUNCATED_TAG,
10
+ SHELL_DID_NOT_START_MESSAGE,
10
11
  } from "./shell-output.js";
11
12
 
12
13
  describe("BoundedStdioCollector", () => {
@@ -85,6 +86,15 @@ describe("attachBoundedStdio", () => {
85
86
  });
86
87
  });
87
88
 
89
+ describe("formatShellOutput launch start", () => {
90
+ test("does not report command_completed when the process never started", () => {
91
+ const result = formatShellOutput("", "", 0, false, 120, { started: false });
92
+ expect(result.isError).toBe(true);
93
+ expect(result.content).toBe(SHELL_DID_NOT_START_MESSAGE);
94
+ expect(result.content).not.toContain("<command_completed />");
95
+ });
96
+ });
97
+
88
98
  describe("formatShellOutput truncation", () => {
89
99
  test("truncates an already-materialized oversized string without a file path", () => {
90
100
  const longOutput = "x".repeat(30_000);
@@ -4,6 +4,9 @@ export const MAX_OUTPUT_LENGTH = 20_000;
4
4
 
5
5
  export const OUTPUT_TRUNCATED_TAG = `<output_truncated limit="20K" />`;
6
6
 
7
+ export const SHELL_DID_NOT_START_MESSAGE =
8
+ "Error: the shell command did not start. No process was created, so the command did not run.";
9
+
7
10
  export interface ShellOutputResult {
8
11
  content: string;
9
12
  status: string | undefined;
@@ -31,8 +34,16 @@ export function formatShellOutput(
31
34
  code: number | null,
32
35
  timedOut: boolean,
33
36
  timeoutSec: number,
34
- options?: { truncated?: boolean },
37
+ options?: { truncated?: boolean; started?: boolean },
35
38
  ): ShellOutputResult {
39
+ if (options?.started === false) {
40
+ return {
41
+ content: SHELL_DID_NOT_START_MESSAGE,
42
+ status: undefined,
43
+ isError: true,
44
+ };
45
+ }
46
+
36
47
  let output = stdout;
37
48
  if (stderr) {
38
49
  output += (output ? "\n" : "") + stderr;
@@ -137,6 +148,7 @@ export class BoundedStdioCollector {
137
148
  code: number | null,
138
149
  timedOut: boolean,
139
150
  timeoutSec: number,
151
+ options?: { started?: boolean },
140
152
  ): ShellOutputResult {
141
153
  return formatShellOutput(
142
154
  Buffer.concat(this.stdoutParts).toString(),
@@ -144,7 +156,7 @@ export class BoundedStdioCollector {
144
156
  code,
145
157
  timedOut,
146
158
  timeoutSec,
147
- { truncated: this.truncated },
159
+ { truncated: this.truncated, started: options?.started },
148
160
  );
149
161
  }
150
162
  }
@@ -7,9 +7,12 @@ import { conversationRevealNonce } from "../../runtime/reveal-nonce.js";
7
7
  import { computeSkillVersionHash } from "../../skills/version-hash.js";
8
8
  import {
9
9
  buildShellInvocation,
10
+ buildShellSpawnFlags,
10
11
  terminateProcessTree,
12
+ watchShellProcessStart,
11
13
  } from "../../util/host-process.js";
12
14
  import { safeStringSlice } from "../../util/unicode.js";
15
+ import { SHELL_DID_NOT_START_MESSAGE } from "../shared/shell-output.js";
13
16
  import { buildSanitizedEnv } from "../terminal/safe-env.js";
14
17
  import type { ToolContext, ToolExecutionResult } from "../types.js";
15
18
 
@@ -160,9 +163,9 @@ function spawnRunner(
160
163
  cwd: runDir,
161
164
  env,
162
165
  stdio: ["ignore", "pipe", "pipe"],
163
- detached: true,
164
- windowsHide: true,
166
+ ...buildShellSpawnFlags(),
165
167
  });
168
+ const launch = watchShellProcessStart(child);
166
169
 
167
170
  const timer = setTimeout(() => {
168
171
  timedOut = true;
@@ -188,6 +191,14 @@ function spawnRunner(
188
191
  clearTimeout(timer);
189
192
  context.signal?.removeEventListener("abort", onAbort);
190
193
 
194
+ if (!launch.didStart()) {
195
+ resolve({
196
+ content: `Failed to spawn skill tool script "${executorPath}": ${SHELL_DID_NOT_START_MESSAGE}`,
197
+ isError: true,
198
+ });
199
+ return;
200
+ }
201
+
191
202
  if (timedOut) {
192
203
  resolve({
193
204
  content: `Skill tool script "${executorPath}" timed out after ${timeoutMs}ms`,
@@ -134,7 +134,8 @@ function notifyBackgroundSkillUpdate(args: {
134
134
  sourceEventName: "activity.complete",
135
135
  dedupeKey: `skill-updated:${args.skillId}:${day}`,
136
136
  contextPayload: {
137
- // `summary` feeds the copy composer; `title`/`body` are the home feed's
137
+ // `summary` feeds the copy composer and `title` is the headline both
138
+ // it and the home feed keep; `title`/`body` are also the feed's
138
139
  // fallback when no channel copy was rendered. Without them a fully
139
140
  // suppressed delivery (the intended shape for this signal: low urgency,
140
141
  // background, no interruption) leaves the feed writer with no summary