@vellumai/assistant 0.8.1 → 0.8.2

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 (506) hide show
  1. package/ARCHITECTURE.md +2 -7
  2. package/Dockerfile +75 -1
  3. package/bun.lock +11 -1
  4. package/docker-entrypoint.sh +5 -0
  5. package/docker-init-apt-root.sh +94 -0
  6. package/docker-kata-apt-env.sh +39 -0
  7. package/docs/plugins.md +88 -47
  8. package/docs/skills.md +9 -7
  9. package/examples/plugins/echo/README.md +27 -27
  10. package/examples/plugins/echo/package.json +3 -0
  11. package/examples/plugins/echo/register.ts +31 -31
  12. package/node_modules/@vellumai/slack-text/src/index.test.ts +114 -14
  13. package/node_modules/@vellumai/slack-text/src/index.ts +82 -18
  14. package/openapi.yaml +325 -3
  15. package/package.json +3 -1
  16. package/scripts/generate-openapi.ts +83 -10
  17. package/scripts/sync-llm-catalog.ts +2 -2
  18. package/scripts/sync-web-search-catalog.ts +47 -25
  19. package/src/__tests__/agent-image-optimize.test.ts +11 -3
  20. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
  21. package/src/__tests__/anthropic-provider.test.ts +45 -0
  22. package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
  23. package/src/__tests__/app-executors.test.ts +220 -4
  24. package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
  25. package/src/__tests__/bundled-asset.test.ts +6 -6
  26. package/src/__tests__/channel-availability-routes.test.ts +206 -0
  27. package/src/__tests__/channel-delivery-store.test.ts +289 -1
  28. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
  29. package/src/__tests__/clawhub.test.ts +75 -16
  30. package/src/__tests__/compactor-tail-resolution.test.ts +41 -0
  31. package/src/__tests__/config-schema.test.ts +21 -0
  32. package/src/__tests__/config-set-route.test.ts +80 -0
  33. package/src/__tests__/config-sounds-sync.test.ts +97 -0
  34. package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
  35. package/src/__tests__/context-search-conversations-source.test.ts +117 -2
  36. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  37. package/src/__tests__/context-search-workspace-source.test.ts +7 -0
  38. package/src/__tests__/context-token-estimator.test.ts +1 -0
  39. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  40. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  41. package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
  42. package/src/__tests__/conversation-agent-loop.test.ts +2 -0
  43. package/src/__tests__/conversation-error.test.ts +42 -3
  44. package/src/__tests__/conversation-fork-crud.test.ts +82 -0
  45. package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
  46. package/src/__tests__/conversation-lifecycle.test.ts +173 -0
  47. package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
  48. package/src/__tests__/conversation-pairing.test.ts +54 -0
  49. package/src/__tests__/conversation-process-callsite.test.ts +4 -1
  50. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
  51. package/src/__tests__/conversation-queue.test.ts +4 -1
  52. package/src/__tests__/conversation-runtime-assembly.test.ts +76 -9
  53. package/src/__tests__/conversation-slash-queue.test.ts +59 -1
  54. package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
  55. package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
  56. package/src/__tests__/conversation-sync-tags.test.ts +235 -0
  57. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  59. package/src/__tests__/credential-security-invariants.test.ts +3 -2
  60. package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
  61. package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
  62. package/src/__tests__/disk-pressure-tools.test.ts +1 -0
  63. package/src/__tests__/dm-backfill.test.ts +121 -10
  64. package/src/__tests__/document-tool-security.test.ts +258 -0
  65. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  66. package/src/__tests__/edit-propagation.test.ts +33 -0
  67. package/src/__tests__/empty-response-pipeline.test.ts +0 -4
  68. package/src/__tests__/external-plugin-loader.test.ts +60 -36
  69. package/src/__tests__/filing-service.test.ts +140 -0
  70. package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
  71. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
  72. package/src/__tests__/helpers/tar-fixtures.ts +39 -0
  73. package/src/__tests__/helpers/wait-for.ts +21 -0
  74. package/src/__tests__/history-repair-pipeline.test.ts +0 -3
  75. package/src/__tests__/history-repair.test.ts +73 -0
  76. package/src/__tests__/host-app-control-proxy.test.ts +266 -10
  77. package/src/__tests__/image-credentials.test.ts +1 -1
  78. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  79. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
  80. package/src/__tests__/inference-profile-reaper.test.ts +4 -2
  81. package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
  82. package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
  83. package/src/__tests__/injector-chain.test.ts +10 -8
  84. package/src/__tests__/install-skill-routing.test.ts +155 -37
  85. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +92 -3
  86. package/src/__tests__/list-messages-page-latest.test.ts +55 -0
  87. package/src/__tests__/llm-call-pipeline.test.ts +0 -3
  88. package/src/__tests__/llm-catalog-parity.test.ts +55 -13
  89. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +34 -0
  90. package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
  91. package/src/__tests__/llm-usage-store.test.ts +114 -0
  92. package/src/__tests__/managed-profile-guard.test.ts +31 -29
  93. package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
  94. package/src/__tests__/managed-store.test.ts +84 -192
  95. package/src/__tests__/media-generate-image.test.ts +1 -1
  96. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
  97. package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
  98. package/src/__tests__/oauth-commands-routes.test.ts +168 -16
  99. package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
  100. package/src/__tests__/openai-provider.test.ts +24 -0
  101. package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
  102. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
  103. package/src/__tests__/persistence-pipeline.test.ts +0 -2
  104. package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +1 -1
  105. package/src/__tests__/platform.test.ts +2 -0
  106. package/src/__tests__/plugin-api-shim.test.ts +125 -0
  107. package/src/__tests__/plugin-bootstrap.test.ts +10 -36
  108. package/src/__tests__/plugin-external-api.test.ts +68 -0
  109. package/src/__tests__/plugin-registry.test.ts +0 -77
  110. package/src/__tests__/plugin-route-contribution.test.ts +0 -1
  111. package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
  112. package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
  113. package/src/__tests__/plugin-types.test.ts +3 -13
  114. package/src/__tests__/process-message-background-slack.test.ts +8 -1
  115. package/src/__tests__/process-message-display-content.test.ts +421 -0
  116. package/src/__tests__/provider-catalog-visibility.test.ts +142 -0
  117. package/src/__tests__/provider-error-scenarios.test.ts +111 -0
  118. package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +8 -8
  119. package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
  120. package/src/__tests__/schedule-routes.test.ts +50 -3
  121. package/src/__tests__/schedule-store.test.ts +94 -0
  122. package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
  123. package/src/__tests__/schema-transforms.test.ts +20 -0
  124. package/src/__tests__/search-skills-unified.test.ts +0 -5
  125. package/src/__tests__/server-history-render.test.ts +43 -0
  126. package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
  127. package/src/__tests__/skill-load-tool.test.ts +27 -89
  128. package/src/__tests__/skill-memory.test.ts +23 -3
  129. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
  130. package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
  131. package/src/__tests__/skills-install-extract.test.ts +49 -38
  132. package/src/__tests__/skills-install-staging.test.ts +159 -0
  133. package/src/__tests__/skills-uninstall.test.ts +9 -41
  134. package/src/__tests__/skills.test.ts +51 -58
  135. package/src/__tests__/slack-channel-config.test.ts +9 -0
  136. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  137. package/src/__tests__/system-prompt.test.ts +737 -63
  138. package/src/__tests__/terminal-tools.test.ts +28 -1
  139. package/src/__tests__/thread-backfill.test.ts +557 -27
  140. package/src/__tests__/title-generate-pipeline.test.ts +0 -13
  141. package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
  142. package/src/__tests__/tool-error-pipeline.test.ts +0 -3
  143. package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
  144. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  145. package/src/__tests__/tool-executor.test.ts +16 -4
  146. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
  147. package/src/__tests__/turn-events-store.test.ts +256 -0
  148. package/src/__tests__/twilio-routes.test.ts +4 -0
  149. package/src/__tests__/user-plugin-loader.test.ts +0 -7
  150. package/src/__tests__/voice-session-bridge.test.ts +198 -0
  151. package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
  152. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
  153. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
  154. package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
  155. package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
  156. package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
  157. package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
  158. package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
  159. package/src/acp/resolve-agent.ts +1 -1
  160. package/src/agent/image-optimize.ts +13 -5
  161. package/src/calls/voice-session-bridge.ts +61 -42
  162. package/src/channels/types.ts +108 -0
  163. package/src/cli/__tests__/unknown-command.test.ts +24 -0
  164. package/src/cli/commands/__tests__/changelog.test.ts +304 -319
  165. package/src/cli/commands/__tests__/schedules.test.ts +491 -0
  166. package/src/cli/commands/changelog.ts +106 -42
  167. package/src/cli/commands/conversations.ts +102 -17
  168. package/src/cli/commands/default-action.ts +10 -53
  169. package/src/cli/commands/notifications.ts +329 -317
  170. package/src/cli/commands/plugins.ts +185 -0
  171. package/src/cli/commands/schedules.ts +391 -0
  172. package/src/cli/commands/telemetry.ts +40 -0
  173. package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
  174. package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
  175. package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
  176. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
  177. package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
  178. package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
  179. package/src/cli/lib/cli-colors.ts +12 -0
  180. package/src/cli/lib/confirm-prompt.ts +79 -0
  181. package/src/cli/lib/install-from-github.ts +304 -0
  182. package/src/cli/lib/list-installed-plugins.ts +137 -0
  183. package/src/cli/lib/uninstall-plugin.ts +82 -0
  184. package/src/cli/lib/unknown-command.ts +111 -0
  185. package/src/cli/program.ts +38 -2
  186. package/src/config/bundled-skills/app-builder/SKILL.md +23 -21
  187. package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
  188. package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
  189. package/src/config/bundled-skills/document/SKILL.md +23 -3
  190. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  191. package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
  192. package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
  193. package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
  194. package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
  195. package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
  196. package/src/config/bundled-tool-registry.ts +6 -0
  197. package/src/config/feature-flag-registry.json +41 -1
  198. package/src/config/loader.ts +64 -38
  199. package/src/config/schema.ts +7 -10
  200. package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
  201. package/src/config/schemas/channels.ts +8 -0
  202. package/src/config/schemas/compaction.ts +28 -0
  203. package/src/config/schemas/heartbeat.ts +9 -0
  204. package/src/config/schemas/llm-request-logs.ts +31 -7
  205. package/src/config/schemas/llm.ts +3 -0
  206. package/src/config/schemas/memory-retrieval.ts +18 -0
  207. package/src/config/schemas/tools.ts +14 -0
  208. package/src/config/skills.ts +3 -96
  209. package/src/context/compactor.ts +1047 -0
  210. package/src/context/token-estimator.ts +2 -2
  211. package/src/context/window-manager.ts +197 -1520
  212. package/src/credential-execution/managed-catalog.ts +37 -0
  213. package/src/credential-health/credential-health-service.ts +280 -19
  214. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +34 -0
  215. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
  216. package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
  217. package/src/daemon/approval-generators.ts +8 -6
  218. package/src/daemon/config-watcher.ts +94 -31
  219. package/src/daemon/conversation-agent-loop.ts +169 -9
  220. package/src/daemon/conversation-error.ts +171 -37
  221. package/src/daemon/conversation-lifecycle.ts +53 -40
  222. package/src/daemon/conversation-messaging.ts +25 -6
  223. package/src/daemon/conversation-process.ts +49 -12
  224. package/src/daemon/conversation-runtime-assembly.ts +16 -1
  225. package/src/daemon/conversation-slash.ts +12 -5
  226. package/src/daemon/conversation-store.ts +11 -4
  227. package/src/daemon/conversation-tool-setup.ts +39 -7
  228. package/src/daemon/conversation.ts +33 -1
  229. package/src/daemon/external-plugins-bootstrap.ts +217 -181
  230. package/src/daemon/first-greeting.ts +22 -2
  231. package/src/daemon/handlers/config-model.ts +6 -5
  232. package/src/daemon/handlers/config-slack-channel.ts +15 -3
  233. package/src/daemon/handlers/shared.ts +14 -5
  234. package/src/daemon/handlers/skills.ts +111 -108
  235. package/src/daemon/history-repair.ts +28 -1
  236. package/src/daemon/host-app-control-proxy.ts +98 -23
  237. package/src/daemon/lifecycle.ts +45 -35
  238. package/src/daemon/meet-host-supervisor.ts +5 -4
  239. package/src/daemon/memory-v2-startup.ts +49 -0
  240. package/src/daemon/message-protocol.ts +1 -0
  241. package/src/daemon/message-types/conversations.ts +25 -0
  242. package/src/daemon/message-types/messages.ts +61 -0
  243. package/src/daemon/message-types/subagents.ts +1 -0
  244. package/src/daemon/message-types/sync.ts +1 -0
  245. package/src/daemon/pkb-reminder-builder.test.ts +1 -1
  246. package/src/daemon/pkb-reminder-builder.ts +1 -1
  247. package/src/daemon/plugin-source-watcher.ts +146 -0
  248. package/src/daemon/process-message.ts +21 -3
  249. package/src/daemon/server.ts +11 -2
  250. package/src/daemon/skill-memory-refresh.ts +29 -0
  251. package/src/documents/document-store.ts +221 -3
  252. package/src/embedded/plugin-api.ts +40 -0
  253. package/src/filing/filing-service.ts +39 -0
  254. package/src/heartbeat/__tests__/heartbeat-service.test.ts +91 -6
  255. package/src/heartbeat/heartbeat-run-store.ts +2 -1
  256. package/src/heartbeat/heartbeat-service.ts +41 -0
  257. package/src/home/__tests__/feed-types.test.ts +40 -0
  258. package/src/home/feed-types.ts +22 -0
  259. package/src/home/post-connect-feed.ts +1 -0
  260. package/src/index.ts +18 -1
  261. package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
  262. package/src/mcp/client.ts +20 -4
  263. package/src/media/image-credentials.ts +3 -3
  264. package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
  265. package/src/memory/__tests__/conversation-queries.test.ts +263 -0
  266. package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
  267. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
  268. package/src/memory/__tests__/message-content.test.ts +35 -0
  269. package/src/memory/bookmark-crud.ts +42 -10
  270. package/src/memory/context-search/sources/conversations.ts +62 -2
  271. package/src/memory/context-search/sources/workspace.ts +4 -0
  272. package/src/memory/conversation-crud.ts +63 -19
  273. package/src/memory/conversation-queries.ts +110 -10
  274. package/src/memory/db-init.ts +6 -0
  275. package/src/memory/delivery-crud.ts +152 -5
  276. package/src/memory/embedding-backend.ts +4 -4
  277. package/src/memory/external-conversation-store.ts +66 -5
  278. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +66 -9
  279. package/src/memory/graph/conversation-graph-memory.ts +31 -15
  280. package/src/memory/graph/tools.ts +3 -3
  281. package/src/memory/indexer.ts +34 -29
  282. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
  283. package/src/memory/jobs/embed-concept-page.ts +20 -11
  284. package/src/memory/jobs-worker.ts +6 -1
  285. package/src/memory/llm-request-log-source-clickhouse.ts +17 -10
  286. package/src/memory/llm-request-log-source.ts +19 -52
  287. package/src/memory/llm-usage-store.ts +125 -5
  288. package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
  289. package/src/memory/message-content.ts +1 -1
  290. package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
  291. package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
  292. package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
  293. package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
  294. package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
  295. package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
  296. package/src/memory/migrations/index.ts +6 -0
  297. package/src/memory/migrations/registry.ts +8 -0
  298. package/src/memory/onboarding-events-store.ts +106 -0
  299. package/src/memory/schema/bookmarks.ts +0 -2
  300. package/src/memory/schema/calls.ts +1 -0
  301. package/src/memory/schema/inference.ts +1 -3
  302. package/src/memory/schema/infrastructure.ts +12 -0
  303. package/src/memory/turn-events-store.ts +127 -2
  304. package/src/memory/v2/__tests__/activation.test.ts +0 -8
  305. package/src/memory/v2/__tests__/injection.test.ts +98 -8
  306. package/src/memory/v2/__tests__/migration.test.ts +87 -0
  307. package/src/memory/v2/__tests__/page-index.test.ts +83 -0
  308. package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
  309. package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
  310. package/src/memory/v2/__tests__/router.test.ts +15 -0
  311. package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
  312. package/src/memory/v2/injection.ts +32 -6
  313. package/src/memory/v2/migration.ts +49 -19
  314. package/src/memory/v2/page-index.ts +35 -5
  315. package/src/memory/v2/prompts/router.ts +11 -8
  316. package/src/memory/v2/prompts/sweep.ts +2 -2
  317. package/src/memory/v2/qdrant.ts +135 -7
  318. package/src/memory/v2/router.ts +9 -8
  319. package/src/memory/v2/skill-store.ts +120 -35
  320. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
  321. package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
  322. package/src/messaging/providers/slack/adapter.ts +43 -5
  323. package/src/messaging/providers/slack/client.ts +27 -0
  324. package/src/messaging/providers/slack/deep-link.ts +65 -0
  325. package/src/messaging/providers/slack/download.ts +104 -0
  326. package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
  327. package/src/messaging/providers/slack/message-metadata.ts +27 -0
  328. package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
  329. package/src/messaging/providers/slack/render-transcript.ts +69 -5
  330. package/src/messaging/providers/slack/types.ts +20 -1
  331. package/src/notifications/conversation-pairing.ts +2 -1
  332. package/src/notifications/decision-engine.ts +2 -1
  333. package/src/notifications/emit-signal.ts +20 -1
  334. package/src/notifications/home-feed-side-effect.ts +54 -0
  335. package/src/notifications/signal.ts +3 -1
  336. package/src/oauth/connection-resolver.ts +8 -4
  337. package/src/oauth/platform-connection.ts +6 -2
  338. package/src/oauth/seed-providers.ts +10 -1
  339. package/src/permissions/checker.ts +2 -0
  340. package/src/permissions/ipc-risk-types.ts +1 -0
  341. package/src/permissions/question-prompter.test.ts +416 -0
  342. package/src/permissions/question-prompter.ts +294 -0
  343. package/src/platform/client.test.ts +1 -1
  344. package/src/platform/client.ts +1 -1
  345. package/src/plugin-api/constants.ts +26 -0
  346. package/src/plugin-api/index.ts +34 -1
  347. package/src/plugin-api/types.ts +104 -22
  348. package/src/plugins/defaults/circuit-breaker.ts +0 -5
  349. package/src/plugins/defaults/compaction.ts +0 -4
  350. package/src/plugins/defaults/empty-response.ts +0 -2
  351. package/src/plugins/defaults/history-repair.ts +0 -2
  352. package/src/plugins/defaults/injectors.ts +36 -3
  353. package/src/plugins/defaults/llm-call.ts +0 -2
  354. package/src/plugins/defaults/memory-retrieval.ts +0 -1
  355. package/src/plugins/defaults/overflow-reduce.ts +0 -1
  356. package/src/plugins/defaults/persistence.ts +0 -2
  357. package/src/plugins/defaults/title-generate.ts +0 -5
  358. package/src/plugins/defaults/token-estimate.ts +0 -2
  359. package/src/plugins/defaults/tool-error.ts +0 -7
  360. package/src/plugins/defaults/tool-execute.ts +0 -2
  361. package/src/plugins/defaults/tool-result-truncate.ts +0 -4
  362. package/src/plugins/ensure-plugin-api-shim.ts +96 -0
  363. package/src/plugins/external-api.ts +104 -0
  364. package/src/plugins/external-plugin-loader.ts +105 -32
  365. package/src/plugins/feature-gate.ts +22 -0
  366. package/src/plugins/pipeline.ts +37 -0
  367. package/src/plugins/registry.ts +48 -80
  368. package/src/plugins/types.ts +31 -26
  369. package/src/plugins/user-loader.ts +21 -2
  370. package/src/proactive-artifact/aux-message-injector.ts +11 -0
  371. package/src/proactive-artifact/job.test.ts +37 -5
  372. package/src/prompts/__tests__/system-prompt.test.ts +12 -0
  373. package/src/prompts/__tests__/task-progress-hint-section.test.ts +99 -0
  374. package/src/prompts/normalize-onboarding.ts +27 -0
  375. package/src/prompts/sections.ts +302 -0
  376. package/src/prompts/system-prompt.ts +63 -166
  377. package/src/prompts/templates/BOOTSTRAP.md +17 -1
  378. package/src/prompts/templates/system-sections.ts +173 -0
  379. package/src/providers/__tests__/inference.test.ts +22 -7
  380. package/src/providers/anthropic/client.ts +28 -28
  381. package/src/providers/connection-resolution.ts +7 -0
  382. package/src/providers/inference/adapter-factory.ts +41 -4
  383. package/src/providers/inference/connections.ts +74 -29
  384. package/src/providers/inference/resolve-auth.ts +12 -4
  385. package/src/providers/model-catalog.ts +294 -12
  386. package/src/providers/openai/chat-completions-provider.ts +10 -2
  387. package/src/providers/openrouter/client.ts +7 -0
  388. package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -1
  389. package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
  390. package/src/providers/provider-availability.ts +17 -2
  391. package/src/providers/provider-catalog-visibility.ts +36 -0
  392. package/src/providers/registry.ts +22 -14
  393. package/src/providers/retry.ts +47 -1
  394. package/src/runtime/__tests__/agent-wake.test.ts +152 -0
  395. package/src/runtime/agent-wake.ts +42 -14
  396. package/src/runtime/auth/route-policy.ts +8 -1
  397. package/src/runtime/btw-sidechain.ts +2 -0
  398. package/src/runtime/http-types.ts +19 -0
  399. package/src/runtime/migrations/origin-mode.ts +1 -1
  400. package/src/runtime/pending-interactions.ts +1 -0
  401. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
  402. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
  403. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +107 -20
  404. package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
  405. package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
  406. package/src/runtime/routes/acp-routes-list.test.ts +143 -0
  407. package/src/runtime/routes/acp-routes.ts +5 -3
  408. package/src/runtime/routes/auth-routes.ts +1 -1
  409. package/src/runtime/routes/bookmark-routes.ts +5 -3
  410. package/src/runtime/routes/btw-routes.ts +5 -1
  411. package/src/runtime/routes/channel-availability-routes.ts +121 -0
  412. package/src/runtime/routes/conversation-cli-routes.ts +44 -3
  413. package/src/runtime/routes/conversation-list-routes.ts +3 -20
  414. package/src/runtime/routes/conversation-management-routes.ts +17 -42
  415. package/src/runtime/routes/conversation-query-routes.ts +40 -35
  416. package/src/runtime/routes/conversation-routes.ts +90 -11
  417. package/src/runtime/routes/documents-routes.ts +25 -86
  418. package/src/runtime/routes/group-routes.ts +5 -0
  419. package/src/runtime/routes/inbound-conversation.ts +28 -8
  420. package/src/runtime/routes/inbound-message-handler.ts +236 -41
  421. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
  422. package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
  423. package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
  424. package/src/runtime/routes/index.ts +6 -0
  425. package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
  426. package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
  427. package/src/runtime/routes/inference-provider-connection-routes.ts +65 -21
  428. package/src/runtime/routes/integrations/slack/share.ts +4 -52
  429. package/src/runtime/routes/integrations/slack/token.ts +43 -0
  430. package/src/runtime/routes/integrations/twilio.ts +6 -13
  431. package/src/runtime/routes/notification-routes.ts +1 -1
  432. package/src/runtime/routes/oauth-commands-routes.ts +105 -15
  433. package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
  434. package/src/runtime/routes/question-routes.ts +259 -0
  435. package/src/runtime/routes/rename-conversation-routes.ts +2 -33
  436. package/src/runtime/routes/schedule-routes.ts +4 -7
  437. package/src/runtime/routes/subagents-routes.ts +57 -18
  438. package/src/runtime/routes/telemetry-routes.ts +27 -0
  439. package/src/runtime/routes/tts-routes.ts +27 -2
  440. package/src/runtime/routes/workspace-routes.test.ts +43 -0
  441. package/src/runtime/routes/workspace-routes.ts +28 -0
  442. package/src/runtime/services/conversation-serializer.ts +39 -7
  443. package/src/runtime/sync/resource-sync-events.ts +93 -1
  444. package/src/schedule/schedule-store.ts +27 -2
  445. package/src/schedule/scheduler.ts +9 -1
  446. package/src/security/__tests__/untrusted-content.test.ts +86 -0
  447. package/src/security/untrusted-content.ts +93 -8
  448. package/src/skills/catalog-files.ts +1 -1
  449. package/src/skills/catalog-install.ts +233 -116
  450. package/src/skills/clawhub.ts +70 -13
  451. package/src/skills/managed-store.ts +4 -119
  452. package/src/skills/skillssh-registry.ts +27 -48
  453. package/src/subagent/manager.ts +15 -7
  454. package/src/telemetry/types.ts +113 -1
  455. package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
  456. package/src/telemetry/usage-telemetry-reporter.ts +113 -7
  457. package/src/tools/apps/executors.ts +58 -7
  458. package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
  459. package/src/tools/ask-question/ask-question-tool.ts +304 -0
  460. package/src/tools/browser/browser-execution.ts +15 -11
  461. package/src/tools/computer-use/definitions.ts +3 -3
  462. package/src/tools/credentials/vault.ts +1 -1
  463. package/src/tools/document/document-tool.ts +124 -1
  464. package/src/tools/filesystem/edit.ts +1 -1
  465. package/src/tools/filesystem/list.ts +1 -1
  466. package/src/tools/filesystem/read.ts +1 -1
  467. package/src/tools/filesystem/write.ts +5 -2
  468. package/src/tools/host-filesystem/transfer.ts +1 -1
  469. package/src/tools/host-terminal/host-shell.ts +1 -1
  470. package/src/tools/permission-checker.ts +1 -1
  471. package/src/tools/registry.ts +17 -7
  472. package/src/tools/schedule/create.ts +2 -2
  473. package/src/tools/schema-transforms.ts +7 -2
  474. package/src/tools/side-effects.ts +1 -0
  475. package/src/tools/skills/delete-managed.ts +4 -4
  476. package/src/tools/skills/execute.ts +1 -1
  477. package/src/tools/skills/scaffold-managed.ts +3 -2
  478. package/src/tools/subagent/notify-parent.ts +1 -1
  479. package/src/tools/system/request-permission.ts +2 -2
  480. package/src/tools/terminal/safe-env.ts +60 -1
  481. package/src/tools/tool-manifest.ts +2 -0
  482. package/src/tools/types.ts +72 -21
  483. package/src/tools/ui-surface/definitions.ts +6 -5
  484. package/src/tts/__tests__/provider-adapters.test.ts +76 -2
  485. package/src/tts/providers/elevenlabs-provider.ts +75 -1
  486. package/src/types/onboarding-context.ts +2 -0
  487. package/src/util/errors.ts +17 -0
  488. package/src/util/platform.ts +10 -0
  489. package/src/watcher/__tests__/engine.test.ts +22 -0
  490. package/src/watcher/engine.ts +6 -2
  491. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
  492. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
  493. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
  494. package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
  495. package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
  496. package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
  497. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
  498. package/src/workspace/migrations/registry.ts +8 -0
  499. package/src/workspace/migrations/runner.ts +39 -9
  500. package/src/workspace/migrations/types.ts +4 -0
  501. package/examples/plugins/echo/bun.lock +0 -25
  502. package/src/__tests__/context-window-manager.test.ts +0 -2481
  503. package/src/context/__tests__/compact-prompt.test.ts +0 -63
  504. package/src/context/prompts/compact.md +0 -26
  505. package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
  506. /package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +0 -0
