@vellumai/assistant 0.7.2 → 0.7.3

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 (347) hide show
  1. package/ARCHITECTURE.md +16 -1
  2. package/docs/architecture/memory.md +5 -2
  3. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
  4. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
  5. package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
  6. package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
  7. package/openapi.yaml +449 -22
  8. package/package.json +1 -1
  9. package/src/__tests__/app-control-flow.test.ts +21 -11
  10. package/src/__tests__/assistant-event-hub.test.ts +48 -0
  11. package/src/__tests__/assistant-event.test.ts +0 -10
  12. package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
  13. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
  14. package/src/__tests__/auto-analysis-end-to-end.test.ts +62 -1
  15. package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
  16. package/src/__tests__/call-conversation-messages.test.ts +8 -2
  17. package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
  18. package/src/__tests__/channel-readiness-service.test.ts +4 -2
  19. package/src/__tests__/config-loader-backfill.test.ts +379 -0
  20. package/src/__tests__/config-schema.test.ts +1 -0
  21. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
  22. package/src/__tests__/config-watcher.test.ts +140 -69
  23. package/src/__tests__/context-search-agent-runner.test.ts +61 -3
  24. package/src/__tests__/context-search-conversations-source.test.ts +0 -24
  25. package/src/__tests__/context-search-fanout.test.ts +0 -1
  26. package/src/__tests__/context-search-memory-source.test.ts +3 -7
  27. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
  28. package/src/__tests__/context-search-pkb-source.test.ts +0 -1
  29. package/src/__tests__/context-search-workspace-source.test.ts +0 -1
  30. package/src/__tests__/conversation-abort-tool-results.test.ts +6 -0
  31. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
  32. package/src/__tests__/conversation-agent-loop.test.ts +454 -5
  33. package/src/__tests__/conversation-error.test.ts +150 -3
  34. package/src/__tests__/conversation-process-callsite.test.ts +43 -0
  35. package/src/__tests__/conversation-provider-retry-repair.test.ts +6 -0
  36. package/src/__tests__/conversation-runtime-assembly.test.ts +65 -0
  37. package/src/__tests__/conversation-slash-unknown.test.ts +6 -0
  38. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  39. package/src/__tests__/conversation-store.test.ts +0 -18
  40. package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
  41. package/src/__tests__/conversation-surfaces-data-persist.test.ts +404 -0
  42. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +2 -5
  43. package/src/__tests__/conversation-workspace-injection.test.ts +6 -0
  44. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -0
  45. package/src/__tests__/credentials-cli.test.ts +7 -0
  46. package/src/__tests__/cu-unified-flow.test.ts +176 -10
  47. package/src/__tests__/date-context.test.ts +164 -2
  48. package/src/__tests__/disk-pressure-guard.test.ts +262 -0
  49. package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
  50. package/src/__tests__/disk-pressure-policy.test.ts +241 -0
  51. package/src/__tests__/disk-pressure-routes.test.ts +379 -0
  52. package/src/__tests__/disk-pressure-tools.test.ts +277 -0
  53. package/src/__tests__/disk-usage.test.ts +150 -0
  54. package/src/__tests__/events-client-registration.test.ts +52 -0
  55. package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
  56. package/src/__tests__/file-write-tool.test.ts +4 -10
  57. package/src/__tests__/filing-service.test.ts +3 -4
  58. package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
  59. package/src/__tests__/heartbeat-service.test.ts +260 -11
  60. package/src/__tests__/host-app-control-proxy.test.ts +195 -25
  61. package/src/__tests__/host-bash-proxy.test.ts +227 -34
  62. package/src/__tests__/host-bash-routes.test.ts +178 -13
  63. package/src/__tests__/host-cu-proxy.test.ts +210 -3
  64. package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
  65. package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
  66. package/src/__tests__/host-file-proxy.test.ts +268 -6
  67. package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
  68. package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
  69. package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
  70. package/src/__tests__/http-user-message-parity.test.ts +107 -1
  71. package/src/__tests__/injector-chain.test.ts +18 -6
  72. package/src/__tests__/injector-disk-pressure.test.ts +224 -0
  73. package/src/__tests__/managed-profile-guard.test.ts +18 -0
  74. package/src/__tests__/mcp-abort-signal.test.ts +130 -0
  75. package/src/__tests__/memory-admin-recall.test.ts +3 -11
  76. package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
  77. package/src/__tests__/normalize-onboarding.test.ts +180 -0
  78. package/src/__tests__/oauth-connect-routes.test.ts +316 -0
  79. package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
  80. package/src/__tests__/onboarding-persona-write.test.ts +308 -0
  81. package/src/__tests__/openai-provider.test.ts +45 -8
  82. package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
  83. package/src/__tests__/platform-callback-registration.test.ts +21 -4
  84. package/src/__tests__/platform.test.ts +2 -1
  85. package/src/__tests__/playbook-execution.test.ts +0 -43
  86. package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
  87. package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
  88. package/src/__tests__/provider-tool-name.test.ts +23 -0
  89. package/src/__tests__/relay-server.test.ts +15 -4
  90. package/src/__tests__/runtime-events-sse.test.ts +4 -8
  91. package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
  92. package/src/__tests__/secret-ingress-http.test.ts +0 -1
  93. package/src/__tests__/suggestion-routes.test.ts +46 -0
  94. package/src/__tests__/twilio-validation.test.ts +2 -2
  95. package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
  96. package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
  97. package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
  98. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +90 -0
  99. package/src/approvals/guardian-decision-primitive.ts +13 -0
  100. package/src/approvals/guardian-request-resolvers.ts +16 -17
  101. package/src/backup/snapshot-lock.ts +2 -27
  102. package/src/bundler/compiler-tools.ts +3 -2
  103. package/src/calls/call-conversation-messages.ts +46 -10
  104. package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
  105. package/src/cli/commands/bash.ts +35 -108
  106. package/src/cli/commands/contacts.ts +64 -25
  107. package/src/cli/commands/credentials.ts +56 -0
  108. package/src/cli/commands/memory-v2.ts +7 -6
  109. package/src/cli/commands/oauth/__tests__/connect.test.ts +437 -1
  110. package/src/cli/commands/oauth/connect.ts +127 -1
  111. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
  112. package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
  113. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  114. package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
  115. package/src/cli/commands/platform/index.ts +16 -7
  116. package/src/cli/commands/status.ts +57 -0
  117. package/src/cli/program.ts +4 -2
  118. package/src/config/assistant-feature-flags.ts +13 -3
  119. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
  120. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
  121. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
  122. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
  123. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
  124. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
  125. package/src/config/env.ts +0 -8
  126. package/src/config/feature-flag-registry.json +27 -3
  127. package/src/config/loader.ts +127 -8
  128. package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
  129. package/src/config/schemas/call-site-catalog.ts +14 -0
  130. package/src/config/schemas/channels.ts +0 -5
  131. package/src/config/schemas/heartbeat.ts +1 -1
  132. package/src/config/schemas/llm.ts +2 -0
  133. package/src/config/schemas/memory-lifecycle.ts +13 -0
  134. package/src/config/schemas/memory-v2.ts +75 -11
  135. package/src/config/schemas/platform.ts +43 -3
  136. package/src/config/schemas/services.ts +28 -0
  137. package/src/config/seed-inference-profiles.ts +230 -33
  138. package/src/contacts/contact-store.ts +0 -25
  139. package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
  140. package/src/daemon/assistant-attachments.ts +4 -4
  141. package/src/daemon/config-watcher.ts +85 -57
  142. package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
  143. package/src/daemon/conversation-agent-loop.ts +170 -33
  144. package/src/daemon/conversation-error.ts +87 -15
  145. package/src/daemon/conversation-lifecycle.ts +1 -3
  146. package/src/daemon/conversation-process.ts +8 -0
  147. package/src/daemon/conversation-runtime-assembly.ts +26 -0
  148. package/src/daemon/conversation-store.ts +2 -2
  149. package/src/daemon/conversation-surfaces.ts +195 -15
  150. package/src/daemon/conversation-tool-setup.ts +57 -14
  151. package/src/daemon/conversation.ts +17 -22
  152. package/src/daemon/date-context.ts +71 -22
  153. package/src/daemon/disk-pressure-background-gate.ts +73 -0
  154. package/src/daemon/disk-pressure-guard.ts +343 -0
  155. package/src/daemon/disk-pressure-policy.ts +163 -0
  156. package/src/daemon/handlers/shared.ts +0 -1
  157. package/src/daemon/handlers/skills.ts +3 -4
  158. package/src/daemon/host-app-control-proxy.ts +137 -41
  159. package/src/daemon/host-bash-proxy.ts +46 -21
  160. package/src/daemon/host-cu-proxy.ts +49 -3
  161. package/src/daemon/host-file-proxy.ts +43 -7
  162. package/src/daemon/host-transfer-proxy.ts +95 -4
  163. package/src/daemon/lifecycle.ts +79 -28
  164. package/src/daemon/meet-host-supervisor.ts +4 -4
  165. package/src/daemon/meet-manifest-loader.ts +0 -1
  166. package/src/daemon/memory-v2-startup.ts +14 -4
  167. package/src/daemon/message-protocol.ts +3 -0
  168. package/src/daemon/message-types/conversations.ts +4 -0
  169. package/src/daemon/message-types/disk-pressure.ts +9 -0
  170. package/src/daemon/message-types/messages.ts +3 -0
  171. package/src/daemon/profiler-run-store.ts +5 -5
  172. package/src/daemon/tool-setup-types.ts +2 -2
  173. package/src/documents/document-store.ts +85 -0
  174. package/src/filing/filing-service.ts +30 -5
  175. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
  176. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
  177. package/src/heartbeat/heartbeat-run-store.ts +13 -0
  178. package/src/heartbeat/heartbeat-service.ts +205 -31
  179. package/src/home/feed-scheduler.ts +18 -0
  180. package/src/inbound/platform-callback-registration.ts +8 -15
  181. package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
  182. package/src/ipc/assistant-server.ts +56 -2
  183. package/src/ipc/gateway-client.ts +37 -3
  184. package/src/live-voice/live-voice-archive.ts +4 -4
  185. package/src/live-voice/protocol.ts +5 -7
  186. package/src/media/image-service.ts +1 -7
  187. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
  188. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +52 -22
  189. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
  190. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
  191. package/src/memory/admin.ts +5 -9
  192. package/src/memory/context-search/agent-runner.ts +19 -2
  193. package/src/memory/context-search/sources/conversations.ts +2 -11
  194. package/src/memory/context-search/sources/memory-v2.ts +5 -4
  195. package/src/memory/context-search/sources/memory.ts +0 -1
  196. package/src/memory/context-search/types.ts +0 -1
  197. package/src/memory/conversation-crud.ts +4 -12
  198. package/src/memory/db-init.ts +2 -0
  199. package/src/memory/embedding-runtime-manager.ts +119 -5
  200. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +32 -21
  201. package/src/memory/graph/conversation-graph-memory.ts +42 -54
  202. package/src/memory/graph/extraction.ts +1 -3
  203. package/src/memory/graph/graph-search.test.ts +10 -67
  204. package/src/memory/graph/graph-search.ts +1 -20
  205. package/src/memory/graph/retriever.test.ts +6 -0
  206. package/src/memory/graph/retriever.ts +6 -10
  207. package/src/memory/indexer.ts +54 -45
  208. package/src/memory/job-handlers/backfill.ts +2 -11
  209. package/src/memory/job-handlers/cleanup.ts +43 -0
  210. package/src/memory/job-handlers/embedding.ts +6 -8
  211. package/src/memory/job-handlers/summarization.ts +2 -7
  212. package/src/memory/jobs-store.ts +48 -0
  213. package/src/memory/jobs-worker.ts +81 -43
  214. package/src/memory/memory-v2-activation-log-store.ts +32 -14
  215. package/src/memory/memory-v2-concept-frequency.ts +169 -0
  216. package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
  217. package/src/memory/migrations/index.ts +1 -0
  218. package/src/memory/pkb/pkb-search.test.ts +6 -0
  219. package/src/memory/qdrant-client.ts +0 -13
  220. package/src/memory/rerank-local.ts +374 -0
  221. package/src/memory/search/semantic.ts +6 -67
  222. package/src/memory/trace-event-store.ts +1 -17
  223. package/src/memory/v2/__tests__/activation.test.ts +311 -250
  224. package/src/memory/v2/__tests__/consolidation-job.test.ts +40 -8
  225. package/src/memory/v2/__tests__/injection.test.ts +157 -167
  226. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
  227. package/src/memory/v2/__tests__/qdrant.test.ts +16 -0
  228. package/src/memory/v2/__tests__/reranker.test.ts +338 -0
  229. package/src/memory/v2/__tests__/sim.test.ts +5 -199
  230. package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
  231. package/src/memory/v2/__tests__/static-context.test.ts +76 -1
  232. package/src/memory/v2/activation.ts +149 -156
  233. package/src/memory/v2/consolidation-job.ts +62 -12
  234. package/src/memory/v2/injection.ts +47 -60
  235. package/src/memory/v2/prompts/consolidation.ts +36 -1
  236. package/src/memory/v2/qdrant.ts +99 -0
  237. package/src/memory/v2/reranker.ts +177 -0
  238. package/src/memory/v2/sim.ts +10 -84
  239. package/src/memory/v2/skill-content.ts +4 -3
  240. package/src/memory/v2/skill-store.ts +82 -59
  241. package/src/memory/v2/static-context.ts +22 -0
  242. package/src/memory/v2/types.ts +10 -10
  243. package/src/notifications/copy-composer.ts +13 -0
  244. package/src/notifications/signal.ts +4 -0
  245. package/src/oauth/AGENTS.md +3 -1
  246. package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
  247. package/src/oauth/connect-orchestrator.ts +2 -0
  248. package/src/oauth/connection-resolver.test.ts +66 -1
  249. package/src/oauth/connection-resolver.ts +55 -1
  250. package/src/oauth/oauth-connect-state.ts +77 -0
  251. package/src/oauth/seed-providers.ts +58 -1
  252. package/src/plugins/defaults/injectors.ts +35 -2
  253. package/src/plugins/defaults/memory-retrieval.ts +5 -6
  254. package/src/plugins/types.ts +7 -0
  255. package/src/proactive-artifact/aux-message-injector.ts +74 -0
  256. package/src/proactive-artifact/decision.test.ts +226 -0
  257. package/src/proactive-artifact/decision.ts +165 -0
  258. package/src/proactive-artifact/index.ts +7 -0
  259. package/src/proactive-artifact/job.test.ts +867 -0
  260. package/src/proactive-artifact/job.ts +352 -0
  261. package/src/proactive-artifact/message-copy.ts +41 -0
  262. package/src/proactive-artifact/trigger-state.test.ts +277 -0
  263. package/src/proactive-artifact/trigger-state.ts +119 -0
  264. package/src/prompts/normalize-onboarding.ts +80 -0
  265. package/src/prompts/persona-resolver.ts +101 -9
  266. package/src/prompts/system-prompt.ts +21 -7
  267. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  268. package/src/providers/__tests__/retry-callsite.test.ts +222 -1
  269. package/src/providers/model-intents.ts +7 -0
  270. package/src/providers/openrouter/client.ts +8 -0
  271. package/src/providers/retry.ts +50 -0
  272. package/src/providers/types.ts +1 -0
  273. package/src/runtime/__tests__/agent-wake.test.ts +456 -3
  274. package/src/runtime/agent-wake.ts +238 -100
  275. package/src/runtime/assistant-event-hub.ts +36 -6
  276. package/src/runtime/assistant-event.ts +0 -1
  277. package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
  278. package/src/runtime/auth/route-policy.ts +14 -1
  279. package/src/runtime/auth/same-actor.ts +216 -0
  280. package/src/runtime/channel-retry-sweep.ts +65 -1
  281. package/src/runtime/guardian-reply-router.ts +10 -0
  282. package/src/runtime/local-actor-identity.ts +52 -11
  283. package/src/runtime/pending-interactions.ts +8 -0
  284. package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
  285. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
  286. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  287. package/src/runtime/routes/client-routes.ts +20 -2
  288. package/src/runtime/routes/contact-routes.ts +0 -25
  289. package/src/runtime/routes/conversation-routes.ts +35 -26
  290. package/src/runtime/routes/debug-bash-routes.ts +163 -0
  291. package/src/runtime/routes/disk-pressure-routes.ts +121 -0
  292. package/src/runtime/routes/document-pdf-renderer.ts +6 -2
  293. package/src/runtime/routes/documents-routes.ts +2 -75
  294. package/src/runtime/routes/events-routes.ts +41 -9
  295. package/src/runtime/routes/host-bash-routes.ts +23 -3
  296. package/src/runtime/routes/host-cu-routes.ts +33 -6
  297. package/src/runtime/routes/host-file-routes.ts +32 -6
  298. package/src/runtime/routes/host-transfer-routes.ts +79 -16
  299. package/src/runtime/routes/identity-routes.ts +7 -138
  300. package/src/runtime/routes/inbound-message-handler.ts +77 -12
  301. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -0
  302. package/src/runtime/routes/index.ts +6 -0
  303. package/src/runtime/routes/memory-item-routes.test.ts +41 -15
  304. package/src/runtime/routes/memory-v2-routes.ts +33 -0
  305. package/src/runtime/routes/oauth-connect-routes.ts +153 -0
  306. package/src/runtime/verification-outbound-actions.ts +4 -4
  307. package/src/schedule/run-script.ts +37 -5
  308. package/src/schedule/scheduler.ts +20 -1
  309. package/src/security/encrypted-store.ts +2 -0
  310. package/src/security/secure-keys.ts +55 -0
  311. package/src/skills/remote-skill-policy.ts +4 -10
  312. package/src/subagent/index.ts +1 -7
  313. package/src/subagent/manager.ts +1 -15
  314. package/src/tasks/task-runner.ts +0 -1
  315. package/src/tasks/task-store.ts +0 -3
  316. package/src/tools/background-tool-registry.ts +17 -3
  317. package/src/tools/host-filesystem/edit.test.ts +151 -0
  318. package/src/tools/host-filesystem/edit.ts +43 -1
  319. package/src/tools/host-filesystem/read.test.ts +129 -0
  320. package/src/tools/host-filesystem/read.ts +43 -1
  321. package/src/tools/host-filesystem/transfer.test.ts +127 -2
  322. package/src/tools/host-filesystem/transfer.ts +56 -11
  323. package/src/tools/host-filesystem/write.test.ts +134 -0
  324. package/src/tools/host-filesystem/write.ts +43 -1
  325. package/src/tools/host-terminal/host-shell.ts +13 -6
  326. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  327. package/src/tools/memory/register.test.ts +12 -9
  328. package/src/tools/memory/register.ts +1 -2
  329. package/src/tools/provider-tool-name.ts +28 -0
  330. package/src/tools/registry.ts +30 -9
  331. package/src/tools/terminal/shell.ts +9 -1
  332. package/src/tools/tool-approval-handler.ts +31 -6
  333. package/src/tools/types.ts +24 -2
  334. package/src/tts/provider-catalog.ts +3 -5
  335. package/src/util/disk-usage.ts +138 -0
  336. package/src/util/platform.ts +21 -11
  337. package/src/util/process-liveness.ts +26 -0
  338. package/src/workspace/heartbeat-service.ts +19 -0
  339. package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
  340. package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
  341. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +72 -0
  342. package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
  343. package/src/workspace/migrations/registry.ts +8 -0
  344. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
  345. package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
  346. package/src/memory/v2/skill-qdrant.ts +0 -404
  347. package/src/signals/bash.ts +0 -198
