@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
package/ARCHITECTURE.md CHANGED
@@ -21,6 +21,20 @@ This document owns assistant-runtime architecture details. The repo-level archit
21
21
  - Voice calls mirror the same prompt contract: `CallController` receives guardian context on setup and refreshes it immediately after successful voice challenge verification, so the first post-verification turn is grounded as `actor_role: guardian`.
22
22
  - Voice-specific behavior (DTMF/speech verification flow, relay state machine) remains voice-local; only actor-role resolution is shared.
23
23
 
24
+ ### Safe Storage Limits
25
+
26
+ Safe storage limits are gated by the assistant feature flag `safe-storage-limits`, default off. When the flag is off, the disk pressure guard reports a disabled status and no runtime path blocks work, injects cleanup guidance, or changes tool access.
27
+
28
+ **Disk pressure state:** `src/daemon/disk-pressure-guard.ts` samples workspace storage usage every 60 seconds through `src/util/disk-usage.ts`. At or above the 95% critical threshold it creates an in-memory lock with `lockId`, usage snapshot, `acknowledged`, `overrideActive`, `effectivelyLocked`, and the blocked capabilities `agent-turns`, `background-work`, and `remote-ingress`. The lock clears when usage drops below the threshold or the process restarts. `acknowledgeDiskPressureLock()` only lets the guardian enter cleanup mode; `overrideDiskPressureLock()` requires the exact phrase `I understand the risks` and disables the effective lock while usage remains critical.
29
+
30
+ **Runtime API and events:** `src/runtime/routes/disk-pressure-routes.ts` exposes `GET /v1/disk-pressure/status`, `POST /v1/disk-pressure/acknowledge`, and `POST /v1/disk-pressure/override`. Route auth policies require normal runtime protection, and `disk_pressure_status_changed` events are emitted when the status changes so clients can update live.
31
+
32
+ **Turn policy:** `src/daemon/disk-pressure-policy.ts` classifies turns before the main agent loop. Local guardian/owner turns are allowed in cleanup mode; trusted contacts, non-guardian actors, unknown remote senders, background conversations, direct wakes, and non-main LLM call sites are blocked while `effectivelyLocked` is true. Blocked turns emit terminal conversation errors rather than reaching the provider.
33
+
34
+ **Background work:** Heartbeats, scheduled tasks, filing work, retry sweeps, and background tool completions call `src/daemon/disk-pressure-background-gate.ts` before starting work. While effectively locked they skip the wake or job and log throttled disk-pressure fields.
35
+
36
+ **Prompt and tools:** Cleanup-mode turns carry `diskPressureContext` through runtime assembly and receive the `<disk_pressure_warning>` injector in `src/plugins/defaults/injectors.ts`. The instruction tells the assistant to warn first, focus only on freeing storage, inspect before deleting, ask for deletion approval, and explain that background processes and trusted-contact messages are blocked. Tool setup marks the turn as cleanup mode; `src/tools/tool-approval-handler.ts` rejects non-cleanup-safe tools, and foreground shell inspection remains available while background `bash` and `host_bash` modes are rejected. When a new lock is created, active background terminal tools are cancelled with reason `disk_pressure`.
37
+
24
38
  ### Single-Header JWT Auth Model
25
39
 
26
40
  All HTTP API requests use a single `Authorization: Bearer <jwt>` header for authentication. The JWT carries identity, permissions, and policy versioning in a unified token.
@@ -966,6 +980,7 @@ The daemon emits two distinct error message types via SSE:
966
980
  | -------------------------------- | ----------------------------------------------------------------------- | --------- |
967
981
  | `PROVIDER_NETWORK` | Unable to reach the LLM provider (connection refused, timeout, DNS) | Yes |
968
982
  | `PROVIDER_RATE_LIMIT` | LLM provider rate-limited the request (HTTP 429) | Yes |
983
+ | `MANAGED_USAGE_LIMIT` | Vellum managed inference usage limit or quota was exceeded (HTTP 429) | Yes |
969
984
  | `PROVIDER_API` | Provider returned a server error (5xx) or retryable 4xx | Yes |
970
985
  | `PROVIDER_BILLING` | Invalid/expired API key or insufficient credits (HTTP 401, billing 4xx) | No |
971
986
  | `CONTEXT_TOO_LARGE` | Request exceeds the model's context window (HTTP 413, token limit) | No |
