@vellumai/assistant 0.5.13 → 0.5.15

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 (425) hide show
  1. package/.env.example +1 -6
  2. package/AGENTS.md +4 -0
  3. package/ARCHITECTURE.md +0 -1
  4. package/bunfig.toml +1 -0
  5. package/docs/architecture/memory.md +3 -3
  6. package/openapi.yaml +127 -22
  7. package/package.json +1 -1
  8. package/src/__tests__/access-request-decision.test.ts +2 -32
  9. package/src/__tests__/actor-token-service.test.ts +1 -31
  10. package/src/__tests__/anthropic-provider.test.ts +53 -40
  11. package/src/__tests__/app-git-history.test.ts +9 -17
  12. package/src/__tests__/app-git-service.test.ts +14 -20
  13. package/src/__tests__/app-store-dir-names.test.ts +10 -20
  14. package/src/__tests__/approval-cascade.test.ts +2 -19
  15. package/src/__tests__/approval-primitive.test.ts +2 -27
  16. package/src/__tests__/approval-routes-http.test.ts +2 -30
  17. package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -28
  18. package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -45
  19. package/src/__tests__/attachments-store.test.ts +5 -32
  20. package/src/__tests__/audit-log-rotation.test.ts +5 -36
  21. package/src/__tests__/avatar-e2e.test.ts +1 -9
  22. package/src/__tests__/avatar-generator.test.ts +1 -7
  23. package/src/__tests__/browser-fill-credential.test.ts +0 -4
  24. package/src/__tests__/browser-manager.test.ts +0 -6
  25. package/src/__tests__/call-controller.test.ts +1 -22
  26. package/src/__tests__/call-conversation-messages.test.ts +0 -21
  27. package/src/__tests__/call-domain.test.ts +0 -25
  28. package/src/__tests__/call-pointer-messages.test.ts +0 -21
  29. package/src/__tests__/call-recovery.test.ts +0 -22
  30. package/src/__tests__/call-routes-http.test.ts +0 -24
  31. package/src/__tests__/call-store.test.ts +0 -21
  32. package/src/__tests__/cancel-resolves-conversation-key.test.ts +0 -24
  33. package/src/__tests__/canonical-guardian-store.test.ts +48 -21
  34. package/src/__tests__/channel-approval-routes.test.ts +6 -26
  35. package/src/__tests__/channel-approvals.test.ts +1 -38
  36. package/src/__tests__/channel-delivery-store.test.ts +0 -21
  37. package/src/__tests__/channel-guardian.test.ts +0 -26
  38. package/src/__tests__/channel-reply-delivery.test.ts +5 -0
  39. package/src/__tests__/channel-retry-sweep.test.ts +0 -21
  40. package/src/__tests__/checker.test.ts +26 -61
  41. package/src/__tests__/clawhub.test.ts +9 -25
  42. package/src/__tests__/cli-command-risk-guard.test.ts +0 -18
  43. package/src/__tests__/config-loader-backfill.test.ts +9 -28
  44. package/src/__tests__/config-schema-cmd.test.ts +5 -25
  45. package/src/__tests__/config-schema.test.ts +21 -40
  46. package/src/__tests__/config-watcher.test.ts +4 -91
  47. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -21
  48. package/src/__tests__/contacts-tools.test.ts +0 -21
  49. package/src/__tests__/context-memory-e2e.test.ts +0 -21
  50. package/src/__tests__/context-window-manager.test.ts +130 -3
  51. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -4
  52. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -4
  53. package/src/__tests__/conversation-agent-loop.test.ts +0 -4
  54. package/src/__tests__/conversation-attachments.test.ts +1 -24
  55. package/src/__tests__/conversation-attention-store.test.ts +0 -21
  56. package/src/__tests__/conversation-attention-telegram.test.ts +0 -22
  57. package/src/__tests__/conversation-clear-safety.test.ts +0 -22
  58. package/src/__tests__/conversation-confirmation-signals.test.ts +2 -21
  59. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -24
  60. package/src/__tests__/conversation-disk-view-integration.test.ts +1 -23
  61. package/src/__tests__/conversation-disk-view.test.ts +5 -27
  62. package/src/__tests__/conversation-error.test.ts +1 -1
  63. package/src/__tests__/conversation-fork-crud.test.ts +1 -33
  64. package/src/__tests__/conversation-fork-route.test.ts +0 -27
  65. package/src/__tests__/conversation-history-web-search.test.ts +23 -16
  66. package/src/__tests__/conversation-init.benchmark.test.ts +22 -43
  67. package/src/__tests__/conversation-key-store-disk-view.test.ts +8 -34
  68. package/src/__tests__/conversation-load-history-repair.test.ts +0 -4
  69. package/src/__tests__/conversation-pre-run-repair.test.ts +0 -4
  70. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -4
  71. package/src/__tests__/conversation-queue.test.ts +8 -8
  72. package/src/__tests__/conversation-routes-disk-view.test.ts +13 -51
  73. package/src/__tests__/conversation-runtime-assembly.test.ts +64 -38
  74. package/src/__tests__/conversation-slash-commands.test.ts +5 -0
  75. package/src/__tests__/conversation-slash-queue.test.ts +0 -4
  76. package/src/__tests__/conversation-slash-unknown.test.ts +0 -4
  77. package/src/__tests__/conversation-speed-override.test.ts +326 -0
  78. package/src/__tests__/conversation-starter-routes.test.ts +0 -23
  79. package/src/__tests__/conversation-store.test.ts +0 -21
  80. package/src/__tests__/conversation-unread-route.test.ts +0 -24
  81. package/src/__tests__/conversation-usage.test.ts +56 -21
  82. package/src/__tests__/conversation-wipe.test.ts +0 -21
  83. package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -4
  84. package/src/__tests__/conversation-workspace-injection.test.ts +0 -4
  85. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -4
  86. package/src/__tests__/credential-execution-shell-lockdown.test.ts +8 -5
  87. package/src/__tests__/credential-vault-unit.test.ts +9 -428
  88. package/src/__tests__/credentials-cli.test.ts +10 -10
  89. package/src/__tests__/daemon-assistant-events.test.ts +0 -19
  90. package/src/__tests__/date-context.test.ts +77 -97
  91. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +7 -24
  92. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +29 -42
  93. package/src/__tests__/delete-managed-skill-tool.test.ts +2 -10
  94. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -26
  95. package/src/__tests__/docker-signing-key-bootstrap.test.ts +61 -15
  96. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +2 -36
  97. package/src/__tests__/email-cli.test.ts +6 -6
  98. package/src/__tests__/ephemeral-permissions.test.ts +5 -17
  99. package/src/__tests__/first-greeting.test.ts +4 -32
  100. package/src/__tests__/followup-tools.test.ts +0 -21
  101. package/src/__tests__/gateway-only-enforcement.test.ts +0 -20
  102. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -23
  103. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -23
  104. package/src/__tests__/guardian-action-followup-store.test.ts +0 -21
  105. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -21
  106. package/src/__tests__/guardian-action-late-reply.test.ts +0 -21
  107. package/src/__tests__/guardian-action-store.test.ts +0 -21
  108. package/src/__tests__/guardian-action-sweep.test.ts +0 -21
  109. package/src/__tests__/guardian-binding-drift-heal.test.ts +0 -23
  110. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +172 -22
  111. package/src/__tests__/guardian-dispatch.test.ts +0 -21
  112. package/src/__tests__/guardian-grant-minting.test.ts +0 -22
  113. package/src/__tests__/guardian-outbound-http.test.ts +0 -22
  114. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +0 -23
  115. package/src/__tests__/guardian-routing-invariants.test.ts +0 -22
  116. package/src/__tests__/guardian-routing-state.test.ts +0 -22
  117. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -24
  118. package/src/__tests__/headless-browser-interactions.test.ts +0 -4
  119. package/src/__tests__/headless-browser-navigate.test.ts +0 -4
  120. package/src/__tests__/headless-browser-read-tools.test.ts +0 -4
  121. package/src/__tests__/headless-browser-snapshot.test.ts +0 -4
  122. package/src/__tests__/heartbeat-service.test.ts +99 -26
  123. package/src/__tests__/hooks-blocking.test.ts +3 -3
  124. package/src/__tests__/hooks-config.test.ts +7 -7
  125. package/src/__tests__/hooks-discovery.test.ts +3 -3
  126. package/src/__tests__/hooks-integration.test.ts +5 -5
  127. package/src/__tests__/hooks-manager.test.ts +3 -3
  128. package/src/__tests__/hooks-runner.test.ts +5 -23
  129. package/src/__tests__/hooks-settings.test.ts +3 -3
  130. package/src/__tests__/hooks-templates.test.ts +3 -3
  131. package/src/__tests__/http-conversation-lineage.test.ts +0 -27
  132. package/src/__tests__/identity-intro-cache.test.ts +0 -4
  133. package/src/__tests__/inbound-invite-redemption.test.ts +0 -22
  134. package/src/__tests__/inline-skill-load-permissions.test.ts +5 -16
  135. package/src/__tests__/intent-routing.test.ts +2 -55
  136. package/src/__tests__/invite-redemption-service.test.ts +0 -21
  137. package/src/__tests__/invite-routes-http.test.ts +0 -21
  138. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +0 -17
  139. package/src/__tests__/journal-context.test.ts +8 -75
  140. package/src/__tests__/list-messages-attachments.test.ts +0 -22
  141. package/src/__tests__/llm-context-route-provider.test.ts +0 -21
  142. package/src/__tests__/llm-request-log-turn-query.test.ts +46 -28
  143. package/src/__tests__/llm-usage-store.test.ts +0 -21
  144. package/src/__tests__/log-export-workspace.test.ts +1 -1
  145. package/src/__tests__/managed-skill-lifecycle.test.ts +1 -1
  146. package/src/__tests__/managed-store.test.ts +1 -1
  147. package/src/__tests__/mcp-cli.test.ts +7 -10
  148. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -21
  149. package/src/__tests__/memory-jobs-worker-backoff.test.ts +0 -11
  150. package/src/__tests__/memory-lifecycle-e2e.test.ts +0 -21
  151. package/src/__tests__/memory-recall-log-store.test.ts +0 -27
  152. package/src/__tests__/memory-recall-quality.test.ts +0 -21
  153. package/src/__tests__/memory-regressions.experimental.test.ts +31 -30
  154. package/src/__tests__/memory-regressions.test.ts +282 -70
  155. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -21
  156. package/src/__tests__/memory-upsert-concurrency.test.ts +0 -21
  157. package/src/__tests__/messaging-send-tool.test.ts +201 -0
  158. package/src/__tests__/migration-cross-version-compatibility.test.ts +18 -13
  159. package/src/__tests__/migration-export-http.test.ts +7 -1
  160. package/src/__tests__/migration-import-commit-http.test.ts +16 -14
  161. package/src/__tests__/migration-import-preflight-http.test.ts +27 -44
  162. package/src/__tests__/migration-validate-http.test.ts +1 -28
  163. package/src/__tests__/native-web-search.test.ts +25 -22
  164. package/src/__tests__/non-member-access-request.test.ts +0 -22
  165. package/src/__tests__/notification-guardian-path.test.ts +0 -21
  166. package/src/__tests__/notification-schedule-dedup.test.ts +1 -25
  167. package/src/__tests__/oauth-apps-routes.test.ts +103 -2
  168. package/src/__tests__/oauth-cli.test.ts +52 -0
  169. package/src/__tests__/oauth-provider-profiles.test.ts +0 -16
  170. package/src/__tests__/oauth-provider-serializer.test.ts +232 -0
  171. package/src/__tests__/oauth-providers-routes.test.ts +257 -0
  172. package/src/__tests__/oauth-store.test.ts +0 -21
  173. package/src/__tests__/onboarding-template-contract.test.ts +2 -2
  174. package/src/__tests__/openai-provider.test.ts +261 -0
  175. package/src/__tests__/pairing-concurrent.test.ts +6 -6
  176. package/src/__tests__/pairing-routes.test.ts +7 -1
  177. package/src/__tests__/path-policy.test.ts +1 -1
  178. package/src/__tests__/platform.test.ts +64 -88
  179. package/src/__tests__/playbook-execution.test.ts +0 -21
  180. package/src/__tests__/playbook-tools.test.ts +0 -21
  181. package/src/__tests__/pricing.test.ts +100 -0
  182. package/src/__tests__/relay-server.test.ts +1 -25
  183. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -24
  184. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -24
  185. package/src/__tests__/runtime-events-sse.test.ts +0 -24
  186. package/src/__tests__/sandbox-diagnostics.test.ts +2 -1
  187. package/src/__tests__/scaffold-managed-skill-tool.test.ts +1 -1
  188. package/src/__tests__/schedule-store.test.ts +0 -21
  189. package/src/__tests__/schedule-tools.test.ts +0 -21
  190. package/src/__tests__/scheduler-recurrence.test.ts +0 -21
  191. package/src/__tests__/scoped-approval-grants.test.ts +0 -21
  192. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -21
  193. package/src/__tests__/secret-allowlist.test.ts +1 -1
  194. package/src/__tests__/secret-ingress-channel.test.ts +0 -5
  195. package/src/__tests__/secret-ingress-cli.test.ts +0 -6
  196. package/src/__tests__/secret-ingress-http.test.ts +0 -5
  197. package/src/__tests__/secret-ingress.test.ts +0 -5
  198. package/src/__tests__/send-endpoint-busy.test.ts +0 -24
  199. package/src/__tests__/sequence-store.test.ts +0 -21
  200. package/src/__tests__/server-history-render.test.ts +0 -24
  201. package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -4
  202. package/src/__tests__/skill-load-inline-command.test.ts +9 -0
  203. package/src/__tests__/skill-load-inline-includes.test.ts +9 -0
  204. package/src/__tests__/skill-load-tool.test.ts +11 -0
  205. package/src/__tests__/skills-uninstall.test.ts +10 -8
  206. package/src/__tests__/skills.test.ts +1 -1
  207. package/src/__tests__/slack-channel-config.test.ts +1 -1
  208. package/src/__tests__/slack-inbound-verification.test.ts +0 -22
  209. package/src/__tests__/starter-bundle.test.ts +4 -1
  210. package/src/__tests__/suggestion-routes.test.ts +2 -0
  211. package/src/__tests__/system-prompt.test.ts +1 -1
  212. package/src/__tests__/terminal-tools.test.ts +1 -1
  213. package/src/__tests__/test-preload.ts +31 -0
  214. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -1
  215. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  216. package/src/__tests__/tool-executor.test.ts +0 -20
  217. package/src/__tests__/tool-input-summary.test.ts +124 -0
  218. package/src/__tests__/tool-preview-lifecycle.test.ts +2 -1
  219. package/src/__tests__/trust-store.test.ts +7 -1
  220. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -1
  221. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +1 -1
  222. package/src/__tests__/trusted-contact-multichannel.test.ts +1 -1
  223. package/src/__tests__/trusted-contact-verification.test.ts +1 -1
  224. package/src/__tests__/turn-boundary-resolution.test.ts +1 -1
  225. package/src/__tests__/twilio-routes.test.ts +1 -1
  226. package/src/__tests__/update-bulletin.test.ts +1 -1
  227. package/src/__tests__/vbundle-pax-and-symlink.test.ts +1 -1
  228. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -0
  229. package/src/__tests__/voice-scoped-grant-consumer.test.ts +1 -1
  230. package/src/__tests__/voice-session-bridge.test.ts +1 -1
  231. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +4 -4
  232. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +1 -1
  233. package/src/__tests__/workspace-migration-down-functions.test.ts +15 -3
  234. package/src/__tests__/workspace-migration-seed-device-id.test.ts +40 -4
  235. package/src/agent/loop.ts +6 -9
  236. package/src/approvals/guardian-decision-primitive.ts +46 -18
  237. package/src/approvals/guardian-request-resolvers.ts +19 -2
  238. package/src/calls/active-call-lease.ts +2 -2
  239. package/src/cli/AGENTS.md +1 -1
  240. package/src/cli/commands/doctor.ts +9 -9
  241. package/src/cli/commands/memory.ts +142 -0
  242. package/src/cli/commands/oauth/__tests__/connect.test.ts +13 -11
  243. package/src/cli/commands/oauth/__tests__/ping.test.ts +1 -1
  244. package/src/cli/commands/oauth/connect.ts +13 -12
  245. package/src/cli/commands/oauth/index.ts +1 -1
  246. package/src/cli/commands/oauth/providers.ts +47 -62
  247. package/src/cli/commands/platform/__tests__/connect.test.ts +72 -46
  248. package/src/cli/commands/platform/__tests__/disconnect.test.ts +54 -1
  249. package/src/cli/commands/platform/__tests__/status.test.ts +36 -0
  250. package/src/cli/commands/platform/connect.ts +17 -7
  251. package/src/cli/commands/platform/disconnect.ts +28 -3
  252. package/src/cli/commands/platform/index.ts +3 -3
  253. package/src/cli.ts +1 -299
  254. package/src/config/assistant-feature-flags.ts +23 -15
  255. package/src/config/bundled-skills/app-builder/TOOLS.json +16 -0
  256. package/src/config/bundled-skills/app-builder/tools/app-create.ts +4 -0
  257. package/src/config/bundled-skills/app-builder/tools/app-delete.ts +5 -1
  258. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +9 -1
  259. package/src/config/bundled-skills/app-builder/tools/app-refresh.ts +5 -1
  260. package/src/config/bundled-skills/contacts/TOOLS.json +8 -0
  261. package/src/config/bundled-skills/contacts/tools/contact-search.ts +10 -1
  262. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +16 -2
  263. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +1 -0
  264. package/src/config/bundled-skills/messaging/SKILL.md +7 -7
  265. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +37 -0
  266. package/src/config/bundled-skills/slack/SKILL.md +18 -0
  267. package/src/config/env-registry.ts +15 -11
  268. package/src/config/env.ts +1 -11
  269. package/src/config/feature-flag-registry.json +16 -0
  270. package/src/config/schema.ts +4 -0
  271. package/src/config/schemas/heartbeat.ts +6 -1
  272. package/src/config/schemas/inference.ts +14 -3
  273. package/src/config/schemas/memory-processing.ts +16 -8
  274. package/src/config/schemas/memory-retrieval.ts +3 -3
  275. package/src/config/skills.ts +1 -1
  276. package/src/context/window-manager.ts +174 -51
  277. package/src/credential-execution/executable-discovery.ts +2 -2
  278. package/src/daemon/approved-devices-store.ts +2 -2
  279. package/src/daemon/assistant-attachments.ts +2 -0
  280. package/src/daemon/config-watcher.ts +4 -50
  281. package/src/daemon/conversation-agent-loop-handlers.ts +9 -1
  282. package/src/daemon/conversation-agent-loop.ts +12 -0
  283. package/src/daemon/conversation-error.ts +3 -5
  284. package/src/daemon/conversation-history.ts +7 -3
  285. package/src/daemon/conversation-lifecycle.ts +16 -0
  286. package/src/daemon/conversation-messaging.ts +1 -0
  287. package/src/daemon/conversation-notifiers.ts +67 -30
  288. package/src/daemon/conversation-process.ts +161 -2
  289. package/src/daemon/conversation-queue-manager.ts +2 -0
  290. package/src/daemon/conversation-runtime-assembly.ts +33 -11
  291. package/src/daemon/conversation-slash.ts +14 -3
  292. package/src/daemon/conversation-tool-setup.ts +2 -0
  293. package/src/daemon/conversation-usage.ts +32 -4
  294. package/src/daemon/conversation.ts +33 -1
  295. package/src/daemon/daemon-control.ts +32 -16
  296. package/src/daemon/date-context.ts +47 -45
  297. package/src/daemon/dictation-profile-store.ts +2 -2
  298. package/src/daemon/handlers/conversations.ts +19 -0
  299. package/src/daemon/handlers/shared.ts +14 -21
  300. package/src/daemon/lifecycle.ts +5 -7
  301. package/src/daemon/message-types/conversations.ts +2 -0
  302. package/src/daemon/message-types/guardian-actions.ts +3 -17
  303. package/src/daemon/message-types/integrations.ts +11 -1
  304. package/src/daemon/message-types/messages.ts +1 -0
  305. package/src/daemon/pairing-store.ts +2 -79
  306. package/src/daemon/server.ts +154 -8
  307. package/src/daemon/watch-handler.ts +65 -21
  308. package/src/email/guardrails.ts +3 -3
  309. package/src/heartbeat/heartbeat-service.ts +14 -7
  310. package/src/hooks/cli.ts +2 -2
  311. package/src/hooks/config.ts +2 -2
  312. package/src/hooks/discovery.ts +2 -2
  313. package/src/hooks/manager.ts +2 -2
  314. package/src/hooks/runner.ts +5 -2
  315. package/src/hooks/templates.ts +2 -2
  316. package/src/memory/admin.ts +181 -2
  317. package/src/memory/app-git-service.ts +61 -4
  318. package/src/memory/attachments-store.ts +2 -0
  319. package/src/memory/canonical-guardian-store.ts +16 -0
  320. package/src/memory/db-init.ts +8 -0
  321. package/src/memory/embedding-local.ts +5 -2
  322. package/src/memory/indexer.ts +44 -26
  323. package/src/memory/items-extractor.ts +34 -82
  324. package/src/memory/job-handlers/batch-extraction.ts +741 -0
  325. package/src/memory/job-handlers/journal-carry-forward.test.ts +383 -0
  326. package/src/memory/job-handlers/journal-carry-forward.ts +255 -0
  327. package/src/memory/jobs-store.ts +28 -0
  328. package/src/memory/jobs-worker.ts +56 -9
  329. package/src/memory/lifecycle-events-store.ts +4 -2
  330. package/src/memory/llm-request-log-store.ts +40 -2
  331. package/src/memory/llm-usage-store.ts +4 -3
  332. package/src/memory/migrations/199-guardian-request-enrichment-columns.ts +71 -0
  333. package/src/memory/migrations/200-usage-llm-call-count.ts +20 -0
  334. package/src/memory/migrations/index.ts +2 -0
  335. package/src/memory/query-expansion.ts +83 -0
  336. package/src/memory/retriever.test.ts +119 -0
  337. package/src/memory/retriever.ts +513 -105
  338. package/src/memory/schema/guardian.ts +4 -0
  339. package/src/memory/schema/infrastructure.ts +1 -0
  340. package/src/memory/search/formatting.test.ts +140 -0
  341. package/src/memory/search/formatting.ts +143 -198
  342. package/src/memory/search/mmr.ts +136 -0
  343. package/src/memory/search/staleness.ts +0 -15
  344. package/src/memory/search/tier-classifier.ts +10 -21
  345. package/src/memory/search/types.ts +17 -0
  346. package/src/messaging/providers/slack/adapter.ts +51 -5
  347. package/src/notifications/broadcaster.ts +13 -0
  348. package/src/notifications/copy-composer.ts +8 -0
  349. package/src/oauth/connect-orchestrator.ts +1 -1
  350. package/src/oauth/connection-resolver.ts +2 -2
  351. package/src/oauth/provider-serializer.ts +116 -0
  352. package/src/permissions/trust-store.ts +24 -7
  353. package/src/prompts/__tests__/build-cli-reference-section.test.ts +5 -0
  354. package/src/prompts/journal-context.ts +50 -35
  355. package/src/prompts/persona-resolver.ts +1 -1
  356. package/src/prompts/system-prompt.ts +27 -28
  357. package/src/prompts/templates/BOOTSTRAP.md +14 -1
  358. package/src/prompts/templates/HEARTBEAT.md +10 -0
  359. package/src/prompts/templates/NOW.md +19 -25
  360. package/src/prompts/templates/SOUL.md +13 -1
  361. package/src/prompts/templates/UPDATES.md +12 -0
  362. package/src/prompts/update-bulletin.ts +1 -1
  363. package/src/providers/anthropic/client.ts +89 -18
  364. package/src/providers/model-catalog.ts +22 -2
  365. package/src/providers/model-intents.ts +2 -2
  366. package/src/providers/openai/client.ts +40 -1
  367. package/src/providers/retry.ts +23 -4
  368. package/src/providers/types.ts +2 -0
  369. package/src/runtime/assistant-scope.ts +1 -1
  370. package/src/runtime/auth/__tests__/credential-service.test.ts +1 -0
  371. package/src/runtime/auth/route-policy.ts +1 -0
  372. package/src/runtime/auth/token-service.ts +51 -29
  373. package/src/runtime/confirmation-request-guardian-bridge.ts +3 -1
  374. package/src/runtime/guardian-decision-types.ts +16 -10
  375. package/src/runtime/http-server.ts +3 -14
  376. package/src/runtime/http-types.ts +1 -0
  377. package/src/runtime/migrations/vbundle-builder.ts +7 -4
  378. package/src/runtime/migrations/vbundle-import-analyzer.ts +0 -4
  379. package/src/runtime/migrations/vbundle-importer.ts +1 -1
  380. package/src/runtime/routes/conversation-query-routes.ts +40 -8
  381. package/src/runtime/routes/conversation-routes.ts +125 -3
  382. package/src/runtime/routes/guardian-action-routes.ts +9 -3
  383. package/src/runtime/routes/identity-routes.ts +25 -4
  384. package/src/runtime/routes/llm-context-normalization.ts +1 -0
  385. package/src/runtime/routes/log-export-routes.ts +34 -12
  386. package/src/runtime/routes/migration-routes.ts +6 -10
  387. package/src/runtime/routes/oauth-apps.ts +2 -9
  388. package/src/runtime/routes/oauth-providers.ts +60 -0
  389. package/src/runtime/routes/pairing-routes.ts +0 -8
  390. package/src/runtime/routes/settings-routes.ts +0 -1
  391. package/src/runtime/routes/telemetry-routes.ts +16 -4
  392. package/src/security/encrypted-store.ts +2 -2
  393. package/src/security/secret-allowlist.ts +3 -3
  394. package/src/signals/emit-event.ts +42 -0
  395. package/src/signals/user-message.ts +37 -0
  396. package/src/telemetry/usage-telemetry-reporter.test.ts +83 -19
  397. package/src/telemetry/usage-telemetry-reporter.ts +23 -17
  398. package/src/tools/browser/runtime-check.ts +2 -2
  399. package/src/tools/credentials/vault.ts +2 -249
  400. package/src/tools/memory/definitions.ts +1 -1
  401. package/src/tools/memory/handlers.test.ts +50 -8
  402. package/src/tools/memory/handlers.ts +3 -1
  403. package/src/tools/side-effects.ts +1 -6
  404. package/src/tools/terminal/safe-env.ts +3 -2
  405. package/src/tools/terminal/shell.ts +11 -14
  406. package/src/tools/tool-approval-handler.ts +20 -1
  407. package/src/tools/tool-input-summary.ts +66 -0
  408. package/src/tools/types.ts +4 -0
  409. package/src/usage/types.ts +4 -0
  410. package/src/util/device-id.ts +10 -10
  411. package/src/util/platform.ts +71 -33
  412. package/src/util/pricing.ts +19 -6
  413. package/src/util/strip-comment-lines.ts +28 -0
  414. package/src/workspace/git-service.ts +8 -18
  415. package/src/workspace/migrations/003-seed-device-id.ts +6 -4
  416. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +7 -1
  417. package/src/workspace/migrations/017-seed-persona-dirs.ts +2 -4
  418. package/src/workspace/migrations/021-move-signals-to-workspace.ts +84 -0
  419. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +94 -0
  420. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +86 -0
  421. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +126 -0
  422. package/src/workspace/migrations/migrate-to-workspace-volume.ts +3 -6
  423. package/src/workspace/migrations/registry.ts +8 -0
  424. package/src/signals/confirm.ts +0 -82
  425. package/src/signals/trust-rule.ts +0 -174