@@ -0,0 +1,309 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ mkdtempSync,
5
+ readFileSync,
6
+ rmSync,
7
+ writeFileSync,
8
+ } from "node:fs";
9
+ import * as fs from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
13
+
14
+ import { removeLegacySkillsIndexMigration } from "../workspace/migrations/084-remove-legacy-skills-index.js";
15
+
16
+ let workspaceDir: string;
17
+
18
+ beforeEach(() => {
19
+ workspaceDir = mkdtempSync(join(tmpdir(), "vellum-migration-084-test-"));
20
+ });
21
+
22
+ afterEach(() => {
23
+ if (existsSync(workspaceDir)) {
24
+ rmSync(workspaceDir, { recursive: true, force: true });
25
+ }
26
+ });
27
+
28
+ function writeSkill(skillId: string, body = "Body."): string {
29
+ const skillDir = join(workspaceDir, "skills", skillId);
30
+ mkdirSync(skillDir, { recursive: true });
31
+ const skillFilePath = join(skillDir, "SKILL.md");
32
+ writeFileSync(
33
+ skillFilePath,
34
+ `---\nname: "${skillId}"\ndescription: "Test skill."\n---\n\n${body}\n`,
35
+ "utf-8",
36
+ );
37
+ return skillFilePath;
38
+ }
39
+
40
+ function writeLegacyIndex(contents = "- alpha\n"): string {
41
+ const skillsDir = join(workspaceDir, "skills");
42
+ mkdirSync(skillsDir, { recursive: true });
43
+ const legacyIndexPath = join(skillsDir, "SKILLS.md");
44
+ writeFileSync(legacyIndexPath, contents, "utf-8");
45
+ return legacyIndexPath;
46
+ }
47
+
48
+ function expectNestedSkillPreserved(
49
+ legacyIndexPath: string,
50
+ nestedSkillPath: string,
51
+ ): void {
52
+ const topLevelSkillPath = join(
53
+ workspaceDir,
54
+ "skills",
55
+ "my-skill",
56
+ "SKILL.md",
57
+ );
58
+ expect(existsSync(legacyIndexPath)).toBe(false);
59
+ expect(existsSync(nestedSkillPath)).toBe(true);
60
+ expect(readFileSync(topLevelSkillPath, "utf-8")).toContain("org/my-skill");
61
+ }
62
+
63
+ describe("084-remove-legacy-skills-index migration", () => {
64
+ test("has correct id and description", () => {
65
+ expect(removeLegacySkillsIndexMigration.id).toBe(
66
+ "084-remove-legacy-skills-index",
67
+ );
68
+ expect(removeLegacySkillsIndexMigration.description).toContain("SKILLS.md");
69
+ expect(removeLegacySkillsIndexMigration.retryFailedCheckpoint).toBe(true);
70
+ });
71
+
72
+ test("removes only skills/SKILLS.md when present", () => {
73
+ const skillsDir = join(workspaceDir, "skills");
74
+ const legacyIndexPath = writeLegacyIndex();
75
+
76
+ const alphaSkillPath = writeSkill("alpha");
77
+ const betaSkillPath = writeSkill("beta");
78
+ const topLevelSkillPath = join(skillsDir, "SKILL.md");
79
+ writeFileSync(
80
+ topLevelSkillPath,
81
+ "---\nname: Root\ndescription: Root file.\n---\n\nRoot.\n",
82
+ "utf-8",
83
+ );
84
+
85
+ removeLegacySkillsIndexMigration.run(workspaceDir);
86
+
87
+ expect(existsSync(legacyIndexPath)).toBe(false);
88
+ expect(readFileSync(alphaSkillPath, "utf-8")).toContain("alpha");
89
+ expect(readFileSync(betaSkillPath, "utf-8")).toContain("beta");
90
+ expect(readFileSync(topLevelSkillPath, "utf-8")).toContain("Root");
91
+ });
92
+
93
+ test("is a no-op when skills/SKILLS.md is absent", () => {
94
+ writeSkill("alpha");
95
+
96
+ expect(() =>
97
+ removeLegacySkillsIndexMigration.run(workspaceDir),
98
+ ).not.toThrow();
99
+ expect(existsSync(join(workspaceDir, "skills", "alpha", "SKILL.md"))).toBe(
100
+ true,
101
+ );
102
+ });
103
+
104
+ test("removing a stale SKILLS.md index preserves omitted valid skill directories", () => {
105
+ const legacyIndexPath = writeLegacyIndex("- indexed-skill\n");
106
+
107
+ writeSkill("indexed-skill");
108
+ writeSkill("omitted-skill");
109
+
110
+ removeLegacySkillsIndexMigration.run(workspaceDir);
111
+
112
+ expect(existsSync(legacyIndexPath)).toBe(false);
113
+ expect(
114
+ readFileSync(
115
+ join(workspaceDir, "skills", "omitted-skill", "SKILL.md"),
116
+ "utf-8",
117
+ ),
118
+ ).toContain("Body.");
119
+ });
120
+
121
+ test("copies nested indexed skills to top-level discovery location", () => {
122
+ const legacyIndexPath = writeLegacyIndex("- org/my-skill\n");
123
+ const nestedSkillPath = writeSkill("org/my-skill");
124
+
125
+ removeLegacySkillsIndexMigration.run(workspaceDir);
126
+
127
+ const topLevelSkillPath = join(
128
+ workspaceDir,
129
+ "skills",
130
+ "my-skill",
131
+ "SKILL.md",
132
+ );
133
+ expect(existsSync(legacyIndexPath)).toBe(false);
134
+ expect(existsSync(nestedSkillPath)).toBe(true);
135
+ expect(readFileSync(topLevelSkillPath, "utf-8")).toContain("org/my-skill");
136
+
137
+ expect(readFileSync(topLevelSkillPath, "utf-8")).toContain("Body.");
138
+ });
139
+
140
+ test("copies nested indexed skills from plain entries that point to SKILL.md", () => {
141
+ const legacyIndexPath = writeLegacyIndex("- org/my-skill/SKILL.md\n");
142
+ const nestedSkillPath = writeSkill("org/my-skill");
143
+
144
+ removeLegacySkillsIndexMigration.run(workspaceDir);
145
+
146
+ expectNestedSkillPreserved(legacyIndexPath, nestedSkillPath);
147
+ });
148
+
149
+ test("copies nested indexed skills from markdown links that point to SKILL.md", () => {
150
+ const legacyIndexPath = writeLegacyIndex(
151
+ "- [My Skill](org/my-skill/skill.md)\n",
152
+ );
153
+ const nestedSkillPath = writeSkill("org/my-skill");
154
+
155
+ removeLegacySkillsIndexMigration.run(workspaceDir);
156
+
157
+ expectNestedSkillPreserved(legacyIndexPath, nestedSkillPath);
158
+ });
159
+
160
+ test("preserves nested indexed skill with alternate id when top-level basename exists", () => {
161
+ const legacyIndexPath = writeLegacyIndex("- org/my-skill\n");
162
+ const nestedSkillPath = writeSkill("org/my-skill", "Nested body.");
163
+ const topLevelSkillPath = writeSkill("my-skill", "Top-level body.");
164
+
165
+ removeLegacySkillsIndexMigration.run(workspaceDir);
166
+
167
+ const preservedSkillPath = join(
168
+ workspaceDir,
169
+ "skills",
170
+ "org__my-skill",
171
+ "SKILL.md",
172
+ );
173
+ expect(existsSync(legacyIndexPath)).toBe(false);
174
+ expect(readFileSync(nestedSkillPath, "utf-8")).toContain("Nested body.");
175
+ expect(readFileSync(topLevelSkillPath, "utf-8")).toContain(
176
+ "Top-level body.",
177
+ );
178
+ expect(readFileSync(preservedSkillPath, "utf-8")).toContain("Nested body.");
179
+
180
+ expect(readFileSync(topLevelSkillPath, "utf-8")).toContain(
181
+ "Top-level body.",
182
+ );
183
+ expect(readFileSync(preservedSkillPath, "utf-8")).toContain("Nested body.");
184
+ });
185
+
186
+ test("preserves same-basename nested indexed skills with unique top-level ids", () => {
187
+ const legacyIndexPath = writeLegacyIndex(
188
+ "- team-a/deploy\n- team-b/deploy\n",
189
+ );
190
+ const teamASkillPath = writeSkill("team-a/deploy", "Team A body.");
191
+ const teamBSkillPath = writeSkill("team-b/deploy", "Team B body.");
192
+
193
+ removeLegacySkillsIndexMigration.run(workspaceDir);
194
+
195
+ const primaryPreservedSkillPath = join(
196
+ workspaceDir,
197
+ "skills",
198
+ "deploy",
199
+ "SKILL.md",
200
+ );
201
+ const alternatePreservedSkillPath = join(
202
+ workspaceDir,
203
+ "skills",
204
+ "team-b__deploy",
205
+ "SKILL.md",
206
+ );
207
+
208
+ expect(existsSync(legacyIndexPath)).toBe(false);
209
+ expect(existsSync(teamASkillPath)).toBe(true);
210
+ expect(existsSync(teamBSkillPath)).toBe(true);
211
+ expect(readFileSync(primaryPreservedSkillPath, "utf-8")).toContain(
212
+ "Team A body.",
213
+ );
214
+ expect(readFileSync(alternatePreservedSkillPath, "utf-8")).toContain(
215
+ "Team B body.",
216
+ );
217
+
218
+ expect(readFileSync(primaryPreservedSkillPath, "utf-8")).toContain(
219
+ "Team A body.",
220
+ );
221
+ expect(readFileSync(alternatePreservedSkillPath, "utf-8")).toContain(
222
+ "Team B body.",
223
+ );
224
+ });
225
+
226
+ test("does not follow legacy index entries outside the skills root", () => {
227
+ const legacyIndexPath = writeLegacyIndex("- ../outside/my-skill\n");
228
+ const outsideSkillDir = join(workspaceDir, "outside", "my-skill");
229
+ mkdirSync(outsideSkillDir, { recursive: true });
230
+ writeFileSync(
231
+ join(outsideSkillDir, "SKILL.md"),
232
+ "---\nname: Outside\ndescription: Outside skill.\n---\n\nOutside.\n",
233
+ "utf-8",
234
+ );
235
+
236
+ removeLegacySkillsIndexMigration.run(workspaceDir);
237
+
238
+ expect(existsSync(legacyIndexPath)).toBe(false);
239
+ expect(existsSync(join(workspaceDir, "skills", "my-skill"))).toBe(false);
240
+ });
241
+
242
+ test("is safe to re-run", () => {
243
+ const legacyIndexPath = writeLegacyIndex();
244
+
245
+ removeLegacySkillsIndexMigration.run(workspaceDir);
246
+ expect(() =>
247
+ removeLegacySkillsIndexMigration.run(workspaceDir),
248
+ ).not.toThrow();
249
+ expect(existsSync(legacyIndexPath)).toBe(false);
250
+ });
251
+
252
+ test("does not recursively delete a directory named SKILLS.md", () => {
253
+ const legacyIndexDir = join(workspaceDir, "skills", "SKILLS.md");
254
+ mkdirSync(legacyIndexDir, { recursive: true });
255
+ writeFileSync(join(legacyIndexDir, "nested.txt"), "keep\n", "utf-8");
256
+
257
+ removeLegacySkillsIndexMigration.run(workspaceDir);
258
+
259
+ expect(readFileSync(join(legacyIndexDir, "nested.txt"), "utf-8")).toBe(
260
+ "keep\n",
261
+ );
262
+ });
263
+
264
+ test("rethrows unexpected lstat failures", () => {
265
+ const legacyIndexPath = writeLegacyIndex();
266
+
267
+ const lstatError = Object.assign(new Error("simulated lstat failure"), {
268
+ code: "EACCES",
269
+ });
270
+ const lstatSpy = spyOn(fs, "lstatSync").mockImplementation(() => {
271
+ throw lstatError;
272
+ });
273
+
274
+ try {
275
+ expect(() => removeLegacySkillsIndexMigration.run(workspaceDir)).toThrow(
276
+ lstatError,
277
+ );
278
+ expect(existsSync(legacyIndexPath)).toBe(true);
279
+ } finally {
280
+ lstatSpy.mockRestore();
281
+ }
282
+ });
283
+
284
+ test("rethrows unexpected unlink failures and leaves SKILLS.md for retry", () => {
285
+ const legacyIndexPath = writeLegacyIndex();
286
+
287
+ const unlinkError = Object.assign(new Error("simulated unlink failure"), {
288
+ code: "EACCES",
289
+ });
290
+ const unlinkSpy = spyOn(fs, "unlinkSync").mockImplementation(() => {
291
+ throw unlinkError;
292
+ });
293
+
294
+ try {
295
+ expect(() => removeLegacySkillsIndexMigration.run(workspaceDir)).toThrow(
296
+ unlinkError,
297
+ );
298
+ expect(existsSync(legacyIndexPath)).toBe(true);
299
+ } finally {
300
+ unlinkSpy.mockRestore();
301
+ }
302
+ });
303
+
304
+ test("down() is a no-op", () => {
305
+ expect(() =>
306
+ removeLegacySkillsIndexMigration.down(workspaceDir),
307
+ ).not.toThrow();
308
+ });
309
+ });
@@ -137,9 +137,10 @@ describe("runWorkspaceMigrations", () => {
137
137
  expect(m1.run).toHaveBeenCalledTimes(1);
138
138
  expect(m2.run).toHaveBeenCalledTimes(1);
139
139
 
140
- // Checkpoints saved: started m1, completed m1, started m2, failed m2 = 4 writes
141
- expect(writeFileSyncFn).toHaveBeenCalledTimes(4);
142
- expect(renameSyncFn).toHaveBeenCalledTimes(4);
140
+ // Checkpoints saved: started m1, completed m1, started m2, failed m2,
141
+ // then the post-loop flip clearing isNewWorkspace = 5 writes.
142
+ expect(writeFileSyncFn).toHaveBeenCalledTimes(5);
143
+ expect(renameSyncFn).toHaveBeenCalledTimes(5);
143
144
 
144
145
  // Verify the completed checkpoint contains m1
145
146
  // The second write is the "completed" marker for m1
@@ -246,6 +247,33 @@ describe("runWorkspaceMigrations", () => {
246
247
  expect(m1.run).not.toHaveBeenCalled();
247
248
  });
248
249
 
250
+ test("skips failed migrations by default", async () => {
251
+ mockCheckpointContents = JSON.stringify({
252
+ applied: {
253
+ "001": { appliedAt: "2025-01-01T00:00:00.000Z", status: "failed" },
254
+ },
255
+ });
256
+
257
+ const m1 = makeMigration("001");
258
+ await runWorkspaceMigrations(WORKSPACE_DIR, [m1]);
259
+
260
+ expect(m1.run).not.toHaveBeenCalled();
261
+ });
262
+
263
+ test("retries failed migrations that opt in", async () => {
264
+ mockCheckpointContents = JSON.stringify({
265
+ applied: {
266
+ "001": { appliedAt: "2025-01-01T00:00:00.000Z", status: "failed" },
267
+ },
268
+ });
269
+
270
+ const m1 = makeMigration("001");
271
+ m1.retryFailedCheckpoint = true;
272
+ await runWorkspaceMigrations(WORKSPACE_DIR, [m1]);
273
+
274
+ expect(m1.run).toHaveBeenCalledTimes(1);
275
+ });
276
+
249
277
  test("supports async migrations", async () => {
250
278
  const asyncMigration: WorkspaceMigration = {
251
279
  id: "001",
@@ -278,6 +306,86 @@ describe("runWorkspaceMigrations", () => {
278
306
  expect(m1.run).not.toHaveBeenCalled();
279
307
  });
280
308
 
309
+ test("persists isNewWorkspace=true on first creation, then flips to false after sweep", async () => {
310
+ // No checkpoint file → fresh workspace.
311
+ const m1 = makeMigration("001");
312
+ let observed: boolean | undefined;
313
+ (m1.run as ReturnType<typeof mock>).mockImplementation(
314
+ (_dir: string, ctx?: { isNewWorkspace: boolean }) => {
315
+ observed = ctx?.isNewWorkspace;
316
+ },
317
+ );
318
+
319
+ await runWorkspaceMigrations(WORKSPACE_DIR, [m1]);
320
+
321
+ // The migration saw the new-workspace flag.
322
+ expect(observed).toBe(true);
323
+
324
+ // The first persisted checkpoint (m1's "started" save) carries the flag.
325
+ const firstSave = JSON.parse(
326
+ (writeFileSyncFn.mock.calls[0] as unknown[])[1] as string,
327
+ );
328
+ expect(firstSave.isNewWorkspace).toBe(true);
329
+
330
+ // The final persisted checkpoint clears the flag so subsequent boots
331
+ // treat this workspace as an upgrade.
332
+ const finalSave = JSON.parse(
333
+ (writeFileSyncFn.mock.calls.at(-1) as unknown[])[1] as string,
334
+ );
335
+ expect(finalSave.isNewWorkspace).toBe(false);
336
+ });
337
+
338
+ test("preserves isNewWorkspace=true across a crash before seeding migrations run", async () => {
339
+ // Simulate a crash mid-first-boot: an earlier migration recorded its
340
+ // "started" marker (writing the checkpoint file) and the daemon then
341
+ // died before reaching the seeding migration. The persisted flag must
342
+ // survive the reboot so the seeding migration still observes the
343
+ // brand-new workspace.
344
+ mockCheckpointContents = JSON.stringify({
345
+ isNewWorkspace: true,
346
+ applied: {
347
+ "001": { appliedAt: "2025-01-01T00:00:00.000Z", status: "started" },
348
+ },
349
+ });
350
+
351
+ const seedingMigration = makeMigration("seed");
352
+ let observed: boolean | undefined;
353
+ (seedingMigration.run as ReturnType<typeof mock>).mockImplementation(
354
+ (_dir: string, ctx?: { isNewWorkspace: boolean }) => {
355
+ observed = ctx?.isNewWorkspace;
356
+ },
357
+ );
358
+
359
+ const m1 = makeMigration("001");
360
+ await runWorkspaceMigrations(WORKSPACE_DIR, [m1, seedingMigration]);
361
+
362
+ expect(m1.run).toHaveBeenCalledTimes(1);
363
+ expect(seedingMigration.run).toHaveBeenCalledTimes(1);
364
+ expect(observed).toBe(true);
365
+ });
366
+
367
+ test("treats pre-existing checkpoint without isNewWorkspace field as upgrade", async () => {
368
+ // Workspaces created before this field was introduced have a checkpoint
369
+ // file with only `applied`. They must not be re-seeded.
370
+ mockCheckpointContents = JSON.stringify({
371
+ applied: {
372
+ "001": { appliedAt: "2025-01-01T00:00:00.000Z", status: "completed" },
373
+ },
374
+ });
375
+
376
+ const seedingMigration = makeMigration("seed");
377
+ let observed: boolean | undefined;
378
+ (seedingMigration.run as ReturnType<typeof mock>).mockImplementation(
379
+ (_dir: string, ctx?: { isNewWorkspace: boolean }) => {
380
+ observed = ctx?.isNewWorkspace;
381
+ },
382
+ );
383
+
384
+ await runWorkspaceMigrations(WORKSPACE_DIR, [seedingMigration]);
385
+
386
+ expect(observed).toBe(false);
387
+ });
388
+
281
389
  test("warns on malformed checkpoint file", async () => {
282
390
  mockCheckpointContents = "not valid json";
283
391
 
@@ -73,7 +73,7 @@ function installHintFor(command: string): string {
73
73
  */
74
74
  function findAgentBinary(agent: AcpAgentConfig): string | null {
75
75
  const PATH = agent.env?.PATH ?? process.env.PATH;
76
- return Bun.which(agent.command, PATH ? { PATH } : undefined);
76
+ return Bun.which(agent.command, PATH != null ? { PATH } : undefined);
77
77
  }
78
78
 
79
79
  /**
@@ -21,6 +21,11 @@ const MAX_DIMENSION = 1568;
21
21
  // Threshold below which we skip optimization — small images don't need it.
22
22
  const OPTIMIZE_THRESHOLD_BYTES = 300 * 1024; // 300 KB
23
23
 
24
+ // Anthropic rejects any single image whose source payload exceeds 5 MB,
25
+ // regardless of pixel dimensions. Cap at ~3.5 MB raw so the base64-encoded
26
+ // form (raw * 4/3) stays comfortably under 5 MB even after re-encoding.
27
+ const MAX_TRANSPORT_BYTES = Math.floor(3.5 * 1024 * 1024); // ~3.5 MB raw
28
+
24
29
  const JPEG_QUALITY = 80;
25
30
 
26
31
  // Content-addressed disk cache to avoid re-running sips on the same image.
@@ -130,10 +135,13 @@ function runSips(inputBytes: Buffer): Buffer | null {
130
135
  /**
131
136
  * Decide whether an image needs to be rescaled before sending.
132
137
  *
133
- * Anthropic rejects many-image requests when any image exceeds 2000 px on a
134
- * side, so dimensions — not file size are the authoritative gate. A sparse
135
- * screenshot can be under 300 KB while still being 3000+ px wide, which the
136
- * byte-size heuristic alone would let slip through.
138
+ * Two independent gates apply:
139
+ * 1. Pixel dimensions — Anthropic rejects many-image requests when any
140
+ * image exceeds 2000 px on a side. A sparse screenshot can be under
141
+ * 300 KB while still being 3000+ px wide.
142
+ * 2. Byte size — Anthropic rejects any image whose source payload
143
+ * exceeds 5 MB. A 1500×1500 high-color screenshot can produce a >5 MB
144
+ * payload while staying well under the dimension cap.
137
145
  *
138
146
  * Exported for unit testing.
139
147
  */
@@ -141,8 +149,8 @@ export function shouldRescaleImage(
141
149
  dims: { width: number; height: number } | null,
142
150
  byteLength: number,
143
151
  ): boolean {
152
+ if (byteLength > MAX_TRANSPORT_BYTES) return true;
144
153
  if (dims) {
145
- // Dimensions known — they are the authoritative check.
146
154
  return dims.width > MAX_DIMENSION || dims.height > MAX_DIMENSION;
147
155
  }
148
156
  // Dimensions unparseable — fall back to file size as a rough proxy.
@@ -318,8 +318,9 @@ export async function startVoiceTurn(
318
318
 
319
319
  // Phone voice has no interactive permission/secret UI, so apply explicit
320
320
  // per-role policies by default. Local live voice opts into the normal
321
- // client approval path instead. Side-effect double-defense is wired
322
- // below at the conversation-configure point.
321
+ // client approval path instead. Side-effect double-defense
322
+ // (forcePromptSideEffects) is wired inside the agent-loop IIFE so it
323
+ // is always paired with cleanup() in the IIFE's finally.
323
324
  const trustClass = opts.trustContext?.trustClass;
324
325
  const isGuardian = trustClass === "guardian";
325
326
  const approvalMode = opts.approvalMode ?? "phone-call";
@@ -391,34 +392,57 @@ export async function startVoiceTurn(
391
392
  }
392
393
  }
393
394
 
394
- // Non-guardian phone voice forces side-effect tools to prompt so the
395
- // auto-deny handler below reliably sees a confirmation_request. Without
396
- // this, a broad allow trust rule (e.g. wildcard bash) would let
397
- // side-effect tools execute without ever emitting an event for the
398
- // auto-deny / scoped-grant handler to intercept.
399
- conversation.forcePromptSideEffects =
400
- !isGuardian && !usesLocalInteractiveApprovals;
401
- conversation.setAssistantId(opts.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID);
402
- conversation.callSessionId = voiceSessionId;
403
- conversation.setTrustContext(opts.trustContext ?? null);
404
- conversation.setCommandIntent(null);
405
- conversation.setTurnChannelContext(turnChannelContext);
406
- conversation.setTurnInterfaceContext?.(turnInterfaceContext);
407
- conversation.setChannelCapabilities(
408
- resolveChannelCapabilities(
409
- turnChannelContext.userMessageChannel,
410
- turnInterfaceContext.userMessageInterface,
411
- ),
412
- );
413
- conversation.setVoiceCallControlPrompt(voiceCallControlPrompt);
395
+ // Hoisted so the catch below can clear partially-applied turn state
396
+ // when a setter or `persistUserMessage` throws otherwise `trustContext`,
397
+ // `callSessionId`, etc. leak into subsequent non-voice turns on the same
398
+ // conversation. The client callback is only reset when this turn actually
399
+ // installed it (tracked via `clientCallbackInstalled`); otherwise cleanup
400
+ // would detach an active sender installed by a prior turn.
401
+ let clientCallbackInstalled = false;
402
+ const cleanup = () => {
403
+ conversation.setChannelCapabilities(null);
404
+ conversation.setTrustContext(null);
405
+ conversation.setCommandIntent(null);
406
+ conversation.setAssistantId("self");
407
+ conversation.setVoiceCallControlPrompt(null);
408
+ conversation.callSessionId = undefined;
409
+ conversation.forcePromptSideEffects = false;
410
+ if (clientCallbackInstalled) {
411
+ // Reset the client callback to a no-op so the stale closure doesn't
412
+ // intercept events from future turns on the same conversation.
413
+ conversation.updateClient(() => {}, true);
414
+ }
415
+ };
414
416
 
415
417
  const requestId = crypto.randomUUID();
416
418
  const turnId = crypto.randomUUID();
417
- const messageId = await conversation.persistUserMessage(
418
- persistedContent,
419
- [],
420
- requestId,
421
- );
419
+ let messageId: string;
420
+ try {
421
+ conversation.setAssistantId(
422
+ opts.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
423
+ );
424
+ conversation.callSessionId = voiceSessionId;
425
+ conversation.setTrustContext(opts.trustContext ?? null);
426
+ conversation.setCommandIntent(null);
427
+ conversation.setTurnChannelContext(turnChannelContext);
428
+ conversation.setTurnInterfaceContext?.(turnInterfaceContext);
429
+ conversation.setChannelCapabilities(
430
+ resolveChannelCapabilities(
431
+ turnChannelContext.userMessageChannel,
432
+ turnInterfaceContext.userMessageInterface,
433
+ ),
434
+ );
435
+ conversation.setVoiceCallControlPrompt(voiceCallControlPrompt);
436
+
437
+ messageId = await conversation.persistUserMessage(
438
+ persistedContent,
439
+ [],
440
+ requestId,
441
+ );
442
+ } catch (err) {
443
+ cleanup();
444
+ throw err;
445
+ }
422
446
  try {
423
447
  opts.callbacks?.persisted_user_message_id?.(messageId);
424
448
  } catch (err) {
@@ -556,25 +580,20 @@ export async function startVoiceTurn(
556
580
  }
557
581
  broadcastMessage(msg);
558
582
  });
583
+ clientCallbackInstalled = true;
559
584
 
560
585
  // Fire-and-forget the agent loop
561
- const cleanup = () => {
562
- // Reset channel capabilities so a subsequent desktop session on the
563
- // same conversation is not incorrectly treated as a voice client.
564
- conversation.setChannelCapabilities(null);
565
- conversation.setTrustContext(null);
566
- conversation.setCommandIntent(null);
567
- conversation.setAssistantId("self");
568
- conversation.setVoiceCallControlPrompt(null);
569
- conversation.callSessionId = undefined;
570
- conversation.forcePromptSideEffects = false;
571
- // Reset the conversation's client callback to a no-op so the stale
572
- // closure doesn't intercept events from future turns on the same conversation.
573
- conversation.updateClient(() => {}, true);
574
- };
575
-
576
586
  void (async () => {
577
587
  try {
588
+ // Non-guardian phone voice forces side-effect tools to prompt so the
589
+ // auto-deny handler above reliably sees a confirmation_request. Without
590
+ // this, a broad allow trust rule (e.g. wildcard bash) would let
591
+ // side-effect tools execute without ever emitting an event for the
592
+ // auto-deny / scoped-grant handler to intercept. Set inside the
593
+ // try/finally so a failed setup before this point cannot leak the
594
+ // flag into subsequent non-voice turns on the same conversation.
595
+ conversation.forcePromptSideEffects =
596
+ !isGuardian && !usesLocalInteractiveApprovals;
578
597
  await conversation.runAgentLoop(
579
598
  persistedContent,
580
599
  messageId,