@vellumai/assistant 0.12.1 → 0.12.2-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 (548) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +20 -21
  3. package/docs/architecture/turn-actor.md +15 -8
  4. package/docs/guardian-request-flow.md +16 -0
  5. package/openapi.yaml +327 -52
  6. package/package.json +3 -1
  7. package/scripts/smoke-desktop-window-theme.ts +186 -0
  8. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  9. package/src/__tests__/anthropic-provider.test.ts +0 -63
  10. package/src/__tests__/app-compiler.test.ts +6 -0
  11. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  12. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  13. package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
  14. package/src/__tests__/assistant-event-hub.test.ts +44 -0
  15. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  16. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  17. package/src/__tests__/computer-use-tools.test.ts +19 -7
  18. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  19. package/src/__tests__/config-schema.test.ts +28 -29
  20. package/src/__tests__/config-watcher.test.ts +23 -8
  21. package/src/__tests__/connection-model-compat.test.ts +2 -2
  22. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  23. package/src/__tests__/conversation-pairing.test.ts +26 -0
  24. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  25. package/src/__tests__/conversation-queue.test.ts +48 -207
  26. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  27. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  28. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  29. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  30. package/src/__tests__/conversation-title-service.test.ts +14 -10
  31. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  32. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  33. package/src/__tests__/credential-record-retire.test.ts +171 -1
  34. package/src/__tests__/credential-routes.test.ts +212 -3
  35. package/src/__tests__/credential-security-invariants.test.ts +4 -2
  36. package/src/__tests__/cu-unified-flow.test.ts +8 -2
  37. package/src/__tests__/document-sync-tags.test.ts +25 -0
  38. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  39. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  40. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  41. package/src/__tests__/git-porcelain.test.ts +47 -0
  42. package/src/__tests__/heartbeat-service.test.ts +56 -40
  43. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  44. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  45. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  46. package/src/__tests__/host-cu-proxy.test.ts +539 -0
  47. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  48. package/src/__tests__/host-file-proxy.test.ts +11 -23
  49. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  50. package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
  51. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  52. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  53. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  54. package/src/__tests__/mcp-health-check.test.ts +8 -11
  55. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  56. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  57. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  58. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  59. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  60. package/src/__tests__/model-intents.test.ts +3 -0
  61. package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
  62. package/src/__tests__/oauth-commands-routes.test.ts +113 -0
  63. package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
  64. package/src/__tests__/openai-provider.test.ts +1 -0
  65. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  66. package/src/__tests__/platform-callback-registration.test.ts +8 -16
  67. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  68. package/src/__tests__/plugin-import-boundary-guard.test.ts +14 -0
  69. package/src/__tests__/qdrant-manager.test.ts +132 -0
  70. package/src/__tests__/resolve-app.test.ts +135 -0
  71. package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
  72. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  73. package/src/__tests__/service-account-token.test.ts +247 -0
  74. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  75. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  76. package/src/__tests__/skills.test.ts +9 -4
  77. package/src/__tests__/source-fingerprint.test.ts +184 -0
  78. package/src/__tests__/subagent-tool-gate-mode.test.ts +19 -3
  79. package/src/__tests__/subagent-tools.test.ts +3 -2
  80. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  81. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  82. package/src/__tests__/unicode.test.ts +13 -0
  83. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  84. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  85. package/src/__tests__/workspace-git-service.test.ts +26 -0
  86. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  87. package/src/acp/__tests__/agent-process.test.ts +258 -4
  88. package/src/acp/__tests__/auth-required.test.ts +96 -0
  89. package/src/acp/__tests__/client-handler.test.ts +90 -2
  90. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  91. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  92. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  93. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  94. package/src/acp/__tests__/session-manager.test.ts +895 -4
  95. package/src/acp/agent-process.test.ts +4 -1
  96. package/src/acp/agent-process.ts +96 -10
  97. package/src/acp/auth-required.ts +50 -1
  98. package/src/acp/auto-install.test.ts +9 -7
  99. package/src/acp/client-handler.ts +16 -3
  100. package/src/acp/model-config.test.ts +166 -0
  101. package/src/acp/model-config.ts +138 -0
  102. package/src/acp/resolve-agent.test.ts +220 -6
  103. package/src/acp/resolve-agent.ts +88 -23
  104. package/src/acp/session-manager.test.ts +196 -3
  105. package/src/acp/session-manager.ts +441 -18
  106. package/src/acp/types.ts +50 -5
  107. package/src/agent/loop.ts +1 -0
  108. package/src/api/events/acp-session-model-update.test.ts +88 -0
  109. package/src/api/events/acp-session-model-update.ts +46 -0
  110. package/src/api/events/app-files-changed.ts +4 -3
  111. package/src/api/events/notification-intent.ts +2 -0
  112. package/src/api/events/user-message-echo.ts +7 -1
  113. package/src/api/index.ts +6 -0
  114. package/src/api/responses/conversation-message.ts +5 -0
  115. package/src/apps/app-store.ts +1 -1
  116. package/src/apps/resolve-app.ts +81 -0
  117. package/src/apps/source-fingerprint.ts +266 -0
  118. package/src/avatar/identity-avatar.ts +7 -2
  119. package/src/bundler/app-compiler.ts +15 -0
  120. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  121. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  122. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  123. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  124. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  125. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  126. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  127. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  128. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  129. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  130. package/src/calls/__tests__/voice-control-protocol.test.ts +58 -25
  131. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  132. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  133. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  134. package/src/calls/__tests__/voice-session-bridge.test.ts +37 -1
  135. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  136. package/src/calls/barge-in-guard.ts +118 -0
  137. package/src/calls/call-constants.ts +24 -4
  138. package/src/calls/call-controller.ts +499 -199
  139. package/src/calls/call-transport.ts +16 -0
  140. package/src/calls/media-stream-output.ts +32 -0
  141. package/src/calls/media-stream-server.ts +82 -30
  142. package/src/calls/media-stream-stt-session.ts +15 -0
  143. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  144. package/src/calls/resolve-call-tts-provider.ts +33 -2
  145. package/src/calls/tts-call-strategy.ts +9 -5
  146. package/src/calls/types.ts +2 -0
  147. package/src/calls/voice-control-protocol.ts +53 -21
  148. package/src/calls/voice-leg-coordinator.ts +189 -0
  149. package/src/calls/voice-progress-cadence.ts +423 -0
  150. package/src/calls/voice-session-bridge.ts +59 -46
  151. package/src/calls/voice-triage-escalate.ts +175 -56
  152. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  153. package/src/cli/commands/__tests__/bash.test.ts +53 -0
  154. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  155. package/src/cli/commands/apps.help.ts +67 -13
  156. package/src/cli/commands/apps.ts +183 -0
  157. package/src/cli/commands/bash.help.ts +8 -13
  158. package/src/cli/commands/bash.ts +7 -25
  159. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  160. package/src/cli/commands/channels/index.help.ts +70 -2
  161. package/src/cli/commands/channels/index.ts +16 -3
  162. package/src/cli/commands/channels/send.ts +128 -0
  163. package/src/cli/commands/credentials.help.ts +6 -2
  164. package/src/cli/commands/credentials.ts +2 -2
  165. package/src/cli/commands/mcp.help.ts +8 -8
  166. package/src/cli/commands/mcp.ts +1 -1
  167. package/src/cli/commands/roadmap.help.ts +5 -1
  168. package/src/cli/commands/roadmap.ts +1 -1
  169. package/src/cli/lib/bundled-marketplace.json +103 -90
  170. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  171. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  172. package/src/config/__tests__/platform-identity.test.ts +255 -0
  173. package/src/config/__tests__/platform-rehydration.test.ts +99 -9
  174. package/src/config/acp-agent-write.ts +43 -0
  175. package/src/config/acp-defaults.test.ts +13 -3
  176. package/src/config/acp-defaults.ts +23 -6
  177. package/src/config/acp-schema.test.ts +81 -0
  178. package/src/config/acp-schema.ts +30 -2
  179. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  180. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  181. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  182. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  183. package/src/config/bundled-skills/computer-use/SKILL.md +20 -2
  184. package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
  185. package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
  186. package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
  187. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
  188. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
  189. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  190. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  191. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  192. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  193. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
  194. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
  195. package/src/config/bundled-skills/schedule/SKILL.md +3 -4
  196. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  197. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  198. package/src/config/bundled-skills/skill-management/TOOLS.json +6 -2
  199. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
  200. package/src/config/default-profile-catalog.ts +1 -1
  201. package/src/config/env.ts +2 -2
  202. package/src/config/feature-flag-registry.json +24 -12
  203. package/src/config/loader.ts +1 -0
  204. package/src/config/memory-v3-gate.test.ts +31 -0
  205. package/src/config/memory-v3-gate.ts +20 -16
  206. package/src/config/platform-identity.ts +201 -0
  207. package/src/config/platform-rehydration.ts +11 -27
  208. package/src/config/schema.ts +2 -2
  209. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  210. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  211. package/src/config/schemas/calls.ts +1 -28
  212. package/src/config/schemas/live-voice.ts +1 -173
  213. package/src/config/schemas/mcp.ts +1 -1
  214. package/src/config/schemas/memory-retrospective.ts +10 -0
  215. package/src/config/schemas/services.ts +8 -0
  216. package/src/config/schemas/skills.ts +0 -14
  217. package/src/config/schemas/voice.ts +150 -0
  218. package/src/credential-execution/process-manager.test.ts +18 -19
  219. package/src/credential-health/credential-health-service.ts +11 -17
  220. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  221. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  222. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  223. package/src/daemon/app-change-notify.ts +45 -0
  224. package/src/daemon/config-watcher.ts +13 -194
  225. package/src/daemon/conversation-error.ts +3 -2
  226. package/src/daemon/conversation-media-retry.ts +2 -1
  227. package/src/daemon/conversation-process.ts +35 -64
  228. package/src/daemon/conversation-runtime-assembly.ts +2 -1
  229. package/src/daemon/conversation-skill-tools.ts +13 -31
  230. package/src/daemon/conversation-store.ts +1 -1
  231. package/src/daemon/conversation-surfaces.ts +15 -9
  232. package/src/daemon/conversation-tool-setup.ts +37 -43
  233. package/src/daemon/conversation.ts +0 -3
  234. package/src/daemon/dictation-profile-store.ts +2 -1
  235. package/src/daemon/handlers/shared.ts +2 -1
  236. package/src/daemon/handlers/skills.ts +6 -3
  237. package/src/daemon/host-bash-proxy.ts +8 -12
  238. package/src/daemon/host-browser-proxy.ts +15 -1
  239. package/src/daemon/host-cu-proxy.ts +186 -19
  240. package/src/daemon/host-file-proxy.ts +8 -10
  241. package/src/daemon/host-proxy-preactivation.ts +2 -1
  242. package/src/daemon/host-transfer-proxy.ts +14 -18
  243. package/src/daemon/lifecycle.ts +0 -5
  244. package/src/daemon/mcp-reload-service.ts +5 -5
  245. package/src/daemon/message-types/sync.ts +1 -0
  246. package/src/daemon/orphan-reaper.ts +1 -1
  247. package/src/daemon/providers-setup.ts +1 -1
  248. package/src/daemon/reaction-history-render.ts +2 -1
  249. package/src/daemon/shutdown-handlers.ts +1 -9
  250. package/src/daemon/skill-memory-refresh.ts +64 -0
  251. package/src/daemon/tool-side-effects.ts +7 -18
  252. package/src/desktop/__tests__/fake-desktop.ts +122 -0
  253. package/src/desktop/desktop-chrome-policy.test.ts +79 -0
  254. package/src/desktop/desktop-chrome-policy.ts +47 -0
  255. package/src/desktop/desktop-chrome-session.ts +37 -0
  256. package/src/desktop/desktop-dependencies.test.ts +102 -0
  257. package/src/desktop/desktop-dependencies.ts +308 -0
  258. package/src/desktop/desktop-feature.test.ts +18 -0
  259. package/src/desktop/desktop-feature.ts +19 -0
  260. package/src/desktop/desktop-panel-config.test.ts +135 -0
  261. package/src/desktop/desktop-panel-config.ts +149 -0
  262. package/src/desktop/desktop-session-manager.test.ts +730 -0
  263. package/src/desktop/desktop-session-manager.ts +850 -0
  264. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  265. package/src/desktop/desktop-stream-bridge.ts +214 -0
  266. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  267. package/src/desktop/desktop-wallpaper.ts +113 -0
  268. package/src/desktop/desktop-window-drag.ts +177 -0
  269. package/src/desktop/desktop-window-manager-config.test.ts +230 -0
  270. package/src/desktop/desktop-window-manager-config.ts +238 -0
  271. package/src/desktop/desktop-window-theme.ts +170 -0
  272. package/src/heartbeat/heartbeat-service.ts +21 -3
  273. package/src/home/__tests__/feed-writer.test.ts +22 -0
  274. package/src/home/feed-writer.ts +11 -2
  275. package/src/hooks/types.ts +33 -0
  276. package/src/i18n/AGENTS.md +22 -0
  277. package/src/i18n/__tests__/i18n.test.ts +108 -0
  278. package/src/i18n/index.ts +16 -0
  279. package/src/i18n/locales.ts +93 -0
  280. package/src/i18n/messages.ts +61 -0
  281. package/src/i18n/title.ts +25 -0
  282. package/src/inbound/platform-callback-registration.ts +4 -5
  283. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  284. package/src/live-voice/__tests__/live-voice-progress.test.ts +15 -15
  285. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
  286. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  287. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  288. package/src/live-voice/live-voice-photo.ts +25 -6
  289. package/src/live-voice/live-voice-session.ts +213 -693
  290. package/src/live-voice/protocol.ts +0 -2
  291. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  292. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  293. package/src/mcp/effective-config.ts +3 -3
  294. package/src/mcp/mcp-header-store.ts +29 -45
  295. package/src/mcp/spec-schema.ts +111 -0
  296. package/src/mcp/startup.ts +8 -4
  297. package/src/mcp/workspace-mcp-config.ts +188 -0
  298. package/src/messaging/provider.ts +10 -3
  299. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  300. package/src/messaging/providers/callback-routing.ts +18 -0
  301. package/src/messaging/providers/channel-transport.ts +56 -0
  302. package/src/messaging/providers/discord/transport.ts +17 -0
  303. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  304. package/src/messaging/providers/slack/adapter.ts +7 -44
  305. package/src/messaging/providers/slack/transport.ts +16 -0
  306. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  307. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  308. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  309. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  310. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  311. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  312. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  313. package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
  314. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  315. package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
  316. package/src/notifications/adapters/macos.ts +3 -0
  317. package/src/notifications/assistant-reply-producer.ts +8 -3
  318. package/src/notifications/conversation-pairing.ts +36 -16
  319. package/src/notifications/delivered-post-record.ts +11 -1
  320. package/src/notifications/guardian-feed-projection.ts +125 -5
  321. package/src/notifications/home-feed-side-effect.ts +55 -7
  322. package/src/notifications/schedule-result-producer.ts +66 -22
  323. package/src/oauth/AGENTS.md +2 -0
  324. package/src/oauth/__tests__/seed-providers-managed.test.ts +54 -0
  325. package/src/oauth/byo-connection.test.ts +105 -0
  326. package/src/oauth/byo-connection.ts +36 -6
  327. package/src/oauth/connection-resolver.test.ts +73 -0
  328. package/src/oauth/connection-resolver.ts +95 -2
  329. package/src/oauth/scope-utils.ts +24 -0
  330. package/src/oauth/seed-providers.ts +204 -25
  331. package/src/outbound-proxy/certs.ts +3 -5
  332. package/src/permissions/checker.test.ts +11 -6
  333. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  334. package/src/persistence/conversation-crud.ts +71 -63
  335. package/src/persistence/conversation-lineage.ts +12 -4
  336. package/src/persistence/conversation-queries.ts +2 -2
  337. package/src/persistence/conversation-title-placeholders.ts +23 -0
  338. package/src/persistence/conversation-title-service.ts +9 -26
  339. package/src/persistence/embeddings/qdrant-manager.ts +163 -18
  340. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  341. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  342. package/src/persistence/message-cursor.ts +78 -0
  343. package/src/persistence/message-reads.ts +30 -0
  344. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  345. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  346. package/src/persistence/schema/memory-core.ts +5 -0
  347. package/src/platform/client.test.ts +4 -2
  348. package/src/platform/client.ts +2 -10
  349. package/src/plugin-api/constants.ts +2 -0
  350. package/src/plugin-api/index.ts +1 -0
  351. package/src/plugin-api/types.ts +2 -0
  352. package/src/plugins/defaults/index.ts +2 -0
  353. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  354. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  355. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  356. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  357. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  358. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
  359. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  360. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  361. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  362. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  363. package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
  364. package/src/plugins/defaults/memory/context-search/format.ts +3 -2
  365. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
  366. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
  367. package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
  368. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  369. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  370. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  371. package/src/plugins/defaults/memory/host-utils.ts +10 -0
  372. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  373. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  374. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  375. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  376. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  377. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  378. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  379. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  380. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  381. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  382. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  383. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  384. package/src/plugins/defaults/memory/startup.ts +28 -2
  385. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  386. package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
  387. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  388. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
  389. package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
  390. package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
  391. package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
  392. package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
  393. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
  394. package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
  395. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
  396. package/src/plugins/defaults/memory/v3/card.ts +3 -2
  397. package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
  398. package/src/plugins/defaults/memory/v3/sections.ts +9 -3
  399. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
  400. package/src/plugins/defaults/memory/worker.ts +6 -0
  401. package/src/plugins/mcp-servers.ts +15 -75
  402. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  403. package/src/providers/__tests__/retry-callsite.test.ts +41 -0
  404. package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
  405. package/src/providers/anthropic/client.ts +0 -47
  406. package/src/providers/gemini/client.ts +9 -23
  407. package/src/providers/gemini-thought-signature.test.ts +63 -0
  408. package/src/providers/gemini-thought-signature.ts +44 -0
  409. package/src/providers/inference/__tests__/endpoint-probe.test.ts +42 -3
  410. package/src/providers/inference/auth.ts +2 -3
  411. package/src/providers/inference/connection-availability.ts +27 -8
  412. package/src/providers/inference/endpoint-probe.ts +10 -1
  413. package/src/providers/inference/resolve-auth.ts +22 -4
  414. package/src/providers/inference/service-account-token.ts +248 -0
  415. package/src/providers/model-catalog.ts +34 -0
  416. package/src/providers/model-intents.ts +10 -5
  417. package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
  418. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  419. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  420. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  421. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  422. package/src/providers/openai/chat-completions-provider.ts +212 -79
  423. package/src/providers/openai/codex-models.ts +0 -4
  424. package/src/providers/openai/google-thought-signature.ts +320 -0
  425. package/src/providers/opencode/client.test.ts +73 -4
  426. package/src/providers/opencode/client.ts +60 -3
  427. package/src/providers/openrouter/client.ts +20 -0
  428. package/src/providers/outbound-request-sanitize.ts +96 -0
  429. package/src/providers/retry.ts +30 -19
  430. package/src/providers/unparseable-tool-args.ts +5 -1
  431. package/src/runtime/AGENTS.md +3 -1
  432. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  433. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  434. package/src/runtime/__tests__/host-observe.test.ts +40 -1
  435. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  436. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  437. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  438. package/src/runtime/agent-wake.ts +2 -1
  439. package/src/runtime/assistant-event-hub.ts +21 -3
  440. package/src/runtime/auth/same-actor.ts +38 -3
  441. package/src/runtime/channel-send.ts +316 -0
  442. package/src/runtime/host-observe.ts +11 -1
  443. package/src/runtime/http-server.ts +160 -151
  444. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  445. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +205 -0
  446. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  447. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  448. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  449. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  450. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  451. package/src/runtime/routes/acp-routes.test.ts +15 -0
  452. package/src/runtime/routes/acp-routes.ts +72 -9
  453. package/src/runtime/routes/app-management-routes.ts +73 -0
  454. package/src/runtime/routes/app-routes.ts +1 -1
  455. package/src/runtime/routes/channel-send-routes.ts +85 -0
  456. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  457. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  458. package/src/runtime/routes/conversation-routes.ts +16 -3
  459. package/src/runtime/routes/credential-routes.ts +19 -10
  460. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  461. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  462. package/src/runtime/routes/diagnostics-routes.ts +61 -4
  463. package/src/runtime/routes/events-routes.ts +4 -0
  464. package/src/runtime/routes/host-cu-routes.ts +6 -0
  465. package/src/runtime/routes/index.ts +4 -2
  466. package/src/runtime/routes/log-export-routes.ts +2 -0
  467. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  468. package/src/runtime/routes/migration-routes.ts +6 -20
  469. package/src/runtime/routes/oauth-commands-routes.ts +36 -0
  470. package/src/runtime/routes/platform-routes.ts +18 -15
  471. package/src/runtime/routes/workspace-routes.test.ts +55 -26
  472. package/src/runtime/routes/workspace-routes.ts +46 -63
  473. package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
  474. package/src/runtime/routes/workspace-tree-walk.ts +240 -0
  475. package/src/runtime/routes/workspace-utils.ts +17 -0
  476. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  477. package/src/runtime/services/conversation-serializer.ts +16 -3
  478. package/src/runtime/sync/resource-sync-events.ts +2 -1
  479. package/src/schedule/run-script.ts +2 -1
  480. package/src/schedule/schedule-store.ts +9 -2
  481. package/src/schedule/worker.ts +1 -1
  482. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  483. package/src/security/ces-http-record-backend.ts +120 -0
  484. package/src/security/ces-rpc-record-backend.ts +7 -0
  485. package/src/security/untrusted-content.ts +3 -1
  486. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  487. package/src/skills/workspace-skill-md-signature.ts +50 -0
  488. package/src/subagent/consult-context.ts +12 -25
  489. package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
  490. package/src/tools/acp/list-agents.test.ts +3 -1
  491. package/src/tools/acp/spawn.test.ts +146 -11
  492. package/src/tools/acp/spawn.ts +21 -3
  493. package/src/tools/acp/status.test.ts +94 -0
  494. package/src/tools/acp/status.ts +40 -3
  495. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  496. package/src/tools/browser/browser-execution.ts +1 -1
  497. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
  498. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
  499. package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
  500. package/src/tools/browser/network-recorder.ts +2 -1
  501. package/src/tools/capability-offer.test.ts +22 -2
  502. package/src/tools/capability-offer.ts +16 -1
  503. package/src/tools/computer-use/definitions.ts +121 -2
  504. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  505. package/src/tools/credentials/metadata-store.ts +100 -0
  506. package/src/tools/filesystem/search.ts +2 -1
  507. package/src/tools/host-filesystem/edit.test.ts +2 -1
  508. package/src/tools/host-filesystem/edit.ts +2 -2
  509. package/src/tools/host-filesystem/read.test.ts +2 -1
  510. package/src/tools/host-filesystem/read.ts +2 -2
  511. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  512. package/src/tools/host-filesystem/transfer.ts +2 -2
  513. package/src/tools/host-filesystem/write.test.ts +2 -1
  514. package/src/tools/host-filesystem/write.ts +2 -2
  515. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  516. package/src/tools/host-terminal/host-shell.ts +4 -20
  517. package/src/tools/network/web-search.ts +6 -1
  518. package/src/tools/policy-context.ts +6 -7
  519. package/src/tools/skills/scaffold-managed.ts +61 -3
  520. package/src/tools/subagent/spawn.ts +5 -8
  521. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
  522. package/src/tools/terminal/sanitized-bash.ts +105 -0
  523. package/src/tools/watch/watch-retro-report.ts +0 -8
  524. package/src/types/host-capabilities.ts +5 -3
  525. package/src/util/__tests__/language-subtag.test.ts +23 -1
  526. package/src/util/ca-bundle.ts +13 -0
  527. package/src/util/language-subtag.ts +16 -0
  528. package/src/util/platform.ts +8 -0
  529. package/src/util/unicode.ts +15 -1
  530. package/src/watch/watch-timeline.ts +2 -2
  531. package/src/workflows/run-manager.ts +2 -1
  532. package/src/workspace/git-porcelain.ts +66 -0
  533. package/src/workspace/git-service.ts +6 -55
  534. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  535. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  536. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  537. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  538. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  539. package/src/workspace/migrations/registry.ts +6 -0
  540. package/src/__tests__/app-source-watcher.test.ts +0 -218
  541. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  542. package/src/__tests__/speaker-identification.test.ts +0 -56
  543. package/src/calls/speaker-identification.ts +0 -233
  544. package/src/daemon/app-source-watcher.ts +0 -218
  545. package/src/messaging/providers/whatsapp/client.ts +0 -29
  546. package/src/runtime/routes/debug-bash-routes.ts +0 -173
  547. package/src/util/fs-watcher-error.ts +0 -36
  548. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -211,8 +211,8 @@ describe("isTextMimeType", () => {
211
211
  describe("GET /v1/workspace/tree", () => {
212
212
  const { handler } = getRoute("workspace_tree");
213
213
 
214
- test("root listing returns entries", () => {
215
- const result = handler({ queryParams: {} }) as {
214
+ test("root listing returns entries", async () => {
215
+ const result = (await handler({ queryParams: {} })) as {
216
216
  path: string;
217
217
  entries: Array<{ name: string; type: string }>;
218
218
  };
@@ -222,8 +222,8 @@ describe("GET /v1/workspace/tree", () => {
222
222
  expect(names).toContain("subdir");
223
223
  });
224
224
 
225
- test("subdirectory listing returns child entries", () => {
226
- const result = handler({ queryParams: { path: "subdir" } }) as {
225
+ test("subdirectory listing returns child entries", async () => {
226
+ const result = (await handler({ queryParams: { path: "subdir" } })) as {
227
227
  path: string;
228
228
  entries: Array<{ name: string; type: string }>;
229
229
  };
@@ -232,20 +232,20 @@ describe("GET /v1/workspace/tree", () => {
232
232
  expect(result.entries[0].type).toBe("file");
233
233
  });
234
234
 
235
- test("non-existent directory throws NotFoundError", () => {
236
- expect(() => handler({ queryParams: { path: "nope" } })).toThrow(
235
+ test("non-existent directory throws NotFoundError", async () => {
236
+ await expect(handler({ queryParams: { path: "nope" } })).rejects.toThrow(
237
237
  NotFoundError,
238
238
  );
239
239
  });
240
240
 
241
- test("path traversal attempt throws BadRequestError", () => {
242
- expect(() => handler({ queryParams: { path: "../../etc" } })).toThrow(
243
- BadRequestError,
244
- );
241
+ test("path traversal attempt throws BadRequestError", async () => {
242
+ await expect(
243
+ handler({ queryParams: { path: "../../etc" } }),
244
+ ).rejects.toThrow(BadRequestError);
245
245
  });
246
246
 
247
- test("entries have correct type field", () => {
248
- const result = handler({ queryParams: {} }) as {
247
+ test("entries have correct type field", async () => {
248
+ const result = (await handler({ queryParams: {} })) as {
249
249
  entries: Array<{ name: string; type: "file" | "directory" }>;
250
250
  };
251
251
  const subdirEntry = result.entries.find((e) => e.name === "subdir");
@@ -254,8 +254,8 @@ describe("GET /v1/workspace/tree", () => {
254
254
  expect(fileEntry?.type).toBe("file");
255
255
  });
256
256
 
257
- test("dotfiles and dot-directories are excluded", () => {
258
- const result = handler({ queryParams: {} }) as {
257
+ test("dotfiles and dot-directories are excluded", async () => {
258
+ const result = (await handler({ queryParams: {} })) as {
259
259
  entries: Array<{ name: string }>;
260
260
  };
261
261
  const names = result.entries.map((e) => e.name);
@@ -263,8 +263,8 @@ describe("GET /v1/workspace/tree", () => {
263
263
  expect(names).not.toContain(".hidden");
264
264
  });
265
265
 
266
- test("directory entries have null size and mimeType", () => {
267
- const result = handler({ queryParams: {} }) as {
266
+ test("directory entries have null size and mimeType", async () => {
267
+ const result = (await handler({ queryParams: {} })) as {
268
268
  entries: Array<{
269
269
  name: string;
270
270
  type: string;
@@ -278,8 +278,8 @@ describe("GET /v1/workspace/tree", () => {
278
278
  expect(dirEntry!.mimeType).toBeNull();
279
279
  });
280
280
 
281
- test("directories sorted before files", () => {
282
- const result = handler({ queryParams: {} }) as {
281
+ test("directories sorted before files", async () => {
282
+ const result = (await handler({ queryParams: {} })) as {
283
283
  entries: Array<{ type: string }>;
284
284
  };
285
285
  const firstFileIdx = result.entries.findIndex((e) => e.type === "file");
@@ -295,8 +295,10 @@ describe("GET /v1/workspace/tree", () => {
295
295
  }
296
296
  });
297
297
 
298
- test("includeDirSizes=true returns recursive byte size for directories", () => {
299
- const result = handler({ queryParams: { includeDirSizes: "true" } }) as {
298
+ test("includeDirSizes=true returns recursive byte size for directories", async () => {
299
+ const result = (await handler({
300
+ queryParams: { includeDirSizes: "true" },
301
+ })) as {
300
302
  entries: Array<{ name: string; type: string; size: number | null }>;
301
303
  };
302
304
  const subdirEntry = result.entries.find((e) => e.name === "subdir");
@@ -307,8 +309,10 @@ describe("GET /v1/workspace/tree", () => {
307
309
  expect(subdirEntry!.size).toBe("nested content".length);
308
310
  });
309
311
 
310
- test("includeDirSizes=true preserves accurate file sizes", () => {
311
- const result = handler({ queryParams: { includeDirSizes: "true" } }) as {
312
+ test("includeDirSizes=true preserves accurate file sizes", async () => {
313
+ const result = (await handler({
314
+ queryParams: { includeDirSizes: "true" },
315
+ })) as {
312
316
  entries: Array<{ name: string; type: string; size: number | null }>;
313
317
  };
314
318
  const fileEntry = result.entries.find((e) => e.name === "hello.txt");
@@ -316,8 +320,8 @@ describe("GET /v1/workspace/tree", () => {
316
320
  expect(fileEntry!.size).toBe("Hello, world!".length);
317
321
  });
318
322
 
319
- test("includeDirSizes omitted preserves null size for directories", () => {
320
- const result = handler({ queryParams: {} }) as {
323
+ test("includeDirSizes omitted preserves null size for directories", async () => {
324
+ const result = (await handler({ queryParams: {} })) as {
321
325
  entries: Array<{ type: string; size: number | null }>;
322
326
  };
323
327
  const dirEntry = result.entries.find((e) => e.type === "directory");
@@ -325,11 +329,36 @@ describe("GET /v1/workspace/tree", () => {
325
329
  expect(dirEntry!.size).toBeNull();
326
330
  });
327
331
 
328
- test("includeDirSizes=true returns 0 for empty directory", () => {
332
+ test("recursive=true lists nested entries and the bound fields", async () => {
333
+ const result = (await handler({ queryParams: { recursive: "true" } })) as {
334
+ entries: Array<{ path: string }>;
335
+ truncated: boolean;
336
+ skipped: string[];
337
+ };
338
+ expect(result.entries.map((e) => e.path)).toContain("subdir/nested.txt");
339
+ expect(result.truncated).toBe(false);
340
+ expect(Array.isArray(result.skipped)).toBe(true);
341
+ });
342
+
343
+ test("a non-recursive listing carries the same fields, unset", async () => {
344
+ const result = (await handler({ queryParams: {} })) as {
345
+ entries: Array<{ path: string }>;
346
+ truncated: boolean;
347
+ skipped: string[];
348
+ };
349
+ expect(result.entries.map((e) => e.path)).not.toContain(
350
+ "subdir/nested.txt",
351
+ );
352
+ expect(result).toMatchObject({ truncated: false, skipped: [] });
353
+ });
354
+
355
+ test("includeDirSizes=true returns 0 for empty directory", async () => {
329
356
  const emptyDir = join(testWorkspaceDir, "empty-for-size");
330
357
  mkdirSync(emptyDir, { recursive: true });
331
358
 
332
- const result = handler({ queryParams: { includeDirSizes: "true" } }) as {
359
+ const result = (await handler({
360
+ queryParams: { includeDirSizes: "true" },
361
+ })) as {
333
362
  entries: Array<{ name: string; size: number | null }>;
334
363
  };
335
364
  const emptyEntry = result.entries.find((e) => e.name === "empty-for-size");
@@ -30,24 +30,15 @@ import {
30
30
  } from "./errors.js";
31
31
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
32
32
  import { RouteResponse } from "./types.js";
33
+ import { walkWorkspaceTree } from "./workspace-tree-walk.js";
33
34
  import {
34
35
  isTextMimeType,
35
36
  MAX_INLINE_TEXT_SIZE,
36
37
  resolveWorkspacePath,
38
+ workspaceEntrySchema,
37
39
  writeWorkspaceFile,
38
40
  } from "./workspace-utils.js";
39
41
 
40
- const workspaceTreeEntrySchema = z.object({
41
- name: z.string(),
42
- path: z.string(),
43
- type: z.enum(["file", "directory"]),
44
- size: z.number().nullable(),
45
- mimeType: z.string().nullable(),
46
- modifiedAt: z.string(),
47
- });
48
-
49
- type TreeEntry = z.infer<typeof workspaceTreeEntrySchema>;
50
-
51
42
  // Recursive directory sizing walks the filesystem synchronously, so it is
52
43
  // bounded by two caps that work together:
53
44
  //
@@ -175,10 +166,11 @@ function publishSoundsConfigUpdatedForPaths(
175
166
  // GET /v1/workspace/tree — list directory contents
176
167
  // ---------------------------------------------------------------------------
177
168
 
178
- function handleWorkspaceTree({ queryParams }: RouteHandlerArgs) {
169
+ async function handleWorkspaceTree({ queryParams }: RouteHandlerArgs) {
179
170
  const requestedPath = queryParams?.path ?? "";
180
171
  const showHidden = queryParams?.showHidden === "true";
181
172
  const includeDirSizes = queryParams?.includeDirSizes === "true";
173
+ const recursive = queryParams?.recursive === "true";
182
174
  const resolved = resolveWorkspacePath(requestedPath, {
183
175
  allowHidden: showHidden,
184
176
  });
@@ -186,62 +178,36 @@ function handleWorkspaceTree({ queryParams }: RouteHandlerArgs) {
186
178
  throw new BadRequestError("Invalid path");
187
179
  }
188
180
 
189
- try {
190
- const dirents = readdirSync(resolved, { withFileTypes: true });
191
- const workspaceDir = getWorkspaceDir();
192
-
193
- // Listing-wide budget, only constructed when sizes are requested. Each
194
- // directory draws a per-directory allotment from it so one oversized
195
- // subtree can't starve its siblings, while the shared remainder still caps
196
- // total synchronous traversal for the whole listing.
197
- const listingBudget: DirSizeBudget | undefined = includeDirSizes
181
+ // Listing-wide budget, only constructed when sizes are requested. Each
182
+ // directory draws a per-directory allotment from it so one oversized
183
+ // subtree can't starve its siblings, while the shared remainder still caps
184
+ // total synchronous traversal for the whole listing.
185
+ const listingBudget: DirSizeBudget | undefined =
186
+ includeDirSizes && !recursive
198
187
  ? { remaining: DIR_SIZE_TOTAL_ENTRY_BUDGET }
199
188
  : undefined;
200
189
 
201
- const entries: TreeEntry[] = [];
202
- for (const entry of dirents) {
203
- if (!showHidden && entry.name.startsWith(".")) {
204
- continue;
205
- }
206
-
207
- const fullPath = join(resolved, entry.name);
208
-
209
- let stats: ReturnType<typeof statSync>;
210
- try {
211
- stats = statSync(fullPath);
212
- } catch {
213
- continue;
214
- }
215
-
216
- const isDir = stats.isDirectory();
217
- const relativePath = fullPath.slice(workspaceDir.length + 1);
218
-
219
- const dirSize =
220
- isDir && listingBudget
221
- ? computeDirSizeWithinListing(fullPath, listingBudget)
222
- : null;
223
-
224
- entries.push({
225
- name: entry.name,
226
- path: relativePath,
227
- type: isDir ? "directory" : "file",
228
- size: isDir ? dirSize : stats.size,
229
- mimeType: isDir ? null : Bun.file(fullPath).type,
230
- modifiedAt: stats.mtime.toISOString(),
231
- });
232
- }
233
-
234
- entries.sort((a, b) => {
235
- if (a.type !== b.type) {
236
- return a.type === "directory" ? -1 : 1;
237
- }
238
- return a.name.localeCompare(b.name);
190
+ let walk: Awaited<ReturnType<typeof walkWorkspaceTree>>;
191
+ try {
192
+ walk = await walkWorkspaceTree({
193
+ rootPath: resolved,
194
+ workspaceDir: getWorkspaceDir(),
195
+ showHidden,
196
+ recursive,
197
+ directorySize: listingBudget
198
+ ? (absPath) => computeDirSizeWithinListing(absPath, listingBudget)
199
+ : undefined,
239
200
  });
240
-
241
- return { path: requestedPath, entries };
242
201
  } catch {
243
202
  throw new NotFoundError("Directory not found");
244
203
  }
204
+
205
+ return {
206
+ path: requestedPath,
207
+ entries: walk.entries,
208
+ truncated: walk.truncated,
209
+ skipped: walk.skipped,
210
+ };
245
211
  }
246
212
 
247
213
  // ---------------------------------------------------------------------------
@@ -569,14 +535,31 @@ export const ROUTES: RouteDefinition[] = [
569
535
  {
570
536
  name: "includeDirSizes",
571
537
  description:
572
- "Compute recursive byte size for each directory entry (true/false). Budget-bounded large subtrees may return size: null.",
538
+ "Compute recursive byte size for each directory entry (true/false). Budget-bounded: large subtrees may return size: null. Not applied to a recursive listing, which carries every file for the client to sum.",
539
+ },
540
+ {
541
+ name: "recursive",
542
+ description:
543
+ "List the whole subtree in one response (true/false). Depth-first, each directory's entries in listing order, so grouping by parent path recovers per-directory listings. Bounded: sets truncated when the entry cap or deadline stops the walk, and does not enter directories the workspace gitignore rules exclude, which are named in skipped.",
573
544
  },
574
545
  ],
575
546
  responseBody: z.object({
576
547
  path: z.string(),
577
548
  entries: z
578
- .array(workspaceTreeEntrySchema)
549
+ .array(workspaceEntrySchema)
579
550
  .describe("Directory entry objects"),
551
+ truncated: z
552
+ .boolean()
553
+ .optional()
554
+ .describe(
555
+ "A recursive walk stopped at its entry cap or deadline, so deeper entries are missing. Absent from assistants that predate recursive listings.",
556
+ ),
557
+ skipped: z
558
+ .array(z.string())
559
+ .optional()
560
+ .describe(
561
+ "Directories a recursive walk listed but did not enter, whether because a workspace gitignore rule excludes them, they are symlinks, or they could not be read. Each can still be listed on its own. Absent from assistants that predate recursive listings.",
562
+ ),
580
563
  }),
581
564
  handler: handleWorkspaceTree,
582
565
  },
@@ -0,0 +1,244 @@
1
+ /**
2
+ * The recursive workspace walk: what it enters, what it lists without
3
+ * entering, the order it reports, and the bounds that stop it.
4
+ */
5
+ import {
6
+ chmodSync,
7
+ mkdirSync,
8
+ rmSync,
9
+ symlinkSync,
10
+ writeFileSync,
11
+ } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { beforeAll, describe, expect, test } from "bun:test";
14
+
15
+ import { walkWorkspaceTree } from "./workspace-tree-walk.js";
16
+
17
+ const workspaceDir = process.env.VELLUM_WORKSPACE_DIR!;
18
+ const root = join(workspaceDir, "walk-fixture");
19
+
20
+ function write(relative: string, content = "x") {
21
+ const abs = join(root, relative);
22
+ mkdirSync(join(abs, ".."), { recursive: true });
23
+ writeFileSync(abs, content);
24
+ }
25
+
26
+ beforeAll(() => {
27
+ mkdirSync(root, { recursive: true });
28
+ write("b-dir/deep/leaf.md");
29
+ write("b-dir/file-in-b.md");
30
+ write("a-dir/file-in-a.md");
31
+ write("top.md");
32
+ write(".secret/inside-hidden.md");
33
+ write("node_modules/dep/index.js");
34
+ // Rules with an inner slash (data/db/) anchor to the workspace root, so
35
+ // these live there rather than under the fixture directory.
36
+ mkdirSync(join(workspaceDir, "data", "db"), { recursive: true });
37
+ writeFileSync(join(workspaceDir, "data", "db", "state.sqlite"), "x");
38
+ mkdirSync(join(workspaceDir, "data", "avatar"), { recursive: true });
39
+ writeFileSync(join(workspaceDir, "data", "avatar", "face.png"), "x");
40
+ mkdirSync(join(workspaceDir, "walk-link-target"), { recursive: true });
41
+ writeFileSync(join(workspaceDir, "walk-link-target", "behind.md"), "x");
42
+ try {
43
+ symlinkSync(
44
+ join(workspaceDir, "walk-link-target"),
45
+ join(root, "linked"),
46
+ "dir",
47
+ );
48
+ } catch (err) {
49
+ if ((err as NodeJS.ErrnoException).code !== "EEXIST") {
50
+ throw err;
51
+ }
52
+ }
53
+ });
54
+
55
+ function walk(
56
+ overrides: Partial<Parameters<typeof walkWorkspaceTree>[0]> = {},
57
+ ) {
58
+ return walkWorkspaceTree({
59
+ rootPath: root,
60
+ workspaceDir,
61
+ showHidden: false,
62
+ recursive: true,
63
+ ...overrides,
64
+ });
65
+ }
66
+
67
+ describe("walkWorkspaceTree recursive", () => {
68
+ test("a non-recursive walk lists one level and reports nothing skipped", async () => {
69
+ const result = await walk({ recursive: false });
70
+ expect(result.entries.map((e) => e.path)).not.toContain(
71
+ "walk-fixture/a-dir/file-in-a.md",
72
+ );
73
+ expect(result.entries.map((e) => e.name)).toContain("a-dir");
74
+ expect(result).toMatchObject({ truncated: false, skipped: [] });
75
+ });
76
+
77
+ test("lists every level, depth-first, each directory in listing order", async () => {
78
+ const { entries } = await walk();
79
+ const paths = entries.map((e) => e.path.replace(/^walk-fixture\//, ""));
80
+ const indexOf = (p: string) => paths.indexOf(p);
81
+ expect(indexOf("a-dir/file-in-a.md")).toBeGreaterThan(-1);
82
+ expect(indexOf("b-dir/deep/leaf.md")).toBeGreaterThan(-1);
83
+ // Root listing first (directories before files), then a-dir's contents
84
+ // before b-dir's, and b-dir's subdirectory before its file.
85
+ expect(indexOf("a-dir")).toBeLessThan(indexOf("top.md"));
86
+ expect(indexOf("top.md")).toBeLessThan(indexOf("a-dir/file-in-a.md"));
87
+ expect(indexOf("a-dir/file-in-a.md")).toBeLessThan(indexOf("b-dir/deep"));
88
+ expect(indexOf("b-dir/deep")).toBeLessThan(indexOf("b-dir/file-in-b.md"));
89
+ expect(indexOf("b-dir/file-in-b.md")).toBeLessThan(
90
+ indexOf("b-dir/deep/leaf.md"),
91
+ );
92
+ });
93
+
94
+ test("grouping by parent recovers each directory's own listing", async () => {
95
+ const { entries } = await walk();
96
+ const shallow = await walkWorkspaceTree({
97
+ rootPath: join(root, "b-dir"),
98
+ workspaceDir,
99
+ showHidden: false,
100
+ recursive: false,
101
+ });
102
+ const underB = entries.filter(
103
+ (e) =>
104
+ e.path.startsWith("walk-fixture/b-dir/") &&
105
+ !e.path.slice("walk-fixture/b-dir/".length).includes("/"),
106
+ );
107
+ expect(underB.map((e) => e.path)).toEqual(
108
+ shallow.entries.map((e) => e.path),
109
+ );
110
+ });
111
+
112
+ test("paths use forward slashes", async () => {
113
+ const { entries } = await walk();
114
+ expect(entries.every((e) => !e.path.includes("\\"))).toBe(true);
115
+ expect(entries.map((e) => e.path)).toContain(
116
+ "walk-fixture/b-dir/deep/leaf.md",
117
+ );
118
+ });
119
+
120
+ test("hidden directories are neither listed nor entered unless asked", async () => {
121
+ const hidden = (await walk()).entries.map((e) => e.path);
122
+ expect(hidden).not.toContain("walk-fixture/.secret");
123
+ expect(hidden).not.toContain("walk-fixture/.secret/inside-hidden.md");
124
+ const shown = (await walk({ showHidden: true })).entries.map((e) => e.path);
125
+ expect(shown).toContain("walk-fixture/.secret/inside-hidden.md");
126
+ });
127
+
128
+ test("a dependency directory is listed at any depth but not entered", async () => {
129
+ const { entries, skipped } = await walk();
130
+ const paths = entries.map((e) => e.path);
131
+ expect(paths).toContain("walk-fixture/node_modules");
132
+ expect(paths).not.toContain("walk-fixture/node_modules/dep");
133
+ expect(skipped).toContain("walk-fixture/node_modules");
134
+ });
135
+
136
+ test("root-anchored runtime state is listed, not entered, and named", async () => {
137
+ const { entries, skipped } = await walk({ rootPath: workspaceDir });
138
+ const paths = entries.map((e) => e.path);
139
+ expect(paths).toContain("data/db");
140
+ expect(paths).not.toContain("data/db/state.sqlite");
141
+ // A directory the rules do not exclude is entered, and a file the rules
142
+ // match (*.png) is still listed: only descent is decided by them.
143
+ expect(paths).toContain("data/avatar/face.png");
144
+ expect(skipped).toContain("data/db");
145
+ expect(skipped).not.toContain("data/avatar");
146
+ });
147
+
148
+ test("a symlinked directory is listed, not entered, and reported skipped", async () => {
149
+ const { entries, skipped } = await walk();
150
+ const paths = entries.map((e) => e.path);
151
+ expect(paths).toContain("walk-fixture/linked");
152
+ expect(paths).not.toContain("walk-fixture/linked/behind.md");
153
+ expect(skipped).toContain("walk-fixture/linked");
154
+ });
155
+
156
+ test("an unreadable directory is listed, not entered, and reported skipped", async () => {
157
+ // Root bypasses permission bits, so the check proves nothing there.
158
+ if (process.getuid?.() === 0) {
159
+ return;
160
+ }
161
+ const locked = join(root, "locked");
162
+ mkdirSync(locked, { recursive: true });
163
+ writeFileSync(join(locked, "inside.md"), "x");
164
+ chmodSync(locked, 0o000);
165
+ try {
166
+ const { entries, skipped, truncated } = await walk();
167
+ const paths = entries.map((e) => e.path);
168
+ expect(paths).toContain("walk-fixture/locked");
169
+ expect(paths).not.toContain("walk-fixture/locked/inside.md");
170
+ expect(skipped).toContain("walk-fixture/locked");
171
+ expect(truncated).toBe(false);
172
+ } finally {
173
+ chmodSync(locked, 0o755);
174
+ rmSync(locked, { recursive: true, force: true });
175
+ }
176
+ });
177
+
178
+ test("a directory wider than the remaining room is not scanned", async () => {
179
+ // Root (5) leaves one slot at a cap of 6; a-dir (1) fits and b-dir (2)
180
+ // does not. Nothing under b-dir is stat'd: its walk-time stat would
181
+ // otherwise show up as a listed entry.
182
+ const wide = join(root, "wide");
183
+ mkdirSync(wide, { recursive: true });
184
+ for (let i = 0; i < 50; i++) {
185
+ writeFileSync(join(wide, `w${i}.md`), "x");
186
+ }
187
+ try {
188
+ const result = await walk({ maxEntries: 7 });
189
+ const paths = result.entries.map((e) => e.path);
190
+ expect(paths).toContain("walk-fixture/wide");
191
+ expect(paths.some((p) => p.startsWith("walk-fixture/wide/"))).toBe(false);
192
+ expect(result.truncated).toBe(true);
193
+ } finally {
194
+ rmSync(wide, { recursive: true, force: true });
195
+ }
196
+ });
197
+
198
+ test("the deadline is honored inside a directory, not only between them", async () => {
199
+ // A clock that advances one second per read: the deadline is set at 0,
200
+ // the check before the first nested directory reads 1000 and passes,
201
+ // and the first entry inside it reads 2000 and stops the listing. Were
202
+ // the deadline checked only between directories, that one-entry
203
+ // directory would be carried and the walk would stop one step later.
204
+ let reads = 0;
205
+ const result = await walk({ maxWalkMs: 1500, now: () => reads++ * 1000 });
206
+ expect(result.truncated).toBe(true);
207
+ expect(result.entries.length).toBe(5);
208
+ });
209
+
210
+ test("the entry cap stops descent and reports truncated", async () => {
211
+ // The fixture root lists five entries and holds nine in all.
212
+ const result = await walk({ maxEntries: 6 });
213
+ expect(result.entries.length).toBeLessThanOrEqual(6);
214
+ expect(result.entries.length).toBeGreaterThanOrEqual(5);
215
+ expect(result.truncated).toBe(true);
216
+ });
217
+
218
+ test("a directory is carried whole or not at all", async () => {
219
+ // Root (5) plus a-dir (1) fit in 6; b-dir's two entries do not, so
220
+ // neither of them appears rather than one of them.
221
+ const { entries } = await walk({ maxEntries: 6 });
222
+ const paths = entries.map((e) => e.path);
223
+ expect(paths).toContain("walk-fixture/a-dir/file-in-a.md");
224
+ expect(paths).not.toContain("walk-fixture/b-dir/deep");
225
+ expect(paths).not.toContain("walk-fixture/b-dir/file-in-b.md");
226
+ });
227
+
228
+ test("the root listing is whole even under a cap smaller than it", async () => {
229
+ const result = await walk({ maxEntries: 1 });
230
+ expect(result.entries.map((e) => e.name)).toContain("top.md");
231
+ expect(result.truncated).toBe(true);
232
+ });
233
+
234
+ test("the deadline stops the walk and reports truncated", async () => {
235
+ const result = await walk({ maxWalkMs: -1 });
236
+ expect(result.truncated).toBe(true);
237
+ });
238
+
239
+ test("directory sizes are null in a recursive walk even when a sizer is given", async () => {
240
+ const { entries } = await walk({ directorySize: () => 42 });
241
+ const dir = entries.find((e) => e.type === "directory");
242
+ expect(dir?.size).toBeNull();
243
+ });
244
+ });