@vellumai/assistant 0.11.6 → 0.11.7-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (447) hide show
  1. package/ARCHITECTURE.md +31 -19
  2. package/Dockerfile +1 -0
  3. package/docs/architecture/security.md +3 -3
  4. package/docs/flux-turn-detection-spike.md +8 -7
  5. package/docs/runbook-trusted-contacts.md +14 -0
  6. package/docs/stt-provider-onboarding.md +8 -4
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/avatar-manifest/package.json +18 -0
  9. package/node_modules/@vellumai/avatar-manifest/src/__tests__/manifest.test.ts +173 -0
  10. package/node_modules/@vellumai/avatar-manifest/src/__tests__/read.test.ts +114 -0
  11. package/node_modules/@vellumai/avatar-manifest/src/index.ts +29 -0
  12. package/node_modules/@vellumai/avatar-manifest/src/layout.ts +18 -0
  13. package/node_modules/@vellumai/avatar-manifest/src/manifest.ts +156 -0
  14. package/node_modules/@vellumai/avatar-manifest/src/read.ts +109 -0
  15. package/node_modules/@vellumai/avatar-manifest/tsconfig.json +20 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  18. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  19. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  20. package/node_modules/@vellumai/environments/package.json +2 -1
  21. package/node_modules/@vellumai/environments/src/shell.test.ts +56 -0
  22. package/node_modules/@vellumai/environments/src/shell.ts +65 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  24. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  25. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  26. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  27. package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +32 -0
  28. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -0
  29. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +82 -0
  30. package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +56 -0
  31. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +18 -0
  32. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +5 -11
  33. package/node_modules/@vellumai/gateway-client/src/channel-permission-contract.ts +3 -1
  34. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +9 -0
  35. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +4 -0
  36. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +75 -0
  37. package/node_modules/@vellumai/gateway-client/src/index.ts +17 -0
  38. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +8 -0
  39. package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +47 -0
  40. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +11 -0
  41. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  42. package/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  43. package/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  44. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  45. package/openapi.yaml +419 -55
  46. package/package.json +4 -1
  47. package/src/__tests__/approval-card-edit-inbound-handoff.test.ts +199 -0
  48. package/src/__tests__/approval-card-edit.test.ts +153 -0
  49. package/src/__tests__/attachments-store-heic-normalize.test.ts +1 -1
  50. package/src/__tests__/available-skills.test.ts +54 -0
  51. package/src/__tests__/avatar-identity-sync.test.ts +5 -1
  52. package/src/__tests__/catalog-install-normalize.test.ts +9 -1
  53. package/src/__tests__/channel-policy.test.ts +1 -3
  54. package/src/__tests__/channel-readiness-discord.test.ts +9 -70
  55. package/src/__tests__/compactor-call-site-logging.test.ts +61 -0
  56. package/src/__tests__/config-loader-backfill.test.ts +69 -0
  57. package/src/__tests__/config-schema.test.ts +2 -3
  58. package/src/__tests__/conversation-agent-loop.test.ts +238 -0
  59. package/src/__tests__/conversation-attachments.test.ts +0 -1
  60. package/src/__tests__/conversation-error.test.ts +44 -0
  61. package/src/__tests__/conversation-evictor.test.ts +19 -0
  62. package/src/__tests__/conversation-fork-referential.test.ts +73 -63
  63. package/src/__tests__/conversation-fork-retrospective.test.ts +99 -74
  64. package/src/__tests__/conversation-runtime-assembly.test.ts +23 -0
  65. package/src/__tests__/conversation-slash.test.ts +9 -0
  66. package/src/__tests__/conversation-title-service.test.ts +97 -0
  67. package/src/__tests__/conversation-tool-setup-attribution.test.ts +16 -0
  68. package/src/__tests__/credential-broker-server-use.test.ts +0 -218
  69. package/src/__tests__/credential-broker.test.ts +43 -420
  70. package/src/__tests__/credential-routes.test.ts +17 -1
  71. package/src/__tests__/credential-security-invariants.test.ts +8 -9
  72. package/src/__tests__/default-profile-catalog-fallback.test.ts +289 -0
  73. package/src/__tests__/discord-callback-round-trip.test.ts +93 -0
  74. package/src/__tests__/discord-channel-config.test.ts +237 -0
  75. package/src/__tests__/evict-conversations-for-reload.test.ts +51 -1
  76. package/src/__tests__/fallback-breaker.test.ts +1335 -0
  77. package/src/__tests__/gateway-threshold-reader-mock.ts +6 -0
  78. package/src/__tests__/guardian-card-withdrawal.test.ts +30 -0
  79. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -2
  80. package/src/__tests__/image-conversion.test.ts +38 -4
  81. package/src/__tests__/install-skill-routing.test.ts +25 -0
  82. package/src/__tests__/llm-resolver.test.ts +74 -0
  83. package/src/__tests__/llm-schema-fallback-profile.test.ts +359 -0
  84. package/src/__tests__/managed-fallback-dispatch.test.ts +430 -0
  85. package/src/__tests__/managed-profile-guard.test.ts +28 -0
  86. package/src/__tests__/managed-speech-defaults.test.ts +196 -1
  87. package/src/__tests__/mcp-cli.test.ts +23 -1
  88. package/src/__tests__/mcp-tool-annotations-risk.test.ts +43 -34
  89. package/src/__tests__/media-stream-stt-session.test.ts +2 -0
  90. package/src/__tests__/notification-decision-fallback.test.ts +106 -0
  91. package/src/__tests__/notification-discord-adapter.test.ts +162 -0
  92. package/src/__tests__/persist-unsendable-image-downscale.test.ts +6 -8
  93. package/src/__tests__/persist-unsendable-image.test.ts +3 -3
  94. package/src/__tests__/platform.test.ts +19 -1
  95. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
  96. package/src/__tests__/provider-usage-tracking.test.ts +56 -0
  97. package/src/__tests__/require-fresh-approval.test.ts +5 -2
  98. package/src/__tests__/retry-fallback-escalation.test.ts +1093 -0
  99. package/src/__tests__/schedule-retry.test.ts +183 -4
  100. package/src/__tests__/scheduler-reuse-conversation.test.ts +6 -6
  101. package/src/__tests__/secret-routes-platform-proxy.test.ts +55 -0
  102. package/src/__tests__/skill-load-tool.test.ts +34 -0
  103. package/src/__tests__/skill-tool-factory.test.ts +6 -1
  104. package/src/__tests__/skill-tool-manifest.test.ts +20 -0
  105. package/src/__tests__/skills.test.ts +25 -0
  106. package/src/__tests__/subagent-manager-notify.test.ts +38 -0
  107. package/src/__tests__/terminal-tools.test.ts +2 -0
  108. package/src/__tests__/tool-approval-handler.test.ts +146 -2
  109. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +108 -2
  110. package/src/__tests__/tool-audit.test.ts +111 -40
  111. package/src/__tests__/tool-executor-lifecycle-events.test.ts +228 -18
  112. package/src/__tests__/ui-shape-teaching.test.ts +1 -1
  113. package/src/__tests__/verification-control-plane-policy.test.ts +6 -2
  114. package/src/__tests__/voice-config-update.test.ts +43 -0
  115. package/src/__tests__/workspace-migration-147-rename-colliding-backup-profile-names.test.ts +480 -0
  116. package/src/__tests__/workspace-migration-148-strip-unsupported-fallback-profiles.test.ts +167 -0
  117. package/src/__tests__/workspace-migration-149-repoint-backup-profile-selections.test.ts +144 -0
  118. package/src/agent/loop.ts +31 -2
  119. package/src/approvals/guardian-card-withdrawal.ts +22 -0
  120. package/src/avatar/__tests__/ensure-raster.test.ts +291 -0
  121. package/src/avatar/avatar-manifest.ts +48 -100
  122. package/src/avatar/avatar-store.ts +7 -7
  123. package/src/avatar/ensure-raster.ts +146 -0
  124. package/src/avatar/resvg-lazy.test.ts +0 -1
  125. package/src/avatar/traits-png-sync.ts +9 -10
  126. package/src/background-wake/wake-intent-hooks.test.ts +0 -1
  127. package/src/backup/__tests__/paths.test.ts +6 -2
  128. package/src/backup/paths.ts +6 -4
  129. package/src/bundler/app-compiler.ts +2 -2
  130. package/src/bundler/compiler-tools.test.ts +36 -0
  131. package/src/bundler/compiler-tools.ts +54 -18
  132. package/src/calls/__tests__/telephony-synthesis-language.test.ts +81 -0
  133. package/src/calls/__tests__/voice-session-bridge.test.ts +20 -0
  134. package/src/calls/media-stream-stt-session.ts +2 -1
  135. package/src/calls/telephony-synthesis-language.ts +13 -5
  136. package/src/calls/voice-session-bridge.ts +6 -4
  137. package/src/channels/config.ts +23 -12
  138. package/src/cli/AGENTS.md +3 -1
  139. package/src/cli/commands/backup.help.ts +10 -13
  140. package/src/cli/commands/contacts.help.ts +16 -4
  141. package/src/cli/commands/contacts.ts +5 -1
  142. package/src/cli/commands/credentials.help.ts +1 -1
  143. package/src/cli/commands/inference-providers.ts +18 -53
  144. package/src/cli/commands/mcp.help.ts +6 -5
  145. package/src/cli/commands/mcp.ts +1 -1
  146. package/src/cli/commands/memory/memory-retrospective.ts +1 -0
  147. package/src/cli/commands/stt.help.ts +2 -2
  148. package/src/config/__tests__/backup-schema.test.ts +1 -1
  149. package/src/config/__tests__/default-profile-catalog.test.ts +83 -2
  150. package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +1 -1
  151. package/src/config/__tests__/memory-retrospective-schema.test.ts +12 -15
  152. package/src/config/bundled-skills/computer-use/SKILL.md +6 -6
  153. package/src/config/bundled-skills/computer-use/TOOLS.json +9 -6
  154. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +3 -1
  155. package/src/config/bundled-skills/media-processing/services/preprocess.ts +1 -1
  156. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  157. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +16 -15
  158. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +2 -1
  159. package/src/config/bundled-skills/settings/TOOLS.json +11 -5
  160. package/src/config/bundled-skills/settings/tools/open-system-settings.test.ts +85 -0
  161. package/src/config/bundled-skills/settings/tools/open-system-settings.ts +33 -8
  162. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +54 -3
  163. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +1 -1
  164. package/src/config/default-profile-catalog.ts +195 -10
  165. package/src/config/default-profile-names.ts +41 -0
  166. package/src/config/feature-flag-registry.json +18 -2
  167. package/src/config/inference-profile-validation.ts +11 -2
  168. package/src/config/llm-resolver.ts +25 -0
  169. package/src/config/loader.ts +3 -0
  170. package/src/config/managed-speech-defaults.ts +160 -10
  171. package/src/config/schema.ts +2 -0
  172. package/src/config/schemas/__tests__/live-voice.test.ts +7 -6
  173. package/src/config/schemas/__tests__/stt.test.ts +94 -0
  174. package/src/config/schemas/backup.ts +1 -1
  175. package/src/config/schemas/channels.ts +32 -0
  176. package/src/config/schemas/live-voice.ts +13 -4
  177. package/src/config/schemas/llm.ts +259 -11
  178. package/src/config/schemas/mcp.ts +10 -2
  179. package/src/config/schemas/memory-retrospective.ts +1 -19
  180. package/src/config/schemas/services.ts +24 -0
  181. package/src/config/schemas/stt.ts +128 -5
  182. package/src/config/seed-inference-profiles.ts +32 -3
  183. package/src/config/skill-state.ts +5 -0
  184. package/src/config/skills.ts +21 -0
  185. package/src/contacts/notify-contacts-changed.ts +5 -2
  186. package/src/context/compactor.ts +74 -1
  187. package/src/daemon/__tests__/conversation-tool-setup.test.ts +138 -1
  188. package/src/daemon/config-watcher.ts +2 -1
  189. package/src/daemon/conversation-agent-loop-handlers.ts +27 -0
  190. package/src/daemon/conversation-agent-loop.ts +86 -10
  191. package/src/daemon/conversation-error.ts +25 -0
  192. package/src/daemon/conversation-evictor.ts +2 -4
  193. package/src/daemon/conversation-runtime-assembly.ts +19 -7
  194. package/src/daemon/conversation-skill-tools.ts +5 -1
  195. package/src/daemon/conversation-slash.ts +2 -2
  196. package/src/daemon/conversation-store.ts +8 -1
  197. package/src/daemon/conversation-tool-setup.ts +37 -0
  198. package/src/daemon/conversation.ts +0 -4
  199. package/src/daemon/handlers/channel-config-result.ts +30 -0
  200. package/src/daemon/handlers/config-discord-channel.ts +301 -0
  201. package/src/daemon/handlers/config-ingress.ts +9 -0
  202. package/src/daemon/handlers/config-slack-channel.ts +13 -15
  203. package/src/daemon/handlers/config-telegram.ts +2 -5
  204. package/src/daemon/handlers/conversations.ts +0 -2
  205. package/src/daemon/handlers/skills.ts +25 -7
  206. package/src/daemon/lifecycle.ts +3 -1
  207. package/src/daemon/message-types/messages.ts +0 -2
  208. package/src/daemon/tool-setup-types.ts +5 -3
  209. package/src/daemon/trust-context-types.test.ts +33 -0
  210. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  211. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +58 -4
  212. package/src/live-voice/__tests__/live-voice-managed-speech-fallback.test.ts +8 -3
  213. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +73 -11
  214. package/src/live-voice/__tests__/live-voice-session-preflight.test.ts +148 -2
  215. package/src/live-voice/__tests__/live-voice-stt.test.ts +3 -0
  216. package/src/live-voice/__tests__/live-voice-text-turn.test.ts +391 -0
  217. package/src/live-voice/__tests__/protocol.test.ts +224 -0
  218. package/src/live-voice/live-voice-credential-preflight.ts +5 -1
  219. package/src/live-voice/live-voice-session-manager.ts +39 -2
  220. package/src/live-voice/live-voice-session.ts +213 -8
  221. package/src/live-voice/protocol.ts +187 -1
  222. package/src/messaging/providers/discord/api.ts +2 -1
  223. package/src/messaging/providers/discord/withdraw.ts +62 -0
  224. package/src/monitoring/__tests__/file-descriptors.test.ts +32 -0
  225. package/src/monitoring/__tests__/process-memory.test.ts +34 -1
  226. package/src/monitoring/db-integrity-sample.ts +1 -0
  227. package/src/monitoring/file-descriptors.ts +39 -6
  228. package/src/monitoring/process-memory.ts +37 -5
  229. package/src/monitoring/resource-sampler.ts +25 -8
  230. package/src/notifications/__tests__/activity-failed-dedupe.test.ts +161 -0
  231. package/src/notifications/__tests__/background-failure-signal.test.ts +107 -0
  232. package/src/notifications/__tests__/connected-channels.test.ts +12 -0
  233. package/src/notifications/__tests__/copy-composer.test.ts +83 -0
  234. package/src/notifications/__tests__/destination-resolver.test.ts +38 -0
  235. package/src/notifications/activity-failed-dedupe.ts +95 -0
  236. package/src/notifications/adapters/discord.ts +132 -0
  237. package/src/notifications/adapters/shared.ts +16 -0
  238. package/src/notifications/adapters/slack.ts +6 -1
  239. package/src/notifications/adapters/telegram.ts +5 -8
  240. package/src/notifications/approval-card-data.ts +94 -1
  241. package/src/notifications/background-failure-signal.ts +116 -0
  242. package/src/notifications/broadcaster.ts +3 -9
  243. package/src/notifications/copy-composer.ts +41 -8
  244. package/src/notifications/decision-engine.ts +55 -0
  245. package/src/notifications/destination-resolver.ts +45 -5
  246. package/src/notifications/emit-signal.ts +19 -5
  247. package/src/notifications/guardian-question-mode.ts +57 -0
  248. package/src/notifications/types.ts +2 -8
  249. package/src/oauth/__tests__/seed-providers-managed.test.ts +18 -0
  250. package/src/oauth/seed-providers.ts +261 -0
  251. package/src/permissions/checker.test.ts +21 -0
  252. package/src/permissions/checker.ts +6 -0
  253. package/src/permissions/gateway-threshold-reader.test.ts +184 -0
  254. package/src/permissions/gateway-threshold-reader.ts +144 -10
  255. package/src/permissions/prompter.ts +0 -6
  256. package/src/permissions/types.ts +5 -4
  257. package/src/persistence/__tests__/bulk-write-gate.test.ts +0 -38
  258. package/src/persistence/bulk-write-gate.ts +9 -10
  259. package/src/persistence/conversation-crud.ts +85 -133
  260. package/src/persistence/conversation-title-service.ts +43 -29
  261. package/src/persistence/embeddings/embedding-local.ts +14 -85
  262. package/src/persistence/embeddings/embedding-runtime-manager.ts +1 -0
  263. package/src/persistence/lifecycle-events-store.test.ts +50 -0
  264. package/src/persistence/lifecycle-events-store.ts +75 -5
  265. package/src/platform/platform-patch-queue.test.ts +368 -0
  266. package/src/platform/platform-patch-queue.ts +190 -0
  267. package/src/platform/sync-avatar.test.ts +527 -0
  268. package/src/platform/sync-avatar.ts +209 -0
  269. package/src/platform/sync-identity.test.ts +75 -0
  270. package/src/platform/sync-identity.ts +17 -110
  271. package/src/plugin-api/model-profiles.test.ts +2 -2
  272. package/src/plugin-api/model-profiles.ts +7 -7
  273. package/src/plugin-api/transcription-session.ts +1 -1
  274. package/src/plugins/defaults/compaction/window-manager.ts +4 -0
  275. package/src/plugins/defaults/image-recovery/recover.ts +2 -2
  276. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +133 -34
  277. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +21 -31
  278. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +13 -9
  279. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +31 -4
  280. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +0 -11
  281. package/src/plugins/defaults/memory/memory-retrospective-fork-boundary.ts +27 -13
  282. package/src/plugins/defaults/memory/memory-retrospective-job.ts +183 -113
  283. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +13 -13
  284. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +2 -2
  285. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +1 -1
  286. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +4 -2
  287. package/src/plugins/defaults/memory/substrate/sweep-job.ts +9 -42
  288. package/src/plugins/defaults/memory/v2/rerank-local.ts +1 -0
  289. package/src/plugins/defaults/memory/worker.ts +7 -0
  290. package/src/providers/__tests__/retry-callsite.test.ts +55 -0
  291. package/src/providers/connection-resolution.ts +2 -17
  292. package/src/providers/fallback-breaker.ts +389 -0
  293. package/src/providers/inference/__tests__/adapter-factory-opencode.test.ts +92 -0
  294. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +16 -0
  295. package/src/providers/inference/__tests__/base-url-security.test.ts +12 -0
  296. package/src/providers/inference/adapter-factory.ts +231 -3
  297. package/src/providers/inference/auth.ts +1 -0
  298. package/src/providers/inference/chatgpt-subscription-auth.ts +70 -0
  299. package/src/providers/inference/chatgpt-subscription-credentials.ts +84 -0
  300. package/src/providers/inference/resolve-auth.ts +1 -1
  301. package/src/providers/media-resolve.ts +2 -3
  302. package/src/providers/model-catalog.ts +75 -0
  303. package/src/providers/openai/__tests__/api-error-normalization.test.ts +22 -0
  304. package/src/providers/openai/__tests__/chat-template-flatten.test.ts +198 -0
  305. package/src/providers/openai/api-error-normalization.ts +13 -0
  306. package/src/providers/openai/chat-completions-provider.ts +84 -0
  307. package/src/providers/opencode/client.test.ts +150 -0
  308. package/src/providers/opencode/client.ts +61 -0
  309. package/src/providers/provider-resolvability.ts +20 -0
  310. package/src/providers/retry.ts +956 -68
  311. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +42 -0
  312. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +136 -0
  313. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +120 -0
  314. package/src/providers/speech-to-text/__tests__/resolve.test.ts +316 -12
  315. package/src/providers/speech-to-text/deepgram-flux-frames.ts +83 -3
  316. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +132 -4
  317. package/src/providers/speech-to-text/provider-catalog.ts +191 -3
  318. package/src/providers/speech-to-text/resolve.ts +132 -24
  319. package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +296 -0
  320. package/src/providers/types.ts +39 -0
  321. package/src/providers/usage-tracking.ts +12 -1
  322. package/src/runtime/AGENTS.md +1 -2
  323. package/src/runtime/__tests__/agent-wake.test.ts +105 -3
  324. package/src/runtime/__tests__/background-job-runner.test.ts +89 -3
  325. package/src/runtime/agent-wake.ts +44 -5
  326. package/src/runtime/background-job-runner.ts +68 -50
  327. package/src/runtime/channel-readiness-service.ts +0 -39
  328. package/src/runtime/http-server.ts +12 -4
  329. package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +18 -18
  330. package/src/runtime/routes/__tests__/backup-routes.test.ts +9 -5
  331. package/src/runtime/routes/__tests__/chatgpt-subscription-auth-routes.test.ts +372 -0
  332. package/src/runtime/routes/__tests__/contact-routes.test.ts +7 -0
  333. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +96 -0
  334. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +37 -0
  335. package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +2 -1
  336. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +87 -0
  337. package/src/runtime/routes/acp-claude-auth-routes.ts +25 -54
  338. package/src/runtime/routes/approval-routes.ts +0 -12
  339. package/src/runtime/routes/avatar-routes.ts +17 -30
  340. package/src/runtime/routes/backup-routes.ts +2 -2
  341. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +296 -113
  342. package/src/runtime/routes/contact-prompt-routes.ts +65 -6
  343. package/src/runtime/routes/contact-routes.ts +20 -15
  344. package/src/runtime/routes/conversation-query-routes.ts +98 -10
  345. package/src/runtime/routes/conversation-routes.ts +2 -2
  346. package/src/runtime/routes/credential-routes.ts +2 -1
  347. package/src/runtime/routes/guardian-approval-interception.ts +12 -9
  348. package/src/runtime/routes/inbound-message-handler.ts +21 -9
  349. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +18 -13
  350. package/src/runtime/routes/inbound-stages/admission-policy.ts +2 -2
  351. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +9 -8
  352. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -1
  353. package/src/runtime/routes/index.ts +2 -0
  354. package/src/runtime/routes/inference-profile-session-handler.ts +2 -2
  355. package/src/runtime/routes/inference-profiles-routes.ts +12 -7
  356. package/src/runtime/routes/inference-provider-connection-routes.ts +2 -2
  357. package/src/runtime/routes/inference-send-routes.ts +2 -2
  358. package/src/runtime/routes/ingress-status-routes.ts +2 -2
  359. package/src/runtime/routes/integrations/discord.ts +119 -0
  360. package/src/runtime/routes/llm-call-sites-routes.ts +2 -2
  361. package/src/runtime/routes/mcp-auth-routes.ts +12 -6
  362. package/src/runtime/routes/oauth-pending-flows.ts +136 -0
  363. package/src/runtime/routes/secret-routes.ts +7 -0
  364. package/src/runtime/routes/stt-routes.ts +38 -6
  365. package/src/runtime/routes/wake-conversation-routes.ts +8 -1
  366. package/src/runtime/routes/watch-routes.ts +9 -5
  367. package/src/runtime/sync/resource-sync-events.ts +2 -0
  368. package/src/runtime/trust-verdict-consumer.ts +3 -3
  369. package/src/schedule/__tests__/worker-feature-flags.test.ts +58 -0
  370. package/src/schedule/retry-policy.ts +8 -8
  371. package/src/schedule/run-script.ts +14 -23
  372. package/src/schedule/schedule-recovery.ts +0 -1
  373. package/src/schedule/scheduler.ts +89 -51
  374. package/src/schedule/worker.ts +57 -0
  375. package/src/security/__tests__/openai-device-auth.test.ts +355 -0
  376. package/src/security/oauth2.ts +7 -0
  377. package/src/security/openai-device-auth.ts +364 -0
  378. package/src/skills/available-skills.ts +10 -1
  379. package/src/skills/catalog-install.ts +19 -1
  380. package/src/skills/inline-command-runner.ts +6 -2
  381. package/src/skills/platform-compatibility.test.ts +49 -0
  382. package/src/skills/platform-compatibility.ts +65 -0
  383. package/src/skills/tool-manifest.ts +20 -0
  384. package/src/stt/__tests__/roles.test.ts +152 -0
  385. package/src/stt/daemon-batch-transcriber.ts +4 -1
  386. package/src/stt/roles.ts +180 -0
  387. package/src/stt/stt-stream-session.ts +3 -2
  388. package/src/stt/types.ts +33 -3
  389. package/src/subagent/manager.ts +12 -0
  390. package/src/telemetry/__tests__/live-voice-funnel.test.ts +34 -0
  391. package/src/telemetry/live-voice-funnel.ts +12 -2
  392. package/src/telemetry/telemetry-wire.generated.ts +6 -0
  393. package/src/telemetry/tool-audit.ts +83 -30
  394. package/src/telemetry/tool-executed-events-store.test.ts +0 -1
  395. package/src/telemetry/turn-outcome.ts +75 -4
  396. package/src/tools/client-os.ts +115 -0
  397. package/src/tools/computer-use/definitions.ts +11 -7
  398. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -2
  399. package/src/tools/credentials/broker-types.ts +0 -71
  400. package/src/tools/credentials/broker.ts +6 -201
  401. package/src/tools/executor.ts +2 -8
  402. package/src/tools/host-shell.test.ts +75 -0
  403. package/src/tools/host-shell.ts +24 -0
  404. package/src/tools/host-terminal/host-shell.ts +15 -44
  405. package/src/tools/mcp/mcp-tool-factory.ts +44 -9
  406. package/src/tools/permission-checker.ts +73 -21
  407. package/src/tools/policy-context.ts +1 -0
  408. package/src/tools/shared/filesystem/image-read.ts +1 -1
  409. package/src/tools/skills/find-similar-skills.ts +10 -2
  410. package/src/tools/skills/load.ts +41 -6
  411. package/src/tools/skills/sandbox-runner.ts +8 -16
  412. package/src/tools/skills/skill-tool-factory.ts +1 -0
  413. package/src/tools/terminal/__tests__/safe-env.test.ts +64 -1
  414. package/src/tools/terminal/safe-env.ts +39 -9
  415. package/src/tools/terminal/shell.ts +12 -31
  416. package/src/tools/tool-approval-handler.ts +64 -10
  417. package/src/tools/tool-types.ts +10 -0
  418. package/src/tools/types.ts +19 -3
  419. package/src/tools/ui-surface/surface-shape-docs.ts +4 -3
  420. package/src/tools/workflows/manage-workflows.ts +2 -2
  421. package/src/tools/workflows/run-workflow.test.ts +2 -1
  422. package/src/util/__tests__/file-use.test.ts +61 -0
  423. package/src/util/__tests__/process-table.test.ts +66 -0
  424. package/src/util/__tests__/process-tree.test.ts +46 -0
  425. package/src/util/errors.ts +1 -0
  426. package/src/util/file-use.ts +91 -0
  427. package/src/util/host-process.test.ts +17 -0
  428. package/src/util/host-process.ts +75 -0
  429. package/src/util/image-conversion.ts +39 -7
  430. package/src/util/platform.ts +32 -13
  431. package/src/util/process-table.ts +249 -0
  432. package/src/util/process-tree.ts +85 -129
  433. package/src/util/provider-error-patterns.ts +20 -0
  434. package/src/util/spawn.ts +9 -2
  435. package/src/workspace/git-service.ts +4 -14
  436. package/src/workspace/migrations/147-rename-colliding-backup-profile-names.ts +360 -0
  437. package/src/workspace/migrations/148-strip-unsupported-fallback-profiles.ts +117 -0
  438. package/src/workspace/migrations/149-repoint-backup-profile-selections.ts +204 -0
  439. package/src/workspace/migrations/150-stt-flux-provider-to-model-family.ts +139 -0
  440. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +160 -0
  441. package/src/workspace/migrations/registry.ts +8 -0
  442. package/src/__tests__/permission-types.test.ts +0 -17
  443. package/src/channels/gateway-discord-admission.ts +0 -32
  444. package/src/persistence/fork-message-copy.ts +0 -239
  445. package/src/plugins/defaults/memory/__tests__/fork-message-copy.test.ts +0 -221
  446. package/src/security/__tests__/oauth2-device-code.test.ts +0 -431
  447. package/src/security/oauth2-device-code.ts +0 -312
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JPEG conversion for images, backed by macOS `sips`.
2
+ * JPEG conversion for images, backed by sharp with a macOS `sips` fallback.
3
3
  *