@@ -0,0 +1,124 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { summarizeToolInput } from "../tools/tool-input-summary.js";
4
+
5
+ describe("summarizeToolInput", () => {
6
+ test("bash with short command returns full command", () => {
7
+ expect(summarizeToolInput("bash", { command: "ls -la" })).toBe("ls -la");
8
+ });
9
+
10
+ test("bash with long command truncates with ellipsis", () => {
11
+ const longCmd = "a".repeat(200);
12
+ const result = summarizeToolInput("bash", { command: longCmd });
13
+ expect(result.length).toBe(121); // 120 chars + ellipsis
14
+ expect(result.endsWith("…")).toBe(true);
15
+ expect(result.startsWith("a".repeat(120))).toBe(true);
16
+ });
17
+
18
+ test("terminal tool behaves like bash", () => {
19
+ expect(summarizeToolInput("terminal", { command: "echo hello" })).toBe(
20
+ "echo hello",
21
+ );
22
+ });
23
+
24
+ test("file_read returns file path", () => {
25
+ expect(
26
+ summarizeToolInput("file_read", { file_path: "/src/index.ts" }),
27
+ ).toBe("/src/index.ts");
28
+ });
29
+
30
+ test("file_write returns file path from path key", () => {
31
+ expect(summarizeToolInput("file_write", { path: "/src/main.ts" })).toBe(
32
+ "/src/main.ts",
33
+ );
34
+ });
35
+
36
+ test("file_edit prefers file_path over path", () => {
37
+ expect(
38
+ summarizeToolInput("file_edit", {
39
+ file_path: "/preferred.ts",
40
+ path: "/fallback.ts",
41
+ }),
42
+ ).toBe("/preferred.ts");
43
+ });
44
+
45
+ test("web_fetch returns URL truncated to 100 chars", () => {
46
+ const longUrl = `https://example.com/${"x".repeat(200)}`;
47
+ const result = summarizeToolInput("web_fetch", { url: longUrl });
48
+ expect(result.length).toBe(101); // 100 chars + ellipsis
49
+ expect(result.endsWith("…")).toBe(true);
50
+ });
51
+
52
+ test("web_fetch with short URL returns full URL", () => {
53
+ expect(
54
+ summarizeToolInput("web_fetch", { url: "https://example.com" }),
55
+ ).toBe("https://example.com");
56
+ });
57
+
58
+ test("network_request behaves like web_fetch", () => {
59
+ expect(
60
+ summarizeToolInput("network_request", { url: "https://api.test.com" }),
61
+ ).toBe("https://api.test.com");
62
+ });
63
+
64
+ test("empty input returns empty string", () => {
65
+ expect(summarizeToolInput("bash", {})).toBe("");
66
+ expect(summarizeToolInput("file_read", {})).toBe("");
67
+ expect(summarizeToolInput("web_fetch", {})).toBe("");
68
+ expect(summarizeToolInput("unknown_tool", {})).toBe("");
69
+ });
70
+
71
+ test("unknown tool with string input returns first string value", () => {
72
+ expect(
73
+ summarizeToolInput("custom_tool", {
74
+ query: "search for something",
75
+ count: 10,
76
+ }),
77
+ ).toBe("search for something");
78
+ });
79
+
80
+ test("unknown tool with long string truncates to 80 chars", () => {
81
+ const longVal = "b".repeat(150);
82
+ const result = summarizeToolInput("custom_tool", { data: longVal });
83
+ expect(result.length).toBe(81); // 80 chars + ellipsis
84
+ expect(result.endsWith("…")).toBe(true);
85
+ });
86
+
87
+ test("input with no string values returns empty string", () => {
88
+ expect(
89
+ summarizeToolInput("custom_tool", { count: 42, flag: true, obj: {} }),
90
+ ).toBe("");
91
+ });
92
+
93
+ test("whitespace-only string values are treated as empty", () => {
94
+ expect(summarizeToolInput("bash", { command: " " })).toBe("");
95
+ expect(summarizeToolInput("custom_tool", { data: " \n\t " })).toBe("");
96
+ });
97
+
98
+ test("host_bash behaves like bash", () => {
99
+ expect(summarizeToolInput("host_bash", { command: "git status" })).toBe(
100
+ "git status",
101
+ );
102
+ });
103
+
104
+ test("host_file_read behaves like file_read", () => {
105
+ expect(
106
+ summarizeToolInput("host_file_read", { file_path: "/src/index.ts" }),
107
+ ).toBe("/src/index.ts");
108
+ });
109
+
110
+ test("host_file_write behaves like file_write", () => {
111
+ expect(
112
+ summarizeToolInput("host_file_write", { path: "/src/main.ts" }),
113
+ ).toBe("/src/main.ts");
114
+ });
115
+
116
+ test("host_file_edit behaves like file_edit", () => {
117
+ expect(
118
+ summarizeToolInput("host_file_edit", {
119
+ file_path: "/preferred.ts",
120
+ path: "/fallback.ts",
121
+ }),
122
+ ).toBe("/preferred.ts");
123
+ });
124
+ });
@@ -8,12 +8,13 @@
8
8
  * - handleToolResult includes toolUseId in emitted tool_result
