@vellumai/assistant 0.8.12 → 0.9.0-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (414) hide show
  1. package/AGENTS.md +0 -14
  2. package/ARCHITECTURE.md +45 -45
  3. package/README.md +1 -1
  4. package/bun.lock +200 -154
  5. package/docs/architecture/integrations.md +3 -3
  6. package/docs/architecture/memory.md +2 -2
  7. package/docs/architecture/security.md +10 -10
  8. package/docs/runbook-trusted-contacts.md +12 -12
  9. package/docs/skills.md +6 -6
  10. package/docs/workflows-testing.md +221 -0
  11. package/docs/workflows.md +510 -0
  12. package/examples/plugins/echo/README.md +5 -5
  13. package/knip.json +2 -0
  14. package/openapi.yaml +6935 -5545
  15. package/package.json +8 -4
  16. package/scripts/generate-openapi.ts +66 -114
  17. package/src/__tests__/access-request-seed-content-blocks.test.ts +213 -0
  18. package/src/__tests__/adaptive-thinking-repair.test.ts +32 -3
  19. package/src/__tests__/agent-loop-output-hooks.test.ts +183 -0
  20. package/src/__tests__/agent-loop-regrowth-guard.test.ts +506 -0
  21. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  22. package/src/__tests__/agent-wake-override-profile.test.ts +77 -0
  23. package/src/__tests__/app-dir-path-guard.test.ts +27 -3
  24. package/src/__tests__/approval-cascade.test.ts +0 -5
  25. package/src/__tests__/approval-routes-http.test.ts +91 -0
  26. package/src/__tests__/assistant-stream-state.test.ts +107 -0
  27. package/src/__tests__/browser-fill-credential.test.ts +3 -3
  28. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -1
  29. package/src/__tests__/compaction-events.test.ts +63 -7
  30. package/src/__tests__/compaction-trail-store.test.ts +74 -1
  31. package/src/__tests__/compaction.benchmark.test.ts +63 -41
  32. package/src/__tests__/compactor-low-watermark-cut.test.ts +349 -0
  33. package/src/__tests__/context-window-manager-compact-retry.test.ts +64 -0
  34. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -5
  35. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -5
  36. package/src/__tests__/conversation-history-web-search.test.ts +7 -0
  37. package/src/__tests__/conversation-process-callsite.test.ts +0 -5
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -5
  39. package/src/__tests__/conversation-queue.test.ts +0 -5
  40. package/src/__tests__/conversation-slash-queue.test.ts +0 -5
  41. package/src/__tests__/conversation-slash-unknown.test.ts +0 -5
  42. package/src/__tests__/conversation-speed-override.test.ts +0 -5
  43. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  44. package/src/__tests__/conversation-usage.test.ts +2 -0
  45. package/src/__tests__/conversation-workspace-injection.test.ts +0 -5
  46. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -5
  47. package/src/__tests__/credential-broker-browser-fill.test.ts +2 -2
  48. package/src/__tests__/credential-broker-server-use.test.ts +2 -2
  49. package/src/__tests__/credential-broker.test.ts +1 -1
  50. package/src/__tests__/credential-prompt-route.test.ts +417 -0
  51. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  52. package/src/__tests__/credential-vault.test.ts +37 -0
  53. package/src/__tests__/db-schedule-syntax-migration.test.ts +24 -0
  54. package/src/__tests__/dynamic-page-surface.test.ts +125 -0
  55. package/src/__tests__/empty-state-greeting-cache.test.ts +94 -0
  56. package/src/__tests__/gateway-flag-listener.test.ts +24 -7
  57. package/src/__tests__/guardian-action-sweep.test.ts +56 -219
  58. package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
  59. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  60. package/src/__tests__/list-messages-hidden-metadata.test.ts +99 -0
  61. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +87 -1
  62. package/src/__tests__/llm-resolver.test.ts +115 -0
  63. package/src/__tests__/managed-profile-guard.test.ts +6 -5
  64. package/src/__tests__/max-tokens-continue-hook.test.ts +184 -0
  65. package/src/__tests__/media-generate-image.test.ts +20 -9
  66. package/src/__tests__/model-intents.test.ts +1 -1
  67. package/src/__tests__/normalize-onboarding.test.ts +26 -0
  68. package/src/__tests__/notification-decision-strategy.test.ts +4 -2
  69. package/src/__tests__/notification-telegram-adapter.test.ts +21 -3
  70. package/src/__tests__/pending-interactions-resolved-event.test.ts +62 -0
  71. package/src/__tests__/post-turn-tool-result-truncation.test.ts +72 -18
  72. package/src/__tests__/require-fresh-approval.test.ts +425 -1
  73. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -0
  74. package/src/__tests__/schedule-routes-workflow-validation.test.ts +408 -0
  75. package/src/__tests__/schedule-routes.test.ts +257 -4
  76. package/src/__tests__/schedule-store.test.ts +60 -0
  77. package/src/__tests__/schedule-tools.test.ts +247 -2
  78. package/src/__tests__/skill-execute-input.test.ts +85 -0
  79. package/src/__tests__/skill-secret-handling-guard.test.ts +21 -20
  80. package/src/__tests__/skills.test.ts +3 -3
  81. package/src/__tests__/slack-app-setup-skill-regression.test.ts +1 -1
  82. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  83. package/src/__tests__/subagent-tool-gate-mode.test.ts +547 -0
  84. package/src/__tests__/system-prompt.test.ts +1 -1
  85. package/src/__tests__/task-progress-nudge-hook.test.ts +372 -0
  86. package/src/__tests__/task-scheduler.test.ts +299 -0
  87. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +209 -0
  88. package/src/__tests__/tool-result-spool.test.ts +3 -1
  89. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +181 -0
  90. package/src/__tests__/workspace-migration-103-upgrade-quality-profile-to-opus-4-8.test.ts +174 -0
  91. package/src/agent/compaction-circuit.ts +11 -0
  92. package/src/agent/loop.ts +181 -12
  93. package/src/api/constants/call-sites.ts +12 -0
  94. package/src/api/events/assistant-thinking-delta.ts +10 -0
  95. package/src/api/events/usage-update.ts +7 -0
  96. package/src/api/index.ts +4 -1
  97. package/src/api/responses/memory-v3-selection-log.ts +18 -11
  98. package/src/approvals/approval-primitive.ts +2 -2
  99. package/src/background-wake/background-wake-routes.test.ts +5 -2
  100. package/src/bundler/compiler-tools.ts +1 -1
  101. package/src/calls/call-domain.ts +1 -1
  102. package/src/calls/guardian-action-sweep.ts +16 -93
  103. package/src/cli/AGENTS.md +4 -0
  104. package/src/cli/commands/__tests__/schedules.test.ts +430 -1
  105. package/src/cli/commands/credentials.ts +28 -24
  106. package/src/cli/commands/image-generation.ts +23 -9
  107. package/src/cli/commands/notifications.ts +1 -1
  108. package/src/cli/commands/plugins.ts +89 -46
  109. package/src/cli/commands/schedules.ts +384 -11
  110. package/src/cli/lib/__tests__/inspect-plugin.test.ts +69 -5
  111. package/src/cli/lib/__tests__/install-from-github.test.ts +15 -0
  112. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +81 -4
  113. package/src/cli/lib/inspect-plugin.ts +62 -1
  114. package/src/cli/lib/install-from-github.ts +52 -5
  115. package/src/cli/lib/upgrade-plugin.ts +18 -0
  116. package/src/config/__tests__/workflows-schema.test.ts +60 -0
  117. package/src/config/bundled-skills/acp/SKILL.md +2 -2
  118. package/src/config/bundled-skills/image-studio/SKILL.md +66 -19
  119. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -6
  120. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +22 -3
  121. package/src/config/bundled-skills/personal-page/SKILL.md +57 -0
  122. package/src/config/bundled-skills/personal-page/TOOLS.json +27 -0
  123. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +17 -0
  124. package/src/config/bundled-skills/schedule/SKILL.md +7 -2
  125. package/src/config/bundled-skills/schedule/TOOLS.json +48 -4
  126. package/src/config/bundled-skills/workflows/SKILL.md +214 -0
  127. package/src/config/bundled-skills/workflows/TOOLS.json +84 -0
  128. package/src/config/bundled-skills/workflows/tools/manage-workflows.ts +12 -0
  129. package/src/config/bundled-skills/workflows/tools/run-workflow.ts +12 -0
  130. package/src/config/bundled-tool-registry.ts +14 -2
  131. package/src/config/call-site-defaults.ts +5 -0
  132. package/src/config/feature-flag-registry.json +12 -4
  133. package/src/config/llm-context-resolution.ts +8 -0
  134. package/src/config/llm-resolver.ts +30 -0
  135. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +22 -0
  136. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +16 -0
  137. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +77 -0
  138. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +57 -0
  139. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +28 -0
  140. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +234 -0
  141. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +48 -0
  142. package/src/config/preloaded-apps/personal-page/src/components/media.ts +16 -0
  143. package/src/config/preloaded-apps/personal-page/src/index.html +20 -0
  144. package/src/config/preloaded-apps/personal-page/src/main.tsx +7 -0
  145. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +82 -0
  146. package/src/config/preloaded-apps/personal-page/src/styles.css +759 -0
  147. package/src/config/schema.ts +2 -0
  148. package/src/config/schemas/call-site-catalog.ts +7 -0
  149. package/src/config/schemas/heartbeat.ts +4 -1
  150. package/src/config/schemas/llm.ts +33 -26
  151. package/src/config/schemas/memory-retrospective.ts +19 -0
  152. package/src/config/schemas/platform.ts +8 -0
  153. package/src/config/schemas/services.ts +5 -2
  154. package/src/config/schemas/workflows.ts +42 -0
  155. package/src/config/skills.ts +3 -3
  156. package/src/context/compactor.ts +273 -39
  157. package/src/context/post-turn-tool-result-truncation.ts +23 -6
  158. package/src/context/tool-result-spool.ts +12 -17
  159. package/src/credential-execution/executable-discovery.ts +1 -1
  160. package/src/credential-execution/process-manager.ts +37 -3
  161. package/src/credential-execution/prompted-credential.ts +205 -0
  162. package/src/daemon/conversation-agent-loop-handlers.ts +14 -0
  163. package/src/daemon/conversation-process.ts +11 -2
  164. package/src/daemon/conversation-surfaces.ts +75 -0
  165. package/src/daemon/conversation-tool-setup.ts +103 -26
  166. package/src/daemon/conversation-usage.ts +2 -0
  167. package/src/daemon/conversation.ts +115 -11
  168. package/src/daemon/handlers/shared.ts +26 -14
  169. package/src/daemon/host-cu-proxy.ts +15 -12
  170. package/src/daemon/host-file-proxy.ts +15 -12
  171. package/src/daemon/host-transfer-proxy.ts +30 -24
  172. package/src/daemon/lifecycle.ts +40 -3
  173. package/src/daemon/message-protocol.ts +3 -0
  174. package/src/daemon/message-types/messages.ts +2 -10
  175. package/src/daemon/message-types/workflows.ts +49 -0
  176. package/src/daemon/parse-actual-tokens-from-error.test.ts +62 -1
  177. package/src/daemon/parse-actual-tokens-from-error.ts +43 -4
  178. package/src/daemon/process-message.ts +6 -0
  179. package/src/daemon/tool-setup-types.ts +57 -0
  180. package/src/daemon/wake-conversation-ops.ts +18 -0
  181. package/src/heartbeat/heartbeat-run-store.ts +8 -2
  182. package/src/home/feed-types.ts +1 -1
  183. package/src/ipc/gateway-flag-listener.ts +28 -6
  184. package/src/mcp/mcp-auth-state.ts +8 -20
  185. package/src/media/__tests__/image-models.test.ts +57 -0
  186. package/src/media/image-models.ts +66 -0
  187. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +38 -0
  188. package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +12 -2
  189. package/src/memory/__tests__/memory-retrospective-job.test.ts +911 -34
  190. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +227 -5
  191. package/src/memory/__tests__/memory-retrospective-state.test.ts +195 -0
  192. package/src/memory/__tests__/preloaded-apps.test.ts +85 -0
  193. package/src/memory/auto-analysis-enqueue.ts +14 -1
  194. package/src/memory/compaction-log-store-clickhouse.ts +6 -4
  195. package/src/memory/conversation-crud.ts +9 -2
  196. package/src/memory/conversation-disk-view.ts +1 -1
  197. package/src/memory/conversation-queries.ts +22 -7
  198. package/src/memory/db-init.ts +20 -0
  199. package/src/memory/db-maintenance.ts +16 -0
  200. package/src/memory/embedding-runtime-manager.ts +1 -1
  201. package/src/memory/llm-request-log-source-clickhouse.ts +112 -14
  202. package/src/memory/llm-request-log-source-local.ts +19 -1
  203. package/src/memory/llm-request-log-source.ts +35 -6
  204. package/src/memory/llm-request-log-store.ts +90 -2
  205. package/src/memory/memory-retrospective-constants.ts +9 -0
  206. package/src/memory/memory-retrospective-enqueue.ts +3 -6
  207. package/src/memory/memory-retrospective-fork-boundary.ts +94 -0
  208. package/src/memory/memory-retrospective-job.ts +500 -208
  209. package/src/memory/memory-retrospective-startup-cleanup.ts +97 -19
  210. package/src/memory/memory-retrospective-state.ts +85 -2
  211. package/src/memory/migrations/281-memory-retrospective-remembered-log.ts +40 -0
  212. package/src/memory/migrations/282-schedule-inference-profile.test.ts +77 -0
  213. package/src/memory/migrations/282-schedule-inference-profile.ts +26 -0
  214. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.test.ts +102 -0
  215. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.ts +53 -0
  216. package/src/memory/migrations/284-workflow-runs.ts +51 -0
  217. package/src/memory/migrations/285-schedule-workflow-mode.ts +26 -0
  218. package/src/memory/migrations/286-workflow-run-trust.ts +27 -0
  219. package/src/memory/migrations/287-conversation-origin-channel-index.ts +15 -0
  220. package/src/memory/migrations/288-backfill-origin-channel-from-bindings.ts +43 -0
  221. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +115 -0
  222. package/src/memory/migrations/290-schedule-capabilities.test.ts +77 -0
  223. package/src/memory/migrations/290-schedule-capabilities.ts +25 -0
  224. package/src/memory/migrations/__tests__/281-memory-retrospective-remembered-log.test.ts +96 -0
  225. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +571 -0
  226. package/src/memory/migrations/index.ts +10 -0
  227. package/src/memory/preloaded-apps.ts +116 -0
  228. package/src/memory/schema/infrastructure.ts +4 -0
  229. package/src/memory/schema/memory-core.ts +4 -0
  230. package/src/memory/v2/__tests__/concept-page-frontmatter-schema.test.ts +45 -0
  231. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +11 -7
  232. package/src/memory/v2/__tests__/page-store.test.ts +13 -2
  233. package/src/memory/v2/__tests__/qdrant.test.ts +24 -0
  234. package/src/memory/v2/frontmatter-sweep.ts +7 -6
  235. package/src/memory/v2/page-store.ts +4 -3
  236. package/src/memory/v2/qdrant.ts +42 -3
  237. package/src/memory/v2/types.ts +16 -10
  238. package/src/messaging/draft-store.ts +1 -1
  239. package/src/notifications/access-request-copy.ts +200 -113
  240. package/src/notifications/adapters/slack.ts +250 -111
  241. package/src/notifications/adapters/telegram.ts +7 -44
  242. package/src/notifications/approval-card-builder.ts +93 -0
  243. package/src/notifications/broadcaster.ts +74 -0
  244. package/src/notifications/conversation-pairing.ts +8 -6
  245. package/src/notifications/copy-composer.ts +32 -26
  246. package/src/notifications/decision-engine.ts +59 -7
  247. package/src/notifications/guardian-question-mode.ts +145 -155
  248. package/src/notifications/home-feed-side-effect.ts +1 -1
  249. package/src/notifications/notification-utils.ts +66 -0
  250. package/src/notifications/signal.ts +6 -0
  251. package/src/notifications/tool-approval-copy.ts +142 -0
  252. package/src/notifications/types.ts +19 -0
  253. package/src/permissions/threshold.ts +11 -0
  254. package/src/plugin-api/types.ts +16 -4
  255. package/src/plugins/defaults/compaction/window-manager.ts +44 -0
  256. package/src/plugins/defaults/index.ts +46 -0
  257. package/src/plugins/defaults/max-tokens-continue/continue-state-store.ts +53 -0
  258. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +80 -0
  259. package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +20 -0
  260. package/src/plugins/defaults/max-tokens-continue/package.json +14 -0
  261. package/src/plugins/defaults/memory-retrieval/hooks/__tests__/user-prompt-submit.test.ts +37 -0
  262. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +29 -1
  263. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +8 -3
  264. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +4 -2
  265. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +28 -18
  266. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +67 -0
  267. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +122 -22
  268. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +2 -0
  269. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +63 -1
  270. package/src/plugins/defaults/memory-v3-shadow/injector.ts +61 -18
  271. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +1 -1
  272. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +39 -10
  273. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +34 -1
  274. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +112 -47
  275. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +78 -15
  276. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +206 -0
  277. package/src/plugins/defaults/task-progress-nudge/package.json +15 -0
  278. package/src/prompts/__tests__/system-prompt.test.ts +100 -1
  279. package/src/prompts/__tests__/task-progress-hint-section.test.ts +5 -7
  280. package/src/prompts/normalize-onboarding.ts +2 -0
  281. package/src/prompts/persona-resolver.ts +3 -0
  282. package/src/prompts/system-prompt.ts +51 -2
  283. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +3 -1
  284. package/src/prompts/templates/system-sections.ts +8 -3
  285. package/src/providers/call-site-routing.ts +6 -3
  286. package/src/providers/fireworks/client.ts +3 -0
  287. package/src/providers/inference/auth.ts +52 -46
  288. package/src/providers/model-intents.ts +2 -2
  289. package/src/providers/openai/__tests__/coerce-object-args.test.ts +105 -0
  290. package/src/providers/openai/chat-completions-provider.ts +47 -9
  291. package/src/providers/openai/coerce-object-args.ts +104 -0
  292. package/src/providers/retry.ts +8 -5
  293. package/src/providers/types.ts +10 -0
  294. package/src/runtime/__tests__/agent-wake.test.ts +629 -7
  295. package/src/runtime/access-request-helper.ts +16 -9
  296. package/src/runtime/agent-wake.ts +302 -51
  297. package/src/runtime/assistant-stream-state.ts +141 -8
  298. package/src/runtime/background-job-runner.ts +9 -0
  299. package/src/runtime/channel-approval-types.ts +1 -0
  300. package/src/runtime/guardian-action-message-composer.ts +0 -54
  301. package/src/runtime/http-server.ts +6 -14
  302. package/src/runtime/http-types.ts +0 -1
  303. package/src/runtime/message-composer-types.ts +0 -9
  304. package/src/runtime/middleware/__tests__/rate-limiter.test.ts +63 -0
  305. package/src/runtime/middleware/auth.ts +27 -3
  306. package/src/runtime/middleware/rate-limiter.ts +28 -1
  307. package/src/runtime/migrations/vbundle-builder.ts +6 -5
  308. package/src/runtime/pending-interactions.ts +20 -1
  309. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +232 -173
  310. package/src/runtime/routes/__tests__/plugins-routes.test.ts +18 -0
  311. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +436 -0
  312. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +11 -0
  313. package/src/runtime/routes/approval-routes.ts +35 -8
  314. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -1
  315. package/src/runtime/routes/btw-routes.ts +37 -1
  316. package/src/runtime/routes/channel-route-shared.ts +3 -1
  317. package/src/runtime/routes/consolidation-routes.ts +17 -13
  318. package/src/runtime/routes/conversation-compaction-routes.ts +159 -119
  319. package/src/runtime/routes/conversation-list-routes.ts +41 -4
  320. package/src/runtime/routes/conversation-query-routes.ts +196 -11
  321. package/src/runtime/routes/conversation-routes.ts +15 -1
  322. package/src/runtime/routes/credential-prompt-routes.ts +39 -17
  323. package/src/runtime/routes/empty-state-greeting-cache.ts +65 -0
  324. package/src/runtime/routes/heartbeat-routes.ts +18 -13
  325. package/src/runtime/routes/image-generation-routes.ts +20 -2
  326. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +26 -1
  327. package/src/runtime/routes/index.ts +6 -0
  328. package/src/runtime/routes/log-export/AGENTS.md +1 -1
  329. package/src/runtime/routes/log-export/workspace-allowlist.ts +1 -1
  330. package/src/runtime/routes/migration-routes.ts +5 -9
  331. package/src/runtime/routes/plugins-routes.ts +26 -0
  332. package/src/runtime/routes/ps-routes.ts +10 -8
  333. package/src/runtime/routes/retrospective-routes.ts +235 -0
  334. package/src/runtime/routes/runs-pagination.ts +75 -0
  335. package/src/runtime/routes/schedule-routes.ts +367 -59
  336. package/src/runtime/routes/sounds-config-routes.ts +239 -0
  337. package/src/runtime/routes/surface-action-routes.ts +84 -4
  338. package/src/runtime/routes/workflow-routes.test.ts +372 -0
  339. package/src/runtime/routes/workflow-routes.ts +363 -0
  340. package/src/runtime/routes/workspace-routes.test.ts +61 -1
  341. package/src/runtime/routes/workspace-routes.ts +26 -1
  342. package/src/runtime/services/__tests__/analyze-conversation.test.ts +38 -0
  343. package/src/runtime/services/analyze-conversation.ts +26 -13
  344. package/src/schedule/inference-profile.ts +28 -0
  345. package/src/schedule/schedule-store.ts +122 -4
  346. package/src/schedule/scheduler-types.ts +6 -0
  347. package/src/schedule/scheduler.ts +96 -0
  348. package/src/security/secret-allowlist.ts +1 -1
  349. package/src/skills/path-classifier.ts +1 -1
  350. package/src/tools/browser/browser-execution.ts +9 -11
  351. package/src/tools/credentials/broker.ts +4 -4
  352. package/src/tools/credentials/vault.ts +26 -137
  353. package/src/tools/executor.ts +69 -0
  354. package/src/tools/flag-gated-tools.test.ts +76 -0
  355. package/src/tools/permission-checker.ts +8 -1
  356. package/src/tools/registry.ts +51 -0
  357. package/src/tools/schedule/create.ts +77 -1
  358. package/src/tools/schedule/list.ts +1 -0
  359. package/src/tools/schedule/update.ts +73 -1
  360. package/src/tools/skills/execute.ts +56 -0
  361. package/src/tools/terminal/shell.ts +1 -1
  362. package/src/tools/ui-surface/definitions.ts +48 -2
  363. package/src/tools/workflows/manage-workflows.ts +183 -0
  364. package/src/tools/workflows/run-workflow.test.ts +442 -0
  365. package/src/tools/workflows/run-workflow.ts +88 -0
  366. package/src/types/onboarding-context.ts +2 -0
  367. package/src/usage/attribution.ts +24 -0
  368. package/src/util/canonicalize-identity.ts +12 -3
  369. package/src/util/platform.ts +17 -17
  370. package/src/watcher/__tests__/engine.test.ts +24 -0
  371. package/src/watcher/__tests__/telemetry.test.ts +135 -0
  372. package/src/watcher/engine.ts +7 -0
  373. package/src/watcher/telemetry.ts +74 -0
  374. package/src/workflows/capabilities.test.ts +365 -0
  375. package/src/workflows/capabilities.ts +359 -0
  376. package/src/workflows/deterministic-stringify.ts +27 -0
  377. package/src/workflows/engine-integration.test.ts +656 -0
  378. package/src/workflows/engine.test.ts +1144 -0
  379. package/src/workflows/engine.ts +1078 -0
  380. package/src/workflows/fanout-load.test.ts +168 -0
  381. package/src/workflows/journal-store.test.ts +369 -0
  382. package/src/workflows/journal-store.ts +470 -0
  383. package/src/workflows/leaf-runner.test.ts +704 -0
  384. package/src/workflows/leaf-runner.ts +589 -0
  385. package/src/workflows/library.test.ts +134 -0
  386. package/src/workflows/library.ts +124 -0
  387. package/src/workflows/run-manager.test.ts +711 -0
  388. package/src/workflows/run-manager.ts +593 -0
  389. package/src/workflows/sandbox-escape.test.ts +339 -0
  390. package/src/workflows/sandbox.test.ts +251 -0
  391. package/src/workflows/sandbox.ts +447 -0
  392. package/src/workspace/adaptive-thinking-repair.ts +33 -11
  393. package/src/workspace/migrations/021-move-signals-to-workspace.ts +1 -1
  394. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +1 -1
  395. package/src/workspace/migrations/026-backfill-install-meta.ts +1 -1
  396. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +2 -1
  397. package/src/workspace/migrations/031-drop-user-md.ts +1 -4
  398. package/src/workspace/migrations/048-remove-workspace-hooks.ts +1 -1
  399. package/src/workspace/migrations/056-release-notes-inference-profile-reordering.ts +5 -2
  400. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +1 -1
  401. package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +8 -2
  402. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +41 -14
  403. package/src/workspace/migrations/102-preserve-heartbeat-enabled-for-existing-workspaces.ts +69 -0
  404. package/src/workspace/migrations/103-upgrade-quality-profile-to-opus-4-8.ts +83 -0
  405. package/src/workspace/migrations/104-recheck-adaptive-thinking-model-implied-anthropic.ts +133 -0
  406. package/src/workspace/migrations/registry.ts +6 -0
  407. package/src/workspace/migrations/runner.ts +1 -1
  408. package/tsconfig.json +1 -1
  409. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -200
  410. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -579
  411. package/src/__tests__/guardian-action-store.test.ts +0 -106
  412. package/src/daemon/guardian-action-generators.ts +0 -71
  413. package/src/memory/guardian-action-store.ts +0 -484
  414. package/src/runtime/guardian-action-grant-minter.ts +0 -150