4
4
  * Two consumers share this converter:
5
5
  * - transport optimization (`agent/image-optimize.ts`) downscales large
@@ -7,10 +7,8 @@
7
7
  * - storage normalization (attachment ingress + history hydration) converts
8
8
  * HEIF/HEIC — which Chromium-based clients cannot decode — to JPEG.
9
9
  *
10
- * Conversion runs `sips` (a macOS builtin); on other platforms or on any
11
- * failure it returns null and callers keep the original bytes. Results are
12
- * cached on disk keyed by content hash + conversion options, so repeated
13
- * conversions of the same image (or daemon restarts) skip the sips call.
10
+ * Results are cached on disk keyed by content hash + conversion options, so
11
+ * repeated conversions of the same image skip the encoder call.
14
12
  */
15
13
 
16
14
  import { createHash } from "node:crypto";
@@ -291,9 +289,41 @@ async function runSips(
291
289
  }
292
290
  }
293
291
 
292
+ async function runSharp(
293
+ inputBytes: Uint8Array,
294
+ options: ConvertToJpegOptions,
295
+ ): Promise<Buffer | null> {
296
+ try {
297
+ const { default: sharp } = await import("sharp");
298
+ let pipeline = sharp(inputBytes, { failOn: "error" }).autoOrient();
299
+ if (options.resizeToPx != null) {
300
+ pipeline = pipeline.resize(
301
+ options.resizeToPx.width,
302
+ options.resizeToPx.height,
303
+ { fit: "fill" },
304
+ );
305
+ } else if (options.maxDimensionPx != null) {
306
+ pipeline = pipeline.resize(
307
+ options.maxDimensionPx,
308
+ options.maxDimensionPx,
309
+ {
310
+ fit: "inside",
311
+ withoutEnlargement: true,
312
+ },
313
+ );
314
+ }
315
+ const out = await pipeline
316
+ .jpeg({ quality: options.quality ?? DEFAULT_JPEG_QUALITY })
317
+ .toBuffer();
318
+ return isCompleteJpeg(out) ? out : null;
319
+ } catch {
320
+ return null;
321
+ }
322
+ }
323
+
294
324
  /**
295
325
  * Convert an image to JPEG, optionally downscaling. Returns null when
296
- * conversion is unavailable (non-macOS) or fails; callers keep the original.
326
+ * conversion is unavailable or fails; callers keep the original.
297
327
  */
