@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
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Tests for the per-conversation wire surface record
3
+ * (`persistence/conversation-tool-surface.ts`): the send-boundary recorder
4
+ * stores the tools array a live turn sends with the delegation-section state
5
+ * its prompt rendered, fork wakes read both back verbatim, and an unchanged
6
+ * surface is never rewritten.
7
+ */
8
+
9
+ import { Database } from "bun:sqlite";
10
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
11
+
12
+ import { drizzle } from "drizzle-orm/bun-sqlite";
13
+
14
+ import {
15
+ getConversationToolSurface as getConversationToolSurfaceViaPluginApi,
16
+ getRecordedConversationToolSurface,
17
+ } from "../persistence/conversation-plugin-facade.js";
18
+ import {
19
+ type ConversationToolSurface,
20
+ getConversationToolSurface,
21
+ hashConversationToolSurface,
22
+ recordConversationToolSurface,
23
+ } from "../persistence/conversation-tool-surface.js";
24
+ import { clearStoredDb, setStoredDb } from "../persistence/db-singleton.js";
25
+ import { migrateCreateConversationToolSurfaces } from "../persistence/migrations/378-create-conversation-tool-surfaces.js";
26
+ import { migrateConversationToolSurfacesDelegateIndependentTasks } from "../persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.js";
27
+ import * as schema from "../persistence/schema/index.js";
28
+ import type { ToolDefinition } from "../providers/types.js";
29
+
30
+ let sqlite: Database;
31
+
32
+ beforeEach(() => {
33
+ sqlite = new Database(":memory:");
34
+ sqlite.run("PRAGMA foreign_keys = ON");
35
+ sqlite.run(/*sql*/ `CREATE TABLE conversations (id TEXT PRIMARY KEY)`);
36
+ sqlite.query(`INSERT INTO conversations (id) VALUES (?)`).run("conv-1");
37
+ const db = drizzle(sqlite, { schema });
38
+ migrateCreateConversationToolSurfaces(db);
39
+ migrateConversationToolSurfacesDelegateIndependentTasks(db);
40
+ setStoredDb("main", db, () => sqlite.close());
41
+ });
42
+
43
+ afterEach(() => {
44
+ clearStoredDb("main");
45
+ });
46
+
47
+ const TOOLS: ToolDefinition[] = [
48
+ {
49
+ name: "remember",
50
+ description: "Save a fact",
51
+ input_schema: { type: "object", properties: { content: {} } },
52
+ },
53
+ { name: "bell_jingle", description: "Ring", input_schema: {} },
54
+ ];
55
+
56
+ const SURFACE: ConversationToolSurface = {
57
+ tools: TOOLS,
58
+ delegateIndependentTasks: true,
59
+ };
60
+
61
+ function storedRow(): { tools_hash: string; updated_at: number } | null {
62
+ return sqlite
63
+ .query(
64
+ `SELECT tools_hash, updated_at FROM conversation_tool_surfaces WHERE conversation_id = ?`,
65
+ )
66
+ .get("conv-1") as { tools_hash: string; updated_at: number } | null;
67
+ }
68
+
69
+ describe("recordConversationToolSurface", () => {
70
+ test("stores the surface and returns its content hash", () => {
71
+ const hash = recordConversationToolSurface("conv-1", SURFACE);
72
+
73
+ expect(hash).toBe(hashConversationToolSurface(SURFACE));
74
+ expect(storedRow()?.tools_hash).toBe(hash);
75
+ expect(getConversationToolSurface("conv-1")).toEqual(SURFACE);
76
+ });
77
+
78
+ test("replays the exact array, including key order and extra fields", () => {
79
+ const serverTool = {
80
+ type: "web_search_20250305",
81
+ name: "web_search",
82
+ max_uses: 5,
83
+ } as unknown as ToolDefinition;
84
+ const tools = [...TOOLS, serverTool];
85
+
86
+ recordConversationToolSurface("conv-1", {
87
+ tools,
88
+ delegateIndependentTasks: false,
89
+ });
90
+
91
+ expect(JSON.stringify(getConversationToolSurface("conv-1")?.tools)).toBe(
92
+ JSON.stringify(tools),
93
+ );
94
+ });
95
+
96
+ test("the delegation-section state round-trips, unknown included", () => {
97
+ for (const delegateIndependentTasks of [true, false, null]) {
98
+ recordConversationToolSurface("conv-1", {
99
+ tools: TOOLS,
100
+ delegateIndependentTasks,
101
+ });
102
+
103
+ expect(
104
+ getConversationToolSurface("conv-1")?.delegateIndependentTasks,
105
+ ).toBe(delegateIndependentTasks);
106
+ }
107
+ });
108
+
109
+ test("an unchanged surface with a known hash skips the write", () => {
110
+ const hash = recordConversationToolSurface("conv-1", SURFACE);
111
+ const before = storedRow()!.updated_at;
112
+ sqlite
113
+ .query(
114
+ `UPDATE conversation_tool_surfaces SET updated_at = ? WHERE conversation_id = ?`,
115
+ )
116
+ .run(before - 1000, "conv-1");
117
+
118
+ recordConversationToolSurface("conv-1", SURFACE, hash);
119
+
120
+ expect(storedRow()!.updated_at).toBe(before - 1000);
121
+ });
122
+
123
+ test("an unchanged surface with no known hash compares against the stored row before writing", () => {
124
+ recordConversationToolSurface("conv-1", SURFACE);
125
+ const marker = 42;
126
+ sqlite
127
+ .query(
128
+ `UPDATE conversation_tool_surfaces SET updated_at = ? WHERE conversation_id = ?`,
129
+ )
130
+ .run(marker, "conv-1");
131
+
132
+ // A freshly loaded conversation knows no hash yet; the stored row matches,
133
+ // so nothing is rewritten.
134
+ recordConversationToolSurface("conv-1", SURFACE, undefined);
135
+
136
+ expect(storedRow()!.updated_at).toBe(marker);
137
+ });
138
+
139
+ test("a changed array overwrites the stored surface", () => {
140
+ const first = recordConversationToolSurface("conv-1", SURFACE);
141
+ const changed = { ...SURFACE, tools: TOOLS.slice(0, 1) };
142
+
143
+ const second = recordConversationToolSurface("conv-1", changed, first);
144
+
145
+ expect(second).not.toBe(first);
146
+ expect(storedRow()?.tools_hash).toBe(second);
147
+ expect(getConversationToolSurface("conv-1")).toEqual(changed);
148
+ });
149
+
150
+ test("a change to the delegation-section state alone overwrites it too", () => {
151
+ // The state is part of the hash: a turn whose scope flips the section
152
+ // (a background run's allowlist after an interactive turn) records it.
153
+ const first = recordConversationToolSurface("conv-1", SURFACE);
154
+ const changed = { ...SURFACE, delegateIndependentTasks: false };
155
+
156
+ const second = recordConversationToolSurface("conv-1", changed, first);
157
+
158
+ expect(second).not.toBe(first);
159
+ expect(storedRow()?.tools_hash).toBe(second);
160
+ expect(getConversationToolSurface("conv-1")).toEqual(changed);
161
+ });
162
+
163
+ test("a row recorded before the state existed reads unknown and is replaced on the next record", () => {
164
+ sqlite
165
+ .query(
166
+ /*sql*/ `INSERT INTO conversation_tool_surfaces (conversation_id, tools_json, tools_hash, updated_at) VALUES (?, ?, ?, ?)`,
167
+ )
168
+ .run("conv-1", JSON.stringify(TOOLS), "pre-upgrade-hash", 1);
169
+
170
+ expect(getConversationToolSurface("conv-1")).toEqual({
171
+ tools: TOOLS,
172
+ delegateIndependentTasks: null,
173
+ });
174
+
175
+ // The stored hash predates the state, so the next live turn's record
176
+ // rewrites the row with it.
177
+ const hash = recordConversationToolSurface("conv-1", SURFACE, undefined);
178
+
179
+ expect(storedRow()?.tools_hash).toBe(hash);
180
+ expect(getConversationToolSurface("conv-1")).toEqual(SURFACE);
181
+ });
182
+ });
183
+
184
+ describe("getConversationToolSurface", () => {
185
+ test("returns null when no turn has recorded a surface", () => {
186
+ expect(getConversationToolSurface("conv-1")).toBeNull();
187
+ });
188
+
189
+ test("returns null for an unreadable stored payload", () => {
190
+ sqlite
191
+ .query(
192
+ /*sql*/ `INSERT INTO conversation_tool_surfaces (conversation_id, tools_json, tools_hash, updated_at) VALUES (?, ?, ?, ?)`,
193
+ )
194
+ .run("conv-1", "not json", "h", 1);
195
+
196
+ expect(getConversationToolSurface("conv-1")).toBeNull();
197
+ });
198
+
199
+ test("the row cascades with its conversation", () => {
200
+ recordConversationToolSurface("conv-1", SURFACE);
201
+
202
+ sqlite.query(`DELETE FROM conversations WHERE id = ?`).run("conv-1");
203
+
204
+ expect(getConversationToolSurface("conv-1")).toBeNull();
205
+ });
206
+ });
207
+
208
+ describe("plugin facade", () => {
209
+ test("the array accessor is a projection of the recorded surface", async () => {
210
+ expect(await getConversationToolSurfaceViaPluginApi("conv-1")).toBeNull();
211
+ expect(await getRecordedConversationToolSurface("conv-1")).toBeNull();
212
+
213
+ recordConversationToolSurface("conv-1", SURFACE);
214
+
215
+ expect(await getRecordedConversationToolSurface("conv-1")).toEqual(SURFACE);
216
+ // The array shape the public `@vellumai/plugin-api` accessor returns.
217
+ expect(await getConversationToolSurfaceViaPluginApi("conv-1")).toEqual(
218
+ TOOLS,
219
+ );
220
+ });
221
+ });
@@ -85,6 +85,39 @@ describe("GET events/tail", () => {
85
85
  expect(body.frontier).toBe(5);
86
86
  });
