@vellumai/assistant 0.6.0 → 0.6.2

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 (358) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +68 -15
  3. package/Dockerfile +2 -2
  4. package/bun.lock +6 -2
  5. package/docker-entrypoint.sh +42 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
  9. package/openapi.yaml +539 -4
  10. package/package.json +5 -1
  11. package/src/__tests__/anthropic-provider.test.ts +160 -95
  12. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  13. package/src/__tests__/app-executors.test.ts +47 -1
  14. package/src/__tests__/app-source-watcher.test.ts +159 -0
  15. package/src/__tests__/assistant-event-hub.test.ts +30 -0
  16. package/src/__tests__/checker.test.ts +138 -172
  17. package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
  18. package/src/__tests__/config-schema.test.ts +5 -0
  19. package/src/__tests__/context-overflow-approval.test.ts +5 -5
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +4 -51
  22. package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
  23. package/src/__tests__/conversation-directories-parse.test.ts +105 -0
  24. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  25. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  26. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  27. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  28. package/src/__tests__/conversation-wipe.test.ts +2 -6
  29. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  30. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  31. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  32. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  33. package/src/__tests__/credential-execution-approval-bridge.test.ts +0 -2
  34. package/src/__tests__/date-context.test.ts +76 -210
  35. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  36. package/src/__tests__/file-list-tool.test.ts +219 -0
  37. package/src/__tests__/first-greeting.test.ts +1 -1
  38. package/src/__tests__/heartbeat-service.test.ts +180 -3
  39. package/src/__tests__/identity-routes.test.ts +328 -0
  40. package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
  41. package/src/__tests__/injection-block.test.ts +24 -0
  42. package/src/__tests__/inline-command-runner.test.ts +7 -5
  43. package/src/__tests__/install-skill-routing.test.ts +7 -6
  44. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
  45. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  46. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  47. package/src/__tests__/llm-context-route-provider.test.ts +101 -0
  48. package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
  49. package/src/__tests__/log-export-workspace.test.ts +257 -100
  50. package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
  51. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  52. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  53. package/src/__tests__/memory-recall-log-store.test.ts +132 -0
  54. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  55. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  56. package/src/__tests__/mock-fetch.ts +87 -0
  57. package/src/__tests__/navigate-settings-tab.test.ts +14 -1
  58. package/src/__tests__/notification-broadcaster.test.ts +65 -0
  59. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  60. package/src/__tests__/onboarding-template-contract.test.ts +63 -14
  61. package/src/__tests__/parser.test.ts +32 -0
  62. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  63. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  64. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  65. package/src/__tests__/permission-mode-store.test.ts +277 -0
  66. package/src/__tests__/permission-mode.test.ts +101 -0
  67. package/src/__tests__/pkb-autoinject.test.ts +96 -0
  68. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  69. package/src/__tests__/profiler-routes.test.ts +502 -0
  70. package/src/__tests__/profiler-run-store.test.ts +441 -0
  71. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  72. package/src/__tests__/registry.test.ts +1 -1
  73. package/src/__tests__/require-fresh-approval.test.ts +0 -2
  74. package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
  75. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  76. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  77. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  78. package/src/__tests__/search-skills-unified.test.ts +4 -3
  79. package/src/__tests__/send-endpoint-busy.test.ts +42 -3
  80. package/src/__tests__/set-permission-mode.test.ts +274 -0
  81. package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
  82. package/src/__tests__/skill-memory.test.ts +2 -783
  83. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  84. package/src/__tests__/subagent-detail.test.ts +84 -0
  85. package/src/__tests__/subagent-disposal.test.ts +308 -0
  86. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  87. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  88. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  89. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  90. package/src/__tests__/subagent-tools.test.ts +464 -4
  91. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  92. package/src/__tests__/task-memory-cleanup.test.ts +12 -12
  93. package/src/__tests__/terminal-sandbox.test.ts +1 -1
  94. package/src/__tests__/terminal-tools.test.ts +16 -29
  95. package/src/__tests__/test-preload.ts +18 -0
  96. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
  97. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
  98. package/src/__tests__/tool-executor.test.ts +4 -27
  99. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  100. package/src/__tests__/top-level-renderer.test.ts +10 -13
  101. package/src/__tests__/transport-hints-queue.test.ts +77 -0
  102. package/src/__tests__/trust-store.test.ts +4 -4
  103. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
  104. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  105. package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
  106. package/src/__tests__/workspace-policy.test.ts +2 -7
  107. package/src/agent/loop.ts +6 -29
  108. package/src/approvals/guardian-request-resolvers.ts +24 -0
  109. package/src/avatar/traits-png-sync.ts +3 -3
  110. package/src/channels/types.ts +5 -0
  111. package/src/cli/__tests__/run-assistant-command.ts +56 -0
  112. package/src/cli/__tests__/unknown-command.test.ts +33 -0
  113. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  114. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  115. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  116. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  117. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  118. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  119. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  120. package/src/cli/commands/conversations.ts +1 -8
  121. package/src/cli/commands/default-action.ts +68 -1
  122. package/src/cli/commands/email.ts +584 -835
  123. package/src/cli/commands/memory.ts +1 -34
  124. package/src/cli/commands/notifications.ts +7 -2
  125. package/src/cli/commands/oauth/__tests__/connect.test.ts +27 -0
  126. package/src/cli/commands/oauth/connect.ts +25 -5
  127. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  128. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  129. package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
  130. package/src/cli/commands/routes.ts +396 -0
  131. package/src/cli/commands/skills.ts +130 -20
  132. package/src/cli/program.ts +11 -2
  133. package/src/cli.ts +1 -120
  134. package/src/config/assistant-feature-flags.ts +59 -55
  135. package/src/config/bundled-skills/app-builder/SKILL.md +91 -5
  136. package/src/config/bundled-skills/gmail/SKILL.md +13 -8
  137. package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
  138. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
  139. package/src/config/bundled-skills/messaging/SKILL.md +7 -0
  140. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  141. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  142. package/src/config/bundled-skills/settings/TOOLS.json +1 -1
  143. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  144. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  145. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  146. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
  147. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  148. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  149. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  150. package/src/config/env-registry.ts +63 -0
  151. package/src/config/feature-flag-registry.json +17 -1
  152. package/src/config/schema.ts +8 -0
  153. package/src/config/schemas/filing.ts +51 -0
  154. package/src/config/schemas/heartbeat.ts +15 -12
  155. package/src/config/schemas/memory-lifecycle.ts +12 -0
  156. package/src/config/schemas/security.ts +14 -0
  157. package/src/config/schemas/services.ts +8 -0
  158. package/src/credential-execution/approval-bridge.ts +0 -1
  159. package/src/credential-execution/managed-catalog.ts +3 -7
  160. package/src/daemon/app-source-watcher.ts +93 -0
  161. package/src/daemon/config-watcher.ts +85 -3
  162. package/src/daemon/context-overflow-approval.ts +0 -1
  163. package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
  164. package/src/daemon/conversation-agent-loop.ts +179 -65
  165. package/src/daemon/conversation-attachments.ts +0 -1
  166. package/src/daemon/conversation-history.ts +4 -19
  167. package/src/daemon/conversation-lifecycle.ts +8 -14
  168. package/src/daemon/conversation-messaging.ts +3 -0
  169. package/src/daemon/conversation-process.ts +30 -8
  170. package/src/daemon/conversation-queue-manager.ts +8 -0
  171. package/src/daemon/conversation-runtime-assembly.ts +359 -308
  172. package/src/daemon/conversation-surfaces.ts +65 -0
  173. package/src/daemon/conversation-tool-setup.ts +44 -17
  174. package/src/daemon/conversation-workspace.ts +1 -2
  175. package/src/daemon/conversation.ts +19 -3
  176. package/src/daemon/date-context.ts +26 -53
  177. package/src/daemon/first-greeting.ts +1 -1
  178. package/src/daemon/handlers/conversations.ts +5 -7
  179. package/src/daemon/handlers/shared.test.ts +143 -0
  180. package/src/daemon/handlers/shared.ts +70 -5
  181. package/src/daemon/handlers/skills.ts +11 -18
  182. package/src/daemon/lifecycle.ts +220 -158
  183. package/src/daemon/message-types/conversations.ts +29 -6
  184. package/src/daemon/message-types/messages.ts +9 -2
  185. package/src/daemon/message-types/notifications.ts +12 -0
  186. package/src/daemon/message-types/schedules.ts +1 -0
  187. package/src/daemon/message-types/settings.ts +18 -0
  188. package/src/daemon/profiler-run-store.ts +557 -0
  189. package/src/daemon/server.ts +87 -10
  190. package/src/daemon/shutdown-handlers.ts +5 -0
  191. package/src/daemon/tool-side-effects.ts +23 -3
  192. package/src/daemon/transport-hints.ts +33 -0
  193. package/src/export/transcript-formatter.ts +148 -0
  194. package/src/filing/filing-service.ts +228 -0
  195. package/src/heartbeat/heartbeat-service.ts +96 -7
  196. package/src/index.ts +1 -1
  197. package/src/mcp/client.ts +6 -0
  198. package/src/mcp/mcp-oauth-provider.ts +149 -27
  199. package/src/memory/admin.ts +33 -32
  200. package/src/memory/app-store.ts +69 -0
  201. package/src/memory/conversation-bootstrap.ts +1 -1
  202. package/src/memory/conversation-crud.ts +151 -117
  203. package/src/memory/conversation-directories.ts +39 -0
  204. package/src/memory/conversation-group-migration.ts +66 -6
  205. package/src/memory/conversation-queries.ts +58 -12
  206. package/src/memory/conversation-title-service.ts +1 -0
  207. package/src/memory/db-init.ts +182 -376
  208. package/src/memory/embedding-local.ts +1 -1
  209. package/src/memory/graph/bootstrap.ts +75 -66
  210. package/src/memory/graph/capability-seed.ts +167 -17
  211. package/src/memory/graph/consolidation.ts +38 -4
  212. package/src/memory/graph/conversation-graph-memory.ts +133 -104
  213. package/src/memory/graph/extraction-job.ts +9 -4
  214. package/src/memory/graph/extraction.ts +66 -23
  215. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  216. package/src/memory/graph/graph-search.ts +29 -15
  217. package/src/memory/graph/injection.ts +38 -8
  218. package/src/memory/graph/inspect.ts +12 -3
  219. package/src/memory/graph/retriever.ts +365 -262
  220. package/src/memory/graph/store.test.ts +48 -0
  221. package/src/memory/graph/store.ts +150 -11
  222. package/src/memory/graph/tool-handlers.ts +84 -209
  223. package/src/memory/graph/tools.ts +8 -52
  224. package/src/memory/graph/types.ts +24 -0
  225. package/src/memory/group-crud.ts +25 -9
  226. package/src/memory/job-handlers/cleanup.ts +44 -1
  227. package/src/memory/jobs-store.ts +70 -60
  228. package/src/memory/jobs-worker.ts +44 -28
  229. package/src/memory/llm-request-log-store.ts +96 -12
  230. package/src/memory/memory-recall-log-store.ts +49 -5
  231. package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
  232. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  233. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  234. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  235. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  236. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  237. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  238. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  239. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  240. package/src/memory/migrations/index.ts +8 -0
  241. package/src/memory/migrations/registry.ts +8 -0
  242. package/src/memory/schema/conversations.ts +14 -0
  243. package/src/memory/schema/infrastructure.ts +8 -1
  244. package/src/memory/schema/memory-core.ts +0 -51
  245. package/src/memory/schema/memory-graph.ts +15 -0
  246. package/src/memory/task-memory-cleanup.ts +30 -11
  247. package/src/messaging/provider.ts +1 -1
  248. package/src/notifications/broadcaster.ts +6 -0
  249. package/src/notifications/conversation-pairing.ts +12 -4
  250. package/src/notifications/copy-composer.ts +86 -0
  251. package/src/notifications/decision-engine.ts +35 -0
  252. package/src/notifications/emit-signal.ts +14 -0
  253. package/src/notifications/signal.ts +11 -0
  254. package/src/oauth/platform-connection.test.ts +2 -2
  255. package/src/oauth/seed-providers.ts +1 -0
  256. package/src/permissions/checker.ts +15 -4
  257. package/src/permissions/defaults.ts +7 -8
  258. package/src/permissions/permission-mode-store.ts +180 -0
  259. package/src/permissions/permission-mode.ts +31 -0
  260. package/src/permissions/prompter.ts +0 -2
  261. package/src/permissions/workspace-policy.ts +9 -0
  262. package/src/platform/client.ts +1 -1
  263. package/src/prompts/system-prompt.ts +59 -7
  264. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  265. package/src/prompts/templates/BOOTSTRAP.md +76 -162
  266. package/src/prompts/templates/HEARTBEAT.md +3 -1
  267. package/src/prompts/templates/SOUL.md +30 -9
  268. package/src/prompts/templates/UPDATES.md +8 -0
  269. package/src/providers/anthropic/client.ts +107 -219
  270. package/src/runtime/assistant-event-hub.ts +22 -0
  271. package/src/runtime/auth/route-policy.ts +23 -0
  272. package/src/runtime/auth/token-service.ts +8 -0
  273. package/src/runtime/http-server.ts +32 -2
  274. package/src/runtime/http-types.ts +12 -1
  275. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  276. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  277. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  278. package/src/runtime/routes/app-management-routes.ts +1 -11
  279. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  280. package/src/runtime/routes/archive-utils.ts +29 -0
  281. package/src/runtime/routes/avatar-routes.ts +2 -9
  282. package/src/runtime/routes/btw-routes.ts +14 -1
  283. package/src/runtime/routes/conversation-analysis-routes.ts +185 -0
  284. package/src/runtime/routes/conversation-management-routes.ts +1 -14
  285. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  286. package/src/runtime/routes/conversation-routes.ts +270 -44
  287. package/src/runtime/routes/group-routes.ts +22 -8
  288. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  289. package/src/runtime/routes/identity-routes.ts +53 -18
  290. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  291. package/src/runtime/routes/log-export/AGENTS.md +104 -0
  292. package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
  293. package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
  294. package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
  295. package/src/runtime/routes/log-export-routes.ts +41 -278
  296. package/src/runtime/routes/memory-item-routes.test.ts +168 -233
  297. package/src/runtime/routes/migration-routes.ts +18 -7
  298. package/src/runtime/routes/profiler-routes.ts +350 -0
  299. package/src/runtime/routes/schedule-routes.ts +27 -12
  300. package/src/runtime/routes/settings-routes.ts +95 -8
  301. package/src/runtime/routes/subagents-routes.ts +28 -7
  302. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  303. package/src/runtime/routes/user-routes.ts +41 -0
  304. package/src/runtime/routes/workspace-routes.ts +0 -1
  305. package/src/schedule/schedule-store.ts +30 -0
  306. package/src/schedule/scheduler.ts +45 -18
  307. package/src/skills/catalog-install.ts +10 -2
  308. package/src/skills/inline-command-runner.ts +12 -14
  309. package/src/skills/managed-store.ts +2 -2
  310. package/src/skills/skill-memory.ts +1 -293
  311. package/src/subagent/index.ts +13 -3
  312. package/src/subagent/manager.ts +308 -29
  313. package/src/subagent/types.ts +68 -0
  314. package/src/tasks/task-runner.ts +4 -4
  315. package/src/tools/apps/executors.ts +29 -4
  316. package/src/tools/filesystem/list.ts +93 -0
  317. package/src/tools/permission-checker.ts +78 -18
  318. package/src/tools/registry.ts +4 -0
  319. package/src/tools/schedule/create.ts +3 -0
  320. package/src/tools/schedule/list.ts +1 -0
  321. package/src/tools/schedule/update.ts +6 -0
  322. package/src/tools/secret-detection-handler.ts +0 -1
  323. package/src/tools/shared/filesystem/errors.ts +5 -0
  324. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  325. package/src/tools/shared/filesystem/types.ts +17 -0
  326. package/src/tools/shared/shell-output.ts +31 -2
  327. package/src/tools/skills/sandbox-runner.ts +3 -6
  328. package/src/tools/subagent/abort.ts +12 -2
  329. package/src/tools/subagent/message.ts +9 -2
  330. package/src/tools/subagent/notify-parent.ts +79 -0
  331. package/src/tools/subagent/read.ts +29 -8
  332. package/src/tools/subagent/resolve.ts +21 -0
  333. package/src/tools/subagent/spawn.ts +2 -0
  334. package/src/tools/subagent/status.ts +11 -1
  335. package/src/tools/system/avatar-generator.ts +3 -3
  336. package/src/tools/system/register.ts +23 -0
  337. package/src/tools/system/set-permission-mode.ts +103 -0
  338. package/src/tools/terminal/parser.ts +30 -5
  339. package/src/tools/terminal/safe-env.ts +16 -1
  340. package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
  341. package/src/tools/terminal/sandbox.ts +4 -1
  342. package/src/tools/terminal/shell.ts +3 -5
  343. package/src/tools/tool-manifest.ts +6 -0
  344. package/src/tools/types.ts +2 -3
  345. package/src/util/logger.ts +1 -1
  346. package/src/util/platform.ts +50 -17
  347. package/src/watcher/provider-types.ts +1 -1
  348. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  349. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  350. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  351. package/src/workspace/migrations/029-seed-pkb.ts +85 -0
  352. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
  353. package/src/workspace/migrations/registry.ts +6 -0
  354. package/src/workspace/top-level-renderer.ts +5 -9
  355. package/src/__tests__/cli-memory.test.ts +0 -377
  356. package/src/__tests__/clipboard.test.ts +0 -88
  357. package/src/cli/cli-memory.ts +0 -179
  358. package/src/util/clipboard.ts +0 -34
