@vellumai/assistant 0.12.5 → 0.12.6-dev.202609292116.41f7caa

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 (295) hide show
  1. package/.env.example +2 -0
  2. package/ARCHITECTURE.md +26 -3
  3. package/docs/automatic-done.md +32 -0
  4. package/docs/desktop-browser-cli.md +3 -2
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
  9. package/node_modules/@vellumai/environments/src/shell.test.ts +13 -1
  10. package/node_modules/@vellumai/environments/src/shell.ts +22 -1
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
  15. package/node_modules/@vellumai/gateway-client/src/__tests__/status-contracts.test.ts +39 -0
  16. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +25 -0
  17. package/node_modules/@vellumai/service-contracts/package.json +2 -1
  18. package/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
  19. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  20. package/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
  21. package/openapi.yaml +270 -10
  22. package/package.json +1 -1
  23. package/src/__tests__/agent-loop.test.ts +76 -0
  24. package/src/__tests__/anthropic-provider.test.ts +2 -0
  25. package/src/__tests__/byok-default-profile-ensure.test.ts +7 -0
  26. package/src/__tests__/config-get-vision-flag.test.ts +1 -1
  27. package/src/__tests__/config-loader-validation-reset-notice.test.ts +2 -2
  28. package/src/__tests__/conversation-agent-loop-overflow.test.ts +160 -154
  29. package/src/__tests__/conversation-evictor.test.ts +54 -0
  30. package/src/__tests__/conversation-pairing.test.ts +112 -0
  31. package/src/__tests__/document-append-idempotency.test.ts +13 -0
  32. package/src/__tests__/document-create-dedupe.test.ts +13 -0
  33. package/src/__tests__/document-find-replace.test.ts +13 -0
  34. package/src/__tests__/document-revision-history.test.ts +390 -0
  35. package/src/__tests__/document-revision.test.ts +425 -0
  36. package/src/__tests__/document-tool-security.test.ts +13 -0
  37. package/src/__tests__/document-update-default-surface.test.ts +13 -0
  38. package/src/__tests__/emit-signal-routing-intent.test.ts +270 -0
  39. package/src/__tests__/gateway-flag-listener.test.ts +59 -6
  40. package/src/__tests__/headless-browser-navigate.test.ts +20 -30
  41. package/src/__tests__/host-cu-proxy.test.ts +79 -0
  42. package/src/__tests__/host-cu-routes-targeted.test.ts +14 -5
  43. package/src/__tests__/host-shell-tool.test.ts +6 -2
  44. package/src/__tests__/identity-routes.test.ts +4 -3
  45. package/src/__tests__/list-messages-client-message-id.test.ts +43 -0
  46. package/src/__tests__/llm-catalog-parity.test.ts +4 -4
  47. package/src/__tests__/llm-schema.test.ts +1 -1
  48. package/src/__tests__/notification-platform-adapter.test.ts +83 -1
  49. package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
  50. package/src/__tests__/openai-provider.test.ts +38 -11
  51. package/src/__tests__/pricing.test.ts +11 -0
  52. package/src/__tests__/public-ingress-urls.test.ts +26 -0
  53. package/src/__tests__/retry-thinking-tool-choice.test.ts +47 -0
  54. package/src/__tests__/skill-execute-input.test.ts +11 -0
  55. package/src/__tests__/tool-approval-handler.test.ts +4 -0
  56. package/src/__tests__/tool-preview-lifecycle.test.ts +26 -0
  57. package/src/__tests__/usage-attribution.test.ts +31 -0
  58. package/src/__tests__/workspace-migration-133-collapse-provider-connections.test.ts +3 -3
  59. package/src/__tests__/workspace-migration-160-repair-retired-fireworks-model-ids.test.ts +262 -0
  60. package/src/__tests__/workspace-migration-161-repair-retired-fireworks-glm-5p2-model-id.test.ts +233 -0
  61. package/src/acp/agent-process.ts +6 -1
  62. package/src/acp/client-handler.ts +6 -1
  63. package/src/agent/loop-unparseable-tool-retry.test.ts +333 -0
  64. package/src/agent/loop.ts +161 -17
  65. package/src/api/events/document-editor-show.ts +5 -0
  66. package/src/api/events/document-editor-update.ts +10 -0
  67. package/src/api/events/tool-result.ts +1 -0
  68. package/src/calls/__tests__/call-controller.test.ts +273 -1
  69. package/src/calls/__tests__/media-stream-server-integration.test.ts +40 -0
  70. package/src/calls/__tests__/media-stream-stt-session.test.ts +33 -0
  71. package/src/calls/__tests__/voice-session-bridge.test.ts +54 -0
  72. package/src/calls/call-constants.ts +10 -0
  73. package/src/calls/call-controller.ts +359 -25
  74. package/src/calls/channel-admission-reader.ts +15 -9
  75. package/src/calls/media-stream-server.ts +18 -0
  76. package/src/calls/media-stream-stt-session.ts +30 -9
  77. package/src/calls/twilio-provider.ts +14 -31
  78. package/src/calls/voice-session-bridge.ts +18 -1
  79. package/src/cli/commands/browser.ts +1 -2
  80. package/src/cli/lib/__tests__/plugin-artifact.test.ts +23 -0
  81. package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +2 -0
  82. package/src/cli/lib/__tests__/plugin-details.test.ts +16 -0
  83. package/src/cli/lib/bundled-marketplace.json +54 -2
  84. package/src/cli/lib/inspect-plugin.ts +15 -15
  85. package/src/cli/lib/list-installed-plugins.ts +29 -28
  86. package/src/cli/lib/plugin-artifact.ts +74 -63
  87. package/src/cli/lib/plugin-catalog-visibility.ts +1 -0
  88. package/src/cli/lib/plugin-details.ts +27 -42
  89. package/src/cli/lib/plugin-marketplace.ts +6 -8
  90. package/src/cli/lib/plugin-surfaces.ts +8 -7
  91. package/src/cli.ts +1 -3
  92. package/src/config/__tests__/chat-settings.test.ts +121 -0
  93. package/src/config/assistant-feature-flags.ts +28 -23
  94. package/src/config/bundled-skills/computer-use/SKILL.md +20 -0
  95. package/src/config/bundled-skills/computer-use/TOOLS.json +11 -11
  96. package/src/config/chat-settings.ts +100 -0
  97. package/src/config/default-profile-catalog.ts +1 -1
  98. package/src/config/feature-flag-cache.ts +7 -25
  99. package/src/config/feature-flag-registry.json +9 -9
  100. package/src/config/loader.ts +5 -1
  101. package/src/config/profile-text-generation.ts +25 -21
  102. package/src/config/schemas/conversations.ts +14 -0
  103. package/src/config/schemas/llm.ts +1 -0
  104. package/src/config/schemas/notifications.ts +8 -1
  105. package/src/contacts/gateway-channel-read.ts +1 -1
  106. package/src/conversations/__tests__/auto-archive-activity.test.ts +136 -0
  107. package/src/conversations/__tests__/auto-archive.test.ts +304 -0
  108. package/src/conversations/auto-archive-activity.ts +61 -0
  109. package/src/conversations/auto-archive.ts +201 -0
  110. package/src/daemon/computer-use-observation.test.ts +81 -0
  111. package/src/daemon/computer-use-observation.ts +23 -0
  112. package/src/daemon/conversation-agent-loop-handlers.ts +18 -2
  113. package/src/daemon/conversation-agent-loop.ts +14 -6
  114. package/src/daemon/conversation-evictor.ts +50 -16
  115. package/src/daemon/conversation-messaging.ts +4 -0
  116. package/src/daemon/conversation-process.ts +15 -0
  117. package/src/daemon/conversation-queue-manager.ts +2 -0
  118. package/src/daemon/conversation.ts +2 -0
  119. package/src/daemon/doordash-steps.ts +1 -3
  120. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +43 -1
  121. package/src/daemon/handlers/__tests__/config-channels.test.ts +1 -1
  122. package/src/daemon/handlers/config-a2a.ts +75 -2
  123. package/src/daemon/handlers/shared.ts +13 -2
  124. package/src/daemon/host-cu-proxy.ts +34 -3
  125. package/src/daemon/lifecycle.ts +15 -2
  126. package/src/daemon/process-message.ts +5 -0
  127. package/src/daemon/shutdown-handlers.ts +3 -0
  128. package/src/daemon/turn-finalization.ts +4 -0
  129. package/src/desktop/desktop-browser-endpoint.ts +11 -2
  130. package/src/desktop/desktop-chrome-policy.test.ts +1 -0
  131. package/src/desktop/desktop-chrome-policy.ts +7 -1
  132. package/src/desktop/desktop-computer-use.test.ts +14 -4
  133. package/src/desktop/desktop-session-manager.ts +5 -3
  134. package/src/documents/document-revisions-store.ts +153 -0
  135. package/src/documents/document-store.ts +471 -133
  136. package/src/followups/followup-store.ts +0 -4
  137. package/src/i18n/__tests__/i18n.test.ts +25 -0
  138. package/src/i18n/assistant-text.ts +18 -0
  139. package/src/i18n/index.ts +1 -0
  140. package/src/i18n/messages.ts +11 -0
  141. package/src/inbound/__tests__/oauth-callback-url.test.ts +25 -2
  142. package/src/inbound/oauth-callback-url.ts +30 -1
  143. package/src/inbound/public-ingress-urls.ts +12 -2
  144. package/src/ipc/gateway-client.test.ts +40 -0
  145. package/src/ipc/gateway-client.ts +12 -29
  146. package/src/ipc/gateway-flag-listener.ts +7 -10
  147. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +71 -0
  148. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +83 -0
  149. package/src/live-voice/__tests__/protocol.test.ts +77 -0
  150. package/src/live-voice/__tests__/share-targets.test.ts +163 -0
  151. package/src/live-voice/live-voice-session.ts +23 -6
  152. package/src/live-voice/protocol.ts +22 -0
  153. package/src/live-voice/share-targets.ts +225 -0
  154. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +144 -3
  155. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +2 -0
  156. package/src/mcp/client.ts +1 -19
  157. package/src/mcp/mcp-oauth-provider.ts +66 -49
  158. package/src/media/openrouter-image-service.ts +39 -22
  159. package/src/monitoring/__tests__/oom-inheritance-sweep.test.ts +86 -0
  160. package/src/monitoring/__tests__/oom-kill-reporter.test.ts +374 -0
  161. package/src/monitoring/control.ts +2 -1
  162. package/src/monitoring/oom-inheritance-sweep.ts +96 -0
  163. package/src/monitoring/oom-kill-reporter.ts +469 -0
  164. package/src/monitoring/resource-sampler.ts +30 -0
  165. package/src/notifications/README.md +53 -4
  166. package/src/notifications/__tests__/assistant-initiated-thread.test.ts +254 -0
  167. package/src/notifications/__tests__/broadcaster.test.ts +558 -6
  168. package/src/notifications/__tests__/completion-policy.test.ts +37 -0
  169. package/src/notifications/__tests__/from-me-alert-integration.test.ts +192 -0
  170. package/src/notifications/adapters/platform.ts +14 -0
  171. package/src/notifications/assistant-initiated-thread.ts +101 -0
  172. package/src/notifications/broadcaster.ts +94 -16
  173. package/src/notifications/chat-reply-policy.ts +11 -0
  174. package/src/notifications/completion-policy.ts +12 -2
  175. package/src/notifications/conversation-pairing.ts +15 -70
  176. package/src/notifications/emit-signal.ts +34 -3
  177. package/src/notifications/types.ts +2 -0
  178. package/src/oauth/connect-orchestrator.ts +1 -2
  179. package/src/persistence/__tests__/conversation-auto-archive.test.ts +330 -0
  180. package/src/persistence/conversation-auto-archive.ts +170 -0
  181. package/src/persistence/conversation-crud.ts +2 -1
  182. package/src/persistence/conversation-queries.ts +15 -8
  183. package/src/persistence/embeddings/embed.test.ts +96 -0
  184. package/src/persistence/embeddings/embed.ts +20 -2
  185. package/src/persistence/embeddings/embedding-gemini.test.ts +39 -3
  186. package/src/persistence/embeddings/embedding-gemini.ts +39 -1
  187. package/src/persistence/embeddings/embedding-local.ts +6 -0
  188. package/src/persistence/embeddings/qdrant-manager.ts +5 -0
  189. package/src/persistence/job-utils.ts +10 -0
  190. package/src/persistence/migrations/384-add-document-revision.test.ts +83 -0
  191. package/src/persistence/migrations/384-add-document-revision.ts +21 -0
  192. package/src/persistence/migrations/385-create-document-revisions.test.ts +67 -0
  193. package/src/persistence/migrations/385-create-document-revisions.ts +28 -0
  194. package/src/persistence/migrations/386-conversations-last-reopened-at.ts +10 -0
  195. package/src/persistence/migrations/__tests__/386-conversations-last-reopened-at.test.ts +32 -0
  196. package/src/persistence/schema/conversations.ts +1 -0
  197. package/src/persistence/schema/documents.ts +20 -0
  198. package/src/persistence/steps.ts +6 -0
  199. package/src/platform/managed-speech.ts +42 -46
  200. package/src/plugin-api/vision-support.test.ts +21 -21
  201. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +11 -6
  202. package/src/providers/__tests__/dispatch-connection-routing.test.ts +1 -1
  203. package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
  204. package/src/providers/__tests__/unparseable-tool-args.test.ts +25 -0
  205. package/src/providers/anthropic/client.ts +1 -1
  206. package/src/providers/fireworks/client.test.ts +168 -0
  207. package/src/providers/fireworks/client.ts +24 -0
  208. package/src/providers/inference/adapter-factory.ts +6 -0
  209. package/src/providers/ionet/client.test.ts +143 -0
  210. package/src/providers/ionet/client.ts +37 -0
  211. package/src/providers/model-catalog.ts +141 -84
  212. package/src/providers/model-intents.ts +4 -4
  213. package/src/providers/openai/chat-completions-provider.ts +33 -2
  214. package/src/providers/retry.ts +56 -4
  215. package/src/providers/search-provider-catalog.ts +10 -0
  216. package/src/providers/types.ts +10 -2
  217. package/src/providers/unparseable-tool-args.ts +29 -5
  218. package/src/providers/usage-tracking.ts +5 -3
  219. package/src/runtime/assistant-event-hub.ts +23 -0
  220. package/src/runtime/auth/__tests__/guard-tests.test.ts +0 -7
  221. package/src/runtime/auth/__tests__/route-policy.test.ts +1 -1
  222. package/src/runtime/channel-reply-delivery.ts +2 -0
  223. package/src/runtime/channel-retry-sweep.ts +1 -2
  224. package/src/runtime/http-types.ts +3 -0
  225. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +35 -10
  226. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +48 -0
  227. package/src/runtime/migrations/vbundle-builder.ts +19 -3
  228. package/src/runtime/migrations/vbundle-streaming-importer.ts +14 -19
  229. package/src/runtime/migrations/vbundle-streaming-validator.ts +14 -7
  230. package/src/runtime/migrations/vbundle-validator.ts +54 -1
  231. package/src/runtime/pending-interactions.ts +20 -2
  232. package/src/runtime/routes/__tests__/contact-routes.test.ts +5 -3
  233. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +36 -0
  234. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +375 -2
  235. package/src/runtime/routes/__tests__/inference-send-routes.test.ts +1 -1
  236. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +12 -0
  237. package/src/runtime/routes/contact-routes.ts +2 -2
  238. package/src/runtime/routes/conversation-list-routes.ts +10 -2
  239. package/src/runtime/routes/conversation-query-routes.ts +48 -10
  240. package/src/runtime/routes/conversation-routes.ts +7 -0
  241. package/src/runtime/routes/documents-routes.ts +185 -47
  242. package/src/runtime/routes/events-routes.ts +1 -3
  243. package/src/runtime/routes/host-app-control-routes.ts +1 -0
  244. package/src/runtime/routes/host-bash-routes.ts +1 -0
  245. package/src/runtime/routes/host-browser-routes.ts +15 -0
  246. package/src/runtime/routes/host-cu-routes.ts +4 -1
  247. package/src/runtime/routes/host-file-routes.ts +1 -0
  248. package/src/runtime/routes/host-proxy-result-binding.test.ts +83 -0
  249. package/src/runtime/routes/host-proxy-result-binding.ts +25 -0
  250. package/src/runtime/routes/host-transfer-routes.ts +1 -0
  251. package/src/runtime/routes/identity-routes.ts +3 -3
  252. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -0
  253. package/src/runtime/routes/integrations/a2a.ts +4 -1
  254. package/src/runtime/routes/migration-routes.ts +3 -1
  255. package/src/runtime/routes/notification-routes.ts +1 -2
  256. package/src/runtime/routes/ui-snapshot-routes.test.ts +1 -0
  257. package/src/runtime/routes/user-route-dispatcher.ts +5 -1
  258. package/src/runtime/verification-templates.ts +1 -2
  259. package/src/sequence/analytics.ts +0 -4
  260. package/src/sequence/engine.ts +1 -11
  261. package/src/sequence/guardrails.ts +1 -6
  262. package/src/sequence/importer.ts +0 -8
  263. package/src/sequence/store.ts +0 -10
  264. package/src/signals/user-message.ts +0 -6
  265. package/src/skills/catalog-install.ts +52 -23
  266. package/src/tools/browser/browser-execution.ts +20 -23
  267. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +1 -3
  268. package/src/tools/document/document-tool.ts +13 -11
  269. package/src/tools/executor.ts +1 -2
  270. package/src/tools/host-terminal/host-shell.ts +4 -2
  271. package/src/tools/network/__tests__/web-search.test.ts +105 -0
  272. package/src/tools/network/web-fetch.ts +2 -4
  273. package/src/tools/network/web-search.ts +239 -12
  274. package/src/tools/shared/filesystem/format-diff.ts +1 -2
  275. package/src/tools/shared/shell-output.test.ts +42 -0
  276. package/src/tools/shared/shell-output.ts +27 -4
  277. package/src/tools/skills/execute.ts +10 -7
  278. package/src/tools/terminal/shell.ts +3 -3
  279. package/src/tools/tool-approval-handler.ts +9 -1
  280. package/src/tools/ui-surface/visual-validation.ts +1 -2
  281. package/src/usage/attribution.ts +18 -1
  282. package/src/util/__tests__/oom-priority.test.ts +55 -0
  283. package/src/util/__tests__/worker-memory.test.ts +16 -0
  284. package/src/util/debounce.ts +0 -4
  285. package/src/util/oom-priority.ts +79 -0
  286. package/src/util/platform.ts +0 -2
  287. package/src/util/plugin-manifest.ts +5 -7
  288. package/src/util/pricing.ts +25 -0
  289. package/src/util/worker-memory.ts +9 -3
  290. package/src/util/worker-process.ts +8 -0
  291. package/src/workspace/byok-default-profile-ensure.ts +11 -0
  292. package/src/workspace/hatched-date.ts +11 -4
  293. package/src/workspace/migrations/160-repair-retired-fireworks-model-ids.ts +209 -0
  294. package/src/workspace/migrations/161-repair-retired-fireworks-glm-5p2-model-id.ts +197 -0
  295. package/src/workspace/migrations/registry.ts +4 -0
