@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
@@ -14,10 +14,12 @@ import {
14
14
  import type { LoopToolExecutor } from "../agent/loop.js";
15
15
  import type { AssistantEvent } from "../api/index.js";
16
16
  import { stripInjectionsForCompaction } from "../context/strip-injections.js";
17
+ import type { AttachmentResolutionResult } from "../daemon/conversation-attachments.js";
17
18
  import {
18
19
  queueConversationNotice,
19
20
  resetConversationNoticesForTests,
20
21
  } from "../daemon/conversation-notices.js";
22
+ import { desktopAutomationLease } from "../desktop/desktop-automation-lease.js";
21
23
  import { getConversationDirName } from "../persistence/conversation-directories.js";
22
24
  import type { UserPromptSubmitContext } from "../plugin-api/types.js";
23
25
  import { resetPluginRegistryAndRegisterDefaults } from "../plugins/defaults/index.js";
@@ -42,6 +44,11 @@ const conversationDiskViewRealSnapshot = {
42
44
  "../persistence/conversation-disk-view.js",
43
45
  ) as Record<string, unknown>),
44
46
  };
47
+ const channelReplyDeliveryRealSnapshot = {
48
+ ...(createRequire(import.meta.url)(
49
+ "../runtime/channel-reply-delivery.js",
50
+ ) as Record<string, unknown>),
51
+ };
45
52
  // Disable the catalog default so resolution lands on llm.default.
46
53
  const disabledCatalogDefaultProfiles: Record<string, unknown> = {
47
54
  balanced: { source: "managed", status: "disabled" },
@@ -418,6 +425,10 @@ afterAll(() => {
418
425
  "../persistence/conversation-disk-view.js",
419
426
  () => conversationDiskViewRealSnapshot,
420
427
  );
428
+ mock.module(
429
+ "../runtime/channel-reply-delivery.js",
430
+ () => channelReplyDeliveryRealSnapshot,
431
+ );
421
432
  });
422
433
 
423
434
  const syncMessageToDiskMock = mock(() => {});
@@ -428,6 +439,19 @@ mock.module("../persistence/conversation-disk-view.js", () => ({
428
439
  rebuildConversationDiskViewFromDbStateMock,
429
440
  }));
430
441
 