@@ -980,7 +995,7 @@ The daemon classifies errors via `classifyConversationError()` in `conversation-
980
995
 
981
996
  Classification uses a two-tier strategy:
982
997
 
983
- 1. **Structured provider errors**: If the error is a `ProviderError` with a `statusCode`, the status code determines the category deterministically — `413` maps to `CONTEXT_TOO_LARGE` (not retryable), `401` maps to `PROVIDER_BILLING` (not retryable, invalid/expired key), `429` maps to `PROVIDER_RATE_LIMIT` (retryable), `5xx` to `PROVIDER_API` (retryable), other `4xx` to `PROVIDER_API` (retryable) unless a message pattern matches a more specific non-retryable category (context-too-large, billing/auth).
998
+ 1. **Structured provider errors**: If the error is a `ProviderError` with a `statusCode`, the status code determines the category deterministically — `413` maps to `CONTEXT_TOO_LARGE` (not retryable), `401` maps to `PROVIDER_BILLING` (not retryable, invalid/expired key), `429` maps to `MANAGED_USAGE_LIMIT` when the provider is routed through the managed proxy or the payload matches a Vellum managed quota/limit response, otherwise `PROVIDER_RATE_LIMIT` (retryable), `5xx` to `PROVIDER_API` (retryable), other `4xx` to `PROVIDER_API` (retryable) unless a message pattern matches a more specific non-retryable category (context-too-large, billing/auth).
984
999
  2. **Regex fallback**: For non-provider errors or `ProviderError` without a status code, regex pattern matching against the error message detects network failures, rate limits, and API errors. Phase-specific overrides handle regeneration contexts.
985
1000
 
986
1001
  Debug details are capped at 4,000 characters to prevent oversized payloads.
@@ -1263,7 +1278,7 @@ graph TB
1263
1278
  - Managed-store writes are atomic (tmp file + rename) to prevent partial `SKILL.md` or `SKILLS.md` files.
1264
1279
  - After persist or delete, the file watcher triggers conversation eviction; the next turn runs in a fresh conversation. The model's system prompt instructs it to continue normally.
1265
1280
  - macOS UI shows Inspect and Delete controls for managed skills only (source = "managed").
1266
- - `skill_load` validates the recursive include graph (via `include-graph.ts`) before emitting output. Missing children and cycles produce `isError: true` with no `<loaded_skill>` marker. Valid includes produce an "Included Skills (immediate)" metadata section showing child ID, name, description, and path.
1281
+ - `skill_load` resolves the recursive include graph (via `include-graph.ts`) before emitting output. Missing children are listed as suggested skills without child `<loaded_skill>` markers; cycles still produce `isError: true` with no marker. Valid includes produce an "Included Skills (immediate)" metadata section showing child ID, name, description, and path.
1267
1282
 
1268
1283
  ### Skills Authoring via HTTP
1269
1284
 
@@ -1274,32 +1289,33 @@ The Skills page in the macOS client can author managed skills through the daemon
1274
1289
 
1275
1290
  ### Include Graph Validation
1276
1291
 
1277
- Skills can declare child relationships via the `includes` frontmatter field (a JSON array of skill IDs). When `skill_load` loads a parent skill, it validates the full recursive include graph before emitting output.
1292
+ Skills can declare child relationships via the `includes` frontmatter field (a JSON array of skill IDs). When `skill_load` loads a parent skill, it attempts to resolve and auto-install missing includes before emitting output. Available includes are appended to the loaded skill output; unavailable includes are surfaced as suggestions instead of blocking the parent skill.
1278
1293
 
1279
1294
  ```mermaid
1280
1295
  graph LR
1281
1296
  LOAD["skill_load(parent)"] --> CATALOG["loadSkillCatalog()"]
1282
1297
  CATALOG --> INDEX["indexCatalogById()"]