@@ -0,0 +1,67 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { describe, expect, test } from "bun:test";
3
+
4
+ import { drizzle } from "drizzle-orm/bun-sqlite";
5
+
6
+ import * as schema from "../schema.js";
7
+ import { migrateCreateDocumentRevisions } from "./385-create-document-revisions.js";
8
+
9
+ function createTestDb() {
10
+ const sqlite = new Database(":memory:");
11
+ sqlite.exec(/*sql*/ `
12
+ PRAGMA foreign_keys = ON;
13
+ CREATE TABLE documents (
14
+ surface_id TEXT PRIMARY KEY,
15
+ title TEXT NOT NULL,
16
+ content TEXT NOT NULL
17
+ );
18
+ INSERT INTO documents (surface_id, title, content) VALUES ('doc-1', 'Doc', 'body');
19
+ `);
20
+ return { sqlite, db: drizzle(sqlite, { schema }) };
21
+ }
22
+
23
+ function insertSnapshot(sqlite: Database, revision: number): void {
24
+ sqlite
25
+ .query(
26
+ /*sql*/ `INSERT INTO document_revisions (surface_id, revision, title, content, author, created_at)
27
+ VALUES ('doc-1', ?, 'Doc', 'body', 'user', 1000)`,
28
+ )
29
+ .run(revision);
30
+ }
31
+
32
+ describe("migration 385: document_revisions", () => {
33
+ test("creates the table keyed by (surface_id, revision)", () => {
34
+ const { sqlite, db } = createTestDb();
35
+
36
+ migrateCreateDocumentRevisions(db);
37
+
38
+ insertSnapshot(sqlite, 1);
39
+ expect(() => insertSnapshot(sqlite, 1)).toThrow(/UNIQUE/);
40
+ });
41
+
42
+ test("cascades from documents", () => {
43
+ const { sqlite, db } = createTestDb();
44
+ migrateCreateDocumentRevisions(db);
45
+ insertSnapshot(sqlite, 1);
46
+
47
+ sqlite.run("DELETE FROM documents WHERE surface_id = 'doc-1'");
48
+
49
+ const row = sqlite
50
+ .query("SELECT COUNT(*) AS n FROM document_revisions")
51
+ .get() as { n: number };
52
+ expect(row.n).toBe(0);
53
+ });
54
+
55
+ test("is idempotent", () => {
56
+ const { sqlite, db } = createTestDb();
57
+ migrateCreateDocumentRevisions(db);
58
+ insertSnapshot(sqlite, 1);
59
+
60
+ expect(() => migrateCreateDocumentRevisions(db)).not.toThrow();
61
+
62
+ const row = sqlite
63
+ .query("SELECT COUNT(*) AS n FROM document_revisions")
64
+ .get() as { n: number };
65
+ expect(row.n).toBe(1);
66
+ });
67
+ });
@@ -0,0 +1,28 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { getSqliteFrom } from "../db-connection.js";
3
+
4
+ /**
5
+ * Create `document_revisions`: snapshots of a document's title and body as
6
+ * they stood at a given revision, taken just before a write replaces them.
7
+ *
8
+ * `(surface_id, revision)` is the primary key, so a state is stored at most
9
+ * once. The FK cascades from `documents`, so deleting a document drops its
10
+ * history.
11
+ *
12
+ * Idempotent: `IF NOT EXISTS` makes a re-run a no-op.
13
+ */
14
+ export function migrateCreateDocumentRevisions(database: DrizzleDb): void {
15
+ getSqliteFrom(database).exec(/*sql*/ `
16
+ CREATE TABLE IF NOT EXISTS document_revisions (
17
+ surface_id TEXT NOT NULL,
18
+ revision INTEGER NOT NULL,
19
+ title TEXT NOT NULL,
20
+ content TEXT NOT NULL,
21
+ word_count INTEGER NOT NULL DEFAULT 0,
22
+ author TEXT NOT NULL,
23
+ created_at INTEGER NOT NULL,
24
+ PRIMARY KEY (surface_id, revision),
25
+ FOREIGN KEY (surface_id) REFERENCES documents(surface_id) ON DELETE CASCADE
26
+ )
27
+ `);
28
+ }
@@ -0,0 +1,10 @@
1
+ import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";
2
+ import { tableHasColumn } from "./schema-introspection.js";
3
+
4
+ export function migrateConversationsLastReopenedAt(database: DrizzleDb): void {
5
+ if (!tableHasColumn(database, "conversations", "last_reopened_at")) {
6
+ getSqliteFrom(database).exec(
7
+ "ALTER TABLE conversations ADD COLUMN last_reopened_at INTEGER",
8
+ );
9
+ }
10
+ }
@@ -0,0 +1,32 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { expect, test } from "bun:test";
3
+
4
+ import { drizzle } from "drizzle-orm/bun-sqlite";
5
+
6
+ import * as schema from "../../schema/index.js";
7
+ import { migrateConversationsLastReopenedAt } from "../386-conversations-last-reopened-at.js";
8
+
9
+ test("adds a nullable reopen marker without changing existing chats and is idempotent", () => {
10
+ const sqlite = new Database(":memory:");
11
+ try {
12
+ sqlite.exec(
13
+ "CREATE TABLE conversations (id TEXT PRIMARY KEY, archived_at INTEGER)",
14
+ );
15
+ sqlite.exec("INSERT INTO conversations VALUES ('conv-123', 123)");
16
+ const db = drizzle(sqlite, { schema });
17
+ migrateConversationsLastReopenedAt(db);
18
+ migrateConversationsLastReopenedAt(db);
19
+ expect(sqlite.query("SELECT * FROM conversations").get()).toEqual({
20
+ id: "conv-123",
21
+ archived_at: 123,
22
+ last_reopened_at: null,
23
+ });
24
+ sqlite.exec("UPDATE conversations SET last_reopened_at = 456");
25
+ migrateConversationsLastReopenedAt(db);
26
+ expect(
27
+ sqlite.query("SELECT last_reopened_at FROM conversations").get(),
28
+ ).toEqual({ last_reopened_at: 456 });
29
+ } finally {
30
+ sqlite.close();
31
+ }
32
+ });
@@ -83,6 +83,7 @@ export const conversations = sqliteTable(
83
83
  scheduleJobId: text("schedule_job_id"),
84
84
  lastMessageAt: integer("last_message_at"),
85
85
  archivedAt: integer("archived_at"),
86
+ lastReopenedAt: integer("last_reopened_at"),
86
87
  /**
87
88
  * Epoch-ms timestamp set when a background/scheduled conversation is
88
89
  * explicitly promoted into the sidebar's Recents grouping. NULL (the
@@ -22,6 +22,8 @@ export const documents = sqliteTable(
22
22
  wordCount: integer("word_count").notNull().default(0),
23
23
  createdAt: integer("created_at").notNull(),
24
24
  updatedAt: integer("updated_at").notNull(),
25
+ // Bumped by one on every write; conditional saves compare against it.
26
+ revision: integer("revision").notNull().default(0),
25
27
  // Unused by the daemon: no code reads or writes the column, and some rows
26
28
  // carry a non-NULL path. It stays in the model because migration 360 puts
27
29
  // it in the physical table and the model must keep describing that table.
@@ -78,3 +80,21 @@ export const documentComments = sqliteTable(
78
80
  index("idx_document_comments_parent").on(table.parentCommentId),
79
81
  ],
80
82
  );
83
+
84
+ // Snapshots of a document as it stood at `revision`, taken just before a write
85
+ // replaced that state. `author` is who made the replacing write.
86
+ export const documentRevisions = sqliteTable(
87
+ "document_revisions",
88
+ {
89
+ surfaceId: text("surface_id")
90
+ .notNull()
91
+ .references(() => documents.surfaceId, { onDelete: "cascade" }),
92
+ revision: integer("revision").notNull(),
93
+ title: text("title").notNull(),
94
+ content: text("content").notNull(),
95
+ wordCount: integer("word_count").notNull().default(0),
96
+ author: text("author").notNull(),
97
+ createdAt: integer("created_at").notNull(),
98
+ },
99
+ (table) => [primaryKey({ columns: [table.surfaceId, table.revision] })],
100
+ );
@@ -494,6 +494,9 @@ import {
494
494
  } from "./migrations/381-create-conversation-mode-sessions.js";
495
495
  import { migrateCreateClientConnectionEvents } from "./migrations/382-create-client-connection-events.js";
496
496
  import { migrateNormalizeOpencodeHostConnections } from "./migrations/383-normalize-opencode-host-connections.js";
497
+ import { migrateAddDocumentRevision } from "./migrations/384-add-document-revision.js";
498
+ import { migrateCreateDocumentRevisions } from "./migrations/385-create-document-revisions.js";
499
+ import { migrateConversationsLastReopenedAt } from "./migrations/386-conversations-last-reopened-at.js";
497
500
  import type { MigrationStep } from "./migrations/run-migrations.js";
498
501
 
499
502
  export const migrationSteps: MigrationStep[] = [
@@ -1661,4 +1664,7 @@ export const migrationSteps: MigrationStep[] = [
1661
1664
  "migrateProviderConnectionBaseUrlAndModels",
1662
1665
  ],
1663
1666
  },
1667
+ migrateAddDocumentRevision,
1668
+ migrateCreateDocumentRevisions,
1669
+ migrateConversationsLastReopenedAt,
1664
1670
  ];
@@ -17,6 +17,8 @@
17
17
  * imports — the vellum STT/TTS providers build on top of it.
18
18
  */
19
19
 
20
+ import { z } from "zod";
21
+
20
22
  import { VellumPlatformClient } from "./client.js";
21
23
 
22
24
  export type ManagedSpeechResult<T> =
@@ -43,6 +45,11 @@ export interface ManagedSpeechTranscription {
43
45
  durationSeconds: number;
44
46
  }
45
47
 
48
+ const ManagedSpeechTranscriptionSchema = z.object({
49
+ text: z.string(),
50
+ durationSeconds: z.number(),
51
+ });
52
+
46
53
  export interface ManagedSpeechSynthesis {
47
54
  audio: Buffer;
48
55
  contentType: string;
@@ -125,13 +132,10 @@ export async function managedSpeechTranscribe(input: {
125
132
  return await platformError(response, "transcription");
126
133
  }
127
134
 
128
- const body: unknown = await response.json().catch(() => null);
129
- if (
130
- !body ||
131
- typeof body !== "object" ||
132
- typeof (body as { text?: unknown }).text !== "string" ||
133
- typeof (body as { durationSeconds?: unknown }).durationSeconds !== "number"
134
- ) {
135
+ const parsed = ManagedSpeechTranscriptionSchema.safeParse(
136
+ await response.json().catch(() => null),
137
+ );
138
+ if (!parsed.success) {
135
139
  return {
136
140
  ok: false,
137
141
  kind: "platform-error",
@@ -139,10 +143,12 @@ export async function managedSpeechTranscribe(input: {
139
143
  message: "Managed speech transcription returned a malformed response.",
140
144
  };
141
145
  }
142
- const parsed = body as { text: string; durationSeconds: number };
143
146
  return {
144
147
  ok: true,
145
- value: { text: parsed.text, durationSeconds: parsed.durationSeconds },
148
+ value: {
149
+ text: parsed.data.text,
150
+ durationSeconds: parsed.data.durationSeconds,
151
+ },
146
152
  };
147
153
  }
148
154
 
@@ -211,25 +217,21 @@ export interface ManagedSpeechVoiceCatalog {
211
217
  defaultModel: string | null;
212
218
  }
213
219
 
214
- function parseVoice(value: unknown): ManagedSpeechVoice | null {
215
- const voice = value as Partial<Record<keyof ManagedSpeechVoice, unknown>>;
216
- if (
217
- typeof voice?.model !== "string" ||
218
- typeof voice.label !== "string" ||
219
- typeof voice.description !== "string" ||
220
- typeof voice.sampleUrl !== "string" ||
221
- typeof voice.source !== "string"
222
- ) {
223
- return null;
224
- }
225
- return {
226
- model: voice.model,
227
- label: voice.label,
228
- description: voice.description,
229
- sampleUrl: voice.sampleUrl,
230
- source: voice.source,
231
- };
232
- }
220
+ const ManagedSpeechVoiceSchema = z.object({
221
+ model: z.string(),
222
+ label: z.string(),
223
+ description: z.string(),
224
+ sampleUrl: z.string(),
225
+ source: z.string(),
226
+ });
227
+ const ManagedSpeechVoiceCatalogSchema = z.object({
228
+ voices: z.array(ManagedSpeechVoiceSchema.nullable().catch(null)),
229
+ defaultModel: z.string().nullable().optional().catch(null),
230
+ });
231
+ const ManagedSpeechErrorSchema = z.object({
232
+ code: z.string().optional().catch(undefined),
233
+ detail: z.string().optional().catch(undefined),
234
+ });
233
235
 
234
236
  export async function managedSpeechVoices(input?: {
235
237
  signal?: AbortSignal;
@@ -249,12 +251,10 @@ export async function managedSpeechVoices(input?: {
249
251
  return await platformError(response, "voice listing");
250
252
  }
251
253
 
252
- const body: unknown = await response.json().catch(() => null);
253
- const raw = body as { voices?: unknown; defaultModel?: unknown } | null;
254
- const voices = Array.isArray(raw?.voices)
255
- ? raw.voices.map(parseVoice).filter((v): v is ManagedSpeechVoice => !!v)
256
- : null;
257
- if (!voices) {
254
+ const parsed = ManagedSpeechVoiceCatalogSchema.safeParse(
255
+ await response.json().catch(() => null),
256
+ );
257
+ if (!parsed.success) {
258
258
  return {
259
259
  ok: false,
260
260
  kind: "platform-error",
@@ -265,9 +265,10 @@ export async function managedSpeechVoices(input?: {
265
265
  return {
266
266
  ok: true,
267
267
  value: {
268
- voices,
269
- defaultModel:
270
- typeof raw?.defaultModel === "string" ? raw.defaultModel : null,
268
+ voices: parsed.data.voices.filter(
269
+ (voice): voice is ManagedSpeechVoice => voice !== null,
270
+ ),
271
+ defaultModel: parsed.data.defaultModel ?? null,
271
272
  },
272
273
  };
273
274
  }
@@ -279,15 +280,10 @@ async function platformError(
279
280
  let code: string | undefined;
280
281
  let detail: string | undefined;
281
282
  try {
282
- const body = (await response.json()) as {
283
- code?: unknown;
284
- detail?: unknown;
285
- };
286
- if (typeof body?.code === "string") {
287
- code = body.code;
288
- }
289
- if (typeof body?.detail === "string") {
290
- detail = body.detail;
283
+ const parsed = ManagedSpeechErrorSchema.safeParse(await response.json());
284
+ if (parsed.success) {
285
+ code = parsed.data.code;
286
+ detail = parsed.data.detail;
291
287
  }
292
288
  } catch {
293
289
  // Non-JSON error body; status alone will have to do.
@@ -91,7 +91,7 @@ describe("doesSupportVision", () => {
91
91
  setMockConfig({
92
92
  "text-profile": {
93
93
  provider: "fireworks",
94
- model: "accounts/fireworks/models/glm-5p2",
94
+ model: "accounts/fireworks/models/glm-5p3",
95
95
  },
96
96
  });
97
97
  expect(doesSupportVision(profile("text-profile"))).toBe(false);
@@ -162,7 +162,7 @@ describe("doesSupportVision", () => {
162
162
  managed: { provider: "vellum", model: "claude-fable-5" },
163
163
  "managed-text": {
164
164
  provider: "vellum",
165
- model: "accounts/fireworks/models/deepseek-v4-flash-0731",
165
+ model: "accounts/fireworks/models/glm-5p3",
166
166
  },
167
167
  });
168
168
  expect(doesSupportVision(profile("managed"))).toBe(true);
@@ -186,7 +186,7 @@ describe("doesSupportVision", () => {
186
186
  },
187
187
  "text-arm": {
188
188
  provider: "fireworks",
189
- model: "accounts/fireworks/models/glm-5p2",
189
+ model: "accounts/fireworks/models/glm-5p3",
190
190
  },
191
191
  "vision-arm": { provider: "anthropic", model: "claude-opus-4-6" },
192
192
  });
@@ -203,7 +203,7 @@ describe("doesSupportVision", () => {
203
203
  },
204
204
  "text-arm": {
205
205
  provider: "fireworks",
206
- model: "accounts/fireworks/models/glm-5p2",
206
+ model: "accounts/fireworks/models/glm-5p3",
207
207
  },
208
208
  "gateway-arm": {
209
209
  provider: "openai-compatible",
@@ -224,11 +224,11 @@ describe("doesSupportVision", () => {
224
224
  },
225
225
  "text-arm-1": {
226
226
  provider: "fireworks",
227
- model: "accounts/fireworks/models/glm-5p2",
227
+ model: "accounts/fireworks/models/glm-5p3",
228
228
  },
229
229
  "text-arm-2": {
230
230
  provider: "fireworks",
231
- model: "accounts/fireworks/models/glm-5p2",
231
+ model: "accounts/fireworks/models/glm-5p3",
232
232
  },
233
233
  });
234
234
  expect(doesSupportVision(profile("mix-profile"))).toBe(false);
@@ -237,19 +237,19 @@ describe("doesSupportVision", () => {
237
237
 
238
238
  describe("doesSupportVision with a BYO default provider", () => {
239
239
  test("judges a default profile against the default provider's column model", () => {
240
- // Managed/vellum column: cost-optimized → deepseek-v4-flash (text-only,
241
- // supportsVision: false). Anthropic column: cost-optimized carries
242
- // intent "latency-optimized" → claude-haiku-4-5 (supportsVision: true).
240
+ // Managed/vellum column: cost-optimized → deepseek-v4p1-flash
241
+ // (supportsVision: true). OpenRouter column: cost-optimized →
242
+ // deepseek/deepseek-v4-flash (text-only, supportsVision: false).
243
243
  // The judged model must be the one the BYO install actually runs.
244
- setMockConfig({}, { provider: "anthropic" });
245
- expect(doesSupportVision(profile("cost-optimized"))).toBe(true);
244
+ setMockConfig({}, { provider: "openrouter" });
245
+ expect(doesSupportVision(profile("cost-optimized"))).toBe(false);
246
246
  });
247
247
 
248
248
  test("without a default provider, a default profile judges the managed column", () => {
249
249
  // Null-reduction: no defaultProvider resolves cost-optimized through the
250
- // vellum column (deepseek-v4-flash, text-only).
250
+ // vellum column (deepseek-v4p1-flash, vision-capable).
251
251
  setMockConfig({});
252
- expect(doesSupportVision(profile("cost-optimized"))).toBe(false);
252
+ expect(doesSupportVision(profile("cost-optimized"))).toBe(true);
253
253
  });
254
254
 
255
255
  test("mix arms naming a default profile resolve through the same column", () => {
@@ -263,14 +263,14 @@ describe("doesSupportVision with a BYO default provider", () => {
263
263
  },
264
264
  "text-arm": {
265
265
  provider: "fireworks",
266
- model: "accounts/fireworks/models/glm-5p2",
266
+ model: "accounts/fireworks/models/glm-5p3",
267
267
  },
268
268
  },
269
- { provider: "anthropic" },
269
+ { provider: "openrouter" },
270
270
  );
271
- // The "cost-optimized" arm resolves to the anthropic column's
272
- // vision-capable model, so the mix can route to vision.
273
- expect(doesSupportVision(profile("mix-profile"))).toBe(true);
271
+ // The "cost-optimized" arm resolves to the openrouter column's
272
+ // text-only model, so no arm of the mix can route to vision.
273
+ expect(doesSupportVision(profile("mix-profile"))).toBe(false);
274
274
  });
275
275
 
276
276
  test("a user-owned profile is unaffected by the default provider", () => {
@@ -278,7 +278,7 @@ describe("doesSupportVision with a BYO default provider", () => {
278
278
  {
279
279
  "custom-text": {
280
280
  provider: "fireworks",
281
- model: "accounts/fireworks/models/glm-5p2",
281
+ model: "accounts/fireworks/models/glm-5p3",
282
282
  },
283
283
  },
284
284
  { provider: "anthropic" },
@@ -369,7 +369,7 @@ describe("doesSupportVision with a bare string", () => {
369
369
  });
370
370
 
371
371
  test("returns false for a known text-only model id", () => {
372
- expect(doesSupportVision("accounts/fireworks/models/glm-5p2")).toBe(false);
372
+ expect(doesSupportVision("accounts/fireworks/models/glm-5p3")).toBe(false);
373
373
  });
374
374
 
375
375
  test("falls back to resolving the string as a profile key", () => {
@@ -403,7 +403,7 @@ describe("doesSupportVision with a resolved target", () => {
403
403
  expect(
404
404
  doesSupportVision({
405
405
  provider: "fireworks",
406
- model: "accounts/fireworks/models/glm-5p2",
406
+ model: "accounts/fireworks/models/glm-5p3",
407
407
  inputModalities: { image: { enabled: true, supported: true } },
408
408
  }),
409
409
  ).toBe(true);
@@ -203,12 +203,17 @@ const postModelCall: HookFunction<PostModelCallContext> = async (ctx) => {
203
203
  !priorAssistantHadVisibleText;
204
204
 
205
205
  if (isEmptyTurnAfterTools) {
206
- // Only the user-facing reply gets the re-query nudge. Background, subagent,
207
- // and compaction calls have no user awaiting a summary, and the
208
- // post-model-call contract requires self-gating on call site to avoid
209
- // re-querying them. The refusal-rewrite above is a user-facing terminal
210
- // fallback, not a re-query, so it stays ungated.
211
- if (ctx.callSite !== "mainAgent") {
206
+ // Only the user-facing reply and subagent runs get the re-query nudge.
207
+ // Background and compaction calls have no consumer awaiting a summary, and
208
+ // the post-model-call contract requires self-gating on call site to avoid
209
+ // re-querying them. Subagents DO need the nudge: without it, an empty
210
+ // model response after a tool result makes the agent loop exit via
211
+ // "no_tool_calls" and the runner reports the subagent as `completed` with
212
+ // empty output — a silent failure the parent can't distinguish from a
213
+ // real result (see vellum-ai/vellum-assistant#43327). The refusal-rewrite
214
+ // above is a user-facing terminal fallback, not a re-query, so it stays
215
+ // ungated.
216
+ if (ctx.callSite !== "mainAgent" && ctx.callSite !== "subagentSpawn") {
212
217
  return;
213
218
  }
214
219
 
@@ -374,7 +374,7 @@ describe("routing identities", () => {
374
374
  expectedProvider: "anthropic",
375
375
  });
376
376
  expect(
377
- resolveRoutingIdentity("vellum", "accounts/fireworks/models/glm-5p2"),
377
+ resolveRoutingIdentity("vellum", "accounts/fireworks/models/glm-5p3"),
378
378
  ).toEqual({ connectionName: "vellum", expectedProvider: "fireworks" });
379
379
  expect(resolveRoutingIdentity("vellum", "qwen/qwen3-8b")).toEqual({
380
380
  connectionName: "vellum",
@@ -112,4 +112,12 @@ describe("search-provider-catalog", () => {
112
112
  expect(exa?.supportsApiBase).toBeUndefined();
113
113
  expect(exa?.envVar).toBe("EXA_API_KEY");
114
114
  });
115
+
116
+ test("Serply is a keyed BYOK provider without a custom API base", () => {
117
+ const serply = getSearchProvider("serply");
118
+ expect(serply?.kind).toBe("byok");
119
+ expect(serply?.keyless).toBeUndefined();
120
+ expect(serply?.supportsApiBase).toBeUndefined();
121
+ expect(serply?.envVar).toBe("SERPLY_API_KEY");
122
+ });
115
123
  });
@@ -1,6 +1,8 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import {
4
+ boundUnparseableToolArgs,
5
+ boundUnparseableToolArgsRaw,
4
6
  isUnparseableToolArgs,
5
7
  unparseableToolArgsMessage,
6
8
  wrapUnparseableToolArgs,
@@ -31,6 +33,29 @@ describe("unparseable tool args marker", () => {
31
33
  expect(isUnparseableToolArgs({})).toBe(false);
32
34
  });
33
35
 
36
+ test("bounds long raw args while preserving the marker shape", () => {
37
+ const raw = "a".repeat(1_000);
38
+ const bounded = boundUnparseableToolArgs(wrapUnparseableToolArgs(raw));
39
+
40
+ expect(isUnparseableToolArgs(bounded)).toBe(true);
41
+ expect(bounded).toEqual({ _raw: "a".repeat(200) + "…" });
42
+ });
43
+
44
+ test("bounds raw diagnostic copies with the same preview contract", () => {
45
+ expect(boundUnparseableToolArgsRaw("a".repeat(1_000))).toBe(
46
+ "a".repeat(200) + "…",
47
+ );
48
+ expect(boundUnparseableToolArgsRaw("short")).toBe("short");
49
+ });
50
+
51
+ test("leaves short markers and ordinary input unchanged", () => {
52
+ const marker = wrapUnparseableToolArgs("short");
53
+ const ordinary = { command: "ls" };
54
+
55
+ expect(boundUnparseableToolArgs(marker)).toBe(marker);
56
+ expect(boundUnparseableToolArgs(ordinary)).toBe(ordinary);
57
+ });
58
+
34
59
  test("message includes tool name, raw preview, and retry instruction", () => {
35
60
  const msg = unparseableToolArgsMessage("ui_show", '{"surface_type": ');
36
61
  expect(msg).toContain('"ui_show"');
@@ -1008,7 +1008,7 @@ export class AnthropicProvider implements Provider {
1008
1008
  const isHaiku = effectiveModel.includes("haiku");
1009
1009
  // Effort support is per-model: Haiku and Sonnet 4.5 reject the param (see isEffortSupported).
1010
1010
  const supportsEffort = isEffortSupported(effectiveModel);
1011
- // opus-4-7 / opus-4-8 / opus-5 / opus-5-5 and sonnet-5 reject `temperature`,
1011
+ // opus-4-7 / opus-4-8 / opus-5 / opus-5-5 and sonnet-5 / sonnet-5-5 reject `temperature`,
1012
1012
  // `top_p`, and `top_k` with a 400 "`temperature`/`top_p` is deprecated
1013
1013
  // for this model" — model-wide, not effort-conditional (verified
1014
1014
  // 2026-06-23). opus-4-6 / sonnet-4-6 / haiku-4-5 still accept them.