298
328
  export async function convertImageToJpeg(
299
329
  bytes: Uint8Array,
@@ -313,7 +343,9 @@ export async function convertImageToJpeg(
313
343
  return cached;
314
344
  }
315
345
 
316
- const converted = await runSips(bytes, options);
346
+ const converted =
347
+ (await runSharp(bytes, options)) ??
348
+ (process.platform === "darwin" ? await runSips(bytes, options) : null);
317
349
  if (!converted) {
318
350
  return null;
319
351
  }
@@ -1,7 +1,12 @@
1
1
  import { chmodSync, existsSync, mkdirSync, realpathSync } from "node:fs";
2
2
  import { homedir, tmpdir } from "node:os";
3
- import { basename, dirname, join, sep } from "node:path";
3
+ import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
4
4
 
5
+ import {
6
+ AVATAR_IMAGE_FILENAME,
7
+ AVATAR_MANIFEST_FILENAME,
8
+ resolveAvatarDir,
9
+ } from "@vellumai/avatar-manifest";
5
10
  import { SEEDS } from "@vellumai/environments";
6
11
 
7
12
  import { getWorkspaceDirOverride } from "../config/env-registry.js";
@@ -160,20 +165,14 @@ export function getSoundsDir(): string {
160
165
 
161
166
  /** Returns the avatar directory ($VELLUM_WORKSPACE_DIR/data/avatar). */
162
167
  export function getAvatarDir(): string {
163
- return join(getWorkspaceDir(), "data", "avatar");
168
+ return resolveAvatarDir(getWorkspaceDir());
164
169
  }
165
170
 
166
- /** Canonical filename for the custom avatar PNG. */
167
- export const AVATAR_IMAGE_FILENAME = "avatar-image.png";
168
-
169
171
  /** Returns the canonical avatar image path ($VELLUM_WORKSPACE_DIR/data/avatar/avatar-image.png). */
170
172
  export function getAvatarImagePath(): string {
171
173
  return join(getAvatarDir(), AVATAR_IMAGE_FILENAME);
172
174
  }
173
175
 
174
- /** Canonical filename for the avatar state manifest. */
175
- export const AVATAR_MANIFEST_FILENAME = "avatar.json";
176
-
177
176
  /** Returns the canonical avatar manifest path ($VELLUM_WORKSPACE_DIR/data/avatar/avatar.json). */
178
177
  export function getAvatarManifestPath(): string {
179
178
  return join(getAvatarDir(), AVATAR_MANIFEST_FILENAME);
@@ -442,12 +441,32 @@ export function getWorkspaceDir(): string {
442
441
  * /data/.vellum/workspace → /data/.vellum/workspace
443
442
  */
444
443
  export function getWorkspaceDirDisplay(): string {
445
- const abs = getWorkspaceDir();
446
- const home = homedir();
447
- if (abs.startsWith(home + "/") || abs === home) {
448
- return "~" + abs.slice(home.length);
444
+ return formatHomeRelativePath(getWorkspaceDir(), homedir());
445
+ }
446
+
447
+ interface PathOperations {
448
+ isAbsolute(path: string): boolean;
449
+ relative(from: string, to: string): string;
450
+ sep: string;
451
+ }
452
+
453
+ export function formatHomeRelativePath(
454
+ absolutePath: string,
455
+ homePath: string,
456
+ pathOperations: PathOperations = { isAbsolute, relative, sep },
457
+ ): string {
458
+ const relativeToHome = pathOperations.relative(homePath, absolutePath);
459
+ if (relativeToHome === "") {
460
+ return "~";
461
+ }
462
+ if (
463
+ relativeToHome !== ".." &&
464
+ !relativeToHome.startsWith(`..${pathOperations.sep}`) &&
465
+ !pathOperations.isAbsolute(relativeToHome)
466
+ ) {
467
+ return `~${pathOperations.sep}${relativeToHome}`;
449
468
  }
450
- return abs;
469
+ return absolutePath;
451
470
  }
452
471
 
453
472
  /** Returns $VELLUM_WORKSPACE_DIR/config.json */
@@ -0,0 +1,249 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readdirSync, readFileSync } from "node:fs";
3
+ import { promisify } from "node:util";
4
+
5
+ const execFileAsync = promisify(execFile);
6
+
7
+ const PS_PROCESS_TABLE_COMMAND = [
8
+ "ps",
9
+ "-A",
10
+ "-ww",
11
+ "-o",
12
+ "pid=,ppid=,command=",
13
+ ];
14
+ const WINDOWS_PROCESS_TABLE_COMMAND = [
15
+ "powershell.exe",
16
+ "-NoProfile",
17
+ "-NonInteractive",
18
+ "-Command",
19
+ "$OutputEncoding = [Console]::OutputEncoding = [Text.UTF8Encoding]::new(); Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CommandLine,Name,WorkingSetSize,HandleCount | ConvertTo-Json -Compress",
20
+ ];
21
+
22
+ export interface ProcessTableRow {
23
+ pid: number;
24
+ ppid: number;
25
+ /** Raw command line. Callers must redact it before logging or persistence. */
26
+ command: string;
27
+ rssBytes?: number;
28
+ handleCount?: number;
29
+ }
30
+
31
+ export interface ProcessTableOptions {
32
+ platform?: NodeJS.Platform;
33
+ processTable?: readonly ProcessTableRow[] | null;
34
+ }
35
+
36
+ export function getProcessTableRows(
37
+ options: ProcessTableOptions = {},
38
+ ): readonly ProcessTableRow[] {
39
+ if (options.processTable !== undefined) {
40
+ return options.processTable ?? [];
41
+ }
42
+ return listProcessTable(options.platform);
43
+ }
44
+
45
+ interface WindowsProcessRow {
46
+ ProcessId?: unknown;
47
+ ParentProcessId?: unknown;
48
+ CommandLine?: unknown;
49
+ Name?: unknown;
50
+ WorkingSetSize?: unknown;
51
+ HandleCount?: unknown;
52
+ }
53
+
54
+ function nonNegativeNumber(value: unknown): number | null {
55
+ if (typeof value !== "number" && typeof value !== "string") {
56
+ return null;
57
+ }
58
+ const parsed = Number(value);
59
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
60
+ }
61
+
62
+ function parseProcStat(
63
+ content: string,
64
+ ): { command: string; ppid: number } | null {
65
+ const lparen = content.indexOf("(");
66
+ const rparen = content.lastIndexOf(")");
67
+ if (lparen === -1 || rparen === -1 || rparen < lparen) {
68
+ return null;
69
+ }
70
+ const ppid = Number(content.slice(rparen + 2).split(" ")[1]);
71
+ if (!Number.isInteger(ppid)) {
72
+ return null;
73
+ }
74
+ return { command: content.slice(lparen + 1, rparen), ppid };
75
+ }
76
+
77
+ function listProcessesFromProc(): ProcessTableRow[] {
78
+ const rows: ProcessTableRow[] = [];
79
+ for (const entry of readdirSync("/proc")) {
80
+ const pid = Number(entry);
81
+ if (!Number.isInteger(pid) || pid <= 0) {
82
+ continue;
83
+ }
84
+
85
+ try {
86
+ const stat = parseProcStat(readFileSync(`/proc/${pid}/stat`, "utf8"));
87
+ if (!stat) {
88
+ continue;
89
+ }
90
+ const commandLine = readFileSync(`/proc/${pid}/cmdline`, "utf8")
91
+ .split("\0")
92
+ .filter(Boolean)
93
+ .join(" ");
94
+ rows.push({
95
+ pid,
96
+ ppid: stat.ppid,
97
+ command: commandLine || stat.command,
98
+ });
99
+ } catch {
100
+ // Processes can exit between reading the directory and their files.
101
+ }
102
+ }
103
+ return rows;
104
+ }
105
+
106
+ export function parsePsProcessTable(output: string): ProcessTableRow[] {
107
+ const rows: ProcessTableRow[] = [];
108
+ for (const line of output.split("\n")) {
109
+ const match = line.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);
110
+ if (!match) {
111
+ continue;
112
+ }
113
+ rows.push({
114
+ pid: Number(match[1]),
115
+ ppid: Number(match[2]),
116
+ command: match[3].trim(),
117
+ });
118
+ }
119
+ return rows;
120
+ }
121
+
122
+ export function parseWindowsProcessTable(output: string): ProcessTableRow[] {
123
+ const decoded: unknown = JSON.parse(output.trim().replace(/^\uFEFF/, ""));
124
+ const values = Array.isArray(decoded) ? decoded : [decoded];
125
+ const rows: ProcessTableRow[] = [];
126
+ for (const value of values) {
127
+ if (!value || typeof value !== "object") {
128
+ continue;
129
+ }
130
+ const processRow = value as WindowsProcessRow;
131
+ const pid = Number(processRow.ProcessId);
132
+ const ppid = Number(processRow.ParentProcessId);
133
+ if (!Number.isInteger(pid) || pid <= 0 || !Number.isInteger(ppid)) {
134
+ continue;
135
+ }
136
+ const commandLine =
137
+ typeof processRow.CommandLine === "string" ? processRow.CommandLine : "";
138
+ const name = typeof processRow.Name === "string" ? processRow.Name : "";
139
+ const rssBytes = nonNegativeNumber(processRow.WorkingSetSize);
140
+ const handleCount = nonNegativeNumber(processRow.HandleCount);
141
+ rows.push({
142
+ pid,
143
+ ppid,
144
+ command: commandLine || name,
145
+ ...(rssBytes == null ? {} : { rssBytes }),
146
+ ...(handleCount == null ? {} : { handleCount }),
147
+ });
148
+ }
149
+ return rows;
150
+ }
151
+
152
+ function runProcessTableCommand(
153
+ command: string[],
154
+ parser: (output: string) => ProcessTableRow[],
155
+ ): ProcessTableRow[] {
156
+ const result = Bun.spawnSync({
157
+ cmd: command,
158
+ stdout: "pipe",
159
+ stderr: "ignore",
160
+ });
161
+ if (result.exitCode !== 0) {
162
+ throw new Error(
163
+ `Process enumeration failed with exit code ${result.exitCode}`,
164
+ );
165
+ }
166
+ return parser(new TextDecoder().decode(result.stdout));
167
+ }
168
+
169
+ async function runProcessTableCommandAsync(
170
+ command: string[],
171
+ parser: (output: string) => ProcessTableRow[],
172
+ ): Promise<ProcessTableRow[]> {
173
+ const { stdout } = await execFileAsync(command[0], command.slice(1), {
174
+ encoding: "utf8",
175
+ maxBuffer: 8 * 1024 * 1024,
176
+ });
177
+ return parser(stdout);
178
+ }
179
+
180
+ function listProcessesFromPs(): ProcessTableRow[] {
181
+ return runProcessTableCommand(PS_PROCESS_TABLE_COMMAND, parsePsProcessTable);
182
+ }
183
+
184
+ function listProcessesFromPowerShell(): ProcessTableRow[] {
185
+ return runProcessTableCommand(
186
+ WINDOWS_PROCESS_TABLE_COMMAND,
187
+ parseWindowsProcessTable,
188
+ );
189
+ }
190
+
191
+ export function listProcessTable(
192
+ platform: NodeJS.Platform = process.platform,
193
+ ): ProcessTableRow[] {
194
+ if (platform === "win32") {
195
+ return listProcessesFromPowerShell();
196
+ }
197
+ if (platform === "linux") {
198
+ try {
199
+ return listProcessesFromProc();
200
+ } catch {
201
+ return listProcessesFromPs();
202
+ }
203
+ }
204
+ return listProcessesFromPs();
205
+ }
206
+
207
+ export async function listProcessTableAsync(
208
+ platform: NodeJS.Platform = process.platform,
209
+ ): Promise<ProcessTableRow[]> {
210
+ if (platform === "win32") {
211
+ return runProcessTableCommandAsync(
212
+ WINDOWS_PROCESS_TABLE_COMMAND,
213
+ parseWindowsProcessTable,
214
+ );
215
+ }
216
+ if (platform === "linux") {
217
+ try {
218
+ return listProcessesFromProc();
219
+ } catch {
220
+ return runProcessTableCommandAsync(
221
+ PS_PROCESS_TABLE_COMMAND,
222
+ parsePsProcessTable,
223
+ );
224
+ }
225
+ }
226
+ return runProcessTableCommandAsync(
227
+ PS_PROCESS_TABLE_COMMAND,
228
+ parsePsProcessTable,
229
+ );
230
+ }
231
+
232
+ export function readRawProcessCommand(pid: number): string | null {
233
+ if (process.platform === "linux") {
234
+ try {
235
+ return readFileSync(`/proc/${pid}/cmdline`, "utf8")
236
+ .split("\0")
237
+ .filter(Boolean)
238
+ .join(" ");
239
+ } catch {
240
+ return null;
241
+ }
242
+ }
243
+
244
+ try {
245
+ return listProcessTable().find((row) => row.pid === pid)?.command ?? null;
246
+ } catch {
247
+ return null;
248
+ }
249
+ }
@@ -9,11 +9,11 @@
9
9
  * process that is actually parented to it.
10
10
  */
11
11
 
12
- import { execFile } from "node:child_process";
13
- import { readdirSync, readFileSync } from "node:fs";
14
- import { promisify } from "node:util";
15
-
16
- const execFileAsync = promisify(execFile);
12
+ import {
13
+ listProcessTableAsync,
14
+ type ProcessTableRow,
15
+ readRawProcessCommand,
16
+ } from "./process-table.js";
17
17
 
18
18
  /**
19
19
  * Whether a process runs plugin-owned code or is the daemon itself / one of its
@@ -52,6 +52,12 @@ export interface ProcTreeNode {
52
52
  children: ProcTreeNode[];
53
53
  }
54
54
 
55
+ interface ProcessTreeBuilder {
56
+ build: (pid: number) => ProcTreeNode;
57
+ byPid: Map<number, ProcInfo>;
58
+ visited: Set<number>;
59
+ }
60
+
55
61
  /** Interpreters whose script argument is more descriptive than argv[0]. */
56
62
  const RUNTIMES = new Set([
57
63
  "bun",
@@ -64,7 +70,14 @@ const RUNTIMES = new Set([
64
70
  "env",
65
71
  ]);
66
72
 
67
- const basename = (p: string): string => p.split("/").pop() || p;
73
+ const basename = (p: string): string => p.split(/[\\/]/).pop() || p;
74
+
75
+ function splitCommand(command: string): string[] {
76
+ return Array.from(
77
+ command.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g),
78
+ (match) => match[1] ?? match[2] ?? match[3],
79
+ );
80
+ }
68
81
 
69
82
  /** Script extensions whose path we summarize as `<parent>-<file>`. */
70
83
  const SCRIPT_EXT_RE = /\.(ts|js|mjs|cjs|py)$/;
@@ -76,7 +89,7 @@ const SCRIPT_EXT_RE = /\.(ts|js|mjs|cjs|py)$/;
76
89
  * filename when the script sits at the filesystem root.
77
90
  */
78
91
  function scriptName(scriptPath: string): string {
79
- const parts = scriptPath.split("/").filter(Boolean);
92
+ const parts = scriptPath.split(/[\\/]/).filter(Boolean);
80
93
  const file = parts[parts.length - 1].replace(SCRIPT_EXT_RE, "");
81
94
  const parent = parts.length >= 2 ? parts[parts.length - 2] : "";
82
95
  return parent ? `${parent}-${file}` : file;
@@ -93,13 +106,14 @@ function scriptName(scriptPath: string): string {
93
106
  * (`/…/vellum-qdrant`) keep their bare executable name.
94
107
  */
95
108
  export function deriveName(command: string): string {
96
- const tokens = command.trim().split(/\s+/).filter(Boolean);
109
+ const tokens = splitCommand(command.trim());
97
110
  if (tokens.length === 0) {
98
111
  return "(unknown)";
99
112
  }
100
113
 
101
114
  const argv0 = basename(tokens[0]);
102
- if (RUNTIMES.has(argv0)) {
115
+ const runtimeName = argv0.replace(/\.exe$/i, "").toLowerCase();
116
+ if (RUNTIMES.has(runtimeName)) {
103
117
  const args = tokens.slice(1);
104
118
  const script = args.find((t) => SCRIPT_EXT_RE.test(t));
105
119
  if (script) {
@@ -109,7 +123,7 @@ export function deriveName(command: string): string {
109
123
  // "what was run" rather than an opaque `bun`. Flags are dropped as noise.
110
124
  const meaningful = args.filter((t) => !t.startsWith("-"));
111
125
  if (meaningful.length > 0) {
112
- return `${argv0} ${meaningful.join(" ")}`;
126
+ return `${runtimeName} ${meaningful.join(" ")}`;
113
127
  }
114
128
  }
115
129
  return argv0;
@@ -126,12 +140,8 @@ export function deriveName(command: string): string {
126
140
  * {@link deriveName} descriptor.
127
141
  */
128
142
  export function readProcessCommand(pid: number): string | null {
129
- try {
130
- const raw = readFileSync(`/proc/${pid}/cmdline`, "utf8");
131
- return deriveName(raw.split("\0").filter(Boolean).join(" "));
132
- } catch {
133
- return null;
134
- }
143
+ const command = readRawProcessCommand(pid);
144
+ return command == null ? null : deriveName(command);
135
145
  }
136
146
 
137
147
  /**
@@ -143,7 +153,8 @@ export function readProcessCommand(pid: number): string | null {
143
153
  * (`.../plugins/defaults/<name>/`). The `plugins-data/` state directory is
144
154
  * deliberately not matched — the slash after `plugins` excludes it.
145
155
  */
146
- const PLUGIN_PATH_RE = /(?:^|\/)plugins\/([^/\s]+)(?:\/([^/\s]+))?/;
156
+ const PLUGIN_PATH_RE =
157
+ /(?:^|[\\/])plugins[\\/]([^\\/\s]+)(?:[\\/]([^\\/\s]+))?/;
147
158
 
148
159
  /**
149
160
  * Classify whether a raw command line belongs to plugin-owned code, and if so
@@ -169,122 +180,23 @@ export function deriveOrigin(command: string): ProcessOrigin {
169
180
  return `plugin:${name}`;
170
181
  }
171
182
 
172
- /**
173
- * Parse a `/proc/<pid>/stat` line into its leading fields. `comm` (the
174
- * executable name) may itself contain spaces and parentheses, so the fixed
175
- * fields are read relative to the final `)` rather than by naive splitting.
176
- * Returns null if the line is malformed.
177
- */
178
- function parseProcStat(content: string): { comm: string; ppid: number } | null {
179
- const lparen = content.indexOf("(");
180
- const rparen = content.lastIndexOf(")");
181
- if (lparen === -1 || rparen === -1 || rparen < lparen) {
182
- return null;
183
- }
184
- const comm = content.slice(lparen + 1, rparen);
185
- // After ")" come: " <state> <ppid> …" — split the remainder on spaces.
186
- const rest = content.slice(rparen + 2).split(" ");
187
- const ppid = Number(rest[1]);
188
- if (!Number.isInteger(ppid)) {
189
- return null;
190
- }
191
- return { comm, ppid };
192
- }
193
-
194
- /** Read the live process table from Linux `/proc`. Throws if `/proc` is absent. */
195
- function listProcessesFromProc(): ProcInfo[] {
196
- const entries = readdirSync("/proc");
197
- const procs: ProcInfo[] = [];
198
- for (const entry of entries) {
199
- const pid = Number(entry);
200
- if (!Number.isInteger(pid) || pid <= 0) {
201
- continue;
202
- }
203
-
204
- let ppid: number;
205
- let comm: string;
206
- try {
207
- const parsed = parseProcStat(readFileSync(`/proc/${pid}/stat`, "utf8"));
208
- if (!parsed) {
209
- continue;
210
- }
211
- ({ ppid, comm } = parsed);
212
- } catch {
213
- // Process exited between readdir and read — skip.
214
- continue;
215
- }
216
-
217
- // `/proc/<pid>/cmdline` is NUL-delimited and empty for kernel threads.
218
- // Redact the raw command line via deriveName to strip secrets (tokens,
219
- // API keys, database URLs) that are commonly passed as process arguments.
220
- // The raw command line is read here but never stored — only the derived
221
- // safe descriptor is kept.
222
- let command = comm;
223
- // Origin is derived from the raw command path (which carries the plugin
224
- // directory), so it is classified before deriveName redacts the path away.
225
- let origin: ProcessOrigin = "workspace";
226
- try {
227
- const raw = readFileSync(`/proc/${pid}/cmdline`, "utf8");
228
- const joined = raw.split("\0").filter(Boolean).join(" ");
229
- if (joined) {
230
- command = deriveName(joined);
231
- origin = deriveOrigin(joined);
232
- }
233
- } catch {
234
- // Fall back to comm.
235
- }
236
-
237
- procs.push({ pid, ppid, command, origin });
238
- }
239
- return procs;
240
- }
241
-
242
- /** Read the live process table via the `ps` command (macOS / no `/proc`). */
243
- async function listProcessesFromPs(): Promise<ProcInfo[]> {
244
- const { stdout } = await execFileAsync(
245
- "ps",
246
- ["-A", "-o", "pid=,ppid=,command="],
247
- { maxBuffer: 8 * 1024 * 1024 },
248
- );
249
-
250
- const procs: ProcInfo[] = [];
251
- for (const line of stdout.split("\n")) {
252
- const m = line.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);
253
- if (!m) {
254
- continue;
255
- }
256
- const raw = m[3].trim();
257
- procs.push({
258
- pid: Number(m[1]),
259
- ppid: Number(m[2]),
260
- command: deriveName(raw),
261
- origin: deriveOrigin(raw),
262
- });
263
- }
264
- return procs;
265
- }
266
-
267
183
  /**
268
184
  * Enumerate the live process table as `(pid, ppid, command)` rows. Prefers
269
- * Linux `/proc`; falls back to `ps` when `/proc` is unavailable (e.g. macOS).
185
+ * Linux `/proc`, uses `ps` on macOS, and queries Win32_Process on Windows.
270
186
  */
271
- export async function listProcesses(): Promise<ProcInfo[]> {
272
- try {
273
- return listProcessesFromProc();
274
- } catch {
275
- return listProcessesFromPs();
276
- }
187
+ export async function listProcesses(
188
+ rows?: ProcessTableRow[],
189
+ ): Promise<ProcInfo[]> {
190
+ const processRows = rows ?? (await listProcessTableAsync());
191
+ return processRows.map(({ pid, ppid, command }) => ({
192
+ pid,
193
+ ppid,
194
+ command: deriveName(command),
195
+ origin: deriveOrigin(command),
196
+ }));
277
197
  }
278
198
 
279
- /**
280
- * Build the process subtree rooted at `rootPid` from a flat process list.
281
- * Children are ordered by PID. Self-references and already-visited PIDs are
282
- * skipped so a malformed table cannot produce an infinite tree.
283
- */
284
- export function buildProcessTree(
285
- procs: ProcInfo[],
286
- rootPid: number,
287
- ): ProcTreeNode {
199
+ function createProcessTreeBuilder(procs: ProcInfo[]): ProcessTreeBuilder {
288
200
  const byPid = new Map<number, ProcInfo>();
289
201
  const childrenOf = new Map<number, number[]>();
290
202
  for (const p of procs) {
@@ -316,5 +228,49 @@ export function buildProcessTree(
316
228
  };
317
229
  };
318
230
 
319
- return build(rootPid);
231
+ return { build, byPid, visited };
232
+ }
233
+
234
+ /**
235
+ * Build the process subtree rooted at `rootPid` from a flat process list.
236
+ * Children are ordered by PID. Self-references and already-visited PIDs are
237
+ * skipped so a malformed table cannot produce an infinite tree.
238
+ */
239
+ export function buildProcessTree(
240
+ procs: ProcInfo[],
241
+ rootPid: number,
242
+ ): ProcTreeNode {
243
+ return createProcessTreeBuilder(procs).build(rootPid);
244
+ }
245
+
246
+ /** Build a synthetic system root containing every enumerated process once. */
247
+ export function buildSystemProcessTree(procs: ProcInfo[]): ProcTreeNode {
248
+ const { build, byPid, visited } = createProcessTreeBuilder(procs);
249
+ const children: ProcTreeNode[] = [];
250
+ const orderedProcesses = [...byPid.values()].sort((a, b) => a.pid - b.pid);
251
+
252
+ for (const processInfo of orderedProcesses) {
253
+ if (
254
+ processInfo.ppid !== processInfo.pid &&
255
+ !byPid.has(processInfo.ppid) &&
256
+ !visited.has(processInfo.pid)
257
+ ) {
258
+ children.push(build(processInfo.pid));
259
+ }
260
+ }
261
+
262
+ // Malformed process tables can contain parent cycles with no natural root.
263
+ for (const processInfo of orderedProcesses) {
264
+ if (!visited.has(processInfo.pid)) {
265
+ children.push(build(processInfo.pid));
266
+ }
267
+ }
268
+
269
+ return {
270
+ pid: 0,
271
+ name: "system",
272
+ command: "",
273
+ origin: "workspace",
274
+ children,
275
+ };
320
276
  }