@@ -20,10 +20,16 @@
20
20
  * - Total size: 256 KB
21
21
  * - Age: 30 seconds
22
22
  *
23
- * The ring is in-memory and per-daemon-process. After a daemon restart
24
- * the seq resets and reconnecting clients fall through to the snapshot
25
- * path. The ring is sized generously enough that a typical refresh
26
- * round-trip (~1-3s) is well within window.
23
+ * The ring is in-memory and per-daemon-process, but the seq counter
24
+ * itself survives restarts: blocks of seq values are reserved ahead of
25
+ * use and the reserved ceiling is persisted to the workspace
26
+ * (`data/stream-seq.json`), so a restarted daemon resumes stamping
27
+ * above every seq the previous process could have emitted. Clients
28
+ * therefore never observe the counter moving backwards — a restart
29
+ * shows up as (at most) a bounded forward gap, which the normal
30
+ * gap-reconcile / snapshot-resync path already handles. The ring is
31
+ * sized generously enough that a typical refresh round-trip (~1-3s)
32
+ * is well within window.
27
33
  *
28
34
  * Persisted-seq map: alongside the live counter and ring, this module
29
35
  * tracks, per conversation, the `seq` of the last event whose content is
@@ -34,12 +40,17 @@
34
40
  * persistence flush (assistant rows persist incrementally, debounced, so
35
41
  * the snapshot can lag the live counter) -- never the live counter
