@vellumai/assistant 0.7.2 → 0.8.0

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 (424) hide show
  1. package/ARCHITECTURE.md +45 -29
  2. package/Dockerfile +1 -0
  3. package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
  4. package/bun.lock +3 -0
  5. package/docs/architecture/memory.md +5 -2
  6. package/knip.json +1 -0
  7. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
  8. package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
  9. package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
  10. package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
  11. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
  12. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
  13. package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
  14. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
  15. package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
  16. package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
  17. package/openapi.yaml +470 -25
  18. package/package.json +3 -1
  19. package/src/__tests__/annotate-risk-options.test.ts +291 -0
  20. package/src/__tests__/app-control-flow.test.ts +21 -11
  21. package/src/__tests__/approval-cascade.test.ts +8 -16
  22. package/src/__tests__/approval-routes-http.test.ts +6 -0
  23. package/src/__tests__/assistant-event-hub.test.ts +48 -0
  24. package/src/__tests__/assistant-event.test.ts +0 -10
  25. package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
  26. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
  27. package/src/__tests__/auto-analysis-end-to-end.test.ts +48 -0
  28. package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
  29. package/src/__tests__/call-constants.test.ts +10 -1
  30. package/src/__tests__/call-controller.test.ts +127 -0
  31. package/src/__tests__/call-conversation-messages.test.ts +8 -2
  32. package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
  33. package/src/__tests__/channel-readiness-service.test.ts +4 -2
  34. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +58 -28
  35. package/src/__tests__/config-loader-backfill.test.ts +379 -0
  36. package/src/__tests__/config-loader-platform-defaults.test.ts +284 -1
  37. package/src/__tests__/config-schema.test.ts +1 -0
  38. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
  39. package/src/__tests__/config-watcher.test.ts +140 -69
  40. package/src/__tests__/context-search-agent-runner.test.ts +61 -3
  41. package/src/__tests__/context-search-conversations-source.test.ts +0 -24
  42. package/src/__tests__/context-search-fanout.test.ts +0 -1
  43. package/src/__tests__/context-search-memory-source.test.ts +6 -33
  44. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
  45. package/src/__tests__/context-search-pkb-source.test.ts +12 -7
  46. package/src/__tests__/context-search-workspace-source.test.ts +0 -1
  47. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -0
  48. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
  49. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  50. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  51. package/src/__tests__/conversation-agent-loop.test.ts +457 -8
  52. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
  53. package/src/__tests__/conversation-error.test.ts +150 -3
  54. package/src/__tests__/conversation-init.benchmark.test.ts +1 -1
  55. package/src/__tests__/conversation-process-callsite.test.ts +38 -0
  56. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -0
  57. package/src/__tests__/conversation-runtime-assembly.test.ts +74 -0
  58. package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
  59. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  60. package/src/__tests__/conversation-store.test.ts +0 -18
  61. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
  62. package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
  63. package/src/__tests__/conversation-surfaces-data-persist.test.ts +476 -0
  64. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +61 -5
  65. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  66. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  67. package/src/__tests__/credentials-cli.test.ts +7 -0
  68. package/src/__tests__/cu-unified-flow.test.ts +176 -10
  69. package/src/__tests__/date-context.test.ts +164 -2
  70. package/src/__tests__/disk-pressure-guard.test.ts +262 -0
  71. package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
  72. package/src/__tests__/disk-pressure-policy.test.ts +241 -0
  73. package/src/__tests__/disk-pressure-routes.test.ts +379 -0
  74. package/src/__tests__/disk-pressure-tools.test.ts +277 -0
  75. package/src/__tests__/disk-usage.test.ts +150 -0
  76. package/src/__tests__/events-client-registration.test.ts +52 -0
  77. package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
  78. package/src/__tests__/file-write-tool.test.ts +4 -10
  79. package/src/__tests__/filing-service.test.ts +2 -20
  80. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -26
  81. package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
  82. package/src/__tests__/heartbeat-service.test.ts +260 -11
  83. package/src/__tests__/host-app-control-proxy.test.ts +195 -25
  84. package/src/__tests__/host-bash-proxy.test.ts +227 -34
  85. package/src/__tests__/host-bash-routes.test.ts +178 -13
  86. package/src/__tests__/host-cu-proxy.test.ts +210 -3
  87. package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
  88. package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
  89. package/src/__tests__/host-file-proxy.test.ts +268 -6
  90. package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
  91. package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
  92. package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
  93. package/src/__tests__/http-user-message-parity.test.ts +107 -1
  94. package/src/__tests__/injector-chain.test.ts +36 -16
  95. package/src/__tests__/injector-disk-pressure.test.ts +224 -0
  96. package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
  97. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +154 -67
  98. package/src/__tests__/managed-profile-guard.test.ts +18 -0
  99. package/src/__tests__/mcp-abort-signal.test.ts +130 -0
  100. package/src/__tests__/memory-admin-recall.test.ts +3 -11
  101. package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
  102. package/src/__tests__/normalize-onboarding.test.ts +180 -0
  103. package/src/__tests__/notification-decision-fallback.test.ts +91 -0
  104. package/src/__tests__/notification-decision-strategy.test.ts +22 -0
  105. package/src/__tests__/oauth-cli.test.ts +121 -0
  106. package/src/__tests__/oauth-connect-routes.test.ts +316 -0
  107. package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
  108. package/src/__tests__/onboarding-persona-write.test.ts +308 -0
  109. package/src/__tests__/openai-provider.test.ts +45 -8
  110. package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
  111. package/src/__tests__/platform-callback-registration.test.ts +21 -4
  112. package/src/__tests__/platform.test.ts +2 -1
  113. package/src/__tests__/playbook-execution.test.ts +0 -43
  114. package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
  115. package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
  116. package/src/__tests__/provider-tool-name.test.ts +23 -0
  117. package/src/__tests__/relay-server.test.ts +60 -5
  118. package/src/__tests__/runtime-events-sse.test.ts +4 -8
  119. package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
  120. package/src/__tests__/secret-ingress-http.test.ts +0 -1
  121. package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
  122. package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
  123. package/src/__tests__/secret-response-routing.test.ts +7 -5
  124. package/src/__tests__/server-history-render.test.ts +82 -0
  125. package/src/__tests__/skill-include-graph.test.ts +31 -0
  126. package/src/__tests__/skill-load-tool.test.ts +44 -16
  127. package/src/__tests__/skills.test.ts +39 -0
  128. package/src/__tests__/suggestion-routes.test.ts +46 -0
  129. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
  130. package/src/__tests__/tool-executor.test.ts +155 -0
  131. package/src/__tests__/twilio-validation.test.ts +2 -2
  132. package/src/__tests__/voice-session-bridge.test.ts +3 -0
  133. package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
  134. package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
  135. package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
  136. package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +120 -0
  137. package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
  138. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +78 -0
  139. package/src/agent/loop.ts +11 -0
  140. package/src/approvals/guardian-request-resolvers.ts +3 -32
  141. package/src/backup/snapshot-lock.ts +2 -27
  142. package/src/bundler/compiler-tools.ts +3 -2
  143. package/src/calls/call-constants.ts +5 -8
  144. package/src/calls/call-controller.ts +130 -67
  145. package/src/calls/call-conversation-messages.ts +46 -10
  146. package/src/calls/relay-server.ts +7 -1
  147. package/src/calls/voice-session-bridge.ts +1 -1
  148. package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
  149. package/src/cli/commands/bash.ts +35 -108
  150. package/src/cli/commands/contacts.ts +64 -25
  151. package/src/cli/commands/credentials.ts +56 -0
  152. package/src/cli/commands/memory-v2.ts +11 -10
  153. package/src/cli/commands/oauth/__tests__/connect.test.ts +401 -219
  154. package/src/cli/commands/oauth/connect.ts +124 -40
  155. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
  156. package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
  157. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  158. package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
  159. package/src/cli/commands/platform/index.ts +16 -7
  160. package/src/cli/commands/status.ts +57 -0
  161. package/src/cli/program.ts +4 -2
  162. package/src/config/assistant-feature-flags.ts +13 -3
  163. package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
  164. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
  165. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
  166. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
  167. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
  168. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
  169. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
  170. package/src/config/env.ts +0 -8
  171. package/src/config/feature-flag-registry.json +13 -5
  172. package/src/config/loader.ts +199 -27
  173. package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
  174. package/src/config/schemas/call-site-catalog.ts +14 -0
  175. package/src/config/schemas/channels.ts +0 -5
  176. package/src/config/schemas/heartbeat.ts +1 -1
  177. package/src/config/schemas/llm.ts +2 -0
  178. package/src/config/schemas/memory-lifecycle.ts +13 -0
  179. package/src/config/schemas/memory-v2.ts +76 -12
  180. package/src/config/schemas/platform.ts +43 -3
  181. package/src/config/schemas/services.ts +28 -0
  182. package/src/config/seed-inference-profiles.ts +230 -33
  183. package/src/contacts/contact-store.ts +0 -25
  184. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +32 -0
  185. package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
  186. package/src/daemon/assistant-attachments.ts +4 -4
  187. package/src/daemon/config-watcher.ts +85 -57
  188. package/src/daemon/conversation-agent-loop-handlers.ts +38 -0
  189. package/src/daemon/conversation-agent-loop.ts +183 -43
  190. package/src/daemon/conversation-error.ts +87 -15
  191. package/src/daemon/conversation-lifecycle.ts +22 -10
  192. package/src/daemon/conversation-process.ts +8 -0
  193. package/src/daemon/conversation-runtime-assembly.ts +26 -0
  194. package/src/daemon/conversation-store.ts +2 -2
  195. package/src/daemon/conversation-surfaces.ts +211 -29
  196. package/src/daemon/conversation-tool-setup.ts +66 -19
  197. package/src/daemon/conversation.ts +18 -23
  198. package/src/daemon/date-context.ts +71 -22
  199. package/src/daemon/disk-pressure-background-gate.ts +73 -0
  200. package/src/daemon/disk-pressure-guard.ts +343 -0
  201. package/src/daemon/disk-pressure-policy.ts +163 -0
  202. package/src/daemon/handlers/shared.ts +26 -1
  203. package/src/daemon/handlers/skills.ts +3 -4
  204. package/src/daemon/host-app-control-proxy.ts +137 -41
  205. package/src/daemon/host-bash-proxy.ts +47 -22
  206. package/src/daemon/host-browser-proxy.ts +1 -1
  207. package/src/daemon/host-cu-proxy.ts +50 -4
  208. package/src/daemon/host-file-proxy.ts +44 -8
  209. package/src/daemon/host-transfer-proxy.ts +97 -6
  210. package/src/daemon/lifecycle.ts +167 -101
  211. package/src/daemon/meet-host-supervisor.ts +4 -4
  212. package/src/daemon/meet-manifest-loader.ts +0 -1
  213. package/src/daemon/memory-v2-startup.ts +66 -15
  214. package/src/daemon/message-protocol.ts +3 -0
  215. package/src/daemon/message-types/conversations.ts +4 -0
  216. package/src/daemon/message-types/disk-pressure.ts +9 -0
  217. package/src/daemon/message-types/messages.ts +22 -1
  218. package/src/daemon/profiler-run-store.ts +5 -5
  219. package/src/daemon/tool-setup-types.ts +2 -2
  220. package/src/documents/document-store.ts +119 -0
  221. package/src/filing/filing-service.ts +29 -5
  222. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
  223. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
  224. package/src/heartbeat/heartbeat-run-store.ts +13 -0
  225. package/src/heartbeat/heartbeat-service.ts +205 -31
  226. package/src/home/feed-scheduler.ts +18 -0
  227. package/src/inbound/platform-callback-registration.ts +8 -15
  228. package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
  229. package/src/ipc/assistant-server.ts +149 -38
  230. package/src/ipc/gateway-client.ts +37 -3
  231. package/src/ipc/skill-server.ts +99 -42
  232. package/src/live-voice/live-voice-archive.ts +4 -4
  233. package/src/live-voice/protocol.ts +5 -7
  234. package/src/media/image-service.ts +1 -7
  235. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
  236. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +34 -51
  237. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
  238. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
  239. package/src/memory/admin.ts +5 -9
  240. package/src/memory/context-search/agent-runner.ts +19 -2
  241. package/src/memory/context-search/sources/conversations.ts +2 -11
  242. package/src/memory/context-search/sources/memory-v2.ts +1 -16
  243. package/src/memory/context-search/sources/memory.ts +2 -3
  244. package/src/memory/context-search/sources/pkb.ts +2 -3
  245. package/src/memory/context-search/types.ts +0 -1
  246. package/src/memory/conversation-crud.ts +4 -12
  247. package/src/memory/db-init.ts +2 -0
  248. package/src/memory/embedding-runtime-manager.ts +119 -5
  249. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +136 -82
  250. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
  251. package/src/memory/graph/conversation-graph-memory.ts +72 -61
  252. package/src/memory/graph/extraction.ts +1 -3
  253. package/src/memory/graph/graph-search.test.ts +11 -67
  254. package/src/memory/graph/graph-search.ts +4 -24
  255. package/src/memory/graph/retriever.test.ts +12 -1
  256. package/src/memory/graph/retriever.ts +10 -15
  257. package/src/memory/graph/tool-handlers.ts +3 -4
  258. package/src/memory/graph/tools.ts +4 -4
  259. package/src/memory/indexer.ts +53 -45
  260. package/src/memory/job-handlers/backfill.ts +2 -11
  261. package/src/memory/job-handlers/cleanup.ts +43 -0
  262. package/src/memory/job-handlers/embedding.ts +6 -8
  263. package/src/memory/job-handlers/summarization.ts +2 -7
  264. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +116 -0
  265. package/src/memory/jobs/embed-concept-page.ts +223 -87
  266. package/src/memory/jobs-store.ts +48 -0
  267. package/src/memory/jobs-worker.ts +85 -43
  268. package/src/memory/memory-v2-activation-log-store.ts +32 -14
  269. package/src/memory/memory-v2-concept-frequency.ts +169 -0
  270. package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
  271. package/src/memory/migrations/index.ts +1 -0
  272. package/src/memory/pkb/pkb-search.test.ts +7 -0
  273. package/src/memory/pkb/pkb-search.ts +4 -5
  274. package/src/memory/qdrant-client.ts +3 -13
  275. package/src/memory/rerank-local.ts +374 -0
  276. package/src/memory/search/semantic.ts +10 -72
  277. package/src/memory/trace-event-store.ts +1 -17
  278. package/src/memory/v2/__tests__/activation.test.ts +346 -255
  279. package/src/memory/v2/__tests__/consolidation-job.test.ts +61 -40
  280. package/src/memory/v2/__tests__/injection.test.ts +297 -190
  281. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
  282. package/src/memory/v2/__tests__/qdrant.test.ts +326 -9
  283. package/src/memory/v2/__tests__/reranker.test.ts +338 -0
  284. package/src/memory/v2/__tests__/sim.test.ts +113 -196
  285. package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
  286. package/src/memory/v2/__tests__/static-context.test.ts +77 -14
  287. package/src/memory/v2/__tests__/sweep-job.test.ts +19 -33
  288. package/src/memory/v2/activation.ts +149 -156
  289. package/src/memory/v2/consolidation-job.ts +69 -20
  290. package/src/memory/v2/injection.ts +75 -68
  291. package/src/memory/v2/page-store.ts +39 -0
  292. package/src/memory/v2/prompts/consolidation.ts +41 -1
  293. package/src/memory/v2/qdrant.ts +306 -46
  294. package/src/memory/v2/reranker.ts +177 -0
  295. package/src/memory/v2/sim.ts +77 -110
  296. package/src/memory/v2/skill-content.ts +4 -3
  297. package/src/memory/v2/skill-store.ts +82 -59
  298. package/src/memory/v2/static-context.ts +26 -8
  299. package/src/memory/v2/sweep-job.ts +5 -6
  300. package/src/memory/v2/types.ts +17 -10
  301. package/src/notifications/copy-composer.ts +47 -0
  302. package/src/notifications/decision-engine.ts +46 -0
  303. package/src/notifications/signal.ts +4 -0
  304. package/src/oauth/AGENTS.md +3 -1
  305. package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
  306. package/src/oauth/connect-orchestrator.ts +2 -0
  307. package/src/oauth/connection-resolver.test.ts +66 -1
  308. package/src/oauth/connection-resolver.ts +55 -1
  309. package/src/oauth/oauth-connect-state.ts +77 -0
  310. package/src/oauth/seed-providers.ts +58 -1
  311. package/src/permissions/gateway-threshold-reader.ts +116 -8
  312. package/src/permissions/prompter.ts +86 -96
  313. package/src/permissions/secret-prompter.ts +31 -31
  314. package/src/plugins/defaults/injectors.ts +36 -4
  315. package/src/plugins/defaults/memory-retrieval.ts +5 -6
  316. package/src/plugins/types.ts +7 -0
  317. package/src/proactive-artifact/aux-message-injector.ts +74 -0
  318. package/src/proactive-artifact/decision.test.ts +226 -0
  319. package/src/proactive-artifact/decision.ts +165 -0
  320. package/src/proactive-artifact/index.ts +7 -0
  321. package/src/proactive-artifact/job.test.ts +914 -0
  322. package/src/proactive-artifact/job.ts +366 -0
  323. package/src/proactive-artifact/message-copy.ts +58 -0
  324. package/src/proactive-artifact/trigger-state.test.ts +277 -0
  325. package/src/proactive-artifact/trigger-state.ts +119 -0
  326. package/src/prompts/normalize-onboarding.ts +80 -0
  327. package/src/prompts/persona-resolver.ts +101 -9
  328. package/src/prompts/system-prompt.ts +21 -7
  329. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  330. package/src/prompts/templates/SOUL.md +13 -28
  331. package/src/providers/__tests__/retry-callsite.test.ts +222 -1
  332. package/src/providers/model-intents.ts +7 -0
  333. package/src/providers/openrouter/client.ts +8 -0
  334. package/src/providers/retry.ts +50 -0
  335. package/src/providers/types.ts +1 -0
  336. package/src/runtime/__tests__/agent-wake.test.ts +456 -3
  337. package/src/runtime/agent-wake.ts +238 -100
  338. package/src/runtime/assistant-event-hub.ts +36 -6
  339. package/src/runtime/assistant-event.ts +0 -1
  340. package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
  341. package/src/runtime/auth/route-policy.ts +15 -1
  342. package/src/runtime/auth/same-actor.ts +216 -0
  343. package/src/runtime/channel-approvals.ts +3 -2
  344. package/src/runtime/channel-retry-sweep.ts +65 -1
  345. package/src/runtime/local-actor-identity.ts +52 -11
  346. package/src/runtime/pending-interactions.ts +27 -15
  347. package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
  348. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
  349. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  350. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
  351. package/src/runtime/routes/approval-routes.ts +7 -3
  352. package/src/runtime/routes/client-routes.ts +20 -2
  353. package/src/runtime/routes/consolidation-routes.ts +8 -9
  354. package/src/runtime/routes/contact-routes.ts +0 -25
  355. package/src/runtime/routes/conversation-query-routes.ts +44 -1
  356. package/src/runtime/routes/conversation-routes.ts +35 -26
  357. package/src/runtime/routes/debug-bash-routes.ts +165 -0
  358. package/src/runtime/routes/disk-pressure-routes.ts +121 -0
  359. package/src/runtime/routes/document-pdf-renderer.ts +6 -2
  360. package/src/runtime/routes/documents-routes.ts +2 -75
  361. package/src/runtime/routes/events-routes.ts +41 -9
  362. package/src/runtime/routes/filing-routes.ts +2 -3
  363. package/src/runtime/routes/host-bash-routes.ts +23 -3
  364. package/src/runtime/routes/host-cu-routes.ts +33 -6
  365. package/src/runtime/routes/host-file-routes.ts +32 -6
  366. package/src/runtime/routes/host-transfer-routes.ts +79 -16
  367. package/src/runtime/routes/identity-routes.ts +7 -138
  368. package/src/runtime/routes/inbound-message-handler.ts +77 -12
  369. package/src/runtime/routes/index.ts +6 -0
  370. package/src/runtime/routes/memory-item-routes.test.ts +37 -17
  371. package/src/runtime/routes/memory-item-routes.ts +5 -6
  372. package/src/runtime/routes/memory-v2-routes.ts +136 -17
  373. package/src/runtime/routes/oauth-connect-routes.ts +153 -0
  374. package/src/runtime/verification-outbound-actions.ts +4 -4
  375. package/src/schedule/run-script.ts +37 -5
  376. package/src/schedule/scheduler.ts +20 -1
  377. package/src/security/encrypted-store.ts +2 -0
  378. package/src/security/secure-keys.ts +55 -0
  379. package/src/skills/include-graph.ts +35 -13
  380. package/src/skills/remote-skill-policy.ts +4 -10
  381. package/src/subagent/index.ts +1 -7
  382. package/src/subagent/manager.ts +1 -15
  383. package/src/tasks/task-runner.ts +0 -1
  384. package/src/tasks/task-store.ts +0 -3
  385. package/src/tools/background-tool-registry.ts +17 -3
  386. package/src/tools/document/document-tool.ts +20 -0
  387. package/src/tools/executor.ts +18 -2
  388. package/src/tools/host-filesystem/edit.test.ts +151 -0
  389. package/src/tools/host-filesystem/edit.ts +43 -1
  390. package/src/tools/host-filesystem/read.test.ts +129 -0
  391. package/src/tools/host-filesystem/read.ts +43 -1
  392. package/src/tools/host-filesystem/transfer.test.ts +127 -2
  393. package/src/tools/host-filesystem/transfer.ts +56 -11
  394. package/src/tools/host-filesystem/write.test.ts +134 -0
  395. package/src/tools/host-filesystem/write.ts +43 -1
  396. package/src/tools/host-terminal/host-shell.ts +13 -6
  397. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  398. package/src/tools/memory/register.test.ts +14 -9
  399. package/src/tools/memory/register.ts +1 -2
  400. package/src/tools/permission-checker.ts +15 -0
  401. package/src/tools/provider-tool-name.ts +28 -0
  402. package/src/tools/registry.ts +30 -9
  403. package/src/tools/skills/load.ts +24 -20
  404. package/src/tools/terminal/shell.ts +9 -1
  405. package/src/tools/tool-approval-handler.ts +31 -6
  406. package/src/tools/tool-name-aliases.ts +19 -0
  407. package/src/tools/types.ts +43 -3
  408. package/src/tts/provider-catalog.ts +3 -5
  409. package/src/util/disk-usage.ts +138 -0
  410. package/src/util/platform.ts +21 -11
  411. package/src/util/process-liveness.ts +26 -0
  412. package/src/workspace/heartbeat-service.ts +19 -0
  413. package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
  414. package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
  415. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +14 -0
  416. package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
  417. package/src/workspace/migrations/069-seed-onboarding-threads.ts +28 -0
  418. package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
  419. package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
  420. package/src/workspace/migrations/registry.ts +14 -0
  421. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
  422. package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
  423. package/src/memory/v2/skill-qdrant.ts +0 -404
  424. package/src/signals/bash.ts +0 -198