1283
- INDEX --> VALIDATE["validateIncludes(rootId, index)"]
1284
- VALIDATE -->|"ok"| OUTPUT["Emit output +<br/>Included Skills (immediate)<br/>+ loaded_skill marker"]
1285
- VALIDATE -->|"missing child"| ERR_MISSING["isError: true<br/>no loaded_skill marker"]
1286
- VALIDATE -->|"cycle detected"| ERR_CYCLE["isError: true<br/>no loaded_skill marker"]
1298
+ INDEX --> AUTOINSTALL["Attempt catalog auto-install<br/>for missing includes"]
1299
+ AUTOINSTALL --> RESOLVE["collectAllMissing(rootId, index)<br/>+ validateIncludeCycles(rootId, index)"]
1300
+ RESOLVE -->|"ok + no missing child"| OUTPUT["Emit output +<br/>Included Skills (immediate)<br/>+ loaded_skill markers"]
1301
+ RESOLVE -->|"ok + missing child"| OUTPUT_MISSING["Emit parent output +<br/>Suggested Included Skills<br/>without child markers"]
1302
+ RESOLVE -->|"cycle detected"| ERR_CYCLE["isError: true<br/>no loaded_skill marker"]
1287
1303
  ```
1288
1304
 
1289
1305
  **Validation rules:**
1290
1306
 
1291
- - **Missing children**: If any skill in the recursive graph references an `includes` ID not found in the catalog, validation fails with the full path from root to the missing reference.
1307
+ - **Missing children**: Missing includes trigger catalog auto-install attempts. Any include still unavailable is listed under "Suggested Included Skills (not loaded)" and does not receive a `<loaded_skill>` marker.
1292
1308
  - **Cycles**: Three-state DFS (unseen → visiting → done) detects direct and indirect cycles. The error includes the cycle path.
1293
- - **Fail-closed**: On any validation error, `skill_load` returns `isError: true` with no `<loaded_skill>` marker, preventing the agent from using a skill with broken dependencies.
1309
+ - **Fail-closed cycles**: Circular include chains still return `isError: true` with no `<loaded_skill>` marker.
1294
1310
 
1295
- **Key constraint**: Include metadata is metadata-only. Child skills are **not** auto-activated the agent must explicitly call `skill_load` for each child. The `projectSkillTools()` function only projects tools for skills with explicit `<loaded_skill>` markers in conversation history.
1311
+ **Key constraint**: Include metadata is advisory. Available included skills are appended to the parent output and receive explicit `<loaded_skill>` markers; unavailable included skills remain suggestions so the agent can search for and install them if the task needs their guidance or tools.
1296
1312
 
1297
- | Source File | Purpose |
1298
- | --------------------------------------- | ------------------------------------------------------------------------------------------ |
1299
- | `assistant/src/skills/include-graph.ts` | `indexCatalogById()`, `getImmediateChildren()`, `validateIncludes()`, `traverseIncludes()` |
1300
- | `assistant/src/tools/skills/load.ts` | Include validation integration in `skill_load` execute path |
1301
- | `assistant/src/config/skills.ts` | `includes` field parsing from SKILL.md frontmatter |
1302
- | `assistant/src/skills/managed-store.ts` | `includes` emission in `buildSkillMarkdown()` |
1313
+ | Source File | Purpose |
1314
+ | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
1315
+ | `assistant/src/skills/include-graph.ts` | `indexCatalogById()`, `getImmediateChildren()`, `validateIncludes()`, `validateIncludeCycles()`, `traverseIncludes()` |
1316
+ | `assistant/src/tools/skills/load.ts` | Include resolution integration in `skill_load` execute path |
1317
+ | `assistant/src/config/skills.ts` | `includes` field parsing from SKILL.md frontmatter |
1318
+ | `assistant/src/skills/managed-store.ts` | `includes` emission in `buildSkillMarkdown()` |
1303
1319
 
1304
1320
  ---
1305
1321
 
@@ -1538,19 +1554,19 @@ Every layer in the pipeline defaults to rejection rather than silent degradation
1538
1554
 
1539
1555
  ### Key Source Files
1540
1556
 
1541
- | File | Role |
1542
- | --------------------------------------------------- | ------------------------------------------------------------------------------------------ |
1543
- | `assistant/src/config/skills.ts` | Skill catalog loading: bundled, managed, workspace, extra directories |
1544
- | `assistant/src/config/bundled-skills/` | Bundled skill directories (browser, gmail, computer-use, weather, etc.) |
1545
- | `assistant/src/skills/tool-manifest.ts` | `TOOLS.json` parser and validator |
1546
- | `assistant/src/skills/active-skill-tools.ts` | `deriveActiveSkills()` — scans history for `<loaded_skill>` markers |
1547
- | `assistant/src/skills/include-graph.ts` | Include graph builder: `indexCatalogById()`, `validateIncludes()`, cycle/missing detection |
1548
- | `assistant/src/daemon/conversation-skill-tools.ts` | `projectSkillTools()` — per-turn projection, register/unregister lifecycle |
1549
- | `assistant/src/tools/skills/skill-tool-factory.ts` | `createSkillToolsFromManifest()` — manifest entries to Tool objects |
1550
- | `assistant/src/tools/skills/skill-script-runner.ts` | Host runner: dynamic import + `run()` call |
1551
- | `assistant/src/tools/skills/sandbox-runner.ts` | Sandbox runner: isolated subprocess execution |
1552
- | `assistant/src/tools/registry.ts` | `registerSkillTools()` / `unregisterSkillTools()` — global tool registry |
1553
- | `assistant/src/permissions/checker.ts` | Skill-origin default-ask permission policy |
1557
+ | File | Role |
1558
+ | --------------------------------------------------- | -------------------------------------------------------------------------------------------- |
1559
+ | `assistant/src/config/skills.ts` | Skill catalog loading: bundled, managed, workspace, extra directories |
1560
+ | `assistant/src/config/bundled-skills/` | Bundled skill directories (browser, gmail, computer-use, weather, etc.) |
1561
+ | `assistant/src/skills/tool-manifest.ts` | `TOOLS.json` parser and validator |
1562
+ | `assistant/src/skills/active-skill-tools.ts` | `deriveActiveSkills()` — scans history for `<loaded_skill>` markers |
1563
+ | `assistant/src/skills/include-graph.ts` | Include graph builder: `indexCatalogById()`, `validateIncludes()`, `validateIncludeCycles()` |
1564
+ | `assistant/src/daemon/conversation-skill-tools.ts` | `projectSkillTools()` — per-turn projection, register/unregister lifecycle |
1565
+ | `assistant/src/tools/skills/skill-tool-factory.ts` | `createSkillToolsFromManifest()` — manifest entries to Tool objects |
1566
+ | `assistant/src/tools/skills/skill-script-runner.ts` | Host runner: dynamic import + `run()` call |
1567
+ | `assistant/src/tools/skills/sandbox-runner.ts` | Sandbox runner: isolated subprocess execution |
1568
+ | `assistant/src/tools/registry.ts` | `registerSkillTools()` / `unregisterSkillTools()` — global tool registry |
1569
+ | `assistant/src/permissions/checker.ts` | Skill-origin default-ask permission policy |
1554
1570
 
1555
1571
  ---
1556
1572
 
package/Dockerfile CHANGED
@@ -22,6 +22,7 @@ COPY packages/service-contracts ./packages/service-contracts
22
22
  COPY packages/credential-storage ./packages/credential-storage
23
23
  COPY packages/egress-proxy ./packages/egress-proxy
24
24
  COPY packages/gateway-client ./packages/gateway-client
25
+ COPY packages/ipc-server-utils ./packages/ipc-server-utils
25
26
  COPY packages/skill-host-contracts ./packages/skill-host-contracts
26
27
  COPY packages/slack-text ./packages/slack-text
27
28
  COPY packages/twilio-client ./packages/twilio-client
@@ -31,18 +31,70 @@ mock.module("../../src/ipc/gateway-client.js", () => ({
31
31
  },
32
32
  }));
33
33
 
34
- // Suppress logger output in tests
35
- mock.module("../../src/util/logger.js", () => ({
36
- getLogger: () => ({
37
- warn: () => {},
38
- info: () => {},
39
- error: () => {},
40
- debug: () => {},
41
- }),
42
- }));
34
+ // Capture logger output so coalescing tests can assert on it. Existing
35
+ // tests don't read the array, so capturing is invisible to them.
36
+ //
37
+ // Bun's `mock.module("../../src/util/logger.js", ...)` does not intercept
38
+ // transitive imports (see comment in stt-hints.test.ts and avatar-e2e.test.ts).
39
+ // Mocking `pino` at the package level works because getLogger uses pino
40
+ // child loggers under the hood — intercepting pino captures everything.
41
+ interface LogCall {
42
+ level: "warn" | "info" | "error" | "debug";
43
+ fields: Record<string, unknown>;
44
+ message: string;
45
+ }
46
+ const logCalls: LogCall[] = [];
47
+
48
+ function makeLogFn(level: LogCall["level"]) {
49
+ return (
50
+ fieldsOrMsg: Record<string, unknown> | string,
51
+ maybeMsg?: string,
52
+ ) => {
53
+ if (typeof fieldsOrMsg === "string") {
54
+ logCalls.push({ level, fields: {}, message: fieldsOrMsg });
55
+ } else {
56
+ logCalls.push({
57
+ level,
58
+ fields: fieldsOrMsg,
59
+ message: maybeMsg ?? "",
60
+ });
61
+ }
62
+ };
63
+ }
64
+
65
+ const mockChildLogger = {
66
+ debug: () => {},
67
+ info: makeLogFn("info"),
68
+ warn: makeLogFn("warn"),
69
+ error: makeLogFn("error"),
70
+ fatal: () => {},
71
+ trace: () => {},
72
+ silent: () => {},
73
+ // pino loggers are themselves callable as a no-op shorthand; child() returns
74
+ // another logger.
75
+ child(): typeof mockChildLogger {
76
+ return mockChildLogger;
77
+ },
78
+ bindings: () => ({}),
79
+ level: "info",
80
+ };
81
+
82
+ const mockPinoLogger = Object.assign(() => mockChildLogger, {
83
+ destination: () => ({}),
84
+ multistream: () => ({}),
85
+ stdTimeFunctions: { isoTime: () => "" },
86
+ stdSerializers: {},
87
+ symbols: {},
88
+ });
89
+
90
+ mock.module("pino", () => ({ default: mockPinoLogger }));
91
+ mock.module("pino-pretty", () => ({ default: () => ({}) }));
43
92
 
44
93
  import {
45
94
  _clearGlobalCacheForTesting,
95
+ _getFailureStateForTesting,
96
+ _resetFailureCoalesceForTesting,
97
+ _setFailureWarnIntervalForTesting,
46
98
  getAutoApproveThreshold,
47
99
  } from "../../src/permissions/gateway-threshold-reader.js";
48
100
 
@@ -51,7 +103,9 @@ import {
51
103
  function resetMocks(): void {
52
104
  ipcCallLog.length = 0;
53
105
  ipcHandler = () => undefined;
106
+ logCalls.length = 0;
54
107
  _clearGlobalCacheForTesting();
108
+ _resetFailureCoalesceForTesting();
55
109
  }
56
110
 
57
111
  afterEach(resetMocks);
@@ -221,3 +275,176 @@ describe("getAutoApproveThreshold", () => {
221
275
  expect(ipcCallLog).toEqual(["/v1/permissions/thresholds"]);
222
276
  });
223
277
  });
278
+
279
+ // ── Failure coalescing ───────────────────────────────────────────────────────
280
+
281
+ describe("failure-coalescing log behavior", () => {
282
+ test("first failure WARNs immediately and starts a streak", async () => {
283
+ ipcHandler = () => {
284
+ throw new Error("Connection refused");
285
+ };
286
+
287
+ expect(await getAutoApproveThreshold(undefined, "background")).toBe("none");
288
+
289
+ const warns = logCalls.filter((c) => c.level === "warn");
290
+ expect(warns.length).toBe(1);
291
+ expect(warns[0]?.fields).toMatchObject({
292
+ op: "global_thresholds",
293
+ consecutiveFailures: 1,
294
+ event: "ipc_threshold_failure",
295
+ });
296
+
297
+ const state = _getFailureStateForTesting("global_thresholds");
298
+ expect(state).toBeDefined();
299
+ expect(state?.consecutiveFailures).toBe(1);
300
+ });
301
+
302
+ test("subsequent failures within the WARN window do not log but still increment state", async () => {
303
+ // 1-hour window so the test never accidentally crosses it.
304
+ _setFailureWarnIntervalForTesting(60 * 60 * 1000);
305
+ ipcHandler = () => {
306
+ throw new Error("ENOENT");
307
+ };
308
+
309
+ for (let i = 0; i < 100; i++) {
310
+ _clearGlobalCacheForTesting(); // force re-fetch each call
311
+ await getAutoApproveThreshold(undefined, "background");
312
+ }
313
+
314
+ const warns = logCalls.filter((c) => c.level === "warn");
315
+ // At most one WARN — the very first call. All 99 follow-ups suppressed.
316
+ expect(warns.length).toBe(1);
317
+
318
+ const state = _getFailureStateForTesting("global_thresholds");
319
+ expect(state?.consecutiveFailures).toBe(100);
320
+ });
321
+
322
+ test("a fresh WARN fires once the cadence window elapses", async () => {
323
+ // 5ms window so the test runs fast.
324
+ _setFailureWarnIntervalForTesting(5);
325
+ ipcHandler = () => {
326
+ throw new Error("ENOENT");
327
+ };
328
+
329
+ await getAutoApproveThreshold(undefined, "background");
330
+ expect(logCalls.filter((c) => c.level === "warn").length).toBe(1);
331
+
332
+ // Wait past the window then fail again.
333
+ await new Promise((r) => setTimeout(r, 20));
334
+ _clearGlobalCacheForTesting();
335
+ await getAutoApproveThreshold(undefined, "background");
336
+
337
+ const warns = logCalls.filter((c) => c.level === "warn");
338
+ expect(warns.length).toBe(2);
339
+ // Second WARN includes the streak metadata so dashboards can see how
340
+ // many failures were swallowed in between.
341
+ expect(warns[1]?.fields).toMatchObject({
342
+ op: "global_thresholds",
343
+ consecutiveFailures: 2,
344
+ event: "ipc_threshold_failure",
345
+ });
346
+ expect(warns[1]?.fields.streakDurationMs).toBeDefined();
347
+ });
348
+
349
+ test("recovery emits an INFO with the swallowed-failure count and clears state", async () => {
350
+ _setFailureWarnIntervalForTesting(60 * 60 * 1000);
351
+ let working = false;
352
+ ipcHandler = (method) => {
353
+ if (working && method === "get_global_thresholds") {
354
+ return { interactive: "medium", autonomous: "low" };
355
+ }
356
+ throw new Error("ENOENT");
357
+ };
358
+
359
+ // Three failures, then it recovers.
360
+ for (let i = 0; i < 3; i++) {
361
+ _clearGlobalCacheForTesting();
362
+ await getAutoApproveThreshold(undefined, "background");
363
+ }
364
+ expect(_getFailureStateForTesting("global_thresholds")?.consecutiveFailures).toBe(
365
+ 3,
366
+ );
367
+
368
+ working = true;
369
+ _clearGlobalCacheForTesting();
370
+ expect(await getAutoApproveThreshold(undefined, "background")).toBe("low");
371
+
372
+ const infos = logCalls.filter((c) => c.level === "info");
373
+ expect(infos.length).toBe(1);
374
+ expect(infos[0]?.fields).toMatchObject({
375
+ op: "global_thresholds",
376
+ swallowedFailures: 3,
377
+ event: "ipc_threshold_recovered",
378
+ });
379
+ expect(infos[0]?.fields.streakDurationMs).toBeDefined();
380
+
381
+ expect(_getFailureStateForTesting("global_thresholds")).toBeUndefined();
382
+ });
383
+
384
+ test("conversation and global ops have independent failure streaks", async () => {
385
+ _setFailureWarnIntervalForTesting(60 * 60 * 1000);
386
+ // conversation IPC fails (transport — returns undefined), global IPC works.
387
+ ipcHandler = (method) => {
388
+ if (method === "get_conversation_threshold") return undefined;
389
+ if (method === "get_global_thresholds") {
390
+ return { interactive: "medium", autonomous: "low" };
391
+ }
392
+ return undefined;
393
+ };
394
+
395
+ // First call: conversation transport fails, global succeeds.
396
+ expect(await getAutoApproveThreshold("conv-1", "conversation")).toBe(
397
+ "medium",
398
+ );
399
+
400
+ expect(
401
+ _getFailureStateForTesting("conversation_threshold")?.consecutiveFailures,
402
+ ).toBe(1);
403
+ expect(_getFailureStateForTesting("global_thresholds")).toBeUndefined();
404
+
405
+ const warns = logCalls.filter((c) => c.level === "warn");
406
+ expect(warns.length).toBe(1);
407
+ expect(warns[0]?.fields.op).toBe("conversation_threshold");
408
+ });
409
+
410
+ test("a successful conversation override clears the conversation streak even when the gateway returns null (no override)", async () => {
411
+ _setFailureWarnIntervalForTesting(60 * 60 * 1000);
412
+
413
+ // First two calls: conversation IPC returns undefined (transport failure).
414
+ let working = false;
415
+ ipcHandler = (method) => {
416
+ if (method === "get_conversation_threshold") {
417
+ return working ? null : undefined;
418
+ }
419
+ if (method === "get_global_thresholds") {
420
+ return { interactive: "low", autonomous: "none" };
421
+ }
422
+ return undefined;
423
+ };
424
+
425
+ // Force two transport failures.
426
+ await getAutoApproveThreshold("conv-2", "conversation");
427
+ await new Promise((r) => setTimeout(r, 6)); // bypass the 5s convo cache
428
+ _clearGlobalCacheForTesting();
429
+ // Convo cache is keyed on conversationId — change the id to bypass.
430
+ await getAutoApproveThreshold("conv-3", "conversation");
431
+ expect(
432
+ _getFailureStateForTesting("conversation_threshold")?.consecutiveFailures,
433
+ ).toBe(2);
434
+
435
+ // Now the IPC starts working — even a null "no override" response is a
436
+ // successful round-trip and must clear the streak.
437
+ working = true;
438
+ _clearGlobalCacheForTesting();
439
+ await getAutoApproveThreshold("conv-4", "conversation");
440
+
441
+ const infos = logCalls.filter((c) => c.level === "info");
442
+ expect(infos.length).toBe(1);
443
+ expect(infos[0]?.fields).toMatchObject({
444
+ op: "conversation_threshold",
445
+ swallowedFailures: 2,
446
+ event: "ipc_threshold_recovered",
447
+ });
448
+ expect(_getFailureStateForTesting("conversation_threshold")).toBeUndefined();
449
+ });
450
+ });
package/bun.lock CHANGED
@@ -18,6 +18,7 @@
18
18
  "@vellumai/credential-storage": "file:../packages/credential-storage",
19
19
  "@vellumai/egress-proxy": "file:../packages/egress-proxy",
20
20
  "@vellumai/gateway-client": "file:../packages/gateway-client",
21
+ "@vellumai/ipc-server-utils": "file:../packages/ipc-server-utils",
21
22
  "@vellumai/service-contracts": "file:../packages/service-contracts",
22
23
  "@vellumai/skill-host-contracts": "file:../packages/skill-host-contracts",
23
24
  "@vellumai/slack-text": "file:../packages/slack-text",
@@ -421,6 +422,8 @@
421
422
 
422
423
  "@vellumai/gateway-client": ["@vellumai/gateway-client@file:../packages/gateway-client", { "dependencies": { "@vellumai/service-contracts": "file:../service-contracts" }, "devDependencies": { "@types/bun": "1.3.10", "typescript": "5.9.3" } }],
423
424
 
425
+ "@vellumai/ipc-server-utils": ["@vellumai/ipc-server-utils@file:../packages/ipc-server-utils", { "devDependencies": { "@types/bun": "1.3.10", "typescript": "5.9.3" } }],
426
+
424
427
  "@vellumai/service-contracts": ["@vellumai/service-contracts@file:../packages/service-contracts", { "dependencies": { "zod": "4.3.6" }, "devDependencies": { "@types/bun": "1.2.4", "typescript": "5.7.3" } }],
425
428
 
426
429
  "@vellumai/skill-host-contracts": ["@vellumai/skill-host-contracts@file:../packages/skill-host-contracts", { "devDependencies": { "@types/bun": "1.3.10", "typescript": "5.9.3" } }],
@@ -472,7 +472,7 @@ The Anthropic provider places `cache_control: { type: 'ephemeral' }` on the **la
472
472
 
473
473
  The session injects a unified `<turn_context>` block into every user message, giving the model awareness of the current timestamp (with timezone), interface, channel, and actor identity. This replaces the former separate `<temporal_context>`, `<inbound_actor_context>`, and per-channel turn context blocks. The unified block persists in conversation history so the assistant retains temporal and actor grounding across turns. Legacy blocks from pre-change history are stripped for backward compatibility.
474
474
 
475
- The `current_time:` field format is: `2026-04-02 (Wednesday) 14:30:00 -05:00 (America/Chicago)` — date, weekday name, local time, UTC offset, and IANA timezone name.
475
+ The `current_time:` field format is: `2026-04-02 (Wednesday) 14:30:00 -05:00 (America/Chicago)` — date, weekday name, local time, UTC offset, and IANA timezone name. The timestamp is grounded in the user's effective timezone, not UTC, so a message sent at 10pm local time is represented as 10pm for date/time reasoning.
476
476
 
477
477
  ### Per-turn flow
478
478
 
@@ -492,7 +492,10 @@ graph TB
492
492
 
493
493
  - **Fresh each turn**: `buildUnifiedTurnContextBlock()` is called at the start of every agent loop invocation, ensuring the model always sees the current timestamp even in long-running conversations.
494
494
  - **Clock source invariant**: Absolute time (`now`) always comes from the assistant host clock (`Date.now()`), never from channel/client clocks.
495
- - **Timezone precedence**: If `ui.userTimezone` is configured, turn context uses it for local-date interpretation. Otherwise it falls back to memory-stored timezone, then assistant host timezone.
495
+ - **Timezone precedence**: Turn context resolves the effective timezone in this order: explicit runtime override for tests/legacy callers, manual `ui.userTimezone`, current turn `clientTimezone`, persisted `ui.detectedTimezone`, then assistant host timezone. The host clock still supplies the absolute instant; this cascade only selects the local timezone used to render `current_time`.
496
+ - **Manual override semantics**: `ui.userTimezone` is a historical config path, but it is runtime-affecting, not purely presentational. When set, it is authoritative for `current_time` across all clients until the user clears or changes it.
497
+ - **Device timezone semantics**: `clientTimezone` is the timezone reported with the active message for this turn. `ui.detectedTimezone` is the last device-detected timezone persisted by a client and is only used when there is no manual override and the current message does not carry a client timezone.
498
+ - **Timezone mismatch guidance**: When `ui.userTimezone` differs from the current device timezone (`clientTimezone`, or `ui.detectedTimezone` when no current client value exists), `<turn_context>` also includes `configured_user_timezone`, `client_device_timezone`, and `timezone_update_available`. The last line tells the assistant that, after explicit user confirmation, it can persist the device timezone with `assistant config set ui.userTimezone "<IANA zone>"`. This gives the assistant a natural-language path to fix stale manual overrides without adding a dedicated tool.
496
499
  - **Timezone-aware**: Uses `Intl.DateTimeFormat` APIs for DST-safe date arithmetic and timezone validation/canonicalization.
497
500
  - **Persists in history**: The `<turn_context>` block persists in conversation history. Legacy `<temporal_context>`, `<inbound_actor_context>`, and separate channel context blocks from pre-change history are stripped for backward compatibility.
498
501
  - **Retry paths**: Turn context is included in all `applyRuntimeInjections` call sites (main path, compact retry, media-trim retry).
package/knip.json CHANGED
@@ -11,6 +11,7 @@
11
11
  "@vellumai/credential-storage",
12
12
  "@vellumai/egress-proxy",
13
13
  "@vellumai/gateway-client",
14
+ "@vellumai/ipc-server-utils",
14
15
  "@vellumai/service-contracts",
15
16
  "@vellumai/slack-text",
16
17
  "@vellumai/twilio-client",
@@ -62,13 +62,22 @@ const CONNECT_TIMEOUT_MS = 3_000;
62
62
  * Designed for CLI and daemon startup where we need a single RPC call
63
63
  * without leaving open handles. Returns `undefined` on any failure
64
64
  * (socket not found, timeout, parse error) so callers can fall back.
65
+ *
66
+ * @param timeoutMs - Optional override for both the connect and call
67
+ * timeouts. When omitted, defaults to the module constants
68
+ * (CONNECT_TIMEOUT_MS / DEFAULT_CALL_TIMEOUT_MS). Pass a small value
69
+ * (e.g. 200) for opportunistic CLI checks where a slow/absent gateway
70
+ * should fail fast rather than block startup.
65
71
  */
66
72
  export async function ipcCall(
67
73
  socketPath: string,
68
74
  method: string,
69
75
  params?: Record<string, unknown>,
70
76
  log: Logger = noopLogger,
77
+ timeoutMs?: number,
71
78
  ): Promise<unknown> {
79
+ const connectTimeoutMs = timeoutMs ?? CONNECT_TIMEOUT_MS;
80
+ const callTimeoutMs = timeoutMs ?? DEFAULT_CALL_TIMEOUT_MS;
72
81
  return new Promise<unknown>((resolve) => {
73
82
  let settled = false;
74
83
  let callTimer: ReturnType<typeof setTimeout> | undefined;
@@ -84,11 +93,11 @@ export async function ipcCall(
84
93
 
85
94
  const connectTimer = setTimeout(() => {
86
95
  log.warn(
87
- { method, socketPath, timeoutMs: CONNECT_TIMEOUT_MS },
96
+ { method, socketPath, timeoutMs: connectTimeoutMs },
88
97
  "IPC connect timed out",
89
98
  );
90
99
  finish(undefined);
91
- }, CONNECT_TIMEOUT_MS);
100
+ }, connectTimeoutMs);
92
101
 
93
102
  const socket: Socket = connect(socketPath);
94
103
  socket.unref();
@@ -103,11 +112,11 @@ export async function ipcCall(
103
112
 
104
113
  callTimer = setTimeout(() => {
105
114
  log.warn(
106
- { method, socketPath, timeoutMs: DEFAULT_CALL_TIMEOUT_MS },
115
+ { method, socketPath, timeoutMs: callTimeoutMs },
107
116
  "IPC call timed out waiting for response",
108
117
  );
109
118
  finish(undefined);
110
- }, DEFAULT_CALL_TIMEOUT_MS);
119
+ }, callTimeoutMs);
111
120
 
112
121
  socket.on("data", (chunk) => {
113
122
  buffer += chunk.toString();
@@ -0,0 +1,24 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "@vellumai/ipc-server-utils",
7
+ "devDependencies": {
8
+ "@types/bun": "1.3.10",
9
+ "typescript": "5.9.3",
10
+ },
11
+ },
12
+ },
13
+ "packages": {
14
+ "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
15
+
16
+ "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
17
+
18
+ "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
19
+
20
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
21
+
22
+ "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
23
+ }
24
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@vellumai/ipc-server-utils",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": "./src/index.ts"
9
+ },
10
+ "scripts": {
11
+ "typecheck": "bunx tsc --noEmit",
12
+ "test": "bun test src/"
13
+ },
14
+ "devDependencies": {
15
+ "@types/bun": "1.3.10",
16
+ "typescript": "5.9.3"
17
+ }
18
+ }
@@ -0,0 +1,6 @@
1
+ export {
2
+ SocketWatchdog,
3
+ ensureSocketDir,
4
+ type SocketWatchdogOptions,
5
+ type SocketWatchdogLogger,
6
+ } from "./socket-watchdog.js";