36
42
  * itself, which would over-claim events that have streamed but not yet
37
- * been written. It shares the live counter's lifetime by design: both
38
- * are in-memory and reset together on restart, so a stored value can
39
- * never dangle against a fresh counter. The map is LRU-bounded; an
43
+ * been written. The map is in-memory and clears on restart; because the
44
+ * counter resumes above the persisted reservation, a value recorded by
45
+ * a previous process could only ever be lower than any seq the new
46
+ * process assigns -- never ambiguous against it. The map is LRU-bounded; an
40
47
  * evicted conversation reports no seq and the client cold-starts.
41
48
  */
42
49
 
50
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
51
+ import { dirname, join } from "node:path";
52
+
53
+ import { getWorkspaceDir } from "../util/platform.js";
43
54
  import type { AssistantEvent } from "./assistant-event.js";
44
55
 
45
56
  // ── Tunables ─────────────────────────────────────────────────────────
@@ -58,6 +69,15 @@ const RING_AGE_LIMIT_MS = 30_000;
58
69
  */
59
70
  const PERSISTED_SEQ_CONVERSATION_LIMIT = 1024;
60
71
 
72
+ /**
73
+ * How many seq values are reserved per persisted write. The counter can
74
+ * hand out seqs up to the persisted ceiling without touching disk, so
75
+ * the file is written once per block rather than once per event. A
76
+ * restart skips at most one block's worth of unused seqs — a bounded
77
+ * forward gap that clients already treat as a missed-events signal.
78
+ */
79
+ const SEQ_RESERVATION_BLOCK = 1024;
80
+
61
81
  // ── Types ────────────────────────────────────────────────────────────
