@vellumai/assistant 0.11.7 → 0.11.8-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 (446) hide show
  1. package/ARCHITECTURE.md +10 -10
  2. package/Dockerfile +1 -0
  3. package/docs/architecture/security.md +1 -1
  4. package/docs/error-handling.md +1 -2
  5. package/docs/guardian-request-flow.md +51 -5
  6. package/docs/trusted-contact-access.md +13 -12
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/avatar-catalog/package.json +19 -0
  9. package/{src/avatar/character-components.ts → node_modules/@vellumai/avatar-catalog/src/catalog.ts} +12 -44
  10. package/node_modules/@vellumai/avatar-catalog/src/colors.ts +19 -0
  11. package/node_modules/@vellumai/avatar-catalog/src/index.ts +25 -0
  12. package/node_modules/@vellumai/avatar-catalog/src/types.ts +47 -0
  13. package/node_modules/@vellumai/avatar-catalog/tsconfig.json +20 -0
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  18. package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +43 -12
  19. package/node_modules/@vellumai/environments/package.json +2 -1
  20. package/node_modules/@vellumai/environments/src/__tests__/test-path-guard.test.ts +89 -0
  21. package/node_modules/@vellumai/environments/src/test-path-guard.ts +113 -0
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  24. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  25. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  26. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +8 -18
  27. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +46 -0
  28. package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +1 -0
  29. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +46 -0
  30. package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +62 -21
  31. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +14 -0
  32. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +53 -0
  33. package/node_modules/@vellumai/gateway-client/src/index.ts +11 -5
  34. package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +1 -0
  35. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  36. package/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  37. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  38. package/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  39. package/openapi.yaml +481 -9
  40. package/package.json +3 -1
  41. package/scripts/sync-llm-catalog.ts +3 -0
  42. package/src/__tests__/agent-loop-callsite-precedence.test.ts +16 -0
  43. package/src/__tests__/agent-loop-compacted-history-transform.test.ts +304 -0
  44. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +3 -0
  45. package/src/__tests__/agent-wake-override-profile.test.ts +3 -0
  46. package/src/__tests__/anthropic-provider.test.ts +56 -0
  47. package/src/__tests__/approval-cascade.test.ts +0 -13
  48. package/src/__tests__/attachments-store.test.ts +117 -6
  49. package/src/__tests__/attachments.test.ts +122 -0
  50. package/src/__tests__/bundled-asset.test.ts +22 -1
  51. package/src/__tests__/channel-approval.test.ts +14 -7
  52. package/src/__tests__/channel-approvals.test.ts +3 -3
  53. package/src/__tests__/channel-delivery-store.test.ts +58 -0
  54. package/src/__tests__/channel-inbound-disk-pressure.test.ts +1 -0
  55. package/src/__tests__/channel-invite-transport.test.ts +1 -0
  56. package/src/__tests__/channel-readiness-slack-remote.test.ts +3 -3
  57. package/src/__tests__/compaction-events.test.ts +0 -12
  58. package/src/__tests__/compactor-call-site-logging.test.ts +7 -1
  59. package/src/__tests__/compactor-retained-image-validation.test.ts +15 -0
  60. package/src/__tests__/config-schema.test.ts +35 -0
  61. package/src/__tests__/contacts-write.test.ts +141 -3
  62. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -13
  63. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +0 -12
  64. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -13
  65. package/src/__tests__/conversation-agent-loop.test.ts +95 -13
  66. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -13
  67. package/src/__tests__/conversation-load-history-repair.test.ts +418 -0
  68. package/src/__tests__/conversation-media-retry.test.ts +33 -2
  69. package/src/__tests__/conversation-pairing.test.ts +54 -0
  70. package/src/__tests__/conversation-process-callsite.test.ts +0 -12
  71. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -13
  72. package/src/__tests__/conversation-queue.test.ts +4 -13
  73. package/src/__tests__/conversation-retry-route.test.ts +5 -0
  74. package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
  75. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
  76. package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
  77. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
  78. package/src/__tests__/conversation-sight-frames.test.ts +615 -0
  79. package/src/__tests__/conversation-slash-queue.test.ts +0 -13
  80. package/src/__tests__/conversation-slash-unknown.test.ts +0 -13
  81. package/src/__tests__/conversation-speed-override.test.ts +0 -12
  82. package/src/__tests__/conversation-store-ephemeral.test.ts +0 -12
  83. package/src/__tests__/conversation-summarize-route.test.ts +5 -0
  84. package/src/__tests__/conversation-summarize-up-to.test.ts +102 -0
  85. package/src/__tests__/conversation-wait-for-idle.test.ts +0 -12
  86. package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -5
  87. package/src/__tests__/conversation-workspace-injection.test.ts +0 -17
  88. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -17
  89. package/src/__tests__/credential-health-service.test.ts +45 -0
  90. package/src/__tests__/credential-record-retire.test.ts +256 -0
  91. package/src/__tests__/credential-record-write-through.test.ts +8 -4
  92. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +0 -4
  93. package/src/__tests__/db-conversation-inference-profile-migration.test.ts +0 -4
  94. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +0 -8
  95. package/src/__tests__/delete-propagation.test.ts +121 -3
  96. package/src/__tests__/evict-conversations-for-reload.test.ts +46 -6
  97. package/src/__tests__/file-ops-service.test.ts +8 -3
  98. package/src/__tests__/gemini-provider.test.ts +64 -0
  99. package/src/__tests__/guardian-action-service-scope.test.ts +118 -0
  100. package/src/__tests__/guardian-card-withdrawal.test.ts +299 -5
  101. package/src/__tests__/guardian-expiry-notifier.test.ts +117 -13
  102. package/src/__tests__/guardian-gateway-sim.test.ts +119 -0
  103. package/src/__tests__/guardian-gateway-sim.ts +56 -20
  104. package/src/__tests__/guardian-reply-router-answer-mode.test.ts +6 -0
  105. package/src/__tests__/guardian-request-status-sync.test.ts +162 -0
  106. package/src/__tests__/guardian-wait-controller.test.ts +6 -0
  107. package/src/__tests__/headless-browser-mode.test.ts +27 -0
  108. package/src/__tests__/helpers/mock-conversation.ts +2 -0
  109. package/src/__tests__/list-messages-system-card.test.ts +70 -0
  110. package/src/__tests__/llm-catalog-parity.test.ts +2 -0
  111. package/src/__tests__/managed-profile-guard.test.ts +2 -2
  112. package/src/__tests__/media-resolve-image-validation.test.ts +106 -0
  113. package/src/__tests__/media-stream-server-integration.test.ts +6 -0
  114. package/src/__tests__/non-member-access-request.test.ts +12 -0
  115. package/src/__tests__/notification-discord-adapter.test.ts +76 -4
  116. package/src/__tests__/oauth-commands-routes.test.ts +21 -0
  117. package/src/__tests__/openai-provider.test.ts +63 -0
  118. package/src/__tests__/outbound-slack-persistence.test.ts +114 -2
  119. package/src/__tests__/persist-unsendable-image-downscale.test.ts +63 -1
  120. package/src/__tests__/persist-unsendable-image.test.ts +19 -0
  121. package/src/__tests__/plugin-app-serve-routes.test.ts +2 -2
  122. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
  123. package/src/__tests__/process-message-background-slack.test.ts +1 -1
  124. package/src/__tests__/process-message-display-content.test.ts +1 -1
  125. package/src/__tests__/processing-flag-persist-failure.test.ts +0 -5
  126. package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +0 -17
  127. package/src/__tests__/qdrant-manager.test.ts +6 -0
  128. package/src/__tests__/reaction-persistence.test.ts +58 -62
  129. package/src/__tests__/retry-thinking-adaptive-only.test.ts +67 -0
  130. package/src/__tests__/server-history-render.test.ts +22 -0
  131. package/src/__tests__/sight-frame-retention.test.ts +362 -0
  132. package/src/__tests__/slack-reaction-approvals.test.ts +44 -24
  133. package/src/__tests__/slack-reaction-guardian-approval.test.ts +29 -1
  134. package/src/__tests__/subagent-fork-notifications.test.ts +2 -0
  135. package/src/__tests__/subagent-manager-notify.test.ts +2 -0
  136. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  137. package/src/__tests__/subagent-notify-stale-rebuild.test.ts +85 -0
  138. package/src/__tests__/subagent-tool-gate-mode.test.ts +4 -4
  139. package/src/__tests__/terminal-tools.test.ts +9 -9
  140. package/src/__tests__/tool-result-spool.test.ts +5 -8
  141. package/src/__tests__/usage-attribution.test.ts +26 -0
  142. package/src/acp/__tests__/acp-auth-marker-store.test.ts +191 -0
  143. package/src/acp/__tests__/acp-claude-oauth.test.ts +262 -0
  144. package/src/acp/__tests__/acp-connect-card-state.test.ts +17 -0
  145. package/src/acp/__tests__/helpers/acp-history-db.ts +10 -3
  146. package/src/acp/acp-auth-marker-store.ts +196 -0
  147. package/src/acp/acp-claude-oauth.ts +154 -4
  148. package/src/acp/acp-connect-card-state.ts +49 -8
  149. package/src/acp/agent-process.ts +1 -0
  150. package/src/acp/auto-install.ts +1 -0
  151. package/src/acp/client-handler.ts +1 -0
  152. package/src/acp/prepare-agent-env.ts +79 -1
  153. package/src/acp/resolve-agent.ts +12 -0
  154. package/src/acp/session-manager.test.ts +183 -0
  155. package/src/acp/session-manager.ts +110 -6
  156. package/src/acp/types.ts +17 -0
  157. package/src/agent/attachments.ts +7 -2
  158. package/src/agent/loop.ts +59 -1
  159. package/src/api/events/contact-form-closed.ts +32 -0
  160. package/src/api/events/contact-record-request.ts +63 -0
  161. package/src/api/events/contact-request.ts +10 -3
  162. package/src/api/index.ts +20 -0
  163. package/src/api/responses/conversation-message.ts +20 -0
  164. package/src/api/responses/home.ts +101 -0
  165. package/src/approvals/AGENTS.md +1 -1
  166. package/src/approvals/guardian-card-withdrawal.ts +127 -30
  167. package/src/approvals/guardian-decision-primitive.ts +9 -0
  168. package/src/approvals/guardian-expiry-notifier.ts +22 -16
  169. package/src/approvals/guardian-request-status-sync.ts +98 -0
  170. package/src/avatar/svg-compositor.ts +1 -1
  171. package/src/avatar/traits-png-sync.ts +1 -1
  172. package/src/bundler/app-compiler.ts +1 -0
  173. package/src/bundler/compiler-tools.ts +1 -0
  174. package/src/bundler/package-resolver.ts +1 -0
  175. package/src/calls/__tests__/voice-session-bridge.test.ts +183 -3
  176. package/src/calls/guardian-action-sweep.ts +6 -1
  177. package/src/calls/voice-session-bridge.ts +46 -1
  178. package/src/channels/__tests__/gateway-guardian-requests.test.ts +13 -18
  179. package/src/channels/gateway-guardian-requests.ts +37 -18
  180. package/src/cli/__tests__/contacts-record-prompt-cli.test.ts +329 -0
  181. package/src/cli/commands/__tests__/browser.test.ts +65 -1
  182. package/src/cli/commands/browser.ts +30 -10
  183. package/src/cli/commands/contacts.help.ts +173 -11
  184. package/src/cli/commands/contacts.ts +343 -4
  185. package/src/cli/commands/db/refresh.ts +1 -0
  186. package/src/cli/commands/oauth/index.help.ts +6 -3
  187. package/src/cli/commands/oauth/request.test.ts +37 -0
  188. package/src/cli/commands/oauth/request.ts +7 -6
  189. package/src/cli/lib/bundled-marketplace.json +1 -1
  190. package/src/cli/lib/install-from-github.ts +20 -19
  191. package/src/cli/lib/install-plugin-dependencies.ts +3 -5
  192. package/src/cli/lib/merge-plugin-tree.ts +1 -0
  193. package/src/cli/lib/publish-plugin.ts +4 -1
  194. package/src/config/__tests__/balanced-model-experiment.test.ts +63 -31
  195. package/src/config/__tests__/default-profile-catalog.test.ts +2 -2
  196. package/src/config/balanced-model-experiment.ts +15 -9
  197. package/src/config/bundled-skills/contacts/SKILL.md +61 -18
  198. package/src/config/default-profile-catalog.ts +12 -11
  199. package/src/config/feature-flag-registry.json +22 -13
  200. package/src/config/schema.ts +2 -0
  201. package/src/config/schemas/__tests__/live-voice.test.ts +13 -0
  202. package/src/config/schemas/live-voice.ts +7 -0
  203. package/src/config/schemas/sight.ts +45 -0
  204. package/src/config/skills.ts +2 -1
  205. package/src/contacts/__tests__/mirror-reconciler.test.ts +352 -0
  206. package/src/contacts/contact-store.ts +47 -4
  207. package/src/contacts/contacts-write.ts +5 -2
  208. package/src/contacts/mirror-reconciler.ts +210 -0
  209. package/src/contacts/mirror-tombstones.ts +42 -0
  210. package/src/contacts/notify-contacts-changed.ts +11 -2
  211. package/src/context/compactor.ts +47 -19
  212. package/src/context/tool-result-spool.ts +21 -14
  213. package/src/conversations/message-consolidation.ts +2 -2
  214. package/src/credential-health/credential-health-service.ts +37 -6
  215. package/src/daemon/conversation-agent-loop-handlers.ts +19 -2
  216. package/src/daemon/conversation-agent-loop.ts +100 -1
  217. package/src/daemon/conversation-media-retry.ts +11 -6
  218. package/src/daemon/conversation-messaging.ts +293 -30
  219. package/src/daemon/conversation-runtime-assembly.ts +67 -0
  220. package/src/daemon/conversation-sight-frames.ts +187 -0
  221. package/src/daemon/conversation-store.ts +18 -28
  222. package/src/daemon/conversation-surfaces.ts +12 -2
  223. package/src/daemon/conversation-tool-setup.ts +39 -10
  224. package/src/daemon/conversation.ts +159 -16
  225. package/src/daemon/daemon-control.ts +6 -17
  226. package/src/daemon/handlers/config-model.test.ts +1 -0
  227. package/src/daemon/handlers/conversations.ts +10 -11
  228. package/src/daemon/install-assistant-command.ts +1 -0
  229. package/src/daemon/lifecycle.ts +10 -13
  230. package/src/daemon/message-types/sync.ts +4 -0
  231. package/src/daemon/process-message.ts +2 -17
  232. package/src/daemon/reaction-history-render.test.ts +162 -0
  233. package/src/daemon/reaction-history-render.ts +115 -0
  234. package/src/daemon/reaction-record.test.ts +162 -0
  235. package/src/daemon/reaction-record.ts +83 -0
  236. package/src/daemon/shutdown-handlers.ts +0 -2
  237. package/src/daemon/summarize-boundary.ts +12 -0
  238. package/src/daemon/video-thumbnail.ts +1 -1
  239. package/src/home/feed-types.ts +4 -0
  240. package/src/home/feed-writer.ts +25 -0
  241. package/src/live-voice/__tests__/live-voice-attach-frame.test.ts +679 -0
  242. package/src/live-voice/__tests__/live-voice-attach-image.test.ts +8 -5
  243. package/src/live-voice/__tests__/live-voice-photo.test.ts +637 -1
  244. package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +787 -0
  245. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +347 -0
  246. package/src/live-voice/__tests__/live-voice-vad.test.ts +109 -0
  247. package/src/live-voice/live-voice-photo.ts +420 -65
  248. package/src/live-voice/live-voice-session.ts +311 -27
  249. package/src/live-voice/protocol.ts +147 -11
  250. package/src/messaging/provider-message-metadata.ts +7 -0
  251. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +113 -1
  252. package/src/messaging/providers/channel-transport.ts +37 -0
  253. package/src/messaging/providers/discord/api.ts +4 -4
  254. package/src/messaging/providers/discord/send.test.ts +177 -2
  255. package/src/messaging/providers/discord/send.ts +197 -10
  256. package/src/messaging/providers/discord/transport.ts +10 -0
  257. package/src/messaging/providers/index.ts +42 -0
  258. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +129 -3
  259. package/src/messaging/providers/slack/__tests__/auth.test.ts +35 -0
  260. package/src/messaging/providers/slack/__tests__/message-content.test.ts +152 -0
  261. package/src/messaging/providers/slack/adapter.ts +112 -27
  262. package/src/messaging/providers/slack/api.test.ts +10 -0
  263. package/src/messaging/providers/slack/auth.ts +32 -6
  264. package/src/messaging/providers/slack/client.ts +0 -15
  265. package/src/messaging/providers/slack/message-content.ts +137 -0
  266. package/src/messaging/providers/slack/send.test.ts +43 -2
  267. package/src/messaging/providers/slack/send.ts +18 -5
  268. package/src/messaging/providers/slack/transport.ts +10 -0
  269. package/src/messaging/providers/slack/types.ts +21 -2
  270. package/src/messaging/providers/telegram-bot/adapter.ts +5 -1
  271. package/src/messaging/providers/telegram-bot/send.test.ts +49 -2
  272. package/src/messaging/providers/telegram-bot/send.ts +41 -1
  273. package/src/messaging/providers/telegram-bot/transport.ts +10 -0
  274. package/src/messaging/providers/telegram-bot/withdraw.ts +13 -5
  275. package/src/messaging/reaction-envelopes.ts +79 -0
  276. package/src/monitoring/control.ts +2 -2
  277. package/src/monitoring/page-cache.ts +5 -1
  278. package/src/notifications/AGENTS.md +1 -1
  279. package/src/notifications/__tests__/guardian-feed-projection.test.ts +360 -0
  280. package/src/notifications/__tests__/home-feed-side-effect.test.ts +8 -0
  281. package/src/notifications/adapters/discord.ts +62 -9
  282. package/src/notifications/conversation-pairing.ts +31 -4
  283. package/src/notifications/emit-signal.ts +8 -5
  284. package/src/notifications/guardian-delivery-recorder.ts +47 -6
  285. package/src/notifications/guardian-feed-projection.ts +492 -0
  286. package/src/notifications/home-feed-side-effect.ts +59 -11
  287. package/src/oauth/byo-connection.test.ts +22 -0
  288. package/src/oauth/byo-connection.ts +11 -5
  289. package/src/oauth/connection.ts +3 -2
  290. package/src/oauth/platform-connection.test.ts +35 -0
  291. package/src/oauth/platform-connection.ts +18 -13
  292. package/src/oauth/scope-utils.ts +48 -0
  293. package/src/oauth/seed-providers.ts +12 -0
  294. package/src/outbound-proxy/certs.ts +5 -4
  295. package/src/permissions/question-guardian-request.test.ts +7 -1
  296. package/src/persistence/attachments-store.ts +134 -31
  297. package/src/persistence/conversation-crud.ts +137 -4
  298. package/src/persistence/conversation-types.ts +109 -0
  299. package/src/persistence/db-connection.ts +3 -21
  300. package/src/persistence/delivery-crud.ts +42 -12
  301. package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +15 -2
  302. package/src/persistence/embeddings/embedding-local.ts +7 -112
  303. package/src/persistence/embeddings/qdrant-manager.ts +4 -1
  304. package/src/persistence/llm-request-log-store.ts +1 -1
  305. package/src/persistence/migrations/370-acp-session-history-auth-error-code.ts +36 -0
  306. package/src/persistence/migrations/371-acp-session-history-auth-error-credential.ts +40 -0
  307. package/src/persistence/migrations/372-create-acp-refused-credentials.ts +34 -0
  308. package/src/persistence/migrations/373-acp-auth-marker-index.ts +27 -0
  309. package/src/persistence/migrations/__tests__/370-acp-session-history-auth-error-code.test.ts +65 -0
  310. package/src/persistence/migrations/__tests__/371-acp-session-history-auth-error-credential.test.ts +68 -0
  311. package/src/persistence/migrations/__tests__/372-create-acp-refused-credentials.test.ts +64 -0
  312. package/src/persistence/migrations/__tests__/373-acp-auth-marker-index.test.ts +82 -0
  313. package/src/persistence/schema/acp.ts +25 -0
  314. package/src/persistence/steps.ts +8 -0
  315. package/src/platform/platform-patch-queue.test.ts +43 -41
  316. package/src/plugin-api/index.ts +12 -0
  317. package/src/plugins/defaults/compaction/window-manager.ts +2 -0
  318. package/src/plugins/defaults/image-recovery/recover.ts +12 -1
  319. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +166 -1
  320. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +3 -0
  321. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +3 -0
  322. package/src/plugins/defaults/memory/jobs-worker.ts +1 -2
  323. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +53 -9
  324. package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +4 -7
  325. package/src/plugins/defaults/memory/src/memory-worker-routes.ts +1 -1
  326. package/src/plugins/defaults/memory/v1/job-handlers/index-maintenance.ts +15 -1
  327. package/src/plugins/defaults/memory/worker-control.ts +1 -2
  328. package/src/providers/content-block-schema.ts +1 -0
  329. package/src/providers/fireworks/client.ts +9 -1
  330. package/src/providers/gemini/client.ts +22 -15
  331. package/src/providers/media-resolve.ts +24 -10
  332. package/src/providers/model-catalog.ts +91 -0
  333. package/src/providers/openai/chat-completions-provider.ts +44 -5
  334. package/src/providers/retry.ts +16 -11
  335. package/src/providers/types.ts +56 -0
  336. package/src/routes/__tests__/control.test.ts +4 -3
  337. package/src/routes/control.ts +1 -1
  338. package/src/routes/route-host-client.ts +1 -1
  339. package/src/runtime/AGENTS.md +30 -12
  340. package/src/runtime/__tests__/agent-wake.test.ts +71 -6
  341. package/src/runtime/__tests__/guardian-form-registry.test.ts +242 -0
  342. package/src/runtime/access-request-helper.ts +15 -7
  343. package/src/runtime/agent-wake.ts +16 -4
  344. package/src/runtime/channel-approval-types.ts +8 -5
  345. package/src/runtime/channel-invite-transport.ts +2 -0
  346. package/src/runtime/channel-invite-transports/discord.ts +27 -0
  347. package/src/runtime/channel-readiness-service.ts +4 -4
  348. package/src/runtime/channel-reply-delivery.ts +72 -28
  349. package/src/runtime/guardian-action-service.ts +9 -5
  350. package/src/runtime/guardian-form-registry.ts +256 -0
  351. package/src/runtime/guardian-reply-router.ts +33 -14
  352. package/src/runtime/invite-instruction-generator.ts +14 -2
  353. package/src/runtime/migrations/vbundle-metadata-merge.ts +4 -7
  354. package/src/runtime/no-response.ts +13 -74
  355. package/src/runtime/routes/__tests__/acp-routes.test.ts +527 -0
  356. package/src/runtime/routes/__tests__/app-management-shared-apps.test.ts +74 -0
  357. package/src/runtime/routes/__tests__/contact-form-kinds.test.ts +22 -0
  358. package/src/runtime/routes/__tests__/contact-record-prompt-route.test.ts +303 -0
  359. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +3 -4
  360. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +6 -0
  361. package/src/runtime/routes/__tests__/route-host-worker-routes.test.ts +3 -4
  362. package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +5 -6
  363. package/src/runtime/routes/acp-routes-list.test.ts +58 -0
  364. package/src/runtime/routes/acp-routes.ts +253 -47
  365. package/src/runtime/routes/app-management-routes.ts +80 -47
  366. package/src/runtime/routes/approval-routes.ts +9 -0
  367. package/src/runtime/routes/archive-utils.ts +6 -1
  368. package/src/runtime/routes/avatar-routes.ts +1 -1
  369. package/src/runtime/routes/channel-route-shared.ts +18 -20
  370. package/src/runtime/routes/contact-prompt-routes.ts +271 -105
  371. package/src/runtime/routes/conversation-routes.ts +41 -32
  372. package/src/runtime/routes/credential-prompt-routes.ts +10 -1
  373. package/src/runtime/routes/debug-bash-routes.ts +1 -0
  374. package/src/runtime/routes/guardian-expiry-sweep.ts +89 -31
  375. package/src/runtime/routes/guardian-form-routes.ts +115 -0
  376. package/src/runtime/routes/inbound-message-handler.ts +82 -29
  377. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -3
  378. package/src/runtime/routes/inbound-stages/edit-intercept.ts +6 -0
  379. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +6 -0
  380. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +6 -1
  381. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +50 -17
  382. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +97 -94
  383. package/src/runtime/routes/index.ts +2 -0
  384. package/src/runtime/routes/integrations/slack/channel.ts +39 -18
  385. package/src/runtime/routes/migration-routes.ts +1 -3
  386. package/src/runtime/routes/monitoring-routes.ts +0 -3
  387. package/src/runtime/routes/oauth-commands-routes.ts +18 -5
  388. package/src/runtime/routes/route-host-worker-routes.ts +1 -1
  389. package/src/runtime/routes/schedule-worker-routes.ts +1 -1
  390. package/src/runtime/tool-grant-request-helper.ts +4 -0
  391. package/src/schedule/run-script.ts +1 -0
  392. package/src/schedule/scheduler.ts +1 -1
  393. package/src/schedule/worker-control.ts +2 -2
  394. package/src/security/oauth2.ts +2 -11
  395. package/src/security/secure-keys.ts +55 -8
  396. package/src/skills/catalog-install.ts +7 -4
  397. package/src/skills/clawhub.ts +1 -0
  398. package/src/skills/inline-command-runner.ts +1 -0
  399. package/src/stt/__tests__/room-noise-floor.test.ts +108 -0
  400. package/src/stt/room-noise-floor.ts +140 -0
  401. package/src/subagent/notify.ts +49 -3
  402. package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
  403. package/src/tools/acp/spawn.ts +2 -2
  404. package/src/tools/browser/__tests__/browser-status.test.ts +9 -0
  405. package/src/tools/browser/browser-execution.ts +10 -43
  406. package/src/tools/browser/browser-manager.ts +1 -0
  407. package/src/tools/browser/browser-status-constants.ts +6 -5
  408. package/src/tools/browser/runtime-check.ts +2 -0
  409. package/src/tools/channel/react-to-message.test.ts +193 -0
  410. package/src/tools/channel/react-to-message.ts +159 -0
  411. package/src/tools/credentials/metadata-store.ts +115 -19
  412. package/src/tools/credentials/store.ts +0 -2
  413. package/src/tools/filesystem/read.ts +2 -2
  414. package/src/tools/host-filesystem/read.ts +2 -2
  415. package/src/tools/host-terminal/host-shell.test.ts +24 -0
  416. package/src/tools/host-terminal/host-shell.ts +11 -38
  417. package/src/tools/shared/filesystem/file-ops-service.ts +10 -7
  418. package/src/tools/shared/shell-output.test.ts +104 -0
  419. package/src/tools/shared/shell-output.ts +112 -39
  420. package/src/tools/skills/sandbox-runner.ts +1 -0
  421. package/src/tools/system/request-permission.test.ts +34 -0
  422. package/src/tools/system/request-permission.ts +29 -5
  423. package/src/tools/terminal/shell.test.ts +63 -0
  424. package/src/tools/terminal/shell.ts +12 -44
  425. package/src/tools/tool-input-schemas.ts +2 -0
  426. package/src/tools/tool-manifest.ts +2 -0
  427. package/src/usage/attribution.ts +12 -4
  428. package/src/util/__tests__/worker-ownership.test.ts +77 -0
  429. package/src/util/__tests__/worker-process-command.test.ts +246 -1
  430. package/src/util/bun-runtime.ts +44 -13
  431. package/src/util/bundled-asset.ts +9 -1
  432. package/src/util/disk-usage.ts +1 -0
  433. package/src/util/file-use.ts +4 -1
  434. package/src/util/guardian-form-timeouts.ts +50 -0
  435. package/src/util/image-conversion.ts +1 -0
  436. package/src/util/oauth-request-body.test.ts +59 -0
  437. package/src/util/oauth-request-body.ts +56 -5
  438. package/src/util/platform.ts +69 -73
  439. package/src/util/process-table.ts +16 -5
  440. package/src/util/read-stdin.ts +5 -0
  441. package/src/util/spawn.ts +1 -0
  442. package/src/util/sqlite3-runtime.ts +25 -5
  443. package/src/util/worker-ownership.ts +193 -0
  444. package/src/util/worker-process.ts +375 -37
  445. package/src/workspace/git-service.ts +4 -12
  446. package/src/workspace/migrations/010-app-dir-rename.ts +1 -0