442
+ let mockTurnReplyMessageId: string | undefined;
443
+ const resolveTurnReplyMessageIdMock = mock(
444
+ (
445
+ _conversationId: string,
446
+ _userMessageId: string | undefined,
447
+ fallbackMessageId: string,
448
+ ) => mockTurnReplyMessageId ?? fallbackMessageId,
449
+ );
450
+ mock.module("../runtime/channel-reply-delivery.js", () => ({
451
+ ...channelReplyDeliveryRealSnapshot,
452
+ resolveTurnReplyMessageId: resolveTurnReplyMessageIdMock,
453
+ }));
454
+
431
455
  mock.module("../apps/app-store.js", () => ({
432
456
  getApp: () => null,
433
457
  listAppFiles: () => [],
@@ -580,12 +604,16 @@ mock.module("../daemon/conversation-usage.js", () => ({
580
604
  recordUsage: recordUsageMock,
581
605
  }));
582
606
 
583
- const resolveAssistantAttachmentsMock = mock(async () => ({
584
- assistantAttachments: [],
585
- emittedAttachments: [],
586
- directiveWarnings: [],
587
- persistedFiles: [],
588
- }));
607
+ const resolveAssistantAttachmentsMock = mock(
608
+ async (): Promise<AttachmentResolutionResult> => ({
609
+ assistantAttachments: [],
610
+ emittedAttachments: [],
611
+ directiveWarnings: [],
612
+ persistedFiles: [],
613
+ linkedAttachmentIds: [],
614
+ computerUseScreenshotAttachmentIds: [],
615
+ }),
616
+ );
589
617
  mock.module("../daemon/conversation-attachments.js", () => ({
590
618
  resolveAssistantAttachments: resolveAssistantAttachmentsMock,
591
619
  approveHostAttachmentRead: async () => true,
@@ -894,6 +922,26 @@ function makeCtx(
894
922
  return ctx;
895
923
  }
896
924
 
925
+ function makeSendUserMessageCtx(): Conversation {
926
+ return makeCtx({
927
+ currentCallSite: "mainAgent",
928
+ providerResponses: [
929
+ toolUseResponse("tu_1", "send_user_message", {
930
+ message: "Here is the result.",
931
+ }),
932
+ textResponse("Finished delivery."),
933
+ ],
934
+ loopTools: [
935
+ {
936
+ name: "send_user_message",
937
+ description: "deliver",
938
+ input_schema: { type: "object" },
939
+ },
940
+ ],
941
+ toolExecutor: async () => ({ content: "Delivered.", isError: false }),
942
+ });
943
+ }
944
+
897
945
  /**
898
946
  * What `classifyConversationError` returns for a daily-credit-limit 402 (see
899
947
  * `dailyLimitClassification` in conversation-error.ts). Its `userMessage` is
@@ -987,6 +1035,8 @@ beforeEach(() => {
987
1035
  setAgentLoopExitReasonOnLatestLogMock.mockClear();
988
1036
  syncMessageToDiskMock.mockClear();
989
1037
  rebuildConversationDiskViewFromDbStateMock.mockClear();
1038
+ mockTurnReplyMessageId = undefined;
1039
+ resolveTurnReplyMessageIdMock.mockClear();
990
1040
  emitAssistantReplyNotificationMock.mockClear();
991
1041
  updateMessageMetadataMock.mockClear();
992
1042
  updateMessageMetadataMock.mockImplementation(() => {});
@@ -1038,6 +1088,8 @@ beforeEach(() => {
1038
1088
  emittedAttachments: [],
1039
1089
  directiveWarnings: [],
1040
1090
  persistedFiles: [],
1091
+ linkedAttachmentIds: [],
1092
+ computerUseScreenshotAttachmentIds: [],
1041
1093
  }));
1042
1094
  mockMessageById = null;
1043
1095
  resetConversationNoticesForTests();
@@ -1663,6 +1715,30 @@ describe("session-agent-loop", () => {
1663
1715
  });
1664
1716
  });
1665
1717
 
1718
+ test("releases desktop control before a completed turn accepts another message", async () => {
1719
+ const events: AssistantEvent[] = [];
1720
+ const ctx = makeCtx();
1721
+ const release = spyOn(desktopAutomationLease, "releaseForConversation");
1722
+ const setProcessing = ctx.setProcessing.bind(ctx);
1723
+ ctx.setProcessing = (processing) => {
1724
+ if (!processing) {
1725
+ expect(release).toHaveBeenCalledWith(ctx.conversationId);
1726
+ }
1727
+ setProcessing(processing);
1728
+ };
1729
+ try {
1730
+ await runAgentLoopImpl(ctx, "hello", "msg-1", (event) =>
1731
+ events.push(event),
1732
+ );
1733
+ expect(events.some((event) => event.type === "message_complete")).toBe(
1734
+ true,
1735
+ );
1736
+ expect(release).toHaveBeenCalledTimes(1);
1737
+ } finally {
1738
+ release.mockRestore();
1739
+ }
1740
+ });
1741
+
1666
1742
  describe("tool execution errors via agent loop", () => {
1667
1743
  test("error events from agent loop are classified and emitted", async () => {
1668
1744
  const events: AssistantEvent[] = [];
@@ -1698,6 +1774,139 @@ describe("session-agent-loop", () => {
1698
1774
  const complete = events.find((e) => e.type === "message_complete");
1699
1775
  expect(complete).toBeDefined();
1700
1776
  });
1777
+
1778
+ test("carries automatic screenshot provenance on message completion", async () => {
1779
+ resolveAssistantAttachmentsMock.mockImplementation(async () => ({
1780
+ assistantAttachments: [],
1781
+ emittedAttachments: [
1782
+ {
1783
+ id: "screenshot-1",
1784
+ filename: "computer-use-click.png",
1785
+ mimeType: "image/png",
1786
+ data: "c2NyZWVuc2hvdA==",
1787
+ sourceType: "tool_block" as const,
1788
+ computerUseScreenshot: true,
1789
+ },
1790
+ ],
1791
+ directiveWarnings: [],
1792
+ persistedFiles: [],
1793
+ linkedAttachmentIds: ["screenshot-1"],
1794
+ computerUseScreenshotAttachmentIds: ["screenshot-1"],
1795
+ }));
1796
+ const events: AssistantEvent[] = [];
1797
+ const ctx = makeCtx({ providerResponses: [textResponse("Done")] });
1798
+
1799
+ await runAgentLoopImpl(ctx, "click it", "msg-1", (event) =>
1800
+ events.push(event),
1801
+ );
1802
+
1803
+ const complete = events.find(
1804
+ (event) => event.type === "message_complete",
1805
+ );
1806
+ expect(complete?.attachments?.[0]?.computerUseScreenshot).toBe(true);
1807
+ const syncCalls = syncMessageToDiskMock.mock.calls as unknown as Array<
1808
+ [string, string, number]
1809
+ >;
1810
+ const finalRowSyncs = syncCalls.filter(
1811
+ (call) => call[1] === "msg-reserve",
1812
+ );
1813
+ expect(finalRowSyncs).toHaveLength(1);
1814
+ });
1815
+
1816
+ test("defers an earlier delivered attachment reply to ordered turn settlement", async () => {
1817
+ const featureFlags = await import("../config/assistant-feature-flags.js");
1818
+ const flagSpy = spyOn(
1819
+ featureFlags,
1820
+ "isAssistantFeatureFlagEnabled",
1821
+ ).mockImplementation((key: string) => key === "send-user-message");
1822
+ reserveMessageMock
1823
+ .mockImplementationOnce(async () => ({ id: "msg-delivered-reply" }))
1824
+ .mockImplementationOnce(async () => ({ id: "msg-tool-result" }))
1825
+ .mockImplementationOnce(async () => ({ id: "msg-final-private" }));
1826
+ mockTurnReplyMessageId = "msg-delivered-reply";
1827
+ resolveAssistantAttachmentsMock.mockImplementation(async () => ({
1828
+ assistantAttachments: [],
1829
+ emittedAttachments: [],
1830
+ directiveWarnings: [],
1831
+ persistedFiles: [],
1832
+ linkedAttachmentIds: ["attachment-1"],
1833
+ computerUseScreenshotAttachmentIds: [],
1834
+ }));
1835
+ const assistantSyncsAtTerminal: string[][] = [];
1836
+ const ctx = makeSendUserMessageCtx();
1837
+
1838
+ try {
1839
+ await runAgentLoopImpl(ctx, "click it", "msg-1", (event) => {
1840
+ if (event.type !== "message_complete") {
1841
+ return;
1842
+ }
1843
+ const calls = syncMessageToDiskMock.mock.calls as unknown as Array<
1844
+ [string, string, number]
1845
+ >;
1846
+ assistantSyncsAtTerminal.push(
1847
+ calls
1848
+ .map((call) => call[1])
1849
+ .filter((id) =>
1850
+ ["msg-delivered-reply", "msg-final-private"].includes(id),
1851
+ ),
1852
+ );
1853
+ });
1854
+ } finally {
1855
+ flagSpy.mockRestore();
1856
+ }
1857
+
1858
+ expect(resolveTurnReplyMessageIdMock).toHaveBeenCalledWith(
1859
+ "test-conv",
1860
+ "msg-1",
1861
+ "msg-final-private",
1862
+ );
1863
+ expect(assistantSyncsAtTerminal.at(-1)).toEqual([]);
1864
+ const calls = syncMessageToDiskMock.mock.calls as unknown as Array<
1865
+ [string, string, number]
1866
+ >;
1867
+ expect(
1868
+ calls
1869
+ .map((call) => call[1])
1870
+ .filter((id) =>
1871
+ ["msg-delivered-reply", "msg-final-private"].includes(id),
1872
+ ),
1873
+ ).toEqual(["msg-delivered-reply", "msg-final-private"]);
1874
+ });
1875
+
1876
+ test("does not queue an earlier delivered reply without a linked attachment", async () => {
1877
+ const featureFlags = await import("../config/assistant-feature-flags.js");
1878
+ const flagSpy = spyOn(
1879
+ featureFlags,
1880
+ "isAssistantFeatureFlagEnabled",
1881
+ ).mockImplementation((key: string) => key === "send-user-message");
1882
+ reserveMessageMock
1883
+ .mockImplementationOnce(async () => ({ id: "msg-delivered-empty" }))
1884
+ .mockImplementationOnce(async () => ({ id: "msg-tool-result-empty" }))
1885
+ .mockImplementationOnce(async () => ({ id: "msg-final-empty" }));
1886
+ mockTurnReplyMessageId = "msg-delivered-empty";
1887
+
1888
+ try {
1889
+ await runAgentLoopImpl(
1890
+ makeSendUserMessageCtx(),
1891
+ "click it",
1892
+ "msg-1",
1893
+ () => {},
1894
+ );
1895
+ } finally {
1896
+ flagSpy.mockRestore();
1897
+ }
1898
+
1899
+ const calls = syncMessageToDiskMock.mock.calls as unknown as Array<
1900
+ [string, string, number]
1901
+ >;
1902
+ expect(
1903
+ calls
1904
+ .map((call) => call[1])
1905
+ .filter((id) =>
1906
+ ["msg-delivered-empty", "msg-final-empty"].includes(id),
1907
+ ),
1908
+ ).toEqual(["msg-final-empty"]);
1909
+ });
1701
1910
  });
1702
1911
 
1703
1912
  describe("LLM request log persistence", () => {
@@ -2172,6 +2381,48 @@ describe("session-agent-loop", () => {
2172
2381
  );
2173
2382
  });
2174
2383
 
2384
+ test("carries automatic screenshot provenance on generation handoff", async () => {
2385
+ resolveAssistantAttachmentsMock.mockImplementation(async () => ({
2386
+ assistantAttachments: [],
2387
+ emittedAttachments: [
2388
+ {
2389
+ id: "screenshot-1",
2390
+ filename: "computer-use-click.png",
2391
+ mimeType: "image/png",
2392
+ data: "c2NyZWVuc2hvdA==",
2393
+ sourceType: "tool_block" as const,
2394
+ computerUseScreenshot: true,
2395
+ },
2396
+ ],
2397
+ directiveWarnings: [],
2398
+ persistedFiles: [],
2399
+ linkedAttachmentIds: ["screenshot-1"],
2400
+ computerUseScreenshotAttachmentIds: ["screenshot-1"],
2401
+ }));
2402
+ const events: AssistantEvent[] = [];
2403
+ const ctx = makeCtx({
2404
+ providerResponses: [toolUseResponse("tu-1", "file_read", {})],
2405
+ loopTools: [
2406
+ {
2407
+ name: "file_read",
2408
+ description: "Read a file",
2409
+ input_schema: { type: "object", properties: {} },
2410
+ },
2411
+ ],
2412
+ toolExecutor: async () => ({ content: "content", isError: false }),
2413
+ canHandoffAtCheckpoint: () => true,
2414
+ });
2415
+
2416
+ await runAgentLoopImpl(ctx, "hello", "msg-1", (event) =>
2417
+ events.push(event),
2418
+ );
2419
+
2420
+ const handoff = events.find(
2421
+ (event) => event.type === "generation_handoff",
2422
+ );
2423
+ expect(handoff?.attachments?.[0]?.computerUseScreenshot).toBe(true);
2424
+ });
2425
+
2175
2426
  test("continues when canHandoffAtCheckpoint returns false", async () => {
2176
2427
  const events: AssistantEvent[] = [];
2177
2428
 
@@ -56,6 +56,38 @@ function makeBase64(bytes: number): string {
56
56
  return buf.toString("base64");
57
57
  }
58
58
 
59
+ function estimateBase64Bytes(
60
+ value: string | { data?: unknown; sizeBytes?: unknown } | null | undefined,
61
+ ): number {
62
+ if (value == null) {
63
+ return 0;
64
+ }
65
+ if (typeof value !== "string") {
66
+ if (typeof value.sizeBytes === "number") {
67
+ return value.sizeBytes;
68
+ }
69
+ return typeof value.data === "string" ? estimateBase64Bytes(value.data) : 0;
70
+ }
71
+ const trimmed = value.replace(/\s/g, "");
72
+ const padding = trimmed.endsWith("==") ? 2 : trimmed.endsWith("=") ? 1 : 0;
73
+ return Math.max(0, Math.floor((trimmed.length * 3) / 4) - padding);
74
+ }
75
+
76
+ function toolImageFilename(
77
+ mediaType: string,
78
+ toolName?: string,
79
+ title?: string,
80
+ ): string {
81
+ const prefix =
82
+ title ||
83
+ toolName
84
+ ?.replace(/([a-z])([A-Z])/g, "$1-$2")
85
+ .replace(/_/g, "-")
86
+ .toLowerCase() ||
87
+ "tool-output";
88
+ return `${prefix}.${mediaType.split("/")[1] ?? "png"}`;
89
+ }
90
+
59
91
  // ---------------------------------------------------------------------------
60
92
  // resolveAssistantAttachments — all attachments are now file-backed
61
93
  // ---------------------------------------------------------------------------
@@ -92,6 +124,8 @@ describe("resolveAssistantAttachments", () => {
92
124
  accepted: d,
93
125
  warnings: [],
94
126
  }),
127
+ estimateBase64Bytes,
128
+ toolImageFilename,
95
129
  }));
96
130
 
97
131
  // Re-import to pick up mocks
@@ -117,6 +151,7 @@ describe("resolveAssistantAttachments", () => {
117
151
  expect(result.emittedAttachments.length).toBe(1);
118
152
  const emitted = result.emittedAttachments[0];
119
153
  expect(emitted.id).toBeDefined();
154
+ expect(result.linkedAttachmentIds).toEqual([emitted.id!]);
120
155
 
121
156
  // All attachments are file-backed and have a file on disk
122
157
  const filePath = getFilePathForAttachment(emitted.id!);
@@ -152,6 +187,8 @@ describe("resolveAssistantAttachments", () => {
152
187
  accepted: d,
153
188
  warnings: [],
154
189
  }),
190
+ estimateBase64Bytes,
191
+ toolImageFilename,
155
192
  }));
156
193
 
157
194
  const { resolveAssistantAttachments: resolve } =
@@ -234,6 +271,8 @@ describe("resolveAssistantAttachments", () => {
234
271
  accepted: d.filter((draft) => draft.filename !== "huge.bin"),
235
272
  warnings: ['Skipped attachment "huge.bin": too large.'],
236
273
  }),
274
+ estimateBase64Bytes,
275
+ toolImageFilename,
237
276
  }));
238
277
 
239
278
  const { resolveAssistantAttachments: resolve } =
@@ -281,6 +320,10 @@ describe("resolveAssistantAttachments", () => {
281
320
  sourceType: "sandbox_file",
282
321
  },
283
322
  ]);
323
+ expect(result.linkedAttachmentIds).toHaveLength(2);
324
+ expect(result.linkedAttachmentIds).toEqual(
325
+ result.emittedAttachments.map((attachment) => attachment.id!),
326
+ );
284
327
  });
285
328
 
286
329
  test("persistedFiles is empty when there is no message to persist against", async () => {
@@ -303,6 +346,8 @@ describe("resolveAssistantAttachments", () => {
303
346
  accepted: d,
304
347
  warnings: [],
305
348
  }),
349
+ estimateBase64Bytes,
350
+ toolImageFilename,
306
351
  }));
307
352
 
308
353
  const { resolveAssistantAttachments: resolve } =
@@ -328,6 +373,67 @@ describe("resolveAssistantAttachments", () => {
328
373
 
329
374
  expect(result.emittedAttachments).toHaveLength(1);
330
375
  expect(result.persistedFiles).toEqual([]);
376
+ expect(result.linkedAttachmentIds).toEqual([]);
377
+ });
378
+
379
+ test("reports only successful links when a later upload is skipped", async () => {
380
+ const conv = createConversation("test-conv-partial-upload");
381
+ const msg = await addMessage(conv.id, "assistant", "hello");
382
+ const valid: AssistantAttachmentDraft = {
383
+ sourceType: "sandbox_file",
384
+ filename: "valid.txt",
385
+ mimeType: "text/plain",
386
+ dataBase64: makeBase64(16),
387
+ sizeBytes: 16,
388
+ kind: "document",
389
+ };
390
+ const invalid: AssistantAttachmentDraft = {
391
+ sourceType: "sandbox_file",
392
+ filename: "invalid.txt",
393
+ mimeType: "text/plain",
394
+ dataBase64: "%%%",
395
+ sizeBytes: 3,
396
+ kind: "document",
397
+ };
398
+
399
+ mock.module("../daemon/assistant-attachments.js", () => ({
400
+ resolveDirectives: () =>
401
+ Promise.resolve({ drafts: [valid, invalid], warnings: [] }),
402
+ contentBlocksToDrafts: () => [],
403
+ deduplicateDrafts: (drafts: AssistantAttachmentDraft[]) => drafts,
404
+ validateDrafts: (drafts: AssistantAttachmentDraft[]) => ({
405
+ accepted: drafts,
406
+ warnings: [],
407
+ }),
408
+ estimateBase64Bytes,
409
+ toolImageFilename,
410
+ }));
411
+
412
+ const { resolveAssistantAttachments: resolve } =
413
+ await import("../daemon/conversation-attachments.js");
414
+ const result = await resolve(
415
+ [
416
+ {
417
+ source: "sandbox" as const,
418
+ path: "/fake",
419
+ filename: "valid.txt",
420
+ mimeType: "text/plain",
421
+ },
422
+ ],
423
+ [],
424
+ [],
425
+ "/tmp",
426
+ async () => true,
427
+ msg.id,
428
+ );
429
+
430
+ expect(result.emittedAttachments).toHaveLength(1);
431
+ expect(result.linkedAttachmentIds).toEqual([
432
+ result.emittedAttachments[0]!.id!,
433
+ ]);
434
+ expect(result.directiveWarnings).toContain(
435
+ "Attachment invalid.txt skipped: Invalid base64 encoding",
436
+ );
331
437
  });
332
438
  });
333
439