62
82
 
63
83
  /**
@@ -97,6 +117,21 @@ interface RingEntry {
97
117
 
98
118
  interface AssistantStreamState {
99
119
  nextSeq: number;
120
+ /**
121
+ * Highest seq this process may assign without persisting a new
122
+ * reservation. `0` until the first stamp loads (or creates) the
123
+ * persisted reservation.
124
+ */
125
+ reservedSeqCeiling: number;
126
+ /** Whether the persisted reservation has been loaded this process. */
127
+ seqReservationLoaded: boolean;
128
+ /**
129
+ * Seq of the first event stamped by this process, or `0` before any
130
+ * stamp. Distinguishes a reservation skip (seqs below this value were
131
+ * never assigned by this process, so nothing is missing from the
132
+ * ring) from genuine ring eviction when judging replay validity.
133
+ */
134
+ firstStampedSeq: number;
100
135
  ring: RingEntry[];
101
136
  totalSizeBytes: number;
102
137
  /**
@@ -112,6 +147,9 @@ interface AssistantStreamState {
112
147
 
113
148
  const state: AssistantStreamState = {
114
149
  nextSeq: 1,
150
+ reservedSeqCeiling: 0,
151
+ seqReservationLoaded: false,
152
+ firstStampedSeq: 0,
115
153
  ring: [],
116
154
  totalSizeBytes: 0,
117
155
  persistedSeqByConversation: new Map(),
@@ -138,7 +176,9 @@ export function stampAndBuffer(
138
176
  ): void {
139
177
  if (event.conversationId == null) return;
140
178
 
179
+ reserveSeqCapacity();
141
180
  event.seq = state.nextSeq++;
181
+ if (state.firstStampedSeq === 0) state.firstStampedSeq = event.seq;
142
182
 
143
183
  // Approximate size by serialized JSON length. This is the same
144
184
  // bytes-on-wire we'll send, so it tracks ring memory pressure
@@ -187,8 +227,16 @@ export function getReplayWindow(
187
227
 
188
228
  if (state.ring.length === 0) return [];
189
229
 
230
+ // A cursor from before this process started can skip over the
231
+ // reservation gap: seqs below `firstStampedSeq` were never assigned
232
+ // by this process, so as long as the ring still holds everything
233
+ // this process stamped, no replayable event is missing. (Events from
234
+ // the previous process are unrecoverable either way — clients catch
235
+ // up on those via the snapshot path, triggered by the seq jump.)
190
236
  const oldest = state.ring[0]?.seq ?? Infinity;
191
- if (lastSeenSeq < oldest - 1) return null;
237
+ const coversRestartGap =
238
+ lastSeenSeq < state.firstStampedSeq && oldest === state.firstStampedSeq;
239
+ if (lastSeenSeq < oldest - 1 && !coversRestartGap) return null;
192
240
 
193
241
  return state.ring
194
242
  .filter(
@@ -260,6 +308,26 @@ export function getPersistedSeq(conversationId: string): number | null {
260
308
  */