87
87
 
88
+ test("returns terminal attachment provenance without rewriting the event", () => {
89
+ stampAndBuffer(
90
+ mkEvent({
91
+ message: {
92
+ type: "message_complete",
93
+ conversationId: CONV,
94
+ messageId: "reply-1",
95
+ attachments: [
96
+ {
97
+ id: "screenshot-1",
98
+ filename: "computer-use-click.png",
99
+ mimeType: "image/png",
100
+ data: "c2NyZWVuc2hvdA==",
101
+ computerUseScreenshot: true,
102
+ },
103
+ ],
104
+ },
105
+ }),
106
+ );
107
+
108
+ const body = callTail({ conversationId: CONV, fromSeq: "0" });
109
+
110
+ expect(body.events[0]?.message).toMatchObject({
111
+ type: "message_complete",
112
+ attachments: [
113
+ {
114
+ id: "screenshot-1",
115
+ computerUseScreenshot: true,
116
+ },
117
+ ],
118
+ });
119
+ });
120
+
88
121
  test("an up-to-date anchor yields an empty but complete tail", () => {
89
122
  // GIVEN three buffered events (seq 1..3)
90
123
  for (let i = 0; i < 3; i++) {
@@ -643,6 +643,59 @@ describe("repairHistory", () => {
643
643
  ]);
644
644
  });
645
645
 
646
+ test("deferred mixed tail answered by a tool_result plus trailing text gets a synthetic result", () => {
647
+ // Text after the client result closes the assistant turn on the provider
648
+ // side, which then rejects the unanswered search as unpaired. The search
649
+ // is an orphan here, so the synthetic error result keeps the request
650
+ // valid at the cost of that one search. `deepRepairHistory` is the loop's
651
+ // recovery path for that rejection.
652
+ const messages: Message[] = [
653
+ { role: "user", content: [{ type: "text", text: "Check both" }] },
654
+ {
655
+ role: "assistant",
656
+ content: [
657
+ { type: "tool_use", id: "tu_a", name: "web_fetch", input: {} },
658
+ {
659
+ type: "server_tool_use",
660
+ id: "stu_deferred",
661
+ name: "web_search",
662
+ input: { query: "news" },
663
+ },
664
+ ],
665
+ },
666
+ {
667
+ role: "user",
668
+ content: [
669
+ {
670
+ type: "tool_result",
671
+ tool_use_id: "tu_a",
672
+ content: "Error: HTTP 404",
673
+ is_error: true,
674
+ },
675
+ { type: "text", text: "<system_notice>retry</system_notice>" },
676
+ ],
677
+ },
678
+ ];
679
+
680
+ const { messages: repaired, stats } = deepRepairHistory(messages);
681
+
682
+ expect(stats.missingToolResultsInserted).toBe(1);
683
+ expect(repaired).toHaveLength(3);
684
+ expect(repaired[1].content.map((b) => b.type)).toEqual([
685
+ "tool_use",
686
+ "server_tool_use",
687
+ "web_search_tool_result",
688
+ ]);
689
+ expect(repaired[1].content[2]).toMatchObject({
690
+ type: "web_search_tool_result",
691
+ tool_use_id: "stu_deferred",
692
+ });
693
+ expect(repaired[2].content.map((b) => b.type)).toEqual([
694
+ "tool_result",
695
+ "text",
696
+ ]);
697
+ });
698
+
646
699
  test("cross-message server tool pair from a completed deferred execution survives a reload untouched", () => {
647
700
  const messages: Message[] = [
648
701
  { role: "user", content: [{ type: "text", text: "Check both" }] },
@@ -383,6 +383,32 @@ describe("setInferenceProfileSession", () => {
383
383
  );
384
384
  });
385
385
 
386
+ test("open with a non-text catalog model — refuses the pin", async () => {
387
+ const conv = createConversation("sess-handler-jev-profile");
388
+ setConfig("llm", {
389
+ profiles: {
390
+ jev: {
391
+ source: "user",
392
+ provider: "jev",
393
+ model: "jev-latest",
394
+ status: "active",
395
+ },
396
+ },
397
+ });
398
+ try {
399
+ const promise = setInferenceProfileSession({
400
+ conversationId: conv.id,
401
+ profile: "jev",
402
+ });
403
+ await expect(promise).rejects.toThrow(
404
+ /structured answers rather than chat text/,
405
+ );
406
+ expect(getConversation(conv.id)?.inferenceProfile).toBeNull();
407
+ } finally {
408
+ setConfig("llm", {});
409
+ }
410
+ });
411
+
386
412
  test("open with an unavailable profile — refuses the pin and names the fix", async () => {
387
413
  const conv = createConversation("sess-handler-unavailable-profile");
388
414
  managedProxyEnabled = false;
@@ -14,10 +14,17 @@ import { setConfig } from "./helpers/set-config.js";
14
14
  // background indexing kicks in.
15
15
  setConfig("memory", { enabled: false, v2: { enabled: false } });
16
16
 
17
+ import {
18
+ linkAttachmentToMessage,
19
+ uploadAttachment,
20
+ } from "../persistence/attachments-store.js";
17
21
  import {
18
22
  addMessage,
19
23
  createConversation,
24
+ getMessageById,
25
+ updateMessageMetadata,
20
26
  } from "../persistence/conversation-crud.js";
27
+ import { COMPUTER_USE_SCREENSHOT_ATTACHMENT_IDS_KEY } from "../persistence/conversation-types.js";
21
28
  import { getDb } from "../persistence/db-connection.js";
22
29
  import { initializeDb } from "../persistence/db-init.js";
23
30
  import { handleListMessages } from "../runtime/routes/conversation-routes.js";
@@ -50,7 +57,15 @@ interface MessagePayload {
50
57
  toolCalls?: ToolCallPayload[];
51
58
  textSegments?: string[];
52
59
  contentOrder?: string[];
53
- contentBlocks?: Array<{ type: string; [key: string]: unknown }>;
60
+ contentBlocks?: Array<{
61
+ type: string;
62
+ attachment?: { id: string; computerUseScreenshot?: boolean };
63
+ [key: string]: unknown;
64
+ }>;
65
+ attachments?: Array<{
66
+ id: string;
67
+ computerUseScreenshot?: boolean;
68
+ }>;
54
69
  }
55
70
 
56
71
  describe("handleListMessages tool_result merging", () => {
@@ -217,6 +232,75 @@ describe("handleListMessages tool_result merging", () => {
217
232
  });
218
233
  });
219
234
 
235
+ test("preserves donor automatic screenshot provenance during consolidation", async () => {
236
+ const conv = createConversation();
237
+ await addMessage(
238
+ conv.id,
239
+ "user",
240
+ JSON.stringify([{ type: "text", text: "inspect this" }]),
241
+ );
242
+ const anchor = await addMessage(
243
+ conv.id,
244
+ "assistant",
245
+ JSON.stringify([{ type: "text", text: "Working." }]),
246
+ { metadata: { anchorContext: "kept" } },
247
+ );
248
+ const explicit = await uploadAttachment(
249
+ "notes.pdf",
250
+ "application/pdf",
251
+ "JVBERg==",
252
+ );
253
+ linkAttachmentToMessage(anchor.id, explicit.id, 0);
254
+
255
+ const donor = await addMessage(
256
+ conv.id,
257
+ "assistant",
258
+ JSON.stringify([{ type: "text", text: "Done." }]),
259
+ );
260
+ const screenshot = await uploadAttachment(
261
+ "computer-use-click.png",
262
+ "image/png",
263
+ "c2NyZWVuc2hvdA==",
264
+ );
265
+ linkAttachmentToMessage(donor.id, screenshot.id, 0);
266
+ updateMessageMetadata(donor.id, {
267
+ [COMPUTER_USE_SCREENSHOT_ATTACHMENT_IDS_KEY]: [screenshot.id],
268
+ });
269
+
270
+ const anchorBefore = getMessageById(anchor.id)?.metadata;
271
+ const donorBefore = getMessageById(donor.id)?.metadata;
272
+ const response = await handleListMessages(createTestArgs(conv.id));
273
+ const body = response as { messages: MessagePayload[] };
274
+ const merged = body.messages[1]!;
275
+
276
+ expect(merged.attachments?.map((attachment) => attachment.id)).toEqual([
277
+ explicit.id,
278
+ screenshot.id,
279
+ ]);
280
+ expect(merged.attachments?.[0]?.computerUseScreenshot).toBeUndefined();
281
+ expect(merged.attachments?.[1]?.computerUseScreenshot).toBe(true);
282
+ const attachmentBlocks = merged.contentBlocks?.filter(
283
+ (block) => block.type === "attachment",
284
+ );
285
+ expect(
286
+ attachmentBlocks?.map((block) =>
287
+ block.type === "attachment" ? block.attachment?.id : undefined,
288
+ ),
289
+ ).toEqual([explicit.id, screenshot.id]);
290
+ expect(
291
+ attachmentBlocks?.[0]?.type === "attachment"
292
+ ? attachmentBlocks[0].attachment?.computerUseScreenshot
293
+ : undefined,
294
+ ).toBeUndefined();
295
+ expect(
296
+ attachmentBlocks?.[1]?.type === "attachment"
297
+ ? attachmentBlocks[1].attachment?.computerUseScreenshot
298
+ : undefined,
299
+ ).toBe(true);
300
+ expect(getMessageById(anchor.id)?.metadata).toBe(anchorBefore);
301
+ expect(getMessageById(donor.id)?.metadata).toBe(donorBefore);
302
+ });
303
+
220
304
  test("orphan tool_result at pagination boundary is suppressed entirely", async () => {
221
305
  const conv = createConversation();
222
306
  // Orphan tool_result with no preceding assistant in this page. The
@@ -2,7 +2,9 @@ import { readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { describe, expect, test } from "bun:test";
4
4
 
5
+ import { DEFAULT_PROVIDER_CHOICES } from "../config/schemas/llm.js";
5
6
  import {
7
+ catalogModelSupportsText,
6
8
  getCatalogProviderForModel,
7
9
  isModelInCatalog,
8
10
  PROVIDER_CATALOG,
@@ -56,6 +58,7 @@ interface ClientCatalogModel {
56
58
  supportsCaching?: boolean;
57
59
  supportsVision?: boolean;
58
60
  supportsToolUse?: boolean;
61
+ supportsText?: boolean;
59
62
  pricing?: {
60
63
  inputPer1mTokens: number;
61
64
  outputPer1mTokens: number;
@@ -204,6 +207,7 @@ describe("LLM catalog parity: daemon vs client", () => {
204
207
  expect(clientModel.supportsCaching).toBe(daemonModel.supportsCaching);
205
208
  expect(clientModel.supportsVision).toBe(daemonModel.supportsVision);
206
209
  expect(clientModel.supportsToolUse).toBe(daemonModel.supportsToolUse);
210
+ expect(clientModel.supportsText).toBe(daemonModel.supportsText);
207
211
  expect(clientModel.pricing).toEqual(daemonModel.pricing);
208
212
  expect(clientModel.featureFlag).toBe(daemonModel.featureFlag);
209
213
  }
@@ -229,6 +233,16 @@ describe("LLM catalog parity: daemon vs client", () => {
229
233
  }
230
234
  });
231
235
 
236
+ test("jev-latest opts out of chat text generation", () => {
237
+ expect(catalogModelSupportsText("jev", "jev-latest")).toBe(false);
238
+ expect(catalogModelSupportsText("anthropic", "claude-opus-4-8")).toBe(true);
239
+ expect(catalogModelSupportsText("openai-compatible", "local-model")).toBe(
240
+ true,
241
+ );
242
+ expect(DEFAULT_PROVIDER_CHOICES).not.toContain("jev");
243
+ expect(DEFAULT_PROVIDER_CHOICES).toContain("poolside");
244
+ });
245
+
232
246
  test("cache pricing rates are positive when defined", () => {
233
247
  for (const entry of PROVIDER_CATALOG) {
234
248
  for (const model of entry.models) {
@@ -252,7 +266,9 @@ describe("LLM catalog parity: daemon vs client", () => {
252
266
  });
253
267
 
254
268
  test("OpenRouter supportsCaching requires cache-read pricing", () => {
255
- const openrouter = PROVIDER_CATALOG.find((entry) => entry.id === "openrouter");
269
+ const openrouter = PROVIDER_CATALOG.find(
270
+ (entry) => entry.id === "openrouter",
271
+ );
256
272
  expect(openrouter).toBeDefined();
257
273
 
258
274
  for (const model of openrouter!.models) {
@@ -266,7 +282,9 @@ describe("LLM catalog parity: daemon vs client", () => {
266
282
  });
267
283
 
268
284
  test("OpenRouter cache-read pricing implies supportsCaching except xAI", () => {
269
- const openrouter = PROVIDER_CATALOG.find((entry) => entry.id === "openrouter");
285
+ const openrouter = PROVIDER_CATALOG.find(
286
+ (entry) => entry.id === "openrouter",
287
+ );
270
288
  expect(openrouter).toBeDefined();
271
289
 
272
290
  for (const model of openrouter!.models) {
@@ -288,7 +306,9 @@ describe("LLM catalog parity: daemon vs client", () => {
288
306
  });
289
307
 
290
308
  test("OpenRouter catalog drops ids OpenRouter no longer serves", () => {
291
- const openrouter = PROVIDER_CATALOG.find((entry) => entry.id === "openrouter");
309
+ const openrouter = PROVIDER_CATALOG.find(
310
+ (entry) => entry.id === "openrouter",
311
+ );
292
312
  expect(openrouter).toBeDefined();
293
313
  const ids = new Set(openrouter!.models.map((model) => model.id));
294
314
  expect(ids.has("deepseek/deepseek-v3.2-speciale")).toBe(false);
@@ -554,7 +574,10 @@ describe("LLM catalog parity: daemon vs client", () => {
554
574
 
555
575
  test("forced tool choice with thinking is scoped to OpenRouter Kimi K2.6", () => {
556
576
  expect(
557
- supportsForcedToolChoiceWithThinking("openrouter", "moonshotai/kimi-k2.6"),
577
+ supportsForcedToolChoiceWithThinking(
578
+ "openrouter",
579
+ "moonshotai/kimi-k2.6",
580
+ ),
558
581
  ).toBe(false);
559
582
  expect(
560
583
  supportsForcedToolChoiceWithThinking(
@@ -75,6 +75,7 @@ mock.module("../oauth/oauth-store.js", () => ({
75
75
  identityBody: null,
76
76
  identityFormat: null,
77
77
  identityOkField: null,
78
+ responseOkField: null,
78
79
  featureFlag: null,
79
80
  createdAt: 1735689500000,
80
81
  updatedAt: 1735689550000,