@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
@@ -6,71 +6,40 @@
6
6
  */
7
7
  import { z } from "zod";
8
8
 
9
+ import {
10
+ getDocumentRevision,
11
+ listDocumentRevisions,
12
+ } from "../../documents/document-revisions-store.js";
9
13
  import {
10
14
  addDocumentConversation,
11
15
  createDocument,
12
16
  DEFAULT_DOCUMENT_TITLE,
13
17
  getDocumentById,
14
18
  getDocumentsForConversation,
19
+ listAllDocuments,
20
+ restoreDocumentRevision,
15
21
  saveDocument,
16
22
  } from "../../documents/document-store.js";
17
23
  import { getConversation } from "../../persistence/conversation-crud.js";
18
- import { rawAll } from "../../persistence/raw-query.js";
19
24
  import { getLogger } from "../../util/logger.js";
25
+ import { broadcastMessage } from "../assistant-event-hub.js";
20
26
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
21
27
  import {
22
28
  getOriginClientId,
23
29
  publishDocumentsChanged,
24
30
  } from "../sync/resource-sync-events.js";
25
31
  import { renderMarkdownToPDF } from "./document-pdf-renderer.js";
26
- import { BadRequestError, InternalError, NotFoundError } from "./errors.js";
32
+ import {
33
+ BadRequestError,
34
+ ConflictError,
35
+ InternalError,
36
+ NotFoundError,
37
+ } from "./errors.js";
27
38
  import type { RouteDefinition } from "./types.js";
28
39
  import { RouteResponse } from "./types.js";
29
40
 
30
41
  const log = getLogger("documents-routes");
31
42
 
32
- interface DocumentListRow {
33
- surface_id: string;
34
- conversation_id: string;
35
- title: string;
36
- word_count: number;
37
- created_at: number;
38
- updated_at: number;
39
- }
40
-
41
- function listAllDocuments(): Array<{
42
- surfaceId: string;
43
- conversationId: string;
44
- title: string;
45
- wordCount: number;
46
- createdAt: number;
47
- updatedAt: number;
48
- }> {
49
- try {
50
- const results = rawAll<DocumentListRow>(
51
- "documents:listAllDocuments",
52
- /*sql*/ `
53
- SELECT surface_id, conversation_id, title, word_count, created_at, updated_at
54
- FROM documents
55
- ORDER BY updated_at DESC
56
- `,
57
- );
58
-
59
- log.info({ count: results.length }, "Listed documents");
60
- return results.map((row) => ({
61
- surfaceId: row.surface_id,
62
- conversationId: row.conversation_id,
63
- title: row.title,
64
- wordCount: row.word_count,
65
- createdAt: row.created_at,
66
- updatedAt: row.updated_at,
67
- }));
68
- } catch (error) {
69
- log.error({ err: error }, "List error");
70
- return [];
71
- }
72
- }
73
-
74
43
  /** The document payload shape returned by `GET documents/{id}`. */
75
44
  const documentPayloadSchema = z.object({
76
45
  success: z.boolean(),
@@ -81,8 +50,32 @@ const documentPayloadSchema = z.object({
81
50
  wordCount: z.number(),
82
51
  createdAt: z.number(),
83
52
  updatedAt: z.number(),
53
+ revision: z
54
+ .number()
55
+ .optional()
56
+ .describe(
57
+ "Bumped by one on every write. Absent from assistants that predate document revisions.",
58
+ ),
84
59
  });
85
60
 
61
+ const revisionSummarySchema = z.object({
62
+ revision: z.number().describe("The document revision this snapshot holds"),
63
+ author: z
64
+ .enum(["user", "assistant"])
65
+ .describe("Who made the edit that replaced this state"),
66
+ createdAt: z.number().describe("When the snapshot was taken"),
67
+ wordCount: z.number(),
68
+ title: z.string(),
69
+ });
70
+
71
+ function parseRevisionParam(raw: string | undefined): number {
72
+ const revision = Number(raw);
73
+ if (!raw || !Number.isInteger(revision) || revision < 0) {
74
+ throw new BadRequestError("revision must be a non-negative integer");
75
+ }
76
+ return revision;
77
+ }
78
+
86
79
  // ---------------------------------------------------------------------------
87
80
  // Route definitions
88
81
  // ---------------------------------------------------------------------------
@@ -115,6 +108,12 @@ export const ROUTES: RouteDefinition[] = [
115
108
  wordCount: z.number(),
116
109
  createdAt: z.number(),
117
110
  updatedAt: z.number(),
111
+ revision: z
112
+ .number()
113
+ .optional()
114
+ .describe(
115
+ "Bumped by one on every write. Absent from assistants that predate document revisions.",
116
+ ),
118
117
  }),
119
118
  ),