9
9
  * - Event ordering: tool_use_preview_start → input_json_delta → tool_use
10
10
  */
11
+ import { join } from "node:path";
11
12
  import { beforeEach, describe, expect, mock, test } from "bun:test";
12
13
 
13
14
  // ── Mock platform (must precede imports that read it) ─────────────────────────
14
15
  mock.module("../util/platform.js", () => ({
15
16
  getSessionTokenPath: () => "/tmp/test-token",
16
- getRootDir: () => "/tmp/test",
17
+ getProtectedDir: () => join("/tmp/test", "protected"),
17
18
  getDataDir: () => "/tmp/test",
18
19
  getWorkspaceDir: () => "/tmp/test/workspace",
19
20
  getWorkspaceSkillsDir: () => "/tmp/test/skills",
@@ -12,9 +12,15 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
12
12
  // Create a temp directory for the trust file
13
13
  const testDir = mkdtempSync(join(tmpdir(), "trust-store-test-"));
14
14
 
15
+ // Point the file-based trust backend at the test temp dir so
16
+ // getGatewaySecurityDir() (which checks this env var first) writes
17
+ // trust.json under the test directory instead of ~/.vellum/protected.
18
+ process.env.GATEWAY_SECURITY_DIR = join(testDir, "protected");
19
+
15
20
  // Mock platform module so trust-store writes to temp dir instead of ~/.vellum
16
21
  mock.module("../util/platform.js", () => ({
17
- getRootDir: () => testDir,
22
+ getProtectedDir: () => join(testDir, "protected"),
23
+ getWorkspaceDir: () => join(testDir, "workspace"),
18
24
  getDataDir: () => testDir,
19
25
  isMacOS: () => process.platform === "darwin",
20
26
  isLinux: () => process.platform === "linux",
@@ -30,7 +30,7 @@ const testDir = mkdtempSync(join(tmpdir(), "tc-inline-approval-integration-"));
30
30
 
31
31
  mock.module("../util/platform.js", () => ({
32
32
  getDataDir: () => testDir,
33
- getRootDir: () => testDir,
33
+ getProtectedDir: () => join(testDir, "protected"),
34
34
  isMacOS: () => process.platform === "darwin",
35
35
  isLinux: () => process.platform === "linux",
36
36
  isWindows: () => process.platform === "win32",
@@ -23,7 +23,7 @@ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
23
23
  const testDir = mkdtempSync(join(tmpdir(), "trusted-contact-lifecycle-notif-"));
24
24
 
25
25
  mock.module("../util/platform.js", () => ({
26
- getRootDir: () => testDir,
26
+ getProtectedDir: () => join(testDir, "protected"),
27
27
  getDataDir: () => testDir,
28
28
  isMacOS: () => process.platform === "darwin",
29
29
  isLinux: () => process.platform === "linux",
@@ -18,7 +18,7 @@ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
18
18
  const testDir = mkdtempSync(join(tmpdir(), "trusted-contact-multichannel-"));
19
19
 
20
20
  mock.module("../util/platform.js", () => ({
21
- getRootDir: () => testDir,
21
+ getProtectedDir: () => join(testDir, "protected"),
22
22
  getDataDir: () => testDir,
23
23
  isMacOS: () => process.platform === "darwin",
24
24
  isLinux: () => process.platform === "linux",
@@ -21,7 +21,7 @@ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
21
21
  const testDir = mkdtempSync(join(tmpdir(), "trusted-contact-verify-test-"));
22
22
 
23
23
  mock.module("../util/platform.js", () => ({
24
- getRootDir: () => testDir,
24
+ getProtectedDir: () => join(testDir, "protected"),
25
25
  getDataDir: () => testDir,
26
26
  isMacOS: () => process.platform === "darwin",
27
27
  isLinux: () => process.platform === "linux",
@@ -10,7 +10,7 @@ const workspaceDir = join(testDir, ".vellum", "workspace");
10
10
  const conversationsDir = join(workspaceDir, "conversations");
11
11
 
12
12
  mock.module("../util/platform.js", () => ({
13
- getRootDir: () => join(testDir, ".vellum"),
13
+ getProtectedDir: () => join(join(testDir, ".vellum"), "protected"),
14
14
  getDataDir: () => join(workspaceDir, "data"),
15
15
  getWorkspaceDir: () => workspaceDir,
16
16
  getConversationsDir: () => conversationsDir,
@@ -89,7 +89,7 @@ const testDir = realpathSync(
89
89
  );
90
90
 
91
91
  mock.module("../util/platform.js", () => ({
92
- getRootDir: () => testDir,
92
+ getProtectedDir: () => join(testDir, "protected"),
93
93
  getDataDir: () => testDir,
94
94
  isMacOS: () => process.platform === "darwin",
95
95
  isLinux: () => process.platform === "linux",
@@ -44,7 +44,7 @@ let tempTemplateDir: string;
44
44
  mock.module("../util/platform.js", () => ({
45
45
  getWorkspacePromptPath: mock((file: string) => join(tempDir, file)),
46
46
  getWorkspaceDir: () => tempDir,
47
- getRootDir: () => tempDir,
47
+ getProtectedDir: () => join(tempDir, "protected"),
48
48
  getDataDir: () => join(tempDir, "data"),
49
49
  getPlatformName: () => "darwin",
50
50
  isMacOS: () => false,
@@ -23,7 +23,7 @@ const testDir = realpathSync(
23
23
  );
24
24
 
25
25
  mock.module("../util/platform.js", () => ({
26
- getRootDir: () => testDir,
26
+ getProtectedDir: () => join(testDir, "protected"),
27
27
  getDataDir: () => testDir,
28
28
  isMacOS: () => process.platform === "darwin",
29
29
  isLinux: () => process.platform === "linux",
@@ -42,6 +42,7 @@ const SKILL_SRC_DIR = join(
42
42
 
43
43
  const platformOverrides: Record<string, (...args: unknown[]) => unknown> = {
44
44
  getRootDir: () => TEST_DIR,
45
+ getProtectedDir: () => join(TEST_DIR, "protected"),
45
46
  getDataDir: () => join(TEST_DIR, "data"),
46
47
  ensureDataDir: () => {},
47
48
  getPidPath: () => join(TEST_DIR, "vellum.pid"),
@@ -28,7 +28,7 @@ const testDir = mkdtempSync(
28
28
  // ── Platform + logger mocks (must come before any source imports) ────
29
29
 
30
30
  mock.module("../util/platform.js", () => ({
31
- getRootDir: () => testDir,
31
+ getProtectedDir: () => join(testDir, "protected"),
32
32
  getDataDir: () => testDir,
33
33
  isMacOS: () => process.platform === "darwin",
34
34
  isLinux: () => process.platform === "linux",
@@ -21,7 +21,7 @@ let mockedConfig: {
21
21
  };
22
22
 
23
23
  mock.module("../util/platform.js", () => ({
24
- getRootDir: () => testDir,
24
+ getProtectedDir: () => join(testDir, "protected"),
25
25
  getDataDir: () => testDir,
26
26
  isMacOS: () => process.platform === "darwin",
27
27
  isLinux: () => process.platform === "linux",
@@ -31,7 +31,7 @@ mock.module("../util/platform.js", () => ({
31
31
  getDbPath: () => join(testDir, "test.db"),
32
32
  getLogPath: () => join(testDir, "test.log"),
33
33
  ensureDataDir: () => {},
34
- getRootDir: () => testDir,
34
+ getProtectedDir: () => join(testDir, "protected"),
35
35
  }));
36
36
 
37
37
  mock.module("../util/logger.js", () => ({
@@ -252,9 +252,9 @@ describe("009-backfill-conversation-disk-view migration", () => {
252
252
 
253
253
  expect(existsSync(expectedNewDirPath)).toBe(false);
254
254
  expect(existsSync(legacyDirPath)).toBe(true);
255
- expect(
256
- JSON.parse(readFileSync(metaPath, "utf-8")).updatedAt,
257
- ).toBe(new Date(conversationUpdatedAt).toISOString());
255
+ expect(JSON.parse(readFileSync(metaPath, "utf-8")).updatedAt).toBe(
256
+ new Date(conversationUpdatedAt).toISOString(),
257
+ );
258
258
  expect(readFileSync(messagesPath, "utf-8").trim().split("\n")).toHaveLength(
259
259
  1,
260
260
  );
@@ -31,7 +31,7 @@ mock.module("../util/platform.js", () => ({
31
31
  getDbPath: () => join(testDir, "test.db"),
32
32
  getLogPath: () => join(testDir, "test.log"),
33
33
  ensureDataDir: () => {},
34
- getRootDir: () => testDir,
34
+ getProtectedDir: () => join(testDir, "protected"),
35
35
  }));
36
36
 
37
37
  mock.module("../util/logger.js", () => ({
@@ -35,7 +35,7 @@ mock.module("../security/credential-key.js", () => ({
35
35
  // Mock getRootDir for 016-extract-feature-flags-to-protected
36
36
  let mockRootDir: string = "/tmp/mock-root";
37
37
  mock.module("../util/platform.js", () => ({
38
- getRootDir: () => mockRootDir,
38
+ getProtectedDir: () => join(mockRootDir, "protected"),
39
39
  getDataDir: () => join(mockRootDir, "workspace", "data"),
40
40
  getWorkspaceDir: () => join(mockRootDir, "workspace"),
41
41
  }));
@@ -769,9 +769,21 @@ describe("014-migrate-to-workspace-volume down()", () => {
769
769
  // ---------------------------------------------------------------------------
770
770
 
771
771
  describe("016-extract-feature-flags-to-protected down()", () => {
772
+ let savedBaseDataDir: string | undefined;
773
+
772
774
  beforeEach(() => {
773
- // Set up mock root dir for getRootDir() to point to our temp dir
774
- mockRootDir = freshWorkspace();
775
+ // The migration has an inlined platform helpers that reads BASE_DATA_DIR,
776
+ // so we set that env var so the inlined function resolves to mockRootDir.
777
+ const baseDir = freshWorkspace();
778
+ mockRootDir = join(baseDir, ".vellum");
779
+ mkdirSync(mockRootDir, { recursive: true });
780
+ savedBaseDataDir = process.env.BASE_DATA_DIR;
781
+ process.env.BASE_DATA_DIR = baseDir;
782
+ });
783
+
784
+ afterEach(() => {
785
+ if (savedBaseDataDir === undefined) delete process.env.BASE_DATA_DIR;
786
+ else process.env.BASE_DATA_DIR = savedBaseDataDir;
775
787
  });
776
788
 
777
789
  test("moves feature flags from protected dir back to config.json", () => {
@@ -10,6 +10,7 @@ const writeFileSyncFn = mock(
10
10
  (_path: string, _data: string, _opts?: object) => {},
11
11
  );
12
12
  const mkdirSyncFn = mock((_path: string, _opts?: object) => {});
13
+ const homedirFn = mock((): string => "/mock-home");
13
14
  const getDeviceIdBaseDirFn = mock((): string => "/mock-home");
14
15
 
15
16
  // ---------------------------------------------------------------------------
@@ -23,6 +24,10 @@ mock.module("node:fs", () => ({
23
24
  mkdirSync: mkdirSyncFn,
24
25
  }));
25
26
 
27
+ mock.module("node:os", () => ({
28
+ homedir: homedirFn,
29
+ }));
30
+
26
31
  mock.module("../util/device-id.js", () => ({
27
32
  getDeviceIdBaseDir: getDeviceIdBaseDirFn,
28
33
  }));
@@ -63,6 +68,7 @@ describe("003-seed-device-id migration", () => {
63
68
  readFileSyncFn.mockClear();
64
69
  writeFileSyncFn.mockClear();
65
70
  mkdirSyncFn.mockClear();
71
+ homedirFn.mockReturnValue("/mock-home");
66
72
  getDeviceIdBaseDirFn.mockReturnValue("/mock-home");
67
73
  });
68
74
 
@@ -265,16 +271,17 @@ describe("003-seed-device-id migration", () => {
265
271
  expect(parsed.deviceId).toBe("install-legacy");
266
272
  });
267
273
 
268
- test("respects BASE_DATA_DIR override via getDeviceIdBaseDir", () => {
274
+ test("reads lockfile from homedir even when getDeviceIdBaseDir differs", () => {
269
275
  const customBase = "/custom-base";
270
276
  getDeviceIdBaseDirFn.mockReturnValue(customBase);
277
+ homedirFn.mockReturnValue("/mock-home");
271
278
 
272
- const customLockPath = `${customBase}/.vellum.lock.json`;
273
279
  const customDevicePath = `${customBase}/.vellum/device.json`;
274
280
 
275
- existsSyncFn.mockImplementation((path: string) => path === customLockPath);
281
+ // Lockfile is at homedir, NOT at customBase
282
+ existsSyncFn.mockImplementation((path: string) => path === LOCK_PATH);
276
283
  readFileSyncFn.mockImplementation((path: string, _enc: string) => {
277
- if (path === customLockPath) {
284
+ if (path === LOCK_PATH) {
278
285
  return makeLockfile([
279
286
  {
280
287
  name: "custom",
@@ -294,11 +301,40 @@ describe("003-seed-device-id migration", () => {
294
301
  string,
295
302
  object,
296
303
  ];
304
+ // device.json is written under getDeviceIdBaseDir, not homedir
297
305
  expect(path).toBe(customDevicePath);
298
306
  const parsed = JSON.parse(data);
299
307
  expect(parsed.deviceId).toBe("install-custom");
300
308
  });
301
309
 
310
+ test("ignores lockfile under getDeviceIdBaseDir when it differs from homedir", () => {
311
+ const customBase = "/custom-base";
312
+ getDeviceIdBaseDirFn.mockReturnValue(customBase);
313
+ homedirFn.mockReturnValue("/mock-home");
314
+
315
+ // Only a lockfile under customBase exists — should be ignored since
316
+ // the migration always reads the lockfile from homedir().
317
+ const customLockPath = `${customBase}/.vellum.lock.json`;
318
+ existsSyncFn.mockImplementation((path: string) => path === customLockPath);
319
+ readFileSyncFn.mockImplementation((path: string, _enc: string) => {
320
+ if (path === customLockPath) {
321
+ return makeLockfile([
322
+ {
323
+ name: "custom",
324
+ installationId: "install-custom",
325
+ hatchedAt: "2025-01-01T00:00:00Z",
326
+ },
327
+ ]);
328
+ }
329
+ throw new Error(`ENOENT: ${path}`);
330
+ });
331
+
332
+ seedDeviceIdMigration.run(WORKSPACE_DIR);
333
+
334
+ // No lockfile found at homedir, so no device.json is written
335
+ expect(writeFileSyncFn).not.toHaveBeenCalled();
336
+ });
337
+
302
338
  test("entries without hatchedAt are treated as oldest", () => {
303
339
  setupFs({
304
340
  [LOCK_PATH]: makeLockfile([
package/src/agent/loop.ts CHANGED
@@ -24,6 +24,7 @@ export interface AgentLoopConfig {
24
24
  maxInputTokens?: number; // context window size for tool result truncation
25
25
  thinking?: { enabled: boolean };
26
26
  effort: "low" | "medium" | "high" | "max";
27
+ speed?: "standard" | "fast";
27
28
  toolChoice?:
28
29
  | { type: "auto" }
29
30
  | { type: "any" }
@@ -246,6 +247,10 @@ export class AgentLoop {
246
247
  providerConfig.effort = this.config.effort;
247
248
  }
248
249
 
250
+ if (this.config.speed && this.config.speed !== "standard") {
251
+ providerConfig.speed = this.config.speed;
252
+ }
253
+
249
254
  if (this.config.toolChoice) {
250
255
  providerConfig.tool_choice = this.config.toolChoice;
251
256
  }
@@ -396,7 +401,7 @@ export class AgentLoop {
396
401
  );
397
402
 
398
403
  // Check if the assistant turn contained any visible text (used for
399
- // both the empty-response nudge and the anti-repetition notice).
404
+ // the empty-response nudge).
400
405
  const hasTextBlock = response.content.some(
401
406
  (block) => block.type === "text" && block.text.trim().length > 0,
402
407
  );
@@ -601,14 +606,6 @@ export class AgentLoop {
601
606
  });
602
607
  }
603
608
 
604
- // Remind the LLM not to repeat text it already streamed
605
- if (hasTextBlock) {
606
- resultBlocks.push({
607
- type: "text",
608
- text: '<system_notice>Your previous text was already shown to the user in real time. Do not repeat or rephrase it. Do not narrate retries or internal process chatter ("let me try", "that didn\'t work"). Keep working with tools silently unless you need user input, and only send user-facing text when you have concrete progress or final results.</system_notice>',
609
- });
610
- }
611
-
612
609
  // Add tool results as a user message and continue the loop
613
610
  history.push({ role: "user", content: resultBlocks });
614
611
 
@@ -5,7 +5,8 @@
5
5
  * legacy parser, requester self-cancel) call through this module instead of
6
6
  * inlining the decision-application logic. This centralizes:
7
7
  *
8
- * 1. `approve_always` downgrade for guardian-on-behalf requests
8
+ * 1. `approve_always` downgrade for guardian-on-behalf requests (time-bounded
9
+ * modes like `approve_10m` and `approve_conversation` are preserved)
9
10
  * 2. Identity validation (actor must match assigned guardian)
10
11
  * 3. Approval-info capture before the pending interaction is consumed
11
12
  * 4. Atomic decision application via `handleChannelDecision`
@@ -21,7 +22,9 @@
21
22
  * - Decision authorization is purely principal-based:
22
23
  * actor.guardianPrincipalId === request.guardianPrincipalId (strict equality)
23
24
  * - Decisions are first-response-wins (CAS-like stale protection)
24
- * - `approve_always` is rejected/downgraded for guardian-on-behalf requests
25
+ * - `approve_always` is downgraded to `approve_once` for guardian-on-behalf
26
+ * requests; time-bounded modes (`approve_10m`, `approve_conversation`) are
27
+ * preserved since grants are scoped via `scopeMode: "tool_signature"`
25
28
  * - Scoped grant minting only on explicit approve for requests with tool metadata
26
29
  */
27
30
 
@@ -61,6 +64,28 @@ const log = getLogger("guardian-decision-primitive");
61
64
  /** TTL for scoped approval grants minted on guardian approve_once decisions. */
62
65
  export const GRANT_TTL_MS = 5 * 60 * 1000;
63
66
 
67
+ /** TTL for scoped approval grants minted on guardian approve_10m decisions. */
68
+ export const GRANT_TTL_10M_MS = 10 * 60 * 1000;
69
+
70
+ /**
71
+ * Compute the grant `expiresAt` timestamp for a given approval action.
72
+ *
73
+ * - `approve_10m` → 10 minutes from now
74
+ * - `approve_conversation` → no wall-clock expiry (far-future sentinel;
75
+ * conversation lifecycle manages cleanup)
76
+ * - All others (`approve_once`, etc.) → 5 minutes from now (default)
77
+ */
78
+ export function computeGrantExpiresAt(action: ApprovalAction): number {
79
+ switch (action) {
80
+ case "approve_10m":
81
+ return Date.now() + GRANT_TTL_10M_MS;
82
+ case "approve_conversation":
83
+ return Number.MAX_SAFE_INTEGER;
84
+ default:
85
+ return Date.now() + GRANT_TTL_MS;
86
+ }
87
+ }
88
+
64
89
  // ---------------------------------------------------------------------------
65
90
  // Scoped grant minting
66
91
  // ---------------------------------------------------------------------------
@@ -79,8 +104,9 @@ export function tryMintToolApprovalGrant(params: {
79
104
  approval: GuardianApprovalRequest;
80
105
  decisionChannel: ChannelId;
81
106
  guardianExternalUserId: string;
107
+ effectiveAction: ApprovalAction;
82
108
  }): void {
83
- const { approvalInfo, approval, decisionChannel, guardianExternalUserId } =
109
+ const { approvalInfo, approval, decisionChannel, guardianExternalUserId, effectiveAction } =
84
110
  params;
85
111
 
86
112
  if (!approvalInfo.toolName) {
@@ -116,7 +142,7 @@ export function tryMintToolApprovalGrant(params: {
116
142
  callSessionId: null,
117
143
  guardianExternalUserId,
118
144
  requesterExternalUserId: approval.requesterExternalUserId,
119
- expiresAt: Date.now() + GRANT_TTL_MS,
145
+ expiresAt: computeGrantExpiresAt(effectiveAction),
120
146
  });
121
147
 
122
148
  if (result.ok) {
@@ -166,7 +192,8 @@ export interface ApplyGuardianDecisionParams {
166
192
  * self-cancel paths:
167
193
  *
168
194
  * 1. Downgrade `approve_always` to `approve_once` (guardians cannot
169
- * permanently allowlist tools on behalf of requesters)
195
+ * permanently allowlist tools on behalf of requesters). Time-bounded
196
+ * modes (`approve_10m`, `approve_conversation`) are preserved.
170
197
  * 2. Capture pending approval info before resolution
171
198
  * 3. Apply the decision atomically via `handleChannelDecision`
172
199
  * 4. Update the guardian approval record
@@ -186,11 +213,11 @@ export function applyGuardianDecision(
186
213
  decisionContext,
187
214
  } = params;
188
215
 
189
- // Guardians cannot grant broad allow modes on behalf of requesters -- downgrade.
216
+ // Guardians cannot grant permanent allow modes on behalf of requesters.
217
+ // Time-bounded modes (approve_10m, approve_conversation) are safe because
218
+ // grants are scoped to the tool+input signature via scopeMode: "tool_signature".
190
219
  const effectiveDecision: ApprovalDecisionResult =
191
- decision.action === "approve_always" ||
192
- decision.action === "approve_10m" ||
193
- decision.action === "approve_conversation"
220
+ decision.action === "approve_always"
194
221
  ? { ...decision, action: "approve_once" }
195
222
  : decision;
196
223
 
@@ -240,6 +267,7 @@ export function applyGuardianDecision(
240
267
  approval,
241
268
  decisionChannel: actorChannel,
242
269
  guardianExternalUserId: effectiveGuardianId,
270
+ effectiveAction: effectiveDecision.action,
243
271
  });
244
272
  }
245
273
 
@@ -267,8 +295,9 @@ export function mintCanonicalRequestGrant(params: {
267
295
  request: CanonicalGuardianRequest;
268
296
  actorChannel: string;
269
297
  guardianExternalUserId?: string;
298
+ effectiveAction: ApprovalAction;
270
299
  }): { minted: boolean } {
271
- const { request, actorChannel, guardianExternalUserId } = params;
300
+ const { request, actorChannel, guardianExternalUserId, effectiveAction } = params;
272
301
 
273
302
  if (!request.toolName || !request.inputDigest) {
274
303
  return { minted: false };
@@ -285,7 +314,7 @@ export function mintCanonicalRequestGrant(params: {
285
314
  callSessionId: request.callSessionId ?? null,
286
315
  guardianExternalUserId: guardianExternalUserId ?? null,
287
316
  requesterExternalUserId: request.requesterExternalUserId ?? null,
288
- expiresAt: Date.now() + GRANT_TTL_MS,
317
+ expiresAt: computeGrantExpiresAt(effectiveAction),
289
318
  });
290
319
 
291
320
  if (result.ok) {
@@ -371,7 +400,8 @@ export type CanonicalDecisionResult =
371
400
  * Steps:
372
401
  * 1. Look up the canonical request by ID
373
402
  * 2. Validate: exists, pending status, identity match, valid action
374
- * 3. Downgrade approve_always to approve_once (guardian-on-behalf invariant)
403
+ * 3. Downgrade `approve_always` to `approve_once` (guardian-on-behalf invariant;
404
+ * time-bounded modes like `approve_10m`/`approve_conversation` are preserved)
375
405
  * 4. CAS resolve the canonical request atomically
376
406
  * 5. Dispatch to kind-specific resolver
377
407
  * 6. Mint grant if applicable
@@ -491,13 +521,10 @@ export async function applyCanonicalGuardianDecision(
491
521
  return { applied: false, reason: "expired" };
492
522
  }
493
523
 
494
- // 3. Downgrade approve_always and temporary modes to approve_once for
495
- // guardian-on-behalf requests. Guardians cannot grant broad allow modes
496
- // (permanent, timed, or conversation-scoped) on behalf of requesters.
524
+ // 3. Downgrade approve_always to approve_once for guardian-on-behalf requests.
525
+ // Time-bounded modes (approve_10m, approve_conversation) are permitted.
497
526
  const effectiveAction: ApprovalAction =
498
- action === "approve_always" ||
499
- action === "approve_10m" ||
500
- action === "approve_conversation"
527
+ action === "approve_always"
501
528
  ? "approve_once"
502
529
  : action;
503
530
 
@@ -578,6 +605,7 @@ export async function applyCanonicalGuardianDecision(
578
605
  actorContext.actorExternalUserId ??
579
606
  resolved.guardianExternalUserId ??
580
607
  undefined,
608
+ effectiveAction,
581
609
  });
582
610
  grantMinted = grantResult.minted;
583
611
  }
@@ -24,6 +24,7 @@ import {
24
24
  type NotificationSourceChannel,
25
25
  } from "../notifications/signal.js";
26
26
  import { addRule } from "../permissions/trust-store.js";
27
+ import type { UserDecision } from "../permissions/types.js";
27
28
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
28
29
  import { mintDaemonDeliveryToken } from "../runtime/auth/token-service.js";
29
30
  import type { ApprovalAction } from "../runtime/channel-approval-types.js";
@@ -231,8 +232,24 @@ const pendingInteractionResolver: GuardianRequestResolver = {
231
232
  }
232
233
 
233
234
  // Map action to the permission system's UserDecision type and notify session.
234
- const userDecision =
235
- decision.action === "reject" ? ("deny" as const) : ("allow" as const);
235
+ // Mirrors mapApprovalActionToUserDecision from channel-approvals.ts so
236
+ // temporal modes (approve_10m, approve_conversation) reach the session with
237
+ // the correct UserDecision instead of collapsing to plain "allow".
238
+ let userDecision: UserDecision;
239
+ switch (decision.action) {
240
+ case "reject":
241
+ userDecision = "deny";
242
+ break;
243
+ case "approve_10m":
244
+ userDecision = "allow_10m";
245
+ break;
246
+ case "approve_conversation":
247
+ userDecision = "allow_conversation";
248
+ break;
249
+ default:
250
+ userDecision = "allow";
251
+ break;
252
+ }
236
253
  resolved.conversation!.handleConfirmationResponse(
237
254
  request.id,
238
255
  userDecision,