@@ -18,14 +18,14 @@
18
18
  * handles registration, touch (heartbeat), and unregistration (dispose).
19
19
  */
20
20
 
21
+ import { z } from "zod";
22
+
21
23
  import type { HostProxyCapability } from "../../channels/types.js";
22
24
  import { parseInterfaceId, supportsHostProxy } from "../../channels/types.js";
25
+ import { emitContactChange } from "../../contacts/contact-events.js";
23
26
  import { getOrCreateConversation } from "../../memory/conversation-key-store.js";
24
27
  import { getLogger } from "../../util/logger.js";
25
- import {
26
- formatSseFrame,
27
- formatSseHeartbeatWithData,
28
- } from "../assistant-event.js";
28
+ import { formatSseFrame, formatSseHeartbeat } from "../assistant-event.js";
29
29
  import type {
30
30
  AssistantEventCallback,
31
31
  AssistantEventFilter,
@@ -35,13 +35,14 @@ import {
35
35
  AssistantEventHub,
36
36
  assistantEventHub,
37
37
  } from "../assistant-event-hub.js";
38
+ import { resolveActorPrincipalIdForLocalGuardian } from "../local-actor-identity.js";
38
39
  import { BadRequestError, ServiceUnavailableError } from "./errors.js";
39
40
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
40
41
 
41
42
  const log = getLogger("events-routes");
42
43
 
43
- /** Keep-alive comment sent to idle clients every 5 s by default. */
44
- const DEFAULT_HEARTBEAT_INTERVAL_MS = 5_000;
44
+ /** Keep-alive comment sent to idle clients every 7 s by default. */
45
+ const DEFAULT_HEARTBEAT_INTERVAL_MS = 7_000;
45
46
 
46
47
  /**
47
48
  * Stream assistant events as Server-Sent Events.
@@ -61,7 +62,7 @@ const DEFAULT_HEARTBEAT_INTERVAL_MS = 5_000;
61
62
  *
62
63
  * Options (for testing):
63
64
  * hub -- override the event hub (defaults to process singleton).
64
- * heartbeatIntervalMs -- how often to emit keep-alive comments (default 5 s).
65
+ * heartbeatIntervalMs -- how often to emit keep-alive comments (default 7 s).
65
66
  */
66
67
  export function handleSubscribeAssistantEvents(
67
68
  args: RouteHandlerArgs,
@@ -81,10 +82,18 @@ export function handleSubscribeAssistantEvents(
81
82
  const rawClientId = headers?.["x-vellum-client-id"];
82
83
  const rawInterfaceId = headers?.["x-vellum-interface-id"];
83
84
  const rawMachineName = headers?.["x-vellum-machine-name"];
85
+ const rawActorPrincipalId = headers?.["x-vellum-actor-principal-id"];
84
86
  const clientId = rawClientId?.trim() || null;
85
87
  const interfaceId = clientId
86
88
  ? parseInterfaceId(rawInterfaceId?.trim())
87
89
  : null;
90
+ // Verified by RuntimeHttpServer and forwarded by the http-adapter from the
91
+ // bearer token's AuthContext. May be absent for legacy / service-token
92
+ // connections that have no principal. See `resolveActorPrincipalId` for the
93
+ // dev-bypass translation rationale.
94
+ const actorPrincipalId = resolveActorPrincipalIdForLocalGuardian(
95
+ rawActorPrincipalId?.trim() || undefined,
96
+ );
88
97
 
89
98
  if (clientId && !interfaceId) {
90
99
  log.error(
@@ -171,6 +180,7 @@ export function handleSubscribeAssistantEvents(
171
180
  supportsHostProxy(interfaceId, cap),
172
181
  ),
173
182
  machineName: rawMachineName?.trim() || undefined,
183
+ actorPrincipalId,
174
184
  })
175
185
  : hub.subscribe({
176
186
  ...subscriberBase,
@@ -194,7 +204,7 @@ export function handleSubscribeAssistantEvents(
194
204
  return;
195
205
  }
196
206
 
197
- controller.enqueue(encoder.encode(formatSseHeartbeatWithData()));
207
+ controller.enqueue(encoder.encode(formatSseHeartbeat()));
198
208
 
199
209
  heartbeatTimer = setInterval(() => {
200
210
  try {
@@ -206,7 +216,7 @@ export function handleSubscribeAssistantEvents(
206
216
  if (clientId) {
207
217
  hub.touchClient(clientId);
208
218
  }
209
- controller.enqueue(encoder.encode(formatSseHeartbeatWithData()));
219
+ controller.enqueue(encoder.encode(formatSseHeartbeat()));
210
220
  } catch {
211
221
  sub.dispose();
212
222
  cleanup();
@@ -237,7 +247,29 @@ export function handleSubscribeAssistantEvents(
237
247
  // Route definitions
238
248
  // ---------------------------------------------------------------------------
239
249
 
250
+ const EmitEventBodySchema = z.object({
251
+ kind: z.enum(["contacts_changed"]),
252
+ });
253
+
240
254
  export const ROUTES: RouteDefinition[] = [
255
+ {
256
+ operationId: "emit_event",
257
+ endpoint: "events/emit",
258
+ method: "POST",
259
+ summary: "Emit an assistant event",
260
+ description:
261
+ "Trigger an in-process assistant event by kind. Used by the gateway after owning a write that the assistant runtime would normally emit.",
262
+ tags: ["events"],
263
+ requestBody: EmitEventBodySchema,
264
+ responseStatus: "204",
265
+ handler: ({ body }) => {
266
+ const { kind } = EmitEventBodySchema.parse(body);
267
+ if (kind === "contacts_changed") {
268
+ emitContactChange();
269
+ }
270
+ return null;
271
+ },
272
+ },
241
273
  {
242
274
  operationId: "subscribe_assistant_events",
243
275
  endpoint: "events",
@@ -7,6 +7,11 @@
7
7
  import { z } from "zod";
8
8
 
9
9
  import { HostBashProxy } from "../../daemon/host-bash-proxy.js";
10
+ import {
11
+ enforceSameActorOrThrow,
12
+ SAME_ACTOR_FORBIDDEN_DESCRIPTION,
13
+ } from "../auth/same-actor.js";
14
+ import { resolveActorPrincipalIdForLocalGuardian } from "../local-actor-identity.js";
10
15
  import * as pendingInteractions from "../pending-interactions.js";
11
16
  import {
12
17
  BadRequestError,
@@ -37,7 +42,11 @@ function handleHostBashResult({ body, headers }: RouteHandlerArgs) {
37
42
  throw new BadRequestError("requestId is required");
38
43
  }
39
44
 
40
- const submittingClientId = headers?.["x-vellum-client-id"]?.trim() || undefined;
45
+ const submittingClientId =
46
+ headers?.["x-vellum-client-id"]?.trim() || undefined;
47
+ const submittingActorPrincipalId = resolveActorPrincipalIdForLocalGuardian(
48
+ headers?.["x-vellum-actor-principal-id"]?.trim() || undefined,
49
+ );
41
50
 
42
51
  const peeked = pendingInteractions.get(requestId);
43
52
  if (!peeked) {
@@ -62,6 +71,18 @@ function handleHostBashResult({ body, headers }: RouteHandlerArgs) {
62
71
  `Client "${submittingClientId}" is not the target for this request (expected "${targetClientId}"). The targeted client must submit the result.`,
63
72
  );
64
73
  }
74
+
75
+ // Defense-in-depth on top of the client-id header binding above: the
76
+ // submitting actor's principal must match the actor principal stored
77
+ // for the target client at SSE subscription time. This prevents a
78
+ // cross-user submission even when the attacker can guess or spoof the
79
+ // target's client ID.
80
+ enforceSameActorOrThrow({
81
+ sourceActorPrincipalId: submittingActorPrincipalId,
82
+ targetActorPrincipalId: peeked.targetActorPrincipalId,
83
+ targetClientId,
84
+ op: "host_bash",
85
+ });
65
86
  }
66
87
 
67
88
  HostBashProxy.instance.resolveResult(requestId, {
@@ -103,8 +124,7 @@ export const ROUTES: RouteDefinition[] = [
103
124
  "x-vellum-client-id header is missing for a targeted host bash request.",
104
125
  },
105
126
  "403": {
106
- description:
107
- "Submitting client does not match the targeted client for this request.",
127
+ description: SAME_ACTOR_FORBIDDEN_DESCRIPTION,
108
128
  },
109
129
  "404": {
110
130
  description: "No pending interaction found for the given requestId.",
@@ -7,8 +7,18 @@
7
7
  import { z } from "zod";
8
8
 
9
9
  import { findConversation } from "../../daemon/conversation-store.js";
10
+ import {
11
+ enforceSameActorOrThrow,
12
+ SAME_ACTOR_FORBIDDEN_DESCRIPTION,
13
+ } from "../auth/same-actor.js";
14
+ import { resolveActorPrincipalIdForLocalGuardian } from "../local-actor-identity.js";
10
15
  import * as pendingInteractions from "../pending-interactions.js";
11
- import { BadRequestError, ConflictError, ForbiddenError, NotFoundError } from "./errors.js";
16
+ import {
17
+ BadRequestError,
18
+ ConflictError,
19
+ ForbiddenError,
20
+ NotFoundError,
21
+ } from "./errors.js";
12
22
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
13
23
 
14
24
  // ---------------------------------------------------------------------------
@@ -65,16 +75,34 @@ function handleHostCuResult({ body, headers }: RouteHandlerArgs) {
65
75
 
66
76
  // Validate submitting client matches the targeted client (if any).
67
77
  if (peeked.targetClientId != null) {
68
- const rawClientId = (headers as Record<string, string | undefined>)?.["x-vellum-client-id"];
69
- const submittingClientId = rawClientId?.trim() || undefined;
78
+ const headerMap = (headers as Record<string, string | undefined>) ?? {};
79
+ const submittingClientId =
80
+ headerMap["x-vellum-client-id"]?.trim() || undefined;
70
81
  if (!submittingClientId) {
71
- throw new BadRequestError("x-vellum-client-id header is missing for a targeted host CU request.");
82
+ throw new BadRequestError(
83
+ "x-vellum-client-id header is missing for a targeted host CU request.",
84
+ );
72
85
  }
73
86
  if (submittingClientId !== peeked.targetClientId) {
74
87
  throw new ForbiddenError(
75
88
  `Client "${submittingClientId}" is not the target for this request (expected "${peeked.targetClientId}"). The targeted client must submit the result.`,
76
89
  );
77
90
  }
91
+
92
+ // Defense-in-depth: require the submitting actor's principal id to match
93
+ // the actor principal id captured when the target client opened its SSE
94
+ // stream. This prevents a different authenticated user with knowledge of
95
+ // both the requestId and target clientId from submitting a result on
96
+ // behalf of the targeted client.
97
+ const submittingActorPrincipalId = resolveActorPrincipalIdForLocalGuardian(
98
+ headerMap["x-vellum-actor-principal-id"]?.trim() || undefined,
99
+ );
100
+ enforceSameActorOrThrow({
101
+ sourceActorPrincipalId: submittingActorPrincipalId,
102
+ targetActorPrincipalId: peeked.targetActorPrincipalId,
103
+ targetClientId: peeked.targetClientId,
104
+ op: "host_cu",
105
+ });
78
106
  }
79
107
 
80
108
  const conversation = findConversation(peeked.conversationId);
@@ -141,8 +169,7 @@ export const ROUTES: RouteDefinition[] = [
141
169
  "x-vellum-client-id header is missing for a targeted host CU request.",
142
170
  },
143
171
  "403": {
144
- description:
145
- "Submitting client does not match the targeted client for this request.",
172
+ description: SAME_ACTOR_FORBIDDEN_DESCRIPTION,
146
173
  },
147
174
  "404": {
148
175
  description:
@@ -7,8 +7,18 @@
7
7
  import { z } from "zod";
8
8
 
9
9
  import { HostFileProxy } from "../../daemon/host-file-proxy.js";
10
+ import {
11
+ enforceSameActorOrThrow,
12
+ SAME_ACTOR_FORBIDDEN_DESCRIPTION,
13
+ } from "../auth/same-actor.js";
14
+ import { resolveActorPrincipalIdForLocalGuardian } from "../local-actor-identity.js";
10
15
  import * as pendingInteractions from "../pending-interactions.js";
11
- import { BadRequestError, ConflictError, ForbiddenError, NotFoundError } from "./errors.js";
16
+ import {
17
+ BadRequestError,
18
+ ConflictError,
19
+ ForbiddenError,
20
+ NotFoundError,
21
+ } from "./errors.js";
12
22
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
13
23
 
14
24
  // ---------------------------------------------------------------------------
@@ -44,16 +54,33 @@ function handleHostFileResult({ body, headers }: RouteHandlerArgs) {
44
54
 
45
55
  // Validate submitting client matches the targeted client (if any).
46
56
  if (peeked.targetClientId != null) {
47
- const rawClientId = (headers as Record<string, string | undefined>)?.["x-vellum-client-id"];
48
- const submittingClientId = rawClientId?.trim() || undefined;
57
+ const headerMap = (headers as Record<string, string | undefined>) ?? {};
58
+ const submittingClientId =
59
+ headerMap["x-vellum-client-id"]?.trim() || undefined;
49
60
  if (!submittingClientId) {
50
- throw new BadRequestError("x-vellum-client-id header is missing for a targeted host file request.");
61
+ throw new BadRequestError(
62
+ "x-vellum-client-id header is missing for a targeted host file request.",
63
+ );
51
64
  }
52
65
  if (submittingClientId !== peeked.targetClientId) {
53
66
  throw new ForbiddenError(
54
67
  `Client "${submittingClientId}" is not the target for this request (expected "${peeked.targetClientId}"). The targeted client must submit the result.`,
55
68
  );
56
69
  }
70
+
71
+ // Defense-in-depth: also require the submitting actor's principal id to
72
+ // match the actor that opened the target client's SSE stream. This blocks
73
+ // cross-user submissions even if a different user somehow obtains the
74
+ // target client id.
75
+ const submittingActorPrincipalId = resolveActorPrincipalIdForLocalGuardian(
76
+ headerMap["x-vellum-actor-principal-id"]?.trim() || undefined,
77
+ );
78
+ enforceSameActorOrThrow({
79
+ sourceActorPrincipalId: submittingActorPrincipalId,
80
+ targetActorPrincipalId: peeked.targetActorPrincipalId,
81
+ targetClientId: peeked.targetClientId,
82
+ op: "host_file",
83
+ });
57
84
  }
58
85
 
59
86
  HostFileProxy.instance.resolve(requestId, {
@@ -102,8 +129,7 @@ export const ROUTES: RouteDefinition[] = [
102
129
  "x-vellum-client-id header is missing for a targeted host file request.",
103
130
  },
104
131
  "403": {
105
- description:
106
- "Submitting client does not match the targeted client for this request.",
132
+ description: SAME_ACTOR_FORBIDDEN_DESCRIPTION,
107
133
  },
108
134
  "404": {
109
135
  description: "No pending interaction found for the given requestId.",
@@ -8,8 +8,18 @@
8
8
  import { z } from "zod";
9
9
 
10
10
  import { HostTransferProxy } from "../../daemon/host-transfer-proxy.js";
11
+ import {
12
+ enforceSameActorOrThrow,
13
+ SAME_ACTOR_FORBIDDEN_DESCRIPTION,
14
+ } from "../auth/same-actor.js";
15
+ import { resolveActorPrincipalIdForLocalGuardian } from "../local-actor-identity.js";
11
16
  import * as pendingInteractions from "../pending-interactions.js";
12
- import { BadRequestError, ConflictError, ForbiddenError, NotFoundError } from "./errors.js";
17
+ import {
18
+ BadRequestError,
19
+ ConflictError,
20
+ ForbiddenError,
21
+ NotFoundError,
22
+ } from "./errors.js";
13
23
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
14
24
 
15
25
  /**
@@ -44,9 +54,31 @@ function handleTransferContentGet({
44
54
 
45
55
  const targetClientId = match.proxy.getTargetClientIdForTransfer(transferId);
46
56
  if (targetClientId != null) {
47
- const submittingClientId = (headers as Record<string, string>)["x-vellum-client-id"]?.trim() || undefined;
48
- if (!submittingClientId) throw new BadRequestError("x-vellum-client-id header required for targeted transfer");
49
- if (submittingClientId !== targetClientId) throw new ForbiddenError(`Client "${submittingClientId}" is not the owner of this transfer`);
57
+ const headerMap = headers as Record<string, string | undefined>;
58
+ const submittingClientId =
59
+ headerMap["x-vellum-client-id"]?.trim() || undefined;
60
+ if (!submittingClientId)
61
+ throw new BadRequestError(
62
+ "x-vellum-client-id header required for targeted transfer",
63
+ );
64
+ if (submittingClientId !== targetClientId)
65
+ throw new ForbiddenError(
66
+ `Client "${submittingClientId}" is not the owner of this transfer`,
67
+ );
68
+
69
+ // Defense-in-depth: the submitting actor's principal must match the
70
+ // actor that opened the target client's SSE stream. Compare against
71
+ // the value persisted at registration time so a brief reconnect does
72
+ // not 403 a legitimate fetch.
73
+ enforceSameActorOrThrow({
74
+ sourceActorPrincipalId: resolveActorPrincipalIdForLocalGuardian(
75
+ headerMap["x-vellum-actor-principal-id"]?.trim() || undefined,
76
+ ),
77
+ targetActorPrincipalId:
78
+ match.proxy.getTargetActorPrincipalIdForTransfer(transferId),
79
+ targetClientId,
80
+ op: "host_transfer",
81
+ });
50
82
  }
51
83
 
52
84
  const content = match.proxy.getTransferContent(transferId);
@@ -105,9 +137,27 @@ async function handleTransferContentPut({
105
137
 
106
138
  const targetClientId = match.proxy.getTargetClientIdForTransfer(transferId);
107
139
  if (targetClientId != null) {
108
- const submittingClientId = (headers as Record<string, string>)["x-vellum-client-id"]?.trim() || undefined;
109
- if (!submittingClientId) throw new BadRequestError("x-vellum-client-id header required for targeted transfer");
110
- if (submittingClientId !== targetClientId) throw new ForbiddenError(`Client "${submittingClientId}" is not the owner of this transfer`);
140
+ const headerMap = headers as Record<string, string | undefined>;
141
+ const submittingClientId =
142
+ headerMap["x-vellum-client-id"]?.trim() || undefined;
143
+ if (!submittingClientId)
144
+ throw new BadRequestError(
145
+ "x-vellum-client-id header required for targeted transfer",
146
+ );
147
+ if (submittingClientId !== targetClientId)
148
+ throw new ForbiddenError(
149
+ `Client "${submittingClientId}" is not the owner of this transfer`,
150
+ );
151
+
152
+ enforceSameActorOrThrow({
153
+ sourceActorPrincipalId: resolveActorPrincipalIdForLocalGuardian(
154
+ headerMap["x-vellum-actor-principal-id"]?.trim() || undefined,
155
+ ),
156
+ targetActorPrincipalId:
157
+ match.proxy.getTargetActorPrincipalIdForTransfer(transferId),
158
+ targetClientId,
159
+ op: "host_transfer",
160
+ });
111
161
  }
112
162
 
113
163
  const data = rawBody ? Buffer.from(rawBody) : Buffer.alloc(0);
@@ -158,10 +208,26 @@ function handleTransferResult({ body, headers }: RouteHandlerArgs) {
158
208
  }
159
209
 
160
210
  if (peeked.targetClientId != null) {
161
- const rawClientId = (headers as Record<string, string | undefined>)?.["x-vellum-client-id"];
211
+ const headerMap = (headers as Record<string, string | undefined>) ?? {};
212
+ const rawClientId = headerMap["x-vellum-client-id"];
162
213
  const submittingClientId = rawClientId?.trim() || undefined;
163
- if (!submittingClientId) throw new BadRequestError("x-vellum-client-id header is missing for a targeted host transfer request.");
164
- if (submittingClientId !== peeked.targetClientId) throw new ForbiddenError(`Client "${submittingClientId}" is not the target for this request (expected "${peeked.targetClientId}").`);
214
+ if (!submittingClientId)
215
+ throw new BadRequestError(
216
+ "x-vellum-client-id header is missing for a targeted host transfer request.",
217
+ );
218
+ if (submittingClientId !== peeked.targetClientId)
219
+ throw new ForbiddenError(
220
+ `Client "${submittingClientId}" is not the target for this request (expected "${peeked.targetClientId}").`,
221
+ );
222
+
223
+ enforceSameActorOrThrow({
224
+ sourceActorPrincipalId: resolveActorPrincipalIdForLocalGuardian(
225
+ headerMap["x-vellum-actor-principal-id"]?.trim() || undefined,
226
+ ),
227
+ targetActorPrincipalId: peeked.targetActorPrincipalId,
228
+ targetClientId: peeked.targetClientId,
229
+ op: "host_transfer",
230
+ });
165
231
  }
166
232
 
167
233
  HostTransferProxy.instance.resolveTransferResult(requestId, {
@@ -195,8 +261,7 @@ export const ROUTES: RouteDefinition[] = [
195
261
  "x-vellum-client-id header is missing for a targeted transfer.",
196
262
  },
197
263
  "403": {
198
- description:
199
- "Submitting client does not match the targeted client for this transfer.",
264
+ description: SAME_ACTOR_FORBIDDEN_DESCRIPTION,
200
265
  },
201
266
  },
202
267
  handler: handleTransferContentGet,
@@ -217,8 +282,7 @@ export const ROUTES: RouteDefinition[] = [
217
282
  "x-vellum-client-id header is missing for a targeted transfer.",
218
283
  },
219
284
  "403": {
220
- description:
221
- "Submitting client does not match the targeted client for this transfer.",
285
+ description: SAME_ACTOR_FORBIDDEN_DESCRIPTION,
222
286
  },
223
287
  },
224
288
  handler: handleTransferContentPut,
@@ -247,8 +311,7 @@ export const ROUTES: RouteDefinition[] = [
247
311
  "x-vellum-client-id header is missing for a targeted host transfer request.",
248
312
  },
249
313
  "403": {
250
- description:
251
- "Submitting client does not match the targeted client for this transfer.",
314
+ description: SAME_ACTOR_FORBIDDEN_DESCRIPTION,
252
315
  },
253
316
  },
254
317
  handler: handleTransferResult,
@@ -2,24 +2,20 @@
2
2
  * Identity and health endpoint handlers.
3
3
  */
4
4
 
5
- import { spawnSync } from "node:child_process";
6
- import { existsSync, readFileSync, statfsSync } from "node:fs";
5
+ import { existsSync, readFileSync } from "node:fs";
7
6
  import { availableParallelism, cpus, totalmem } from "node:os";
8
7
 
9
8
  import { z } from "zod";
10
9
 
11
- import {
12
- getCpuLimit,
13
- getIsPlatform,
14
- getMinikubeStorageSize,
15
- } from "../../config/env-registry.js";
10
+ import { getCpuLimit, getIsPlatform } from "../../config/env-registry.js";
16
11
  import { parseIdentityFields } from "../../daemon/handlers/identity.js";
17
12
  import { getProfilerRuntimeStatus } from "../../daemon/profiler-run-store.js";
18
13
  import { getMaxMigrationVersion } from "../../memory/migrations/registry.js";
19
14
  import {
20
- getWorkspaceDir,
21
- getWorkspacePromptPath,
22
- } from "../../util/platform.js";
15
+ getDiskUsageInfo,
16
+ parseK8sMemoryBytes,
17
+ } from "../../util/disk-usage.js";
18
+ import { getWorkspacePromptPath } from "../../util/platform.js";
23
19
  import { APP_VERSION } from "../../version.js";
24
20
  import { resolveHatchedAtReadOnly } from "../../workspace/hatched-date.js";
25
21
  import { WORKSPACE_MIGRATIONS } from "../../workspace/migrations/registry.js";
@@ -28,138 +24,11 @@ import { NotFoundError } from "./errors.js";
28
24
  import { getCachedIntro } from "./identity-intro-cache.js";
29
25
  import type { RouteDefinition } from "./types.js";
30
26
 
31
- interface DiskSpaceInfo {
32
- path: string;
33
- totalMb: number;
34
- usedMb: number;
35
- freeMb: number;
36
- }
37
-
38
- /**
39
- * Measure the on-disk usage of one or more directory paths using `du -sb`.
40
- * Returns the sum of all paths in bytes, or null on failure.
41
- */
42
- function getDirectorySizeBytes(paths: string[]): number | null {
43
- try {
44
- const existing = paths.filter((p) => existsSync(p));
45
- if (existing.length === 0) return null;
46
- const result = spawnSync("du", ["-sb", ...existing], {
47
- encoding: "utf-8",
48
- timeout: 30_000,
49
- });
50
- if (result.status !== 0) return null;
51
- let total = 0;
52
- for (const line of result.stdout.trim().split("\n")) {
53
- const size = parseInt(line.split("\t")[0], 10);
54
- if (!isNaN(size) && size > 0) total += size;
55
- }
56
- return total > 0 ? total : null;
57
- } catch {
58
- return null;
59
- }
60
- }
61
-
62
- const DU_CACHE_TTL_MS = 60_000;
63
- let duCacheValue: number | null = null;
64
- let duCacheTime = 0;
65
- let duCachePaths: string | null = null;
66
-
67
- function getCachedDirectorySizeBytes(paths: string[]): number | null {
68
- const key = paths.join("\0");
69
- const now = Date.now();
70
- if (duCachePaths === key && now - duCacheTime < DU_CACHE_TTL_MS) {
71
- return duCacheValue;
72
- }
73
- duCacheValue = getDirectorySizeBytes(paths);
74
- duCacheTime = now;
75
- duCachePaths = key;
76
- return duCacheValue;
77
- }
78
-
79
- function getDiskSpaceInfo(): DiskSpaceInfo | null {
80
- try {
81
- const wsDir = getWorkspaceDir();
82
- const diskPath = existsSync(wsDir) ? wsDir : "/";
83
- const stats = statfsSync(diskPath);
84
- const fsTotalBytes = stats.bsize * stats.blocks;
85
- const fsFreeBytes = stats.bsize * stats.bavail;
86
- const bytesToMb = (b: number) =>
87
- Math.round((b / (1024 * 1024)) * 100) / 100;
88
-
89
- // Minikube mode: the platform passes the PVC storage size so we can
90
- // report accurate capacity. On hostPath-backed PVCs statfsSync reports
91
- // the host's entire filesystem rather than the PVC. Detect this by
92
- // comparing filesystem size against PVC size — if the filesystem is
93
- // larger, measure actual directory usage with `du` instead.
94
- const storageSizeRaw = getMinikubeStorageSize();
95
- if (storageSizeRaw) {
96
- const pvcTotalBytes = parseK8sMemoryBytes(storageSizeRaw);
97
- if (pvcTotalBytes !== null && fsTotalBytes > pvcTotalBytes * 1.1) {
98
- const volumePaths = [diskPath];
99
- if (diskPath !== "/data" && existsSync("/data")) {
100
- volumePaths.push("/data");
101
- }
102
- const usedBytes = getCachedDirectorySizeBytes(volumePaths);
103
- if (usedBytes !== null) {
104
- return {
105
- path: diskPath,
106
- totalMb: bytesToMb(pvcTotalBytes),
107
- usedMb: bytesToMb(usedBytes),
108
- freeMb: bytesToMb(Math.max(0, pvcTotalBytes - usedBytes)),
109
- };
110
- }
111
- }
112
- }
113
-
114
- return {
115
- path: diskPath,
116
- totalMb: bytesToMb(fsTotalBytes),
117
- usedMb: bytesToMb(fsTotalBytes - fsFreeBytes),
118
- freeMb: bytesToMb(fsFreeBytes),
119
- };
120
- } catch {
121
- return null;
122
- }
123
- }
124
-
125
27
  interface MemoryInfo {
126
28
  currentMb: number;
127
29
  maxMb: number;
128
30
  }
129
31
 
130
- /**
131
- * Parse a Kubernetes-style memory string (e.g. "3Gi", "512Mi", "1G") into bytes.
132
- * Returns null if the value is not a recognized format.
133
- */
134
- function parseK8sMemoryBytes(value: string): number | null {
135
- const match = value
136
- .trim()
137
- .match(/^(\d+(?:\.\d+)?)\s*(Ki|Mi|Gi|Ti|Pi|Ei|k|M|G|T|P|E|m)?$/);
138
- if (!match) return null;
139
- const num = parseFloat(match[1]);
140
- const unit = match[2] ?? "";
141
- const multipliers: Record<string, number> = {
142
- "": 1,
143
- m: 1e-3,
144
- k: 1e3,
145
- M: 1e6,
146
- G: 1e9,
147
- T: 1e12,
148
- P: 1e15,
149
- E: 1e18,
150
- Ki: 1024,
151
- Mi: 1024 ** 2,
152
- Gi: 1024 ** 3,
153
- Ti: 1024 ** 4,
154
- Pi: 1024 ** 5,
155
- Ei: 1024 ** 6,
156
- };
157
- const mult = multipliers[unit];
158
- if (mult === undefined) return null;
159
- const bytes = Math.round(num * mult);
160
- return bytes > 0 ? bytes : null;
161
- }
162
-
163
32
  /**
164
33
  * Read the memory limit from the VELLUM_MEMORY_LIMIT env var (K8s resource format),
165
34
  * then fall back to cgroups, then to os.totalmem().
@@ -458,7 +327,7 @@ function getDetailedHealth() {
458
327
  status: "healthy",
459
328
  timestamp: new Date().toISOString(),
460
329
  version: APP_VERSION,
461
- disk: getDiskSpaceInfo(),
330
+ disk: getDiskUsageInfo(),
462
331
  memory: getMemoryInfo(),
463
332
  cpu: getCpuInfo(),
464
333
  migrations: {