120
119
  }),
@@ -157,7 +156,8 @@ export const ROUTES: RouteDefinition[] = [
157
156
  allowedPrincipalTypes: ACTOR_PRINCIPALS,
158
157
  },
159
158
  summary: "Save a document",
160
- description: "Create or upsert a document (by surfaceId).",
159
+ description:
160
+ "Create or upsert a document (by surfaceId). With `baseRevision`, an existing document is only overwritten while its revision still equals `baseRevision`; otherwise the save is rejected with 409 CONFLICT and `error.details` carries the current `revision`, `title`, and `content`.",
161
161
  tags: ["documents"],
162
162
  requestBody: z.object({
163
163
  surfaceId: z.string().describe("Surface ID (unique key)"),
@@ -165,19 +165,40 @@ export const ROUTES: RouteDefinition[] = [
165
165
  title: z.string().describe("Document title"),
166
166
  content: z.string().describe("Document content"),
167
167
  wordCount: z.number().describe("Word count"),
168
+ baseRevision: z
169
+ .number()
170
+ .int()
171
+ .nonnegative()
172
+ .optional()
173
+ .describe(
174
+ "The revision this save was edited from. When set and the stored revision differs, nothing is written and the request fails with 409. Omit for an unconditional write.",
175
+ ),
168
176
  }),
169
177
  responseBody: z.object({
170
178
  success: z.literal(true),
171
179
  surfaceId: z.string(),
180
+ revision: z
181
+ .number()
182
+ .optional()
183
+ .describe(
184
+ "The document's revision after the save. Absent from assistants that predate document revisions.",
185
+ ),
172
186
  }),
173
187
  handler: ({ body, headers }) => {
174
- const { surfaceId, conversationId, title, content, wordCount } = (body ??
175
- {}) as {
188
+ const {
189
+ surfaceId,
190
+ conversationId,
191
+ title,
192
+ content,
193
+ wordCount,
194
+ baseRevision,
195
+ } = (body ?? {}) as {
176
196
  surfaceId?: string;
177
197
  conversationId?: string;
178
198
  title?: string;
179
199
  content?: string;
180
200
  wordCount?: number;
201
+ baseRevision?: number | null;
181
202
  };
182
203
 
183
204
  if (!surfaceId || typeof surfaceId !== "string") {
@@ -195,6 +216,14 @@ export const ROUTES: RouteDefinition[] = [
195
216
  if (typeof wordCount !== "number") {
196
217
  throw new BadRequestError("wordCount is required");
197
218
  }
219
+ if (
220
+ baseRevision != null &&
221
+ (!Number.isInteger(baseRevision) || baseRevision < 0)
222
+ ) {
223
+ throw new BadRequestError(
224
+ "baseRevision must be a non-negative integer",
225
+ );
226
+ }
198
227
 
199
228
  const result = saveDocument({
200
229
  surfaceId,
@@ -202,9 +231,13 @@ export const ROUTES: RouteDefinition[] = [
202
231
  title,
203
232
  content,
204
233
  wordCount,
234
+ baseRevision: baseRevision ?? undefined,
205
235
  });
206
236
 
207
237
  if (!result.success) {
238
+ if (result.conflict) {
239
+ throw new ConflictError(result.error, result.conflict);
240
+ }
208
241
  throw new InternalError(result.error);
209
242
  }
210
243
  // Every save moves `updated_at`, which both document lists order by and
@@ -305,6 +338,111 @@ export const ROUTES: RouteDefinition[] = [
305
338
  },
306
339
  },
307
340
 
341
+ {
342
+ operationId: "listDocumentRevisions",
343
+ endpoint: "documents/:id/revisions",
344
+ method: "GET",
345
+ policy: {
346
+ requiredScopes: ["settings.read"],
347
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
348
+ },
349
+ summary: "List a document's revisions",
350
+ description:
351
+ "Return the document's history snapshots, newest first, without their content.",
352
+ tags: ["documents"],
353
+ responseBody: z.object({ revisions: z.array(revisionSummarySchema) }),
354
+ handler: ({ pathParams }) => {
355
+ if (!getDocumentById(pathParams!.id)) {
356
+ throw new NotFoundError("Document not found");
357
+ }
358
+ return { revisions: listDocumentRevisions(pathParams!.id) };
359
+ },
360
+ },
361
+
362
+ {
363
+ operationId: "getDocumentRevision",
364
+ endpoint: "documents/:id/revisions/:revision",
365
+ method: "GET",
366
+ policy: {
367
+ requiredScopes: ["settings.read"],
368
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
369
+ },
370
+ summary: "Get a document revision",
371
+ description: "Return one history snapshot with its content.",
372
+ tags: ["documents"],
373
+ responseBody: revisionSummarySchema.extend({ content: z.string() }),
374
+ handler: ({ pathParams }) => {
375
+ const revision = parseRevisionParam(pathParams!.revision);
376
+ const snapshot = getDocumentRevision(pathParams!.id, revision);
377
+ if (!snapshot) {
378
+ throw new NotFoundError("Revision not found");
379
+ }
380
+ return snapshot;
381
+ },
382
+ },
383
+
384
+ {
385
+ operationId: "restoreDocumentRevision",
386
+ endpoint: "documents/:id/revisions/:revision/restore",
387
+ method: "POST",
388
+ policy: {
389
+ requiredScopes: ["settings.write"],
390
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
391
+ },
392
+ summary: "Restore a document revision",
393
+ description:
394
+ "Write a snapshot's title and content back as a new revision. The current state is snapshotted first, so the restore can itself be undone.",
395
+ tags: ["documents"],
396
+ responseBody: z.object({
397
+ success: z.literal(true),
398
+ surfaceId: z.string(),
399
+ revision: z
400
+ .number()
401
+ .describe("The document's revision after the restore"),
402
+ title: z.string(),
403
+ content: z.string(),
404
+ }),
405
+ handler: ({ pathParams, headers }) => {
406
+ const surfaceId = pathParams!.id;
407
+ const revision = parseRevisionParam(pathParams!.revision);
408
+ const result = restoreDocumentRevision(surfaceId, revision);
409
+ if (!result.success) {
410
+ throw new NotFoundError(
411
+ result.notFound === "document"
412
+ ? "Document not found"
413
+ : "Revision not found",
414
+ );
415
+ }
416
+ const doc = getDocumentById(surfaceId);
417
+ if (doc) {
418
+ broadcastMessage(
419
+ {
420
+ type: "document_editor_update",
421
+ conversationId: doc.conversationId,
422
+ surfaceId,
423
+ markdown: result.content,
424
+ mode: "replace",
425
+ revision: result.revision,
426
+ title: result.title,
427
+ },
428
+ doc.conversationId,
429
+ );
430
+ }
431
+ publishDocumentsChanged(getOriginClientId(headers));
432
+ log.info(
433
+ { surfaceId, restoredRevision: revision, revision: result.revision },
434
+ "Restored document revision via HTTP",
435
+ );
436
+ return {
437
+ success: true as const,
438
+ surfaceId,
439
+ revision: result.revision,
440
+ title: result.title,
441
+ content: result.content,
442
+ };
443
+ },
444
+ },
445
+
308
446
  {
309
447
  operationId: "exportDocumentPDF",
310
448
  endpoint: "documents/:id/pdf",
@@ -492,7 +492,7 @@ export function handleSubscribeAssistantEvents(
492
492
  });
493
493
  }
494
494
 
495
- const stream = new ReadableStream<Uint8Array>(
495
+ return new ReadableStream<Uint8Array>(
496
496
  {
497
497
  start(controller) {
498
498
  controllerRef = controller;
@@ -573,8 +573,6 @@ export function handleSubscribeAssistantEvents(
573
573
  },
574
574
  new CountQueuingStrategy({ highWaterMark: 16 }),
575
575
  );
576
-
577
- return stream;
578
576
  }
579
577
 
580
578
  /**
@@ -91,6 +91,7 @@ async function handleHostAppControlResult({ body, headers }: RouteHandlerArgs) {
91
91
  await assertHostProxyResultBinding({
92
92
  headers,
93
93
  targetClientId: peeked.targetClientId,
94
+ targetConnectionId: peeked.targetConnectionId,
94
95
  targetActorPrincipalId: peeked.targetActorPrincipalId,
95
96
  op: "host_app_control",
96
97
  missingClientIdMessage:
@@ -64,6 +64,7 @@ async function handleHostBashResult({ body, headers }: RouteHandlerArgs) {
64
64
  await assertHostProxyResultBinding({
65
65
  headers,
66
66
  targetClientId: peeked.targetClientId,
67
+ targetConnectionId: peeked.targetConnectionId,
67
68
  targetActorPrincipalId: peeked.targetActorPrincipalId,
68
69
  op: "host_bash",
69
70
  missingClientIdMessage:
@@ -122,6 +122,21 @@ export async function resolveHostBrowserResultByRequestId(
122
122
  message: `Client "${submittingClientId}" is not the target for this request (expected "${peeked.targetClientId}"). The targeted client must submit the result.`,
123
123
  };
124
124
  }
125
+ if (
126
+ peeked.targetConnectionId &&
127
+ !assistantEventHub.hasActiveClientConnection(
128
+ peeked.targetConnectionId,
129
+ peeked.targetClientId,
130
+ )
131
+ ) {
132
+ return {
133
+ ok: false,
134
+ code: "FORBIDDEN",
135
+ status: 403,
136
+ message:
137
+ "This host-proxy result is bound to a connection that is no longer active.",
138
+ };
139
+ }
125
140
 
126
141
  // Defense-in-depth: require the submitting actor's principal id to match
127
142
  // the actor principal id captured when the target client opened its SSE
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { z } from "zod";
8
8
 
9
+ import { prepareComputerUseObservation } from "../../daemon/computer-use-observation.js";
9
10
  import { findConversation } from "../../daemon/conversation-registry.js";
10
11
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
11
12
  import { SAME_ACTOR_FORBIDDEN_DESCRIPTION } from "../auth/same-actor.js";
@@ -77,6 +78,7 @@ async function handleHostCuResult({ body, headers }: RouteHandlerArgs) {
77
78
  await assertHostProxyResultBinding({
78
79
  headers: headers as Record<string, string | undefined> | undefined,
79
80
  targetClientId: peeked.targetClientId,
81
+ targetConnectionId: peeked.targetConnectionId,
80
82
  targetActorPrincipalId: peeked.targetActorPrincipalId,
81
83
  op: "host_cu",
82
84
  missingClientIdMessage:
@@ -112,7 +114,7 @@ async function handleHostCuResult({ body, headers }: RouteHandlerArgs) {
112
114
  throw new NotFoundError("No host CU proxy for conversation");
113
115
  }
114
116
 
115
- conversation.hostCuProxy.processObservation(requestId, {
117
+ const observation = await prepareComputerUseObservation({
116
118
  axTree,
117
119
  axDiff,
118
120
  screenshot,
@@ -126,6 +128,7 @@ async function handleHostCuResult({ body, headers }: RouteHandlerArgs) {
126
128
  userGuidance,
127
129
  timings,
128
130
  });
131
+ conversation.hostCuProxy.processObservation(requestId, observation);
129
132
 
130
133
  return { accepted: true };
131
134
  }
@@ -63,6 +63,7 @@ async function handleHostFileResult({ body, headers }: RouteHandlerArgs) {
63
63
  await assertHostProxyResultBinding({
64
64
  headers: headers as Record<string, string | undefined> | undefined,
65
65
  targetClientId: peeked.targetClientId,
66
+ targetConnectionId: peeked.targetConnectionId,
66
67
  targetActorPrincipalId: peeked.targetActorPrincipalId,
67
68
  op: "host_file",
68
69
  missingClientIdMessage:
@@ -0,0 +1,83 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+
3
+ import { assistantEventHub } from "../assistant-event-hub.js";
4
+ import * as pendingInteractions from "../pending-interactions.js";
5
+ import { ForbiddenError } from "./errors.js";
6
+ import { assertHostProxyResultBinding } from "./host-proxy-result-binding.js";
7
+
8
+ const ACTOR = "actor-host-proxy-bind";
9
+ const subscriptions: Array<{ dispose(): void }> = [];
10
+
11
+ function subscribeClient(clientId: string) {
12
+ const sub = assistantEventHub.subscribe({
13
+ type: "client",
14
+ clientId,
15
+ interfaceId: "macos",
16
+ capabilities: ["host_bash"],
17
+ actorPrincipalId: ACTOR,
18
+ callback: () => {},
19
+ });
20
+ subscriptions.push(sub);
21
+ return sub;
22
+ }
23
+
24
+ describe("assertHostProxyResultBinding connection snapshot", () => {
25
+ afterEach(() => {
26
+ pendingInteractions.clear();
27
+ for (const sub of subscriptions.splice(0)) {
28
+ sub.dispose();
29
+ }
30
+ });
31
+
32
+ test("allows a result while the dispatch connection is still active", async () => {
33
+ const sub = subscribeClient("host-proxy-bind-1");
34
+ pendingInteractions.register("req-active", {
35
+ kind: "host_bash",
36
+ targetClientId: "host-proxy-bind-1",
37
+ });
38
+ const pending = pendingInteractions.get("req-active");
39
+ expect(pending?.targetConnectionId).toBe(sub.connectionId);
40
+
41
+ await assertHostProxyResultBinding({
42
+ headers: {
43
+ "x-vellum-client-id": "host-proxy-bind-1",
44
+ "x-vellum-actor-principal-id": ACTOR,
45
+ },
46
+ targetClientId: pending?.targetClientId,
47
+ targetConnectionId: pending?.targetConnectionId,
48
+ targetActorPrincipalId: ACTOR,
49
+ op: "host_bash",
50
+ missingClientIdMessage: "missing",
51
+ });
52
+ sub.dispose();
53
+ });
54
+
55
+ test("rejects a result after the same clientId reconnects", async () => {
56
+ const first = subscribeClient("host-proxy-bind-2");
57
+ pendingInteractions.register("req-stale", {
58
+ kind: "host_bash",
59
+ targetClientId: "host-proxy-bind-2",
60
+ });
61
+ const pending = pendingInteractions.get("req-stale");
62
+ expect(pending?.targetConnectionId).toBe(first.connectionId);
63
+
64
+ const second = subscribeClient("host-proxy-bind-2");
65
+ expect(first.active).toBe(false);
66
+ expect(second.connectionId).not.toBe(first.connectionId);
67
+
68
+ await expect(
69
+ assertHostProxyResultBinding({
70
+ headers: {
71
+ "x-vellum-client-id": "host-proxy-bind-2",
72
+ "x-vellum-actor-principal-id": ACTOR,
73
+ },
74
+ targetClientId: pending?.targetClientId,
75
+ targetConnectionId: pending?.targetConnectionId,
76
+ targetActorPrincipalId: ACTOR,
77
+ op: "host_bash",
78
+ missingClientIdMessage: "missing",
79
+ }),
80
+ ).rejects.toBeInstanceOf(ForbiddenError);
81
+ second.dispose();
82
+ });
83
+ });
@@ -18,9 +18,29 @@ import {
18
18
  import { resolveActorPrincipalIdForLocalGuardian } from "../local-actor-identity.js";
19
19
  import { BadRequestError, ForbiddenError } from "./errors.js";
20
20
 
21
+ export function assertHostProxyConnectionBinding(args: {
22
+ targetClientId?: string;
23
+ targetConnectionId?: string;
24
+ }): void {
25
+ if (!args.targetConnectionId) {
26
+ return;
27
+ }
28
+ if (
29
+ !assistantEventHub.hasActiveClientConnection(
30
+ args.targetConnectionId,
31
+ args.targetClientId,
32
+ )
33
+ ) {
34
+ throw new ForbiddenError(
35
+ "This host-proxy result is bound to a connection that is no longer active.",
36
+ );
37
+ }
38
+ }
39
+
21
40
  export async function assertHostProxyResultBinding(args: {
22
41
  headers?: Record<string, string | undefined>;
23
42
  targetClientId?: string;
43
+ targetConnectionId?: string;
24
44
  targetActorPrincipalId?: string;
25
45
  op: SameActorOp;
26
46
  missingClientIdMessage: string;
@@ -43,6 +63,11 @@ export async function assertHostProxyResultBinding(args: {
43
63
  }
44
64
  }
45
65
 
66
+ assertHostProxyConnectionBinding({
67
+ targetClientId: args.targetClientId,
68
+ targetConnectionId: args.targetConnectionId,
69
+ });
70
+
46
71
  if (!args.targetClientId && !args.targetActorPrincipalId) {
47
72
  return;
48
73
  }
@@ -249,6 +249,7 @@ async function handleTransferResult({ body, headers }: RouteHandlerArgs) {
249
249
  await assertHostProxyResultBinding({
250
250
  headers: headers as Record<string, string | undefined> | undefined,
251
251
  targetClientId: peeked.targetClientId,
252
+ targetConnectionId: peeked.targetConnectionId,
252
253
  targetActorPrincipalId: peeked.targetActorPrincipalId,
253
254
  op: "host_transfer",
254
255
  missingClientIdMessage:
@@ -159,6 +159,7 @@ async function getDetailedHealth() {
159
159
  memoryOptOut: true,
160
160
  retryLastTurn: true,
161
161
  appPins: true,
162
+ chatsSettings: true,
162
163
  },
163
164
  ...(profiler ? { profiler } : {}),
164
165
  ...migrationHealthFields,
@@ -226,8 +227,6 @@ function getIdentity() {
226
227
  const content = readFileSync(identityPath, "utf-8");
227
228
  const fields = parseIdentityFields(content);
228
229
 
229
- const version = APP_VERSION;
230
-
231
230
  const createdAt = resolveHatchedAtReadOnly(identityPath);
232
231
 
233
232
  return {
@@ -236,7 +235,7 @@ function getIdentity() {
236
235
  personality: fields.personality ?? "",
237
236
  emoji: fields.emoji ?? "",
238
237
  home: fields.home ?? "",
239
- version,
238
+ version: APP_VERSION,
240
239
  createdAt,
241
240
  };
242
241
  }
@@ -280,6 +279,7 @@ const healthCapabilitiesSchema = z.object({
280
279
  memoryOptOut: z.boolean(),
281
280
  retryLastTurn: z.boolean(),
282
281
  appPins: z.boolean(),
282
+ chatsSettings: z.boolean().optional(),
283
283
  });
284
284
 
285
285
  const healthDiskSchema = z.object({
@@ -19,6 +19,7 @@ import {
19
19
  } from "../../../contacts/guardian-delivery-reader.js";
20
20
  import { isConversationBusyError } from "../../../daemon/conversation-messaging.js";
21
21
  import type { TrustContext } from "../../../daemon/trust-context-types.js";
22
+ import { negotiateLocale } from "../../../i18n/index.js";
22
23
  import type { ProviderMessageMetadata } from "../../../messaging/provider-message-metadata.js";
23
24
  import {
24
25
  channelActivityRefreshMs,
@@ -288,6 +289,9 @@ export function processChannelMessageInBackground(
288
289
  trustContext: trustCtx,
289
290
  author: trustCtx,
290
291
  isInteractive: resolveRoutingState(trustCtx).promptWaitingAllowed,
292
+ ...(sourceLanguageCode
293
+ ? { locale: negotiateLocale([sourceLanguageCode]) }
294
+ : {}),
291
295
  ...(displayContent !== undefined ? { displayContent } : {}),
292
296
  ...(cmdIntent ? { commandIntent: cmdIntent } : {}),
293
297
  ...(slackInbound ? { slackInbound } : {}),
@@ -317,7 +317,10 @@ export const ROUTES: RouteDefinition[] = [
317
317
  operationId: "integrations_a2a_invite_accept_post",
318
318
  endpoint: "integrations/a2a/invite/accept",
319
319
  method: "POST",
320
- policy: null,
320
+ policy: {
321
+ requiredScopes: ["settings.write"],
322
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
323
+ },
321
324
  summary: "Accept A2A invite (self-hosted broker)",
322
325
  description:
323
326
  "Orchestrate cross-daemon invite acceptance for self-hosted deployments. Calls the sender's invite/complete, then creates a local contact via invite/redeem.",
@@ -2056,7 +2056,9 @@ export async function handleMigrationPreflightFromGcs({
2056
2056
  }
2057
2057
 
2058
2058
  const bytes = new Uint8Array(await upstream.arrayBuffer());
2059
- const validationResult = validateVBundle(bytes);
2059
+ const validationResult = validateVBundle(bytes, {
2060
+ enforceStreamingLimits: true,
2061
+ });
2060
2062
 
2061
2063
  if (!validationResult.is_valid || !validationResult.manifest) {
2062
2064
  return {
@@ -94,8 +94,7 @@ async function handleEmitSignal({ body = {} }: RouteHandlerArgs) {
94
94
  parsed.error.issues[0]?.message ?? "Invalid signal parameters",
95
95
  );
96
96
  }
97
- const validated = parsed.data;
98
- const result = await emitNotificationSignal(validated);
97
+ const result = await emitNotificationSignal(parsed.data);
99
98
  return {
100
99
  signalId: result.signalId,
101
100
  dispatched: result.dispatched,
@@ -33,6 +33,7 @@ mock.module("../assistant-event-hub.js", () => ({
33
33
  fakeClient?.clientId === clientId
34
34
  ? fakeClient.actorPrincipalId
35
35
  : undefined,
36
+ getClientById: () => undefined,
36
37
  },
37
38
  broadcastMessage: (
38
39
  msg: Record<string, unknown>,
@@ -135,7 +135,11 @@ export class UserRouteDispatcher {
135
135
  * @returns A Response from the handler, or an error response (404, 405, 500).
136
136
  */
137
137
  async dispatch(routePath: string, request: Request): Promise<Response> {
138
- if (routePath.includes("..")) {
138
+ // ".." covers classic dot-segment traversal. "\" is rejected alongside it
139
+ // because some URL normalizers treat a backslash as a path separator, so
140
+ // a routePath that looks in-namespace after percent-decoding can still
141
+ // resolve outside the routes directory once such a normalizer runs.
142
+ if (routePath.includes("..") || routePath.includes("\\")) {
139
143
  return httpError("BAD_REQUEST", "Path traversal is not allowed", 400);
140
144
  }
141
145
 
@@ -174,8 +174,7 @@ export function composeVerificationText(
174
174
  templateKey: TextVerifyTemplateKey,
175
175
  vars: GuardianVerifyTemplateVars,
176
176
  ): string {
177
- const composer = templates[templateKey];
178
- return composer(vars);
177
+ return templates[templateKey](vars);
179
178
  }
180
179
 
181
180
  // ---------------------------------------------------------------------------