261
309
  export function _resetStreamStateForTesting(): void {
262
310
  state.nextSeq = 1;
311
+ // Mark the reservation as loaded with no ceiling so the next stamp
312
+ // re-reserves from 1, ignoring any reservation file a previous test
313
+ // wrote into the (per-process temp) workspace.
314
+ state.reservedSeqCeiling = 0;
315
+ state.seqReservationLoaded = true;
316
+ state.firstStampedSeq = 0;
317
+ state.ring = [];
318
+ state.totalSizeBytes = 0;
319
+ state.persistedSeqByConversation.clear();
320
+ }
321
+
322
+ /**
323
+ * Simulate a daemon restart: clear all in-memory state and force the
324
+ * next stamp to reload the persisted seq reservation. Test-only.
325
+ */
326
+ export function _simulateRestartForTesting(): void {
327
+ state.nextSeq = 1;
328
+ state.reservedSeqCeiling = 0;
329
+ state.seqReservationLoaded = false;
330
+ state.firstStampedSeq = 0;
263
331
  state.ring = [];
264
332
  state.totalSizeBytes = 0;
265
333
  state.persistedSeqByConversation.clear();
@@ -286,6 +354,71 @@ export function _peekStreamForTesting(): {
286
354
 
287
355
  // ── Internals ────────────────────────────────────────────────────────
288
356
 
357
+ function seqReservationPath(): string {
358
+ return join(getWorkspaceDir(), "data", "stream-seq.json");
359
+ }
360
+
361
+ /**
362
+ * Ensure `state.nextSeq` is covered by the persisted reservation,
363
+ * loading the reservation file on the first stamp of the process and
364
+ * extending it by {@link SEQ_RESERVATION_BLOCK} whenever the counter
365
+ * reaches the ceiling.
366
+ *
367
+ * Persistence is best-effort: if the file cannot be read or written
368
+ * the counter falls back to in-memory-only behavior (the ceiling is
369
+ * still advanced so the write is retried at most once per block, not
370
+ * per event), matching the daemon's degraded-mode philosophy.
371
+ */
372
+ function reserveSeqCapacity(): void {
373
+ if (!state.seqReservationLoaded) {
374
+ state.seqReservationLoaded = true;
375
+ state.reservedSeqCeiling = readReservedCeiling();
376
+ if (state.reservedSeqCeiling >= state.nextSeq) {
377
+ state.nextSeq = state.reservedSeqCeiling + 1;
378
+ }
379
+ }
380
+
381
+ if (state.nextSeq <= state.reservedSeqCeiling) return;
382
+
383
+ const ceiling = state.nextSeq + SEQ_RESERVATION_BLOCK - 1;
384
+ try {
385
+ const path = seqReservationPath();
386
+ mkdirSync(dirname(path), { recursive: true });
387
+ const tmp = `${path}.tmp`;
388
+ writeFileSync(tmp, JSON.stringify({ reservedSeqCeiling: ceiling }));
389
+ renameSync(tmp, path);
390
+ } catch {
391
+ // Degraded mode: keep stamping from memory.
392
+ }
393
+ state.reservedSeqCeiling = ceiling;
394
+ }
395
+
396
+ function readReservedCeiling(): number {
397
+ try {
398
+ const parsed: unknown = JSON.parse(
399
+ readFileSync(seqReservationPath(), "utf8"),
400
+ );
401
+ if (
402
+ typeof parsed === "object" &&
403
+ parsed !== null &&
404
+ "reservedSeqCeiling" in parsed
405
+ ) {
406
+ const ceiling = (parsed as { reservedSeqCeiling: unknown })
407
+ .reservedSeqCeiling;
408
+ if (
409
+ typeof ceiling === "number" &&
410
+ Number.isFinite(ceiling) &&
411
+ ceiling > 0
412
+ ) {
413
+ return Math.floor(ceiling);
414
+ }
415
+ }
416
+ } catch {
417
+ // Missing or unreadable file: cold start from seq 1.
418
+ }
419
+ return 0;
420
+ }
421
+
289
422
  /**
290
423
  * Mirrors the delivery logic in `AssistantEventHub.publish()`. Returns
291
424
  * `true` when `subscriber` would have received the entry during live
@@ -67,6 +67,12 @@ export interface RunBackgroundJobOptions {
67
67
  trustContext: TrustContext;
68
68
  /** LLM call-site identifier — drives provider/model/effort/etc. resolution. */
69
69
  callSite: LLMCallSite;
70
+ /**
71
+ * Optional ad-hoc inference-profile override (`llm.profiles` key) applied
72
+ * to every LLM call the job's turn issues. Used by schedules with a pinned
73
+ * profile; omitted = the call site's default resolution.
74
+ */
75
+ overrideProfile?: string;
70
76
  /** Hard timeout for `processMessage` in milliseconds. */
71
77
  timeoutMs: number;
72
78
  /**
@@ -267,6 +273,9 @@ export async function runBackgroundJob(
267
273
  const work = processMessage(conversation.id, opts.prompt, {
268
274
  trustContext: opts.trustContext,
269
275
  callSite: opts.callSite,
276
+ ...(opts.overrideProfile
277
+ ? { overrideProfile: opts.overrideProfile }
278
+ : {}),
270
279
  });
271
280
  // Absorb late rejections: if the timeout wins the race, `work` keeps
272
281
  // running and may eventually reject — swallow so it doesn't surface as
@@ -92,6 +92,7 @@ export type ApprovalDecisionSource =
92
92
  | "whatsapp_button"
93
93
  | "slack_button"
94
94
  | "slack_reaction"
95
+ | "vellum_surface"
95
96
  | "plain_text";
96
97
 
97
98
  /** The structured result of a user's approval decision. */
@@ -8,10 +8,8 @@
8
8
  *
9
9
  * Follows the same pattern as approval-message-composer.ts.
10
10
  */
11
- import { getLogger } from "../util/logger.js";
12
11
  import type {
13
12
  ComposeGuardianActionMessageOptions,
14
- GuardianActionCopyGenerator,
15
13
  GuardianActionMessageContext,
16
14
  } from "./message-composer-types.js";
17
15
 
@@ -21,21 +19,6 @@ export type {
21
19
  GuardianActionMessageScenario,
22
20
  } from "./message-composer-types.js";
23
21
 
24
- const log = getLogger("guardian-action-message-composer");
25
-
26
- // ---------------------------------------------------------------------------
27
- // Constants (exported for the daemon-injected generator implementation)
28
- // ---------------------------------------------------------------------------
29
-
30
- export const GUARDIAN_ACTION_COPY_TIMEOUT_MS = 4_000;
31
- export const GUARDIAN_ACTION_COPY_MAX_TOKENS = 200;
32
- export const GUARDIAN_ACTION_COPY_SYSTEM_PROMPT =
33
- "You are an assistant writing one user-facing message about a guardian action in a voice call scenario. " +
34
- "Keep it concise, natural, and conversational. Preserve factual details exactly. " +
35
- "These messages are spoken aloud, so use a warm, human tone. " +
36
- "Do not mention internal systems, scenario IDs, or technical details. " +
37
- "Return plain text only.";
38
-
39
22
  // ---------------------------------------------------------------------------
40
23
  // Public API
41
24
  // ---------------------------------------------------------------------------
@@ -51,7 +34,6 @@ export const GUARDIAN_ACTION_COPY_SYSTEM_PROMPT =
51
34
  export async function composeGuardianActionMessageGenerative(
52
35
  context: GuardianActionMessageContext,
53
36
  options: ComposeGuardianActionMessageOptions = {},
54
- generator?: GuardianActionCopyGenerator,
55
37
  ): Promise<string> {
56
38
  const fallbackText =
57
39
  options.fallbackText?.trim() || getGuardianActionFallbackMessage(context);
@@ -60,45 +42,9 @@ export async function composeGuardianActionMessageGenerative(
60
42
  return fallbackText;
61
43
  }
62
44
 
63
- if (generator) {
64
- try {
65
- const generated = await generator(context, options);
66
- if (generated) return generated;
67
- } catch (err) {
68
- log.warn(
69
- { err, scenario: context.scenario },
70
- "Failed to generate guardian action copy, using fallback",
71
- );
72
- }
73
- }
74
-
75
45
  return fallbackText;
76
46
  }
77
47
 
78
- /** @internal Exported for use by the daemon-injected generator implementation. */
79
- export function buildGuardianActionGenerationPrompt(
80
- context: GuardianActionMessageContext,
81
- fallbackText: string,
82
- requiredKeywords: string[] | undefined,
83
- ): string {
84
- const keywordClause =
85
- requiredKeywords && requiredKeywords.length > 0
86
- ? `Required words to include (as standalone words): ${requiredKeywords.join(
87
- ", ",
88
- )}.\n`
89
- : "";
90
- return [
91
- "Rewrite the following guardian action message as a natural, conversational reply.",
92
- "These messages are for voice call scenarios and may be spoken aloud.",
93
- "Keep the same concrete facts and next-step guidance.",
94
- keywordClause,
95
- `Context JSON: ${JSON.stringify(context)}`,
96
- `Fallback message: ${fallbackText}`,
97
- ]
98
- .filter(Boolean)
99
- .join("\n\n");
100
- }
101
-
102
48
  /** @internal Exported for use by the daemon-injected generator implementation. */
103
49
  export function includesRequiredKeywords(
104
50
  text: string,
@@ -7,10 +7,6 @@
7
7
 
8
8
  import type { ServerWebSocket } from "bun";
9
9
 
10
- import {
11
- startGuardianActionSweep,
12
- stopGuardianActionSweep,
13
- } from "../calls/guardian-action-sweep.js";
14
10
  import {
15
11
  activeMediaStreamSessions,
16
12
  MediaStreamCallSession,
@@ -29,7 +25,6 @@ import { isHttpAuthDisabled } from "../config/env.js";
29
25
  import { getIsPlatform } from "../config/env-registry.js";
30
26
  import { getConfig } from "../config/loader.js";
31
27
  import { createApprovalCopyGenerator } from "../daemon/approval-generators.js";
32
- import { createGuardianActionCopyGenerator } from "../daemon/guardian-action-generators.js";
33
28
  import { processMessage } from "../daemon/process-message.js";
34
29
  import { createLiveVoiceSession } from "../live-voice/live-voice-session.js";
35
30
  import { LiveVoiceSessionManager } from "../live-voice/live-voice-session-manager.js";
@@ -61,11 +56,11 @@ import {
61
56
  } from "./middleware/auth.js";
62
57
  import { withErrorHandling } from "./middleware/error-handler.js";
63
58
  import {
64
- apiRateLimiter,
65
59
  extractClientIp,
66
60
  ipRateLimiter,
67
61
  rateLimitHeaders,
68
62
  rateLimitResponse,
63
+ selectAuthenticatedRateLimiter,
69
64
  } from "./middleware/rate-limiter.js";
70
65
  import { withRequestLogging } from "./middleware/request-logger.js";
71
66
  import {
@@ -99,7 +94,6 @@ export { isPrivateAddress } from "./middleware/auth.js";
99
94
 
100
95
  import type {
101
96
  ApprovalCopyGenerator,
102
- GuardianActionCopyGenerator,
103
97
  RuntimeHttpServerOptions,
104
98
  } from "./http-types.js";
105
99
 
@@ -162,7 +156,6 @@ export class RuntimeHttpServer {
162
156
  private hostname: string;
163
157
 
164
158
  private readonly approvalCopyGenerator: ApprovalCopyGenerator;
165
- private readonly guardianActionCopyGenerator: GuardianActionCopyGenerator;
166
159
  private retrySweepTimer: ReturnType<typeof setInterval> | null = null;
167
160
  private sweepInProgress = false;
168
161
 
@@ -174,7 +167,6 @@ export class RuntimeHttpServer {
174
167
  this.hostname = options.hostname ?? DEFAULT_HOSTNAME;
175
168
 
176
169
  this.approvalCopyGenerator = createApprovalCopyGenerator();
177
- this.guardianActionCopyGenerator = createGuardianActionCopyGenerator();
178
170
  this.liveVoiceSessionManager = new LiveVoiceSessionManager({
179
171
  createSession: (context) => createLiveVoiceSession(context),
180
172
  });
@@ -486,9 +478,6 @@ export class RuntimeHttpServer {
486
478
  startGuardianExpirySweep(this.approvalCopyGenerator);
487
479
  log.info("Guardian approval expiry sweep started");
488
480
 
489
- startGuardianActionSweep(this.guardianActionCopyGenerator);
490
- log.info("Guardian action expiry sweep started");
491
-
492
481
  startCanonicalGuardianExpirySweep();
493
482
  log.info("Canonical guardian request expiry sweep started");
494
483
 
@@ -498,7 +487,6 @@ export class RuntimeHttpServer {
498
487
 
499
488
  async stop(): Promise<void> {
500
489
  stopGuardianExpirySweep();
501
- stopGuardianActionSweep();
502
490
  stopCanonicalGuardianExpirySweep();
503
491
  stopInferenceProfileSessionReaper();
504
492
  if (this.retrySweepTimer) {
@@ -702,7 +690,11 @@ export class RuntimeHttpServer {
702
690
  if (!isHttpAuthDisabled()) {
703
691
  const clientIp = extractClientIp(req, server);
704
692
  const token = extractBearerToken(req);
705
- const limiter = token ? apiRateLimiter : ipRateLimiter;
693
+ // Authenticated loopback clients (desktop app, CLI — anything on the
694
+ // daemon's own host) get a higher budget than remote clients.
695
+ const limiter = token
696
+ ? selectAuthenticatedRateLimiter(clientIp)
697
+ : ipRateLimiter;
706
698
  const limiterKind = token ? "authenticated" : "unauthenticated";
707
699
  const result = limiter.check(clientIp, path);
708
700
  if (!result.allowed) {
@@ -22,7 +22,6 @@ import type { AssistantEventHub } from "./assistant-event-hub.js";
22
22
  export type {
23
23
  ApprovalCopyGenerator,
24
24
  ComposeApprovalMessageGenerativeOptions,
25
- GuardianActionCopyGenerator,
26
25
  } from "./message-composer-types.js";
27
26
  import type { TrustContext } from "../daemon/trust-context.js";
28
27
 
@@ -123,12 +123,3 @@ export interface ComposeGuardianActionMessageOptions {
123
123
  maxTokens?: number;
124
124
  timeoutMs?: number;
125
125
  }
126
-
127
- /**
128
- * Daemon-injected function that generates guardian action copy using a provider.
129
- * Returns generated text or `null` on failure (caller falls back to deterministic text).
130
- */
131
- export type GuardianActionCopyGenerator = (
132
- context: GuardianActionMessageContext,
133
- options?: ComposeGuardianActionMessageOptions,
134
- ) => Promise<string | null>;
@@ -0,0 +1,63 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { isLoopbackAddress } from "../auth.js";
4
+ import {
5
+ apiRateLimiter,
6
+ loopbackApiRateLimiter,
7
+ selectAuthenticatedRateLimiter,
8
+ } from "../rate-limiter.js";
9
+
10
+ describe("isLoopbackAddress", () => {
11
+ test("accepts loopback addresses", () => {
12
+ expect(isLoopbackAddress("127.0.0.1")).toBe(true);
13
+ expect(isLoopbackAddress("127.42.0.7")).toBe(true);
14
+ expect(isLoopbackAddress("::1")).toBe(true);
15
+ expect(isLoopbackAddress("::ffff:127.0.0.1")).toBe(true);
16
+ });
17
+
18
+ test("rejects private (non-loopback) and public addresses", () => {
19
+ expect(isLoopbackAddress("10.0.0.1")).toBe(false);
20
+ expect(isLoopbackAddress("192.168.1.20")).toBe(false);
21
+ expect(isLoopbackAddress("172.16.0.5")).toBe(false);
22
+ expect(isLoopbackAddress("169.254.0.1")).toBe(false);
23
+ expect(isLoopbackAddress("::ffff:10.0.0.1")).toBe(false);
24
+ expect(isLoopbackAddress("fe80::1")).toBe(false);
25
+ expect(isLoopbackAddress("fd00::1")).toBe(false);
26
+ expect(isLoopbackAddress("8.8.8.8")).toBe(false);
27
+ });
28
+
29
+ test("rejects malformed input", () => {
30
+ expect(isLoopbackAddress("")).toBe(false);
31
+ expect(isLoopbackAddress("localhost")).toBe(false);
32
+ expect(isLoopbackAddress("127.0.0")).toBe(false);
33
+ expect(isLoopbackAddress("127.0.0.999")).toBe(false);
34
+ });
35
+ });
36
+
37
+ describe("selectAuthenticatedRateLimiter", () => {
38
+ test("loopback clients get the higher-budget limiter", () => {
39
+ expect(selectAuthenticatedRateLimiter("127.0.0.1")).toBe(
40
+ loopbackApiRateLimiter,
41
+ );
42
+ expect(selectAuthenticatedRateLimiter("::1")).toBe(loopbackApiRateLimiter);
43
+ expect(selectAuthenticatedRateLimiter("::ffff:127.0.0.1")).toBe(
44
+ loopbackApiRateLimiter,
45
+ );
46
+ });
47
+
48
+ test("remote and LAN clients get the standard limiter", () => {
49
+ expect(selectAuthenticatedRateLimiter("192.168.1.20")).toBe(apiRateLimiter);
50
+ expect(selectAuthenticatedRateLimiter("203.0.113.9")).toBe(apiRateLimiter);
51
+ });
52
+
53
+ test("loopback budget exceeds the standard budget", () => {
54
+ const loopback = loopbackApiRateLimiter.check(
55
+ "test-loopback-budget",
56
+ "/v1/test",
57
+ );
58
+ const standard = apiRateLimiter.check("test-standard-budget", "/v1/test");
59
+ expect(loopback.limit).toBeGreaterThan(standard.limit);
60
+ expect(loopback.limit).toBe(1200);
61
+ expect(standard.limit).toBe(300);
62
+ });
63
+ });
@@ -12,6 +12,32 @@ export function isLoopbackHost(hostname: string): boolean {
12
12
  );
13
13
  }
14
14
 
15
+ /** Unwrap IPv4-mapped IPv6 (e.g. ::ffff:10.0.0.1) to its IPv4 part. */
16
+ function unwrapV4Mapped(addr: string): string {
17
+ const v4Mapped = addr.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
18
+ return v4Mapped ? v4Mapped[1] : addr;
19
+ }
20
+
21
+ /**
22
+ * Determine whether an IP address string is a loopback address —
23
+ * 127.0.0.0/8, ::1, or their IPv4-mapped IPv6 forms. Strictly narrower
24
+ * than {@link isPrivateAddress}: LAN peers (RFC 1918, link-local) are
25
+ * private but not loopback.
26
+ */
27
+ export function isLoopbackAddress(addr: string): boolean {
28
+ const normalized = unwrapV4Mapped(addr);
29
+
30
+ if (normalized.includes(".")) {
31
+ const parts = normalized.split(".").map(Number);
32
+ if (parts.length !== 4 || parts.some((p) => isNaN(p) || p < 0 || p > 255))
33
+ return false;
34
+ // Loopback: 127.0.0.0/8
35
+ return parts[0] === 127;
36
+ }
37
+
38
+ return normalized.toLowerCase() === "::1";
39
+ }
40
+
15
41
  /**
16
42
  * @internal Exported for testing.
17
43
  *
@@ -24,9 +50,7 @@ export function isLoopbackHost(hostname: string): boolean {
24
50
  * - IPv4-mapped IPv6 variants of all of the above (::ffff:x.x.x.x)
25
51
  */
26
52
  export function isPrivateAddress(addr: string): boolean {
27
- // Handle IPv4-mapped IPv6 (e.g. ::ffff:10.0.0.1) -- extract the IPv4 part
28
- const v4Mapped = addr.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
29
- const normalized = v4Mapped ? v4Mapped[1] : addr;
53
+ const normalized = unwrapV4Mapped(addr);
30
54
 
31
55
  // IPv4 checks
32
56
  if (normalized.includes(".")) {
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { getLogger } from "../../util/logger.js";
6
6
  import type { HttpErrorResponse } from "../http-errors.js";
7
- import { isPrivateAddress } from "./auth.js";
7
+ import { isLoopbackAddress, isPrivateAddress } from "./auth.js";
8
8
 
9
9
  const log = getLogger("rate-limiter");
10
10
 
@@ -12,6 +12,14 @@ const DEFAULT_MAX_REQUESTS = 300;
12
12
  const DEFAULT_WINDOW_MS = 60_000; // 60 seconds
13
13
  const MAX_TRACKED_TOKENS = 10_000;
14
14
 
15
+ // Higher budget for authenticated loopback clients. Loopback means the
16
+ // request originated on the daemon's own host (desktop app, CLI, local
17
+ // scripts) — proxied remote traffic resolves to its forwarded client IP
18
+ // instead (see extractClientIp). Local clients legitimately burst far
19
+ // beyond the remote budget: a cold sidebar load at thousands of
20
+ // conversations pages through hundreds of GETs in a few seconds.
21
+ const LOOPBACK_MAX_REQUESTS = 1200;
22
+
15
23
  // Lower limit for unauthenticated (IP-based) requests to reduce abuse surface.
16
24
  const DEFAULT_IP_MAX_REQUESTS = 20;
17
25
  const DEFAULT_IP_WINDOW_MS = 60_000;
@@ -186,6 +194,15 @@ export function rateLimitResponse(
186
194
  /** Singleton rate limiter for authenticated /v1/* requests (per-client-IP). */
187
195
  export const apiRateLimiter = new TokenRateLimiter();
188
196
 
197
+ /**
198
+ * Singleton rate limiter for authenticated requests from loopback clients.
199
+ * Separate instance (not just a higher cap) so local and remote traffic
200
+ * never share or evict each other's buckets.
201
+ */
202
+ export const loopbackApiRateLimiter = new TokenRateLimiter(
203
+ LOOPBACK_MAX_REQUESTS,
204
+ );
205
+
189
206
  /** Singleton rate limiter for unauthenticated requests (per-IP, lower limits). */
190
207
  export const ipRateLimiter = new TokenRateLimiter(
191
208
  DEFAULT_IP_MAX_REQUESTS,
@@ -193,6 +210,16 @@ export const ipRateLimiter = new TokenRateLimiter(
193
210
  MAX_TRACKED_IPS,
194
211
  );
195
212
 
213
+ /**
214
+ * Pick the rate limiter for an authenticated request: loopback client IPs
215
+ * get the higher local budget, everything else gets the standard one.
216
+ */
217
+ export function selectAuthenticatedRateLimiter(
218
+ clientIp: string,
219
+ ): TokenRateLimiter {
220
+ return isLoopbackAddress(clientIp) ? loopbackApiRateLimiter : apiRateLimiter;
221
+ }
222
+
196
223
  /**
197
224
  * Extract the client IP from a request. Only trusts proxy headers
198
225
  * (X-Forwarded-For, X-Real-IP) when the peer IP is loopback or private,
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * A .vbundle is a gzip-compressed tar archive containing:
5
5
  * - manifest.json: metadata with schema_version, checksums, and bundle info
6
- * - workspace/: the entire ~/.vellum/workspace/ directory tree (DB, config,
6
+ * - workspace/: the entire $VELLUM_WORKSPACE_DIR/ directory tree (DB, config,
7
7
  * skills, prompts, attachments, etc.) — excluding large/regenerable
8
8
  * dirs (embedding-models/, data/qdrant/)
9
9
  */
@@ -668,7 +668,7 @@ export interface BuildExportVBundleOptions {
668
668
  /** Whether secrets were stripped from the bundle before archiving. */
669
669
  secretsRedacted: boolean;
670
670
  /**
671
- * Absolute path to the workspace directory (~/.vellum/workspace/).
671
+ * Absolute path to the workspace directory ($VELLUM_WORKSPACE_DIR/).
672
672
  * When provided and exists, the entire directory tree is walked and
673
673
  * included in the archive under the "workspace/" prefix, skipping
674
674
  * large/regenerable dirs (embedding-models/, data/qdrant/).
@@ -679,8 +679,9 @@ export interface BuildExportVBundleOptions {
679
679
  * Optional callback to checkpoint the WAL before reading the database file.
680
680
  * In WAL mode, committed rows may live in the -wal file and not yet be
681
681
  * flushed to the main .db file. Callers should pass a function that runs
682
- * PRAGMA wal_checkpoint(FULL) on the live database connection. (Not
683
- * TRUNCATE — see assistant/AGENTS.md "SQLite WAL checkpointing".)
682
+ * PRAGMA wal_checkpoint(FULL) on the live database connection — FULL
683
+ * flushes committed frames to the .db file without the extra WAL restart
684
+ * and truncation that TRUNCATE performs.
684
685
  * Called before the workspace walk so the DB file is up to date.
685
686
  *
686
687
  * May return a Promise — `streamExportVBundle` awaits the result so an
@@ -696,7 +697,7 @@ export interface BuildExportVBundleOptions {
696
697
  /**
697
698
  * Build a .vbundle archive populated with real assistant data.
698
699
  *
699
- * Walks the entire workspace directory (~/.vellum/workspace/) and includes
700
+ * Walks the entire workspace directory ($VELLUM_WORKSPACE_DIR/) and includes
700
701
  * all files in the archive, skipping only large/regenerable directories
701
702
  * (embedding-models/, data/qdrant/). Binary files (SQLite DB, attachments)
702
703
  * are included.