@@ -0,0 +1,557 @@
1
+ /**
2
+ * Profiler run store — manages profiler run directories, manifest state,
3
+ * and retention budget enforcement.
4
+ *
5
+ * Each profiler run lives in its own sub-directory under the profiler runs
6
+ * directory (<workspace>/data/profiler/runs/<runId>/). A small manifest.json
7
+ * in each run directory records metadata (status, timestamps, byte count).
8
+ *
9
+ * The startup sweep enumerates all run directories, recomputes sizes,
10
+ * updates manifests, and prunes completed runs oldest-first until the
11
+ * configured byte-count, run-count, and free-space budgets are satisfied.
12
+ * The active run (identified by VELLUM_PROFILER_RUN_ID) is never deleted.
13
+ */
14
+
15
+ import {
16
+ existsSync,
17
+ lstatSync,
18
+ mkdirSync,
19
+ readdirSync,
20
+ readFileSync,
21
+ rmSync,
22
+ statfsSync,
23
+ statSync,
24
+ writeFileSync,
25
+ } from "node:fs";
26
+ import { join } from "node:path";
27
+
28
+ import {
29
+ getProfilerMaxBytes,
30
+ getProfilerMaxRuns,
31
+ getProfilerMinFreeMb,
32
+ getProfilerMode,
33
+ getProfilerRunId,
34
+ } from "../config/env-registry.js";
35
+ import { getLogger } from "../util/logger.js";
36
+ import {
37
+ getProfilerRootDir,
38
+ getProfilerRunDir,
39
+ getProfilerRunsDir,
40
+ } from "../util/platform.js";
41
+
42
+ const log = getLogger("profiler-run-store");
43
+
44
+ // ── Manifest schema ─────────────────────────────────────────────────────
45
+
46
+ export interface ProfilerRunManifest {
47
+ /** Unique run identifier (matches the directory name). */
48
+ runId: string;
49
+ /** "active" while profiling is in progress; "completed" once finished. */
50
+ status: "active" | "completed";
51
+ /** ISO-8601 timestamp when the run was first observed. */
52
+ createdAt: string;
53
+ /** ISO-8601 timestamp of the last manifest update. */
54
+ updatedAt: string;
55
+ /** Total bytes consumed by all files in the run directory. */
56
+ totalBytes: number;
57
+ /** ISO-8601 timestamp when the run transitioned to "completed". */
58
+ completedAt?: string;
59
+ }
60
+
61
+ const MANIFEST_FILENAME = "manifest.json";
62
+
63
+ // ── Default budgets ─────────────────────────────────────────────────────
64
+
65
+ /** Default max total bytes across all runs (including active): 500 MB */
66
+ const DEFAULT_MAX_BYTES = 500 * 1024 * 1024;
67
+
68
+ /** Default max number of completed runs retained: 10 */
69
+ const DEFAULT_MAX_RUNS = 10;
70
+
71
+ /** Default minimum free disk space: 200 MB */
72
+ const DEFAULT_MIN_FREE_MB = 200;
73
+
74
+ // ── Result type ─────────────────────────────────────────────────────────
75
+
76
+ export interface ProfilerSweepResult {
77
+ /** Number of completed runs pruned during this sweep. */
78
+ prunedCount: number;
79
+ /** Total bytes freed by pruning. */
80
+ freedBytes: number;
81
+ /** When true, the active run alone exceeds the byte budget. */
82
+ activeRunOverBudget: boolean;
83
+ /** Number of runs remaining after the sweep (including active). */
84
+ remainingRuns: number;
85
+ }
86
+
87
+ // ── Helpers ─────────────────────────────────────────────────────────────
88
+
89
+ /**
90
+ * Recursively compute the total byte size of all files in a directory.
91
+ * Uses lstatSync to avoid following symlinks (prevents infinite loops
92
+ * from symlink cycles and avoids counting out-of-tree data).
93
+ */
94
+ function computeDirBytes(dirPath: string): number {
95
+ let total = 0;
96
+ if (!existsSync(dirPath)) return 0;
97
+
98
+ const names = readdirSync(dirPath);
99
+ for (const name of names) {
100
+ const entryPath = join(dirPath, name);
101
+ try {
102
+ const stat = lstatSync(entryPath);
103
+ if (stat.isSymbolicLink()) continue;
104
+ if (stat.isDirectory()) {
105
+ total += computeDirBytes(entryPath);
106
+ } else if (stat.isFile()) {
107
+ total += stat.size;
108
+ }
109
+ } catch {
110
+ // Entry may have been removed between readdir and stat
111
+ }
112
+ }
113
+ return total;
114
+ }
115
+
116
+ /**
117
+ * Read a manifest.json from a run directory, returning null if missing or
118
+ * unparseable.
119
+ */
120
+ function readManifest(runDir: string): ProfilerRunManifest | null {
121
+ const manifestPath = join(runDir, MANIFEST_FILENAME);
122
+ try {
123
+ const raw = readFileSync(manifestPath, "utf-8");
124
+ return JSON.parse(raw) as ProfilerRunManifest;
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Write a manifest.json into a run directory, creating the directory if
132
+ * needed.
133
+ */
134
+ function writeManifest(runDir: string, manifest: ProfilerRunManifest): void {
135
+ if (!existsSync(runDir)) {
136
+ mkdirSync(runDir, { recursive: true });
137
+ }
138
+ writeFileSync(
139
+ join(runDir, MANIFEST_FILENAME),
140
+ JSON.stringify(manifest, null, 2) + "\n",
141
+ "utf-8",
142
+ );
143
+ }
144
+
145
+ /**
146
+ * Get the available free bytes on the filesystem containing the given path.
147
+ */
148
+ function getFreeDiskBytes(path: string): number {
149
+ try {
150
+ const stats = statfsSync(path);
151
+ return stats.bavail * stats.bsize;
152
+ } catch {
153
+ // If statfs fails (e.g. unsupported FS), return a large value so the
154
+ // free-space budget doesn't spuriously trigger pruning.
155
+ return Number.MAX_SAFE_INTEGER;
156
+ }
157
+ }
158
+
159
+ // ── Core operations ─────────────────────────────────────────────────────
160
+
161
+ /** Options for {@link rescanRuns}. */
162
+ export interface RescanRunsOptions {
163
+ /**
164
+ * When true, skip all `writeManifest()` calls — just read existing
165
+ * manifests and recompute sizes without mutating the filesystem.
166
+ * Used by health endpoints to avoid write side-effects on every poll.
167
+ */
168
+ readOnly?: boolean;
169
+ }
170
+
171
+ /**
172
+ * Enumerate all profiler run directories, recompute sizes, and return
173
+ * up-to-date manifests. By default also writes updated manifests back
174
+ * to disk; pass `{ readOnly: true }` to suppress writes (e.g. from
175
+ * health-check callers).
176
+ */
177
+ export function rescanRuns(options?: RescanRunsOptions): ProfilerRunManifest[] {
178
+ const readOnly = options?.readOnly ?? false;
179
+ const runsDir = getProfilerRunsDir();
180
+ if (!existsSync(runsDir)) return [];
181
+
182
+ const activeRunId = getProfilerRunId();
183
+ const manifests: ProfilerRunManifest[] = [];
184
+ const now = new Date().toISOString();
185
+
186
+ let names: string[];
187
+ try {
188
+ names = readdirSync(runsDir);
189
+ } catch {
190
+ return [];
191
+ }
192
+
193
+ for (const runId of names) {
194
+ const runDir = getProfilerRunDir(runId);
195
+ try {
196
+ if (!statSync(runDir).isDirectory()) continue;
197
+ } catch {
198
+ continue;
199
+ }
200
+ const totalBytes = computeDirBytes(runDir);
201
+
202
+ const existing = readManifest(runDir);
203
+ const isActive = runId === activeRunId;
204
+
205
+ // For first-time manifests (no existing manifest.json), seed createdAt
206
+ // from the directory's mtime so legacy runs preserve their actual age
207
+ // for oldest-first pruning order.
208
+ let createdAt = existing?.createdAt ?? now;
209
+ if (!existing) {
210
+ try {
211
+ createdAt = statSync(runDir).mtime.toISOString();
212
+ } catch {
213
+ // Fall back to current time if stat fails
214
+ }
215
+ }
216
+
217
+ const newStatus: "active" | "completed" = isActive ? "active" : "completed";
218
+
219
+ // Determine completedAt: set it when a run transitions to completed
220
+ // for the first time, otherwise preserve the existing value.
221
+ let completedAt = existing?.completedAt;
222
+ if (
223
+ newStatus === "completed" &&
224
+ !completedAt &&
225
+ (existing?.status === "active" || !existing)
226
+ ) {
227
+ completedAt = now;
228
+ }
229
+
230
+ // Only bump updatedAt when something meaningful changed compared to
231
+ // the on-disk manifest (status, totalBytes, or first creation).
232
+ const somethingChanged =
233
+ !existing ||
234
+ existing.status !== newStatus ||
235
+ existing.totalBytes !== totalBytes;
236
+ const updatedAt = somethingChanged ? now : existing.updatedAt;
237
+
238
+ const manifest: ProfilerRunManifest = {
239
+ runId,
240
+ status: newStatus,
241
+ createdAt,
242
+ updatedAt,
243
+ totalBytes,
244
+ ...(completedAt ? { completedAt } : {}),
245
+ };
246
+
247
+ if (!readOnly) {
248
+ writeManifest(runDir, manifest);
249
+ }
250
+ manifests.push(manifest);
251
+ }
252
+
253
+ return manifests;
254
+ }
255
+
256
+ /**
257
+ * Run the profiler retention sweep. This is the primary entry point,
258
+ * called on daemon startup and after explicit cleanup operations.
259
+ *
260
+ * The sweep:
261
+ * 1. Rescans all run directories and updates manifests.
262
+ * 2. Separates completed runs from the active run.
263
+ * 3. Sorts completed runs oldest-first by createdAt.
264
+ * 4. Prunes completed runs until all budgets are satisfied:
265
+ * - Total bytes across all runs <= maxBytes
266
+ * - Total completed run count <= maxRuns
267
+ * - Free disk space >= minFreeMb
268
+ *
269
+ * If the active run alone exceeds the byte budget, no runs are pruned
270
+ * (you can't prune the active run), and the over-budget condition is
271
+ * reported.
272
+ */
273
+ export function runProfilerSweep(): ProfilerSweepResult {
274
+ const runsDir = getProfilerRunsDir();
275
+
276
+ // Ensure the runs directory exists for clean first-boot
277
+ if (!existsSync(runsDir)) {
278
+ const rootDir = getProfilerRootDir();
279
+ mkdirSync(rootDir, { recursive: true });
280
+ mkdirSync(runsDir, { recursive: true });
281
+ }
282
+
283
+ const manifests = rescanRuns();
284
+ const activeRunId = getProfilerRunId();
285
+
286
+ // Budget configuration
287
+ const maxBytes = getProfilerMaxBytes() ?? DEFAULT_MAX_BYTES;
288
+ const maxRuns = getProfilerMaxRuns() ?? DEFAULT_MAX_RUNS;
289
+ const minFreeMb = getProfilerMinFreeMb() ?? DEFAULT_MIN_FREE_MB;
290
+ const minFreeBytes = minFreeMb * 1024 * 1024;
291
+
292
+ // Separate active vs completed
293
+ const activeManifest = manifests.find(
294
+ (m) => m.runId === activeRunId && m.status === "active",
295
+ );
296
+ const completedRuns = manifests
297
+ .filter((m) => m.status === "completed")
298
+ .sort(
299
+ (a, b) =>
300
+ new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
301
+ );
302
+
303
+ let totalBytes = manifests.reduce((sum, m) => sum + m.totalBytes, 0);
304
+ let prunedCount = 0;
305
+ let freedBytes = 0;
306
+
307
+ // Prune completed runs oldest-first until all budgets are met
308
+ while (completedRuns.length > 0) {
309
+ const overBytesBudget = totalBytes > maxBytes;
310
+ const overRunCount = completedRuns.length > maxRuns;
311
+ const overFreeSpace = getFreeDiskBytes(runsDir) < minFreeBytes;
312
+
313
+ if (!overBytesBudget && !overRunCount && !overFreeSpace) break;
314
+
315
+ const oldest = completedRuns[0]!;
316
+ const runDir = getProfilerRunDir(oldest.runId);
317
+
318
+ log.info(
319
+ {
320
+ runId: oldest.runId,
321
+ bytes: oldest.totalBytes,
322
+ reason: overBytesBudget
323
+ ? "byte_budget"
324
+ : overRunCount
325
+ ? "run_count"
326
+ : "free_space",
327
+ },
328
+ `Pruning completed profiler run`,
329
+ );
330
+
331
+ try {
332
+ rmSync(runDir, { recursive: true, force: true });
333
+ completedRuns.shift();
334
+ totalBytes -= oldest.totalBytes;
335
+ freedBytes += oldest.totalBytes;
336
+ prunedCount++;
337
+ } catch (err) {
338
+ log.warn(
339
+ { runId: oldest.runId, err },
340
+ "Failed to remove profiler run directory",
341
+ );
342
+ // The run still exists on disk — leave it in completedRuns so
343
+ // remainingRuns stays accurate. Break to avoid an infinite retry
344
+ // loop on the same un-deletable run.
345
+ break;
346
+ }
347
+ }
348
+
349
+ // Check if the active run alone exceeds the byte budget
350
+ const activeRunOverBudget =
351
+ activeManifest !== undefined && activeManifest.totalBytes > maxBytes;
352
+
353
+ if (activeRunOverBudget) {
354
+ log.warn(
355
+ {
356
+ runId: activeManifest.runId,
357
+ activeBytes: activeManifest.totalBytes,
358
+ maxBytes,
359
+ },
360
+ "Active profiler run exceeds byte budget — cannot prune live artifacts",
361
+ );
362
+ }
363
+
364
+ const remainingRuns =
365
+ completedRuns.length + (activeManifest !== undefined ? 1 : 0);
366
+
367
+ if (prunedCount > 0) {
368
+ log.info(
369
+ { prunedCount, freedBytes, remainingRuns },
370
+ "Profiler retention sweep complete",
371
+ );
372
+ }
373
+
374
+ return {
375
+ prunedCount,
376
+ freedBytes,
377
+ activeRunOverBudget,
378
+ remainingRuns,
379
+ };
380
+ }
381
+
382
+ // ── Runtime status helpers ──────────────────────────────────────────────
383
+ // These derive the current profiler state from env vars + filesystem for
384
+ // health-endpoint reporting and control-plane polling.
385
+
386
+ /** Budget state for the active profiler run. */
387
+ export interface ProfilerBudgetStatus {
388
+ /** Configured maximum bytes across all runs. */
389
+ maxBytes: number;
390
+ /** Bytes remaining before the byte-count budget is exceeded. */
391
+ remainingBytes: number;
392
+ /** Configured minimum free disk space in MB. */
393
+ minFreeMb: number;
394
+ /** Current free disk space in MB. */
395
+ freeMb: number;
396
+ /** True when any budget constraint is currently violated. */
397
+ overBudget: boolean;
398
+ }
399
+
400
+ /** Summary of the most recently completed profiler run. */
401
+ export interface ProfilerLastCompletedRun {
402
+ runId: string;
403
+ totalBytes: number;
404
+ artifactCount: number;
405
+ hasSummaries: boolean;
406
+ completedAt: string;
407
+ }
408
+
409
+ /** Full runtime status snapshot for health-endpoint embedding. */
410
+ export interface ProfilerRuntimeStatus {
411
+ /** Whether profiling is enabled (env vars present). */
412
+ enabled: boolean;
413
+ /** The profiling mode ("cpu", "heap", "cpu+heap"), or null when disabled. */
414
+ mode: string | null;
415
+ /** The active run ID, or null when disabled. */
416
+ runId: string | null;
417
+ /** Path to the active run directory, or null when disabled. */
418
+ runDir: string | null;
419
+ /** Total bytes consumed by the active run, or 0 when no active run. */
420
+ totalBytes: number;
421
+ /** Number of profiler artifact files in the active run directory. */
422
+ artifactCount: number;
423
+ /** Budget headroom for the active run. Null when profiling is disabled. */
424
+ budget: ProfilerBudgetStatus | null;
425
+ /** Summary of the most recently completed run, or null when none exist. */
426
+ lastCompletedRun: ProfilerLastCompletedRun | null;
427
+ }
428
+
429
+ /** File extensions that Bun profiler writes as raw artifacts. */
430
+ const PROFILER_ARTIFACT_EXTENSIONS = [".cpuprofile", ".heapsnapshot"];
431
+
432
+ /** File extensions for Bun-generated markdown summaries. */
433
+ const PROFILER_SUMMARY_EXTENSIONS = [".md"];
434
+
435
+ /**
436
+ * Count profiler artifact files (raw profiles) in a run directory.
437
+ */
438
+ function countArtifacts(runDir: string): number {
439
+ if (!existsSync(runDir)) return 0;
440
+ try {
441
+ return readdirSync(runDir).filter((name) =>
442
+ PROFILER_ARTIFACT_EXTENSIONS.some((ext) => name.endsWith(ext)),
443
+ ).length;
444
+ } catch {
445
+ return 0;
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Check whether any Bun-generated markdown summary files exist in a run
451
+ * directory.
452
+ */
453
+ function hasSummaryFiles(runDir: string): boolean {
454
+ if (!existsSync(runDir)) return false;
455
+ try {
456
+ return readdirSync(runDir).some((name) =>
457
+ PROFILER_SUMMARY_EXTENSIONS.some((ext) => name.endsWith(ext)),
458
+ );
459
+ } catch {
460
+ return false;
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Derive the full profiler runtime status from environment variables and
466
+ * the filesystem. This is the main entry point for health-endpoint
467
+ * integration — it never throws.
468
+ */
469
+ export function getProfilerRuntimeStatus(): ProfilerRuntimeStatus {
470
+ const runId = getProfilerRunId() ?? null;
471
+ const mode = getProfilerMode() ?? null;
472
+ const enabled = runId !== null && mode !== null;
473
+
474
+ if (!enabled) {
475
+ return {
476
+ enabled: false,
477
+ mode: null,
478
+ runId: null,
479
+ runDir: null,
480
+ totalBytes: 0,
481
+ artifactCount: 0,
482
+ budget: null,
483
+ lastCompletedRun: null,
484
+ };
485
+ }
486
+
487
+ const runDir = getProfilerRunDir(runId!);
488
+ const runsDir = getProfilerRunsDir();
489
+
490
+ // Rescan to get up-to-date manifests — read-only so health checks
491
+ // don't write to disk on every poll.
492
+ let manifests: ProfilerRunManifest[];
493
+ try {
494
+ manifests = rescanRuns({ readOnly: true });
495
+ } catch {
496
+ manifests = [];
497
+ }
498
+
499
+ const activeManifest = manifests.find(
500
+ (m) => m.runId === runId && m.status === "active",
501
+ );
502
+ const totalBytes = activeManifest?.totalBytes ?? 0;
503
+ const artifactCount = countArtifacts(runDir);
504
+
505
+ // Compute budget state
506
+ const maxBytes = getProfilerMaxBytes() ?? DEFAULT_MAX_BYTES;
507
+ const minFreeMb = getProfilerMinFreeMb() ?? DEFAULT_MIN_FREE_MB;
508
+ const allRunBytes = manifests.reduce((sum, m) => sum + m.totalBytes, 0);
509
+ const remainingBytes = Math.max(0, maxBytes - allRunBytes);
510
+ const freeDiskBytes = getFreeDiskBytes(
511
+ existsSync(runsDir) ? runsDir : runDir,
512
+ );
513
+ const freeMb = Math.round((freeDiskBytes / (1024 * 1024)) * 100) / 100;
514
+ const overBudget =
515
+ allRunBytes > maxBytes || freeDiskBytes < minFreeMb * 1024 * 1024;
516
+
517
+ const budget: ProfilerBudgetStatus = {
518
+ maxBytes,
519
+ remainingBytes,
520
+ minFreeMb,
521
+ freeMb,
522
+ overBudget,
523
+ };
524
+
525
+ // Find most recent completed run for lastCompletedRun summary
526
+ const completedRuns = manifests
527
+ .filter((m) => m.status === "completed")
528
+ .sort(
529
+ (a, b) =>
530
+ new Date(b.completedAt ?? b.createdAt).getTime() -
531
+ new Date(a.completedAt ?? a.createdAt).getTime(),
532
+ );
533
+
534
+ let lastCompletedRun: ProfilerLastCompletedRun | null = null;
535
+ if (completedRuns.length > 0) {
536
+ const latest = completedRuns[0]!;
537
+ const latestDir = getProfilerRunDir(latest.runId);
538
+ lastCompletedRun = {
539
+ runId: latest.runId,
540
+ totalBytes: latest.totalBytes,
541
+ artifactCount: countArtifacts(latestDir),
542
+ hasSummaries: hasSummaryFiles(latestDir),
543
+ completedAt: latest.completedAt ?? latest.updatedAt,
544
+ };
545
+ }
546
+
547
+ return {
548
+ enabled,
549
+ mode,
550
+ runId,
551
+ runDir,
552
+ totalBytes,
553
+ artifactCount,
554
+ budget,
555
+ lastCompletedRun,
556
+ };
557
+ }