@@ -1,6 +1,6 @@
1
- import { chmodSync, existsSync, mkdirSync, realpathSync } from "node:fs";
2
- import { homedir, tmpdir } from "node:os";
3
- import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
1
+ import { chmodSync, existsSync, mkdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { delimiter, dirname, isAbsolute, join, relative, sep } from "node:path";
4
4
 
5
5
  import {
6
6
  AVATAR_IMAGE_FILENAME,
@@ -8,6 +8,7 @@ import {
8
8
  resolveAvatarDir,
9
9
  } from "@vellumai/avatar-manifest";
10
10
  import { SEEDS } from "@vellumai/environments";
11
+ import { assertTestPathIsEphemeral as assertEphemeralInTests } from "@vellumai/environments/test-path-guard";
11
12
 
12
13
  import { getWorkspaceDirOverride } from "../config/env-registry.js";
13
14
 
@@ -54,6 +55,62 @@ export function isWindows(): boolean {
54
55
  return process.platform === "win32";
55
56
  }
56
57
 
58
+ /**
59
+ * Per-user application data root: `~/Library/Application Support` on macOS,
60
+ * `%APPDATA%` on Windows, `$XDG_DATA_HOME` (default `~/.local/share`) elsewhere.
61
+ */
62
+ export function getUserAppDataDir(): string {
63
+ if (isMacOS()) {
64
+ return join(homedir(), "Library", "Application Support");
65
+ }
66
+ if (isWindows()) {
67
+ return process.env.APPDATA ?? join(homedir(), "AppData", "Roaming");
68
+ }
69
+ return process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
70
+ }
71
+
72
+ /**
73
+ * Extra directories to prepend to PATH so daemon-spawned tools are found
74
+ * even when launched from a minimal environment (macOS .app bundle). Empty
75
+ * on Windows, where PATH already carries the installer-managed entries.
76
+ */
77
+ export function getExtraToolPathDirs(): string[] {
78
+ if (isWindows()) {
79
+ return [];
80
+ }
81
+ return [
82
+ "/opt/homebrew/bin",
83
+ "/usr/local/bin",
84
+ join(homedir(), ".local", "bin"),
85
+ ];
86
+ }
87
+
88
+ /**
89
+ * Add missing `dirs` to the PATH entry of `env` in place, using the platform
90
+ * delimiter. Windows keys the variable as `Path`, so the existing key is
91
+ * matched case-insensitively rather than assuming `PATH`.
92
+ */
93
+ export function addToPathEnv(
94
+ env: Record<string, string | undefined>,
95
+ dirs: string[],
96
+ position: "front" | "back" = "front",
97
+ ): void {
98
+ if (dirs.length === 0) {
99
+ return;
100
+ }
101
+ const key =
102
+ Object.keys(env).find((k) => k.toUpperCase() === "PATH") ?? "PATH";
103
+ const entries = (env[key] ?? "").split(delimiter).filter(Boolean);
104
+ const missing = dirs.filter((d) => !entries.includes(d));
105
+ if (missing.length === 0) {
106
+ return;
107
+ }
108
+ env[key] =
109
+ position === "front"
110
+ ? [...missing, ...entries].join(delimiter)
111
+ : [...entries, ...missing].join(delimiter);
112
+ }
113
+
57
114
  /**
58
115
  * Returns the raw platform string from Node.js (e.g. 'darwin', 'linux', 'win32').
59
116
  * Prefer this over accessing process.platform directly so all platform
@@ -341,78 +398,17 @@ export function getProcPidPath(name: string): string {
341
398
  //
342
399
  // A test process must never resolve the workspace (or the vellum root) to a
343
400
  // real, non-temp directory: production code exercised by a test would then
344
- // read and destructively write live state. The tmpdir redirection normally
345
- // comes from the bunfig.toml test preload, but bun only loads bunfig from the
346
- // cwd, so `bun test` run from any other directory (for example a source
347
- // checkout inside a deployed container's workspace) skips the preload and
348
- // inherits the ambient VELLUM_WORKSPACE_DIR. The containment assertion
349
- // therefore lives here, in production code, where it fires no matter how the
350
- // test process was launched.
351
- //
352
- // Containment logic mirrors src/__tests__/assert-not-live-db.ts, which cannot
353
- // be imported here (production code must not depend on test machinery).
354
-
355
- /** Lazily computed: is this process a `bun test` run? */
356
- let isTestProcess: boolean | undefined;
357
-
358
- /**
359
- * Resolve symlinks in the deepest existing ancestor of `p`, then re-append
360
- * the not-yet-created tail. This keeps the containment check honest both for
361
- * paths under a symlinked temp root (macOS /var/folders) and for symlinks
362
- * that point outside it, whether or not the leaf exists yet.
363
- */
364
- function canonicalizeForWorkspaceGuard(p: string): string {
365
- let cur = p;
366
- const tail: string[] = [];
367
- for (;;) {
368
- try {
369
- return join(realpathSync(cur), ...tail);
370
- } catch {
371
- const parent = dirname(cur);
372
- if (parent === cur) {
373
- return p;
374
- }
375
- tail.unshift(basename(cur));
376
- cur = parent;
377
- }
378
- }
379
- }
401
+ // read and destructively write live state. The containment assertion is
402
+ // shared with the gateway via @vellumai/environments/test-path-guard;
403
+ // src/__tests__/assert-not-live-db.ts keeps its own containment check
404
+ // because test machinery must not import production dependencies.
380
405
 
381
406
  function assertTestPathIsEphemeral(dir: string): void {
382
- isTestProcess ??=
383
- process.env.NODE_ENV === "test" ||
384
- process.env.BUN_TEST === "1" ||
385
- // `bun test` sets NODE_ENV=test only when unset; Bun.main being the test
386
- // file itself is the backstop signal that survives a preset NODE_ENV.
387
- (typeof Bun !== "undefined" &&
388
- /\.(test|spec)\.[cm]?[jt]sx?$/.test(Bun.main));
389
- if (!isTestProcess) {
390
- return;
391
- }
392
- // Escape hatch for the rare intentional run against a real workspace,
393
- // shared with assertTestDbIsIsolated() in persistence/db-connection.ts.
394
- // Deliberately NOT added to tools/terminal/safe-env.ts: a daemon-level
395
- // opt-out must not propagate into agent-spawned shells and disarm the
396
- // guard for tests run from there.
397
- if (process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS === "1") {
398
- return;
399
- }
400
- const tmpRoot = canonicalizeForWorkspaceGuard(tmpdir());
401
- const resolved = canonicalizeForWorkspaceGuard(dir);
402
- if (resolved !== tmpRoot && !resolved.startsWith(tmpRoot + sep)) {
403
- throw new Error(
404
- [
405
- `Refusing to use ${dir} (resolves to ${resolved}) in a test process: it is not under the temp directory (${tmpRoot}).`,
406
- "",
407
- "Tests must only touch an ephemeral workspace; a real one would expose",
408
- "live assistant state to destructive test fixtures. This usually means",
409
- "`bun test` ran from a cwd without the repo bunfig.toml, so the test",
410
- "preload that redirects VELLUM_WORKSPACE_DIR to a tmpdir never loaded.",
411
- "Run tests from the assistant package root, or set",
412
- "VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS=1 to bypass deliberately.",
413
- ].join("\n"),
414
- );
415
- }
407
+ assertEphemeralInTests(dir, {
408
+ // Shared with assertTestDbIsIsolated() in persistence/db-connection.ts.
409
+ allowEnvVar: "VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS",
410
+ runHint: "Run tests from the assistant package root.",
411
+ });
416
412
  }
417
413
 
418
414
  /**
@@ -157,6 +157,7 @@ function runProcessTableCommand(
157
157
  cmd: command,
158
158
  stdout: "pipe",
159
159
  stderr: "ignore",
160
+ windowsHide: true,
160
161
  });
161
162
  if (result.exitCode !== 0) {
162
163
  throw new Error(
@@ -173,6 +174,7 @@ async function runProcessTableCommandAsync(
173
174
  const { stdout } = await execFileAsync(command[0], command.slice(1), {
174
175
  encoding: "utf8",
175
176
  maxBuffer: 8 * 1024 * 1024,
177
+ windowsHide: true,
176
178
  });
177
179
  return parser(stdout);
178
180
  }
@@ -229,6 +231,19 @@ export async function listProcessTableAsync(
229
231
  );
230
232
  }
231
233
 
234
+ /**
235
+ * The process-table row for `pid`, or null when the process is gone or the
236
+ * table cannot be read. Callers get `ppid` alongside the command line, so
237
+ * ownership and identity can be decided from a single snapshot.
238
+ */
239
+ function findProcessRow(pid: number): ProcessTableRow | null {
240
+ try {
241
+ return listProcessTable().find((row) => row.pid === pid) ?? null;
242
+ } catch {
243
+ return null;
244
+ }
245
+ }
246
+
232
247
  export function readRawProcessCommand(pid: number): string | null {
233
248
  if (process.platform === "linux") {
234
249
  try {
@@ -241,9 +256,5 @@ export function readRawProcessCommand(pid: number): string | null {
241
256
  }
242
257
  }
243
258
 
244
- try {
245
- return listProcessTable().find((row) => row.pid === pid)?.command ?? null;
246
- } catch {
247
- return null;
248
- }
259
+ return findProcessRow(pid)?.command ?? null;
249
260
  }
@@ -25,3 +25,8 @@ export const STDIN_FD = 0;
25
25
  export function readStdinSync(encoding: BufferEncoding = "utf-8"): string {
26
26
  return readFileSync(STDIN_FD, encoding);
27
27
  }
28
+
29
+ /** Read all data currently available on stdin as raw bytes. */
30
+ export function readStdinBytesSync(): Buffer {
31
+ return readFileSync(STDIN_FD);
32
+ }
package/src/util/spawn.ts CHANGED
@@ -29,6 +29,7 @@ export function spawnWithTimeout(
29
29
  const proc = Bun.spawn(cmd, {
30
30
  stdout: "pipe",
31
31
  stderr: "pipe",
32
+ windowsHide: true,
32
33
  env: {
33
34
  ...process.env,
34
35
  PATH: [
@@ -21,6 +21,10 @@
21
21
  */
22
22
 
23
23
  import { existsSync } from "node:fs";
24
+ import { homedir } from "node:os";
25
+ import { join } from "node:path";
26
+
27
+ import { isWindows } from "./platform.js";
24
28
 
25
29
  let cachedSqlite3Path: string | undefined | null;
26
30
 
@@ -37,11 +41,27 @@ export function findSqlite3(): string | undefined {
37
41
  // 1. Common install locations. We check these before `Bun.which` so we
38
42
  // pick a deterministic, predictable binary even when PATH is
39
43
  // unusual (e.g. a daemon launched with a stripped env).
40
- for (const p of [
41
- "/usr/bin/sqlite3",
42
- "/usr/local/bin/sqlite3",
43
- "/opt/homebrew/bin/sqlite3",
44
- ]) {
44
+ const candidates = isWindows()
45
+ ? [
46
+ join(
47
+ process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"),
48
+ "Programs",
49
+ "sqlite",
50
+ "sqlite3.exe",
51
+ ),
52
+ join(
53
+ process.env.ProgramFiles ?? "C:\\Program Files",
54
+ "SQLite",
55
+ "sqlite3.exe",
56
+ ),
57
+ "C:\\sqlite\\sqlite3.exe",
58
+ ]
59
+ : [
60
+ "/usr/bin/sqlite3",
61
+ "/usr/local/bin/sqlite3",
62
+ "/opt/homebrew/bin/sqlite3",
63
+ ];
64
+ for (const p of candidates) {
45
65
  if (existsSync(p)) {
46
66
  cachedSqlite3Path = p;
47
67
  return p;
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Process-tree ownership for background worker processes.
3
+ *
4
+ * A worker's parent IS its owner. The process table is therefore the
5
+ * authoritative answer to "whose worker is this": unlike a PID file it
6
+ * survives a crash, and a second owner cannot overwrite it.
7
+ *
8
+ * Nothing here is specific to one worker kind. `listWorkerProcesses` matches
9
+ * on the absolute worker path the caller passes, and `classifyWorkerOwnership`
10
+ * takes injected liveness and PID-1 predicates, so any worker whose spawner
11
+ * knows its own PID can use both.
12
+ */
13
+
14
+ import { listProcessTable, readRawProcessCommand } from "./process-table.js";
15
+
16
+ export interface WorkerProcess {
17
+ pid: number;
18
+ ppid: number;
19
+ }
20
+
21
+ /**
22
+ * What this process may do with a worker it found in the process table.
23
+ *
24
+ * - `reclaim`: parented to us, a worker we started and lost track of. Reaping
25
+ * it is what enforces one live worker per owning process.
26
+ * - `orphan`: reparented to init, or its owner is gone. Nobody else will ever
27
+ * clean it up.
28
+ * - `foreign`: owned by another live process. The memory-worker process runs
29
+ * its own backend against this same workspace and is entitled to its own
30
+ * worker; signalling it is what made the two replace each other in a loop.
31
+ *
32
+ * A parent of PID 1 is ambiguous, and resolving it wrongly in either direction
33
+ * costs something real. `docker-entrypoint.sh` execs the daemon, so PID 1 can
34
+ * BE the daemon and its child a healthy sibling's worker. Under `docker run
35
+ * --init`, and on any host, PID 1 is an init process instead, so the same
36
+ * parentage means the owner died and the worker needs reclaiming. Callers
37
+ * therefore pass what PID 1 actually is rather than inferring it from whether
38
+ * the deployment is containerized, which is true in both container shapes.
39
+ */
40
+ export type WorkerOwnership = "reclaim" | "orphan" | "foreign";
41
+
42
+ export function classifyWorkerOwnership(
43
+ worker: WorkerProcess,
44
+ selfPid: number,
45
+ isOwnerAlive: (pid: number) => boolean,
46
+ pid1OwnsWorkers: boolean,
47
+ ): WorkerOwnership {
48
+ if (worker.ppid === selfPid) {
49
+ return "reclaim";
50
+ }
51
+ if (worker.ppid <= 1) {
52
+ return pid1OwnsWorkers ? "foreign" : "orphan";
53
+ }
54
+ if (!isOwnerAlive(worker.ppid)) {
55
+ return "orphan";
56
+ }
57
+ return "foreign";
58
+ }
59
+
60
+ /** The packaged daemon binary, with or without a Windows extension. */
61
+ const PACKAGED_DAEMON_ARGV0 = /^vellum-daemon(\.exe)?$/i;
62
+
63
+ /** The runtime the source daemon is launched under. */
64
+ const BUN_ARGV0 = /^bun(\.exe)?$/i;
65
+
66
+ /** The daemon entry script, as a whole trailing path segment pair. */
67
+ const DAEMON_ENTRY_ARG = /(^|\/)daemon\/main\.(ts|js)$/;
68
+
69
+ /**
70
+ * Split a command line into argv, keeping quoted paths whole.
71
+ *
72
+ * Splitting on whitespace first would shred the ordinary Windows install path
73
+ * `"C:\\Program Files\\Vellum\\...\\vellum-daemon.exe"` into `"C:\\Program` and
74
+ * make the packaged daemon unrecognisable, which is the very install this
75
+ * recovery path exists for. Separators are normalised per token so a caller
76
+ * can take a basename either way.
77
+ */
78
+ function tokenizeCommandLine(command: string): string[] {
79
+ const tokens: string[] = [];
80
+ let current = "";
81
+ let quote: string | null = null;
82
+ let started = false;
83
+
84
+ for (const ch of command) {
85
+ if (quote !== null) {
86
+ if (ch === quote) {
87
+ quote = null;
88
+ } else {
89
+ current += ch;
90
+ }
91
+ continue;
92
+ }
93
+ if (ch === '"' || ch === "'") {
94
+ quote = ch;
95
+ started = true;
96
+ continue;
97
+ }
98
+ if (/\s/.test(ch)) {
99
+ if (started) {
100
+ tokens.push(current);
101
+ current = "";
102
+ started = false;
103
+ }
104
+ continue;
105
+ }
106
+ current += ch;
107
+ started = true;
108
+ }
109
+ if (started) {
110
+ tokens.push(current);
111
+ }
112
+ return tokens.map((token) => token.replaceAll("\\", "/"));
113
+ }
114
+
115
+ /**
116
+ * Whether a command line is an assistant daemon, judged by argv shape rather
117
+ * than by substring.
118
+ *
119
+ * Two accepted shapes: the packaged binary as argv0 (which also covers the
120
+ * source daemon once `process.title` rewrites its argv), or bun as argv0 with
121
+ * the daemon entry script among its arguments.
122
+ *
123
+ * The shape matters because this authorises an irreversible signal. A
124
+ * substring test would accept any command line that merely mentions a
125
+ * `daemon/main` path, so an unrelated service, an editor, or a test runner
126
+ * holding a recycled PID would qualify. `cli/src/lib/orphan-detection.test.ts`
127
+ * already pins that exact collision (`node /opt/unrelated-service/daemon/main.ts`)
128
+ * as something we must not claim.
129
+ */
130
+ export function isDaemonCommand(command: string | null): boolean {
131
+ if (command == null) {
132
+ return false;
133
+ }
134
+ const tokens = tokenizeCommandLine(command);
135
+ const argv0 = tokens[0]?.split("/").pop() ?? "";
136
+ if (PACKAGED_DAEMON_ARGV0.test(argv0)) {
137
+ return true;
138
+ }
139
+ if (!BUN_ARGV0.test(argv0)) {
140
+ return false;
141
+ }
142
+ return tokens.slice(1).some((token) => DAEMON_ENTRY_ARG.test(token));
143
+ }
144
+
145
+ /**
146
+ * Whether PID 1 is an assistant daemon rather than an init process.
147
+ *
148
+ * True only where the daemon was exec'd as PID 1 (`docker-entrypoint.sh`),
149
+ * which is what makes a worker parented to 1 a live sibling's rather than an
150
+ * orphan. Under `docker run --init` PID 1 is docker-init and under launchd or
151
+ * systemd it is the system init, so both answer false and PID-1 orphans stay
152
+ * reclaimable. Unreadable means false, which keeps the reclaiming behaviour.
153
+ *
154
+ * Callers that already hold a process-table snapshot pass PID 1's command line
155
+ * so this does not read the table a second time.
156
+ */
157
+ export function pid1OwnsWorkers(pid1Command?: string | null): boolean {
158
+ if (process.pid === 1) {
159
+ return true;
160
+ }
161
+ return isDaemonCommand(
162
+ pid1Command === undefined ? readRawProcessCommand(1) : pid1Command,
163
+ );
164
+ }
165
+
166
+ /**
167
+ * Live worker processes matching `workerPath`, as `(pid, ppid)` pairs.
168
+ *
169
+ * `workerPath` is an absolute path unique to one worker of one install, so a
170
+ * sibling instance's workers never match. `model`, when given, must appear as
171
+ * a whole argv token rather than a substring, so a backend for `bge-small`
172
+ * cannot reclaim a live `bge-small-v2` worker.
173
+ *
174
+ * Raw command lines are read for matching only, never stored or logged:
175
+ * process arguments can carry secrets (see the redaction note in
176
+ * `util/process-tree.ts`).
177
+ */
178
+ export function listWorkerProcesses(
179
+ workerPath: string,
180
+ model?: string,
181
+ ): WorkerProcess[] {
182
+ try {
183
+ return listProcessTable()
184
+ .filter(
185
+ (row) =>
186
+ row.command.includes(workerPath) &&
187
+ (!model || row.command.split(/\s+/).includes(model)),
188
+ )
189
+ .map(({ pid, ppid }) => ({ pid, ppid }));
190
+ } catch {
191
+ return [];
192
+ }
193
+ }