@@ -47,11 +47,24 @@ mock.module("../daemon/host-transfer-proxy.js", () => ({
47
47
  getTargetClientIdForTransfer(_transferId: string) {
48
48
  return stubTargetClientId;
49
49
  },
50
+ getTargetActorPrincipalIdForTransfer(_transferId: string) {
51
+ return stubTargetClientId
52
+ ? clientActors.get(stubTargetClientId)
53
+ : undefined;
54
+ },
50
55
  getTransferContent(transferId: string) {
51
56
  getTransferContentCalls.push(transferId);
52
- return { buffer: Buffer.from("data"), sizeBytes: 4, sha256: "abc123" };
57
+ return {
58
+ buffer: Buffer.from("data"),
59
+ sizeBytes: 4,
60
+ sha256: "abc123",
61
+ };
53
62
  },
54
- async receiveTransferContent(transferId: string, _data: Buffer, _sha256: string) {
63
+ async receiveTransferContent(
64
+ transferId: string,
65
+ _data: Buffer,
66
+ _sha256: string,
67
+ ) {
55
68
  receiveTransferContentCalls.push(transferId);
56
69
  return { accepted: true };
57
70
  },
@@ -63,12 +76,20 @@ mock.module("../daemon/host-transfer-proxy.js", () => ({
63
76
  },
64
77
  }));
65
78
 
79
+ // Stub event hub so tests control what actorPrincipalId is associated with
80
+ // each connected client.
81
+ const clientActors = new Map<string, string>();
82
+
83
+ mock.module("../runtime/assistant-event-hub.js", () => ({
84
+ assistantEventHub: {
85
+ getActorPrincipalIdForClient: (clientId: string) =>
86
+ clientActors.get(clientId),
87
+ },
88
+ }));
89
+
66
90
  // ── Real imports (after mocks) ──────────────────────────────────────────────
67
91
 
68
- import {
69
- BadRequestError,
70
- ForbiddenError,
71
- } from "../runtime/routes/errors.js";
92
+ import { BadRequestError, ForbiddenError } from "../runtime/routes/errors.js";
72
93
  import { ROUTES } from "../runtime/routes/host-transfer-routes.js";
73
94
 
74
95
  afterAll(() => {
@@ -93,10 +114,19 @@ const TEST_TRANSFER_ID = "transfer-abc";
93
114
  const TEST_REQUEST_ID = "req-1";
94
115
 
95
116
  function registerPending(overrides: Partial<PendingInteraction> = {}): void {
117
+ // Mirror the production proxy: capture the target's actor principal at
118
+ // registration time so the result-route check compares against a stable
119
+ // value rather than the live hub.
120
+ const targetActorPrincipalId =
121
+ overrides.targetActorPrincipalId ??
122
+ (overrides.targetClientId
123
+ ? clientActors.get(overrides.targetClientId)
124
+ : undefined);
96
125
  pendingStore.set(TEST_REQUEST_ID, {
97
126
  conversationId: "conv-1",
98
127
  kind: "host_transfer",
99
128
  ...overrides,
129
+ targetActorPrincipalId,
100
130
  });
101
131
  }
102
132
 
@@ -107,6 +137,7 @@ describe("handleTransferContentGet — Phase 3 targetClientId guard", () => {
107
137
  pendingStore.clear();
108
138
  stubTargetClientId = null;
109
139
  getTransferContentCalls.length = 0;
140
+ clientActors.clear();
110
141
  });
111
142
 
112
143
  // ── 1. Targeted + correct header → success ────────────────────────────────
@@ -114,9 +145,13 @@ describe("handleTransferContentGet — Phase 3 targetClientId guard", () => {
114
145
  describe("targeted + correct x-vellum-client-id header", () => {
115
146
  test("returns Uint8Array and calls getTransferContent", async () => {
116
147
  stubTargetClientId = "client-A";
148
+ clientActors.set("client-A", "actor-1");
117
149
  const result = await handleTransferContentGet({
118
150
  pathParams: { transferId: TEST_TRANSFER_ID },
119
- headers: { "x-vellum-client-id": "client-A" },
151
+ headers: {
152
+ "x-vellum-client-id": "client-A",
153
+ "x-vellum-actor-principal-id": "actor-1",
154
+ },
120
155
  });
121
156
 
122
157
  expect(result).toBeInstanceOf(Uint8Array);
@@ -125,9 +160,13 @@ describe("handleTransferContentGet — Phase 3 targetClientId guard", () => {
125
160
 
126
161
  test("trims whitespace from header before comparing", async () => {
127
162
  stubTargetClientId = "client-A";
163
+ clientActors.set("client-A", "actor-1");
128
164
  const result = await handleTransferContentGet({
129
165
  pathParams: { transferId: TEST_TRANSFER_ID },
130
- headers: { "x-vellum-client-id": " client-A " },
166
+ headers: {
167
+ "x-vellum-client-id": " client-A ",
168
+ "x-vellum-actor-principal-id": " actor-1 ",
169
+ },
131
170
  });
132
171
 
133
172
  expect(result).toBeInstanceOf(Uint8Array);
@@ -199,6 +238,52 @@ describe("handleTransferContentGet — Phase 3 targetClientId guard", () => {
199
238
  expect(getTransferContentCalls).toContain(TEST_TRANSFER_ID);
200
239
  });
201
240
  });
241
+
242
+ // ── 5. Same-user actor binding (defense-in-depth) ─────────────────────────
243
+
244
+ describe("targeted + actor principal binding", () => {
245
+ test("throws ForbiddenError when submitting actor does not match target client's actor", () => {
246
+ stubTargetClientId = "client-A";
247
+ clientActors.set("client-A", "actor-victim");
248
+ expect(() =>
249
+ handleTransferContentGet({
250
+ pathParams: { transferId: TEST_TRANSFER_ID },
251
+ headers: {
252
+ "x-vellum-client-id": "client-A",
253
+ "x-vellum-actor-principal-id": "actor-attacker",
254
+ },
255
+ }),
256
+ ).toThrow(ForbiddenError);
257
+ expect(getTransferContentCalls).toHaveLength(0);
258
+ });
259
+
260
+ test("throws ForbiddenError when actor principal header is missing", () => {
261
+ stubTargetClientId = "client-A";
262
+ clientActors.set("client-A", "actor-victim");
263
+ expect(() =>
264
+ handleTransferContentGet({
265
+ pathParams: { transferId: TEST_TRANSFER_ID },
266
+ headers: { "x-vellum-client-id": "client-A" },
267
+ }),
268
+ ).toThrow(ForbiddenError);
269
+ expect(getTransferContentCalls).toHaveLength(0);
270
+ });
271
+
272
+ test("throws ForbiddenError when target client has no stored actor", () => {
273
+ stubTargetClientId = "client-A";
274
+ // No entry in clientActors for "client-A"
275
+ expect(() =>
276
+ handleTransferContentGet({
277
+ pathParams: { transferId: TEST_TRANSFER_ID },
278
+ headers: {
279
+ "x-vellum-client-id": "client-A",
280
+ "x-vellum-actor-principal-id": "actor-1",
281
+ },
282
+ }),
283
+ ).toThrow(ForbiddenError);
284
+ expect(getTransferContentCalls).toHaveLength(0);
285
+ });
286
+ });
202
287
  });
203
288
 
204
289
  describe("handleTransferContentPut — Phase 3 targetClientId guard", () => {
@@ -206,6 +291,7 @@ describe("handleTransferContentPut — Phase 3 targetClientId guard", () => {
206
291
  pendingStore.clear();
207
292
  stubTargetClientId = null;
208
293
  receiveTransferContentCalls.length = 0;
294
+ clientActors.clear();
209
295
  });
210
296
 
211
297
  // ── 1. Targeted + correct header → success ────────────────────────────────
@@ -213,9 +299,14 @@ describe("handleTransferContentPut — Phase 3 targetClientId guard", () => {
213
299
  describe("targeted + correct x-vellum-client-id header", () => {
214
300
  test("returns { accepted: true } and calls receiveTransferContent", async () => {
215
301
  stubTargetClientId = "client-A";
302
+ clientActors.set("client-A", "actor-1");
216
303
  const result = await handleTransferContentPut({
217
304
  pathParams: { transferId: TEST_TRANSFER_ID },
218
- headers: { "x-vellum-client-id": "client-A", "x-transfer-sha256": "abc" },
305
+ headers: {
306
+ "x-vellum-client-id": "client-A",
307
+ "x-vellum-actor-principal-id": "actor-1",
308
+ "x-transfer-sha256": "abc",
309
+ },
219
310
  rawBody: new Uint8Array(Buffer.from("data")),
220
311
  });
221
312
 
@@ -225,9 +316,14 @@ describe("handleTransferContentPut — Phase 3 targetClientId guard", () => {
225
316
 
226
317
  test("trims whitespace from header before comparing", async () => {
227
318
  stubTargetClientId = "client-A";
319
+ clientActors.set("client-A", "actor-1");
228
320
  const result = await handleTransferContentPut({
229
321
  pathParams: { transferId: TEST_TRANSFER_ID },
230
- headers: { "x-vellum-client-id": " client-A ", "x-transfer-sha256": "abc" },
322
+ headers: {
323
+ "x-vellum-client-id": " client-A ",
324
+ "x-vellum-actor-principal-id": " actor-1 ",
325
+ "x-transfer-sha256": "abc",
326
+ },
231
327
  rawBody: new Uint8Array(Buffer.from("data")),
232
328
  });
233
329
 
@@ -272,7 +368,10 @@ describe("handleTransferContentPut — Phase 3 targetClientId guard", () => {
272
368
  await expect(
273
369
  handleTransferContentPut({
274
370
  pathParams: { transferId: TEST_TRANSFER_ID },
275
- headers: { "x-vellum-client-id": "client-B", "x-transfer-sha256": "abc" },
371
+ headers: {
372
+ "x-vellum-client-id": "client-B",
373
+ "x-transfer-sha256": "abc",
374
+ },
276
375
  rawBody: new Uint8Array(Buffer.from("data")),
277
376
  }),
278
377
  ).rejects.toBeInstanceOf(ForbiddenError);
@@ -283,7 +382,10 @@ describe("handleTransferContentPut — Phase 3 targetClientId guard", () => {
283
382
  try {
284
383
  await handleTransferContentPut({
285
384
  pathParams: { transferId: TEST_TRANSFER_ID },
286
- headers: { "x-vellum-client-id": "client-B", "x-transfer-sha256": "abc" },
385
+ headers: {
386
+ "x-vellum-client-id": "client-B",
387
+ "x-transfer-sha256": "abc",
388
+ },
287
389
  rawBody: new Uint8Array(Buffer.from("data")),
288
390
  });
289
391
  } catch {
@@ -308,6 +410,60 @@ describe("handleTransferContentPut — Phase 3 targetClientId guard", () => {
308
410
  expect(receiveTransferContentCalls).toContain(TEST_TRANSFER_ID);
309
411
  });
310
412
  });
413
+
414
+ // ── 5. Same-user actor binding (defense-in-depth) ─────────────────────────
415
+
416
+ describe("targeted + actor principal binding", () => {
417
+ test("rejects when submitting actor does not match target client's actor", async () => {
418
+ stubTargetClientId = "client-A";
419
+ clientActors.set("client-A", "actor-victim");
420
+ await expect(
421
+ handleTransferContentPut({
422
+ pathParams: { transferId: TEST_TRANSFER_ID },
423
+ headers: {
424
+ "x-vellum-client-id": "client-A",
425
+ "x-vellum-actor-principal-id": "actor-attacker",
426
+ "x-transfer-sha256": "abc",
427
+ },
428
+ rawBody: new Uint8Array(Buffer.from("data")),
429
+ }),
430
+ ).rejects.toBeInstanceOf(ForbiddenError);
431
+ expect(receiveTransferContentCalls).toHaveLength(0);
432
+ });
433
+
434
+ test("rejects when actor principal header is missing", async () => {
435
+ stubTargetClientId = "client-A";
436
+ clientActors.set("client-A", "actor-victim");
437
+ await expect(
438
+ handleTransferContentPut({
439
+ pathParams: { transferId: TEST_TRANSFER_ID },
440
+ headers: {
441
+ "x-vellum-client-id": "client-A",
442
+ "x-transfer-sha256": "abc",
443
+ },
444
+ rawBody: new Uint8Array(Buffer.from("data")),
445
+ }),
446
+ ).rejects.toBeInstanceOf(ForbiddenError);
447
+ expect(receiveTransferContentCalls).toHaveLength(0);
448
+ });
449
+
450
+ test("rejects when target client has no stored actor", async () => {
451
+ stubTargetClientId = "client-A";
452
+ // No entry in clientActors for "client-A"
453
+ await expect(
454
+ handleTransferContentPut({
455
+ pathParams: { transferId: TEST_TRANSFER_ID },
456
+ headers: {
457
+ "x-vellum-client-id": "client-A",
458
+ "x-vellum-actor-principal-id": "actor-1",
459
+ "x-transfer-sha256": "abc",
460
+ },
461
+ rawBody: new Uint8Array(Buffer.from("data")),
462
+ }),
463
+ ).rejects.toBeInstanceOf(ForbiddenError);
464
+ expect(receiveTransferContentCalls).toHaveLength(0);
465
+ });
466
+ });
311
467
  });
312
468
 
313
469
  describe("handleTransferResult — Phase 3 targetClientId guard", () => {
@@ -315,6 +471,7 @@ describe("handleTransferResult — Phase 3 targetClientId guard", () => {
315
471
  pendingStore.clear();
316
472
  stubTargetClientId = null;
317
473
  resolveTransferResultCalls.length = 0;
474
+ clientActors.clear();
318
475
  });
319
476
 
320
477
  function registerHostTransferPending(targetClientId?: string): void {
@@ -329,10 +486,14 @@ describe("handleTransferResult — Phase 3 targetClientId guard", () => {
329
486
 
330
487
  describe("targeted + correct x-vellum-client-id header", () => {
331
488
  test("returns { accepted: true } and calls resolveTransferResult", async () => {
489
+ clientActors.set("client-A", "actor-1");
332
490
  registerHostTransferPending("client-A");
333
491
  const result = await handleTransferResult({
334
492
  body: resultBody(),
335
- headers: { "x-vellum-client-id": "client-A" },
493
+ headers: {
494
+ "x-vellum-client-id": "client-A",
495
+ "x-vellum-actor-principal-id": "actor-1",
496
+ },
336
497
  });
337
498
 
338
499
  expect(result).toEqual({ accepted: true });
@@ -340,10 +501,14 @@ describe("handleTransferResult — Phase 3 targetClientId guard", () => {
340
501
  });
341
502
 
342
503
  test("trims whitespace from header before comparing", async () => {
504
+ clientActors.set("client-A", "actor-1");
343
505
  registerHostTransferPending("client-A");
344
506
  const result = await handleTransferResult({
345
507
  body: resultBody(),
346
- headers: { "x-vellum-client-id": " client-A " },
508
+ headers: {
509
+ "x-vellum-client-id": " client-A ",
510
+ "x-vellum-actor-principal-id": " actor-1 ",
511
+ },
347
512
  });
348
513
 
349
514
  expect(result).toEqual({ accepted: true });
@@ -355,9 +520,9 @@ describe("handleTransferResult — Phase 3 targetClientId guard", () => {
355
520
  describe("targeted + missing x-vellum-client-id header", () => {
356
521
  test("throws BadRequestError when header is absent", () => {
357
522
  registerHostTransferPending("client-A");
358
- expect(() =>
359
- handleTransferResult({ body: resultBody() }),
360
- ).toThrow(BadRequestError);
523
+ expect(() => handleTransferResult({ body: resultBody() })).toThrow(
524
+ BadRequestError,
525
+ );
361
526
  });
362
527
 
363
528
  test("resolveTransferResult NOT called on 400", () => {
@@ -444,4 +609,54 @@ describe("handleTransferResult — Phase 3 targetClientId guard", () => {
444
609
  expect(result).toEqual({ accepted: true });
445
610
  });
446
611
  });
612
+
613
+ // ── 5. Same-user actor binding (defense-in-depth) ─────────────────────────
614
+
615
+ describe("targeted + actor principal binding", () => {
616
+ test("throws ForbiddenError when submitting actor does not match target client's actor", () => {
617
+ registerHostTransferPending("client-A");
618
+ clientActors.set("client-A", "actor-victim");
619
+ expect(() =>
620
+ handleTransferResult({
621
+ body: resultBody(),
622
+ headers: {
623
+ "x-vellum-client-id": "client-A",
624
+ "x-vellum-actor-principal-id": "actor-attacker",
625
+ },
626
+ }),
627
+ ).toThrow(ForbiddenError);
628
+ expect(resolveTransferResultCalls).toHaveLength(0);
629
+ // Pending interaction should still be present (not consumed on 403).
630
+ expect(pendingStore.has(TEST_REQUEST_ID)).toBe(true);
631
+ });
632
+
633
+ test("throws ForbiddenError when actor principal header is missing", () => {
634
+ registerHostTransferPending("client-A");
635
+ clientActors.set("client-A", "actor-victim");
636
+ expect(() =>
637
+ handleTransferResult({
638
+ body: resultBody(),
639
+ headers: { "x-vellum-client-id": "client-A" },
640
+ }),
641
+ ).toThrow(ForbiddenError);
642
+ expect(resolveTransferResultCalls).toHaveLength(0);
643
+ expect(pendingStore.has(TEST_REQUEST_ID)).toBe(true);
644
+ });
645
+
646
+ test("throws ForbiddenError when target client has no stored actor", () => {
647
+ registerHostTransferPending("client-A");
648
+ // No entry in clientActors for "client-A"
649
+ expect(() =>
650
+ handleTransferResult({
651
+ body: resultBody(),
652
+ headers: {
653
+ "x-vellum-client-id": "client-A",
654
+ "x-vellum-actor-principal-id": "actor-1",
655
+ },
656
+ }),
657
+ ).toThrow(ForbiddenError);
658
+ expect(resolveTransferResultCalls).toHaveLength(0);
659
+ expect(pendingStore.has(TEST_REQUEST_ID)).toBe(true);
660
+ });
661
+ });
447
662
  });
@@ -252,12 +252,24 @@ async function sendMessage(
252
252
  content: string,
253
253
  conversationObj: import("../daemon/conversation.js").Conversation,
254
254
  extra: Record<string, unknown> = {},
255
+ options: {
256
+ onGetOrCreateConversation?: (
257
+ conversationId: string,
258
+ opts?: Record<string, unknown>,
259
+ ) => void;
260
+ } = {},
255
261
  ) {
256
262
  return callHandler(
257
263
  (args) =>
258
264
  handleSendMessage(args, {
259
265
  sendMessageDeps: {
260
- getOrCreateConversation: async () => conversationObj,
266
+ getOrCreateConversation: async (conversationId, opts) => {
267
+ options.onGetOrCreateConversation?.(
268
+ conversationId,
269
+ opts as Record<string, unknown> | undefined,
270
+ );
271
+ return conversationObj;
272
+ },
261
273
  assistantEventHub: { publish: async () => {} } as any,
262
274
  resolveAttachments: () => [],
263
275
  },
@@ -326,3 +338,97 @@ describe("HTTP POST /v1/messages does not intercept recording intents (by design
326
338
  expect(runAgentLoop).toHaveBeenCalledTimes(1);
327
339
  });
328
340
  });
341
+
342
+ // ============================================================================
343
+ // CLIENT TIMEZONE — optional HTTP metadata
344
+ // ============================================================================
345
+ describe("HTTP POST /v1/messages clientTimezone transport metadata", () => {
346
+ beforeEach(() => {
347
+ routeGuardianReplyMock.mockClear();
348
+ listPendingByDestinationMock.mockClear();
349
+ listCanonicalMock.mockClear();
350
+ addMessageMock.mockClear();
351
+ });
352
+
353
+ test("passes canonical clientTimezone through host-proxy transport", async () => {
354
+ const persistUserMessage = mock(async () => "persisted-msg-id");
355
+ const runAgentLoop = mock(async () => undefined);
356
+ const conversation = makeConversation({ persistUserMessage, runAgentLoop });
357
+ let capturedOptions: Record<string, unknown> | undefined;
358
+
359
+ const res = await sendMessage(
360
+ "hello",
361
+ conversation,
362
+ { clientTimezone: "america/new_york" },
363
+ {
364
+ onGetOrCreateConversation: (_conversationId, opts) => {
365
+ capturedOptions = opts;
366
+ },
367
+ },
368
+ );
369
+
370
+ expect(res.status).toBe(202);
371
+ expect(capturedOptions).toEqual({
372
+ transport: {
373
+ channelId: "vellum",
374
+ interfaceId: "macos",
375
+ clientTimezone: "America/New_York",
376
+ },
377
+ });
378
+ });
379
+
380
+ test("passes canonical clientTimezone through non-host-proxy transport", async () => {
381
+ const persistUserMessage = mock(async () => "persisted-msg-id");
382
+ const runAgentLoop = mock(async () => undefined);
383
+ const conversation = makeConversation({ persistUserMessage, runAgentLoop });
384
+ let capturedOptions: Record<string, unknown> | undefined;
385
+
386
+ const res = await sendMessage(
387
+ "hello",
388
+ conversation,
389
+ { interface: "ios", clientTimezone: "europe/london" },
390
+ {
391
+ onGetOrCreateConversation: (_conversationId, opts) => {
392
+ capturedOptions = opts;
393
+ },
394
+ },
395
+ );
396
+
397
+ expect(res.status).toBe(202);
398
+ expect(capturedOptions).toEqual({
399
+ transport: {
400
+ channelId: "vellum",
401
+ interfaceId: "ios",
402
+ clientTimezone: "Europe/London",
403
+ },
404
+ });
405
+ });
406
+
407
+ test("drops invalid clientTimezone without rejecting the message", async () => {
408
+ const persistUserMessage = mock(async () => "persisted-msg-id");
409
+ const runAgentLoop = mock(async () => undefined);
410
+ const conversation = makeConversation({ persistUserMessage, runAgentLoop });
411
+ let capturedOptions: Record<string, unknown> | undefined;
412
+
413
+ const res = await sendMessage(
414
+ "hello",
415
+ conversation,
416
+ { clientTimezone: "not-a-timezone" },
417
+ {
418
+ onGetOrCreateConversation: (_conversationId, opts) => {
419
+ capturedOptions = opts;
420
+ },
421
+ },
422
+ );
423
+
424
+ expect(res.status).toBe(202);
425
+ expect(capturedOptions).toEqual({
426
+ transport: {
427
+ channelId: "vellum",
428
+ interfaceId: "macos",
429
+ },
430
+ });
431
+ expect(persistUserMessage).toHaveBeenCalledTimes(1);
432
+ expect(runAgentLoop).toHaveBeenCalledTimes(1);
433
+ });
434
+ });
@@ -4,10 +4,11 @@
4
4
  *
5
5
  * Covers:
6
6
  *
7
- * 1. The nine default injectors registered by `defaultInjectorsPlugin` come
8
- * back from `getInjectors()` in the documented order (workspace-context →
9
- * unified-turn-contextpkb-context → pkb-reminder → memory-v2-static
10
- * now-mdsubagent-statusslack-messagesthread-focus).
7
+ * 1. The ten default injectors registered by `defaultInjectorsPlugin` come
8
+ * back from `getInjectors()` in the documented order
9
+ * (disk-pressure-warningworkspace-context → unified-turn-context
10
+ * pkb-contextpkb-remindermemory-v2-staticnow-md →
11
+ * subagent-status → slack-messages → thread-focus).
11
12
  * 2. A third-party-registered injector at `order: 25` slots between
12
13
  * `unified-turn-context` (order 20) and `pkb` (order 30), proving the
13
14
  * extensibility contract.
@@ -23,16 +24,30 @@
23
24
  * content.
24
25
  */
25
26
 
26
- import { beforeEach, describe, expect, test } from "bun:test";
27
-
28
- import {
29
- applyRuntimeInjections,
30
- composeInjectorChain,
31
- } from "../daemon/conversation-runtime-assembly.js";
32
- import {
33
- DEFAULT_INJECTOR_ORDER,
34
- defaultInjectorsPlugin,
35
- } from "../plugins/defaults/injectors.js";
27
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
28
+
29
+ // This test exercises v1 PKB injection. `config.memory.v2.enabled`
30
+ // (default `true`) makes the PKB injector go silent — force it off here
31
+ // so the v1 injection chain assertions stay meaningful.
32
+ const realLoader = await import("../config/loader.js");
33
+ const realGetConfig = realLoader.getConfig;
34
+ mock.module("../config/loader.js", () => ({
35
+ ...realLoader,
36
+ getConfig: () => {
37
+ const real = realGetConfig();
38
+ return {
39
+ ...real,
40
+ memory: { ...real.memory, v2: { ...real.memory.v2, enabled: false } },
41
+ };
42
+ },
43
+ }));
44
+
45
+ const { applyRuntimeInjections, composeInjectorChain } = await import(
46
+ "../daemon/conversation-runtime-assembly.js"
47
+ );
48
+ const { DEFAULT_INJECTOR_ORDER, defaultInjectorsPlugin } = await import(
49
+ "../plugins/defaults/injectors.js"
50
+ );
36
51
  import {
37
52
  getInjectors,
38
53
  registerPlugin,
@@ -76,11 +91,12 @@ describe("injector chain", () => {
76
91
  resetPluginRegistryForTests();
77
92
  });
78
93
 
79
- test("defaultInjectorsPlugin registers the nine defaults in the documented order", () => {
94
+ test("defaultInjectorsPlugin registers the ten defaults in the documented order", () => {
80
95
  registerPlugin(defaultInjectorsPlugin);
81
96
 
82
97
  const names = getInjectors().map((i) => i.name);
83
98
  expect(names).toEqual([
99
+ "disk-pressure-warning",
84
100
  "workspace-context",
85
101
  "unified-turn-context",
86
102
  "pkb-context",
@@ -97,6 +113,9 @@ describe("injector chain", () => {
97
113
  registerPlugin(defaultInjectorsPlugin);
98
114
 
99
115
  const byName = new Map(getInjectors().map((i) => [i.name, i.order]));
116
+ expect(byName.get("disk-pressure-warning")).toBe(
117
+ DEFAULT_INJECTOR_ORDER.diskPressureWarning,
118
+ );
100
119
  expect(byName.get("workspace-context")).toBe(
101
120
  DEFAULT_INJECTOR_ORDER.workspaceContext,
102
121
  );
@@ -132,6 +151,7 @@ describe("injector chain", () => {
132
151
 
133
152
  const names = getInjectors().map((i) => i.name);
134
153
  expect(names).toEqual([
154
+ "disk-pressure-warning", // 5
135
155
  "workspace-context", // 10
136
156
  "unified-turn-context", // 20
137
157
  "plugin-25", // 25 — slots in
@@ -146,7 +166,7 @@ describe("injector chain", () => {
146
166
  });
147
167
 
148
168
  test("composeInjectorChain returns empty string when every injector opts out", async () => {
149
- // The default chain is the golden-path: all nine defaults return `null`
169
+ // The default chain is the golden-path: all ten defaults return `null`
150
170
  // on an empty turn context, so the composed block is an empty string.
151
171
  registerPlugin(defaultInjectorsPlugin);
152
172