@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
@@ -19,8 +19,21 @@ import {
19
19
  import { dirname, join } from "node:path";
20
20
  import { fileURLToPath } from "node:url";
21
21
 
22
- import { getCurrentLogFilePath } from "./logger.js";
22
+ import { getCurrentLogFilePath, getLogger } from "./logger.js";
23
+ import { isProcessAlive } from "./process-liveness.js";
24
+ import {
25
+ listProcessTable,
26
+ type ProcessTableRow,
27
+ readRawProcessCommand,
28
+ } from "./process-table.js";
23
29
  import { workerMemoryEnv } from "./worker-memory.js";
30
+ import {
31
+ classifyWorkerOwnership,
32
+ isDaemonCommand,
33
+ pid1OwnsWorkers,
34
+ } from "./worker-ownership.js";
35
+
36
+ const log = getLogger("worker-process");
24
37
 
25
38
  export interface WorkerProcessStatus {
26
39
  status: "running" | "not_running";
@@ -104,22 +117,10 @@ export interface SpawnWorkerProcessOptions {
104
117
  * When the wait times out while the child is still alive (a hung or very
105
118
  * slow start), terminate that child before throwing. Callers whose failure
106
119
  * path leaves the worker's config flag off MUST set this: otherwise the
107
- * detached worker keeps coming up and runs alongside whatever in-process
120
+ * worker keeps coming up and runs alongside whatever in-process
108
121
  * fallback the flag re-enabled.
109
122
  */
110
123
  terminateOnTimeout?: boolean;
111
- /**
112
- * Process parentage (default `true`):
113
- * - `true` — the worker is its own session leader and outlives the
114
- * spawning process. Short-lived CLI spawners need this so the worker
115
- * keeps running after the command returns.
116
- * - `false` — the worker is a direct child of the spawning process. The
117
- * daemon passes this so the worker it owns appears in its process tree
118
- * (`assistant ps`) and is torn down with the daemon.
119
- * Either way the child is `unref`'d, so the spawning process never blocks on
120
- * it and the worker is tracked via its PID file rather than the handle.
121
- */
122
- detached?: boolean;
123
124
  }
124
125
 
125
126
  export type PackagedWorkerEntry =
@@ -135,21 +136,40 @@ interface WorkerCommandRuntime {
135
136
  executableExists: (path: string) => boolean;
136
137
  }
137
138
 
138
- export function resolveWorkerCommand(
139
- entry: URL,
140
- packagedEntry: PackagedWorkerEntry | undefined,
141
- runtime: WorkerCommandRuntime = {
139
+ function defaultWorkerCommandRuntime(): WorkerCommandRuntime {
140
+ return {
142
141
  platform: process.platform,
143
142
  execPath: process.execPath,
144
143
  executableExists: existsSync,
145
- },
146
- ): string[] {
144
+ };
145
+ }
146
+
147
+ /**
148
+ * How a packaged Windows runtime spawns this worker, or null when the worker
149
+ * runs from source. Only packaged Windows runtimes ship the executable.
150
+ */
151
+ function packagedWorkerSpawn(
152
+ packagedEntry: PackagedWorkerEntry | undefined,
153
+ runtime: WorkerCommandRuntime,
154
+ ): { executable: string; entry: PackagedWorkerEntry } | null {
147
155
  if (runtime.platform === "win32" && packagedEntry) {
148
156
  const executable = join(dirname(runtime.execPath), "vellum-worker.exe");
149
157
  if (runtime.executableExists(executable)) {
150
- return [executable, packagedEntry];
158
+ return { executable, entry: packagedEntry };
151
159
  }
152
160
  }
161
+ return null;
162
+ }
163
+
164
+ export function resolveWorkerCommand(
165
+ entry: URL,
166
+ packagedEntry: PackagedWorkerEntry | undefined,
167
+ runtime: WorkerCommandRuntime = defaultWorkerCommandRuntime(),
168
+ ): string[] {
169
+ const packaged = packagedWorkerSpawn(packagedEntry, runtime);
170
+ if (packaged) {
171
+ return [packaged.executable, packaged.entry];
172
+ }
153
173
  return ["bun", "--smol", "run", fileURLToPath(entry)];
154
174
  }
155
175
 
@@ -194,10 +214,310 @@ async function waitForWorkerPidFile(
194
214
  return existsSync(pidPath) ? "ready" : "timeout";
195
215
  }
196
216
 
217
+ /** Path separators differ by platform; compare command lines on one form. */
218
+ function normalizeSeparators(value: string): string {
219
+ return value.replaceAll("\\", "/");
220
+ }
221
+
222
+ /** Whether a command line carries every fragment that marks this worker. */
223
+ function matchesSignature(
224
+ command: string,
225
+ signature: readonly string[],
226
+ ): boolean {
227
+ const normalized = normalizeSeparators(command);
228
+ return signature.every((part) =>
229
+ normalized.includes(normalizeSeparators(part)),
230
+ );
231
+ }
232
+
197
233
  /**
198
- * Spawn a worker entry script as a background process and wait for it to
199
- * report readiness by writing its PID file. `opts.detached` (default `true`)
200
- * controls process parentage — see {@link SpawnWorkerProcessOptions}.
234
+ * Fragments of a command line that together mark a process as this worker,
235
+ * whichever install it came from. All must be present.
236
+ *
237
+ * This is a safety guard, not the ownership test. Ownership is decided by
238
+ * parentage; this only keeps us from signalling a process that happens to hold
239
+ * a recycled PID. Three trailing path segments are specific enough
240
+ * (`src/schedule/worker.ts`, `defaults/memory/worker.ts`) that an unrelated
241
+ * program running some other `worker.ts` does not match. Packaged workers all
242
+ * run one executable and are told apart by the subcommand, so the signature
243
+ * carries both: matching on the executable alone would let one packaged
244
+ * worker's slot reclaim another's process.
245
+ */
246
+ export function workerKindSignature(
247
+ entry: URL,
248
+ packagedEntry: PackagedWorkerEntry | undefined,
249
+ runtime: WorkerCommandRuntime = defaultWorkerCommandRuntime(),
250
+ ): readonly string[] {
251
+ const packaged = packagedWorkerSpawn(packagedEntry, runtime);
252
+ if (packaged) {
253
+ // Matched as separate fragments rather than one adjacent string: a Windows
254
+ // command line may quote the executable path, so the two are not reliably
255
+ // neighbours.
256
+ return ["vellum-worker", packaged.entry];
257
+ }
258
+ return [
259
+ normalizeSeparators(fileURLToPath(entry))
260
+ .split("/")
261
+ .filter(Boolean)
262
+ .slice(-3)
263
+ .join("/"),
264
+ ];
265
+ }
266
+
267
+ /**
268
+ * What became of the orphan after an awaited gap, from liveness plus a fresh
269
+ * command-line read.
270
+ *
271
+ * - `gone`: the PID is dead, or alive under a command line that is not this
272
+ * worker (the orphan exited and the OS recycled its PID). The slot may be
273
+ * released.
274
+ * - `orphan`: still alive and still reads as this worker. Escalation may
275
+ * proceed.
276
+ * - `identity-unreadable`: alive, but the command line could not be read, so
277
+ * "still the orphan" and "recycled PID" cannot be told apart. Uncertainty
278
+ * must resolve to reuse: no signal, and the slot must NOT be released, or
279
+ * a second worker would be spawned next to a live one.
280
+ */
281
+ export type OrphanFate = "gone" | "orphan" | "identity-unreadable";
282
+
283
+ export function classifyOrphanAfterWait(
284
+ alive: boolean,
285
+ command: string | null,
286
+ signature: readonly string[],
287
+ ): OrphanFate {
288
+ if (!alive) {
289
+ return "gone";
290
+ }
291
+ if (command == null) {
292
+ return "identity-unreadable";
293
+ }
294
+ return matchesSignature(command, signature) ? "orphan" : "gone";
295
+ }
296
+
297
+ /** Re-read the orphan's fate for `pid` from the live process table. */
298
+ function orphanFate(pid: number, signature: readonly string[]): OrphanFate {
299
+ return classifyOrphanAfterWait(
300
+ isProcessAlive(pid),
301
+ readRawProcessCommand(pid),
302
+ signature,
303
+ );
304
+ }
305
+
306
+ /**
307
+ * How long an orphaned worker gets to exit after SIGTERM.
308
+ *
309
+ * Derived from the longest shutdown any worker can legitimately take, not
310
+ * picked. The memory worker reaps the ONNX embedding subprocess it owns before
311
+ * exiting and bounds that at `EMBEDDING_SHUTDOWN_BUDGET_MS + 1s`, so a shorter
312
+ * ceiling here would SIGKILL it mid-reap and strand the very subprocess that
313
+ * reap exists to collect. A test asserts this stays above that bound.
314
+ */
315
+ export const ORPHAN_STOP_TIMEOUT_MS = 15_000;
316
+
317
+ /** Poll interval while waiting for a stopped worker to exit. */
318
+ const ORPHAN_STOP_POLL_INTERVAL_MS = 100;
319
+
320
+ /**
321
+ * How long to wait for a SIGKILLed worker to actually disappear. Signal
322
+ * delivery and reaping are asynchronous, so a liveness probe taken straight
323
+ * after the kill can still see a process that is on its way out.
324
+ */
325
+ const ORPHAN_KILL_CONFIRM_MS = 1_000;
326
+
327
+ /** Poll until `pid` is gone or `timeoutMs` elapses. */
328
+ async function waitForExit(pid: number, timeoutMs: number): Promise<void> {
329
+ const deadline = Date.now() + timeoutMs;
330
+ while (Date.now() < deadline && isProcessAlive(pid)) {
331
+ await Bun.sleep(ORPHAN_STOP_POLL_INTERVAL_MS);
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Stop a worker orphaned by a previous owner and release its PID file.
337
+ * Reports whether it actually exited.
338
+ *
339
+ * Escalates to SIGKILL because the orphan runs a different generation's
340
+ * shutdown path, which this process cannot make assumptions about. Signalling
341
+ * can still fail outright, most plausibly EPERM for a process owned by another
342
+ * user, so the PID file is released only once the process is confirmed gone:
343
+ * dropping it while the worker still runs would let the caller spawn a second
344
+ * one against the same workspace. The unlink is also identity-checked, since a
345
+ * concurrent launcher may already have replaced the worker and deleting that
346
+ * successor's entry would strand it.
347
+ */
348
+ async function stopOrphanedWorker(
349
+ pid: number,
350
+ pidPath: string,
351
+ workerLabel: string,
352
+ signature: readonly string[],
353
+ ): Promise<boolean> {
354
+ try {
355
+ process.kill(pid, "SIGTERM");
356
+ } catch {
357
+ // Exited between the probe and the signal.
358
+ }
359
+
360
+ await waitForExit(pid, ORPHAN_STOP_TIMEOUT_MS);
361
+ // Identity is re-proven after every awaited gap, not carried by the PID:
362
+ // the orphan may have exited during the wait and the OS may have handed its
363
+ // PID to a stranger. Only a positive re-match escalates, and only a proven
364
+ // "gone" releases the slot.
365
+ let fate = orphanFate(pid, signature);
366
+ if (fate === "orphan") {
367
+ try {
368
+ process.kill(pid, "SIGKILL");
369
+ } catch {
370
+ // Best-effort.
371
+ }
372
+ await waitForExit(pid, ORPHAN_KILL_CONFIRM_MS);
373
+ fate = orphanFate(pid, signature);
374
+ }
375
+
376
+ if (fate !== "gone") {
377
+ log.warn(
378
+ { pid, pidPath, fate },
379
+ fate === "orphan"
380
+ ? `${workerLabel} orphaned by a previous owner could not be stopped; reusing it rather than running a second one`
381
+ : `${workerLabel} is still live but its identity could not be re-read; reusing it rather than risking a duplicate`,
382
+ );
383
+ return false;
384
+ }
385
+
386
+ unlinkPidFileIfNames(pidPath, pid);
387
+ log.warn(
388
+ { pid, pidPath },
389
+ `${workerLabel} orphaned by a previous owner was stopped so this process can start its own`,
390
+ );
391
+ return true;
392
+ }
393
+
394
+ /**
395
+ * What to do about the process a worker's PID file currently names.
396
+ *
397
+ * - `adopt`: reuse it. Either it is this process's own worker, or it belongs
398
+ * to another live owner, or it is not recognisably one of our workers at
399
+ * all and must never be signalled.
400
+ * - `reclaim`: an orphan left by an owner that is gone. Stop it, then spawn.
401
+ * - `spawn`: nothing is holding the slot.
402
+ */
403
+ export type WorkerSlotDecision =
404
+ | { action: "adopt"; pid: number }
405
+ | { action: "reclaim"; pid: number }
406
+ | { action: "spawn" };
407
+
408
+ /**
409
+ * The whole safety decision, with no IO, so every branch is testable.
410
+ *
411
+ * `reclaim` is the only outcome that signals a process, and it requires two
412
+ * independent things to agree: the command line marks the process as this
413
+ * worker, and parentage says no daemon owns it. A missing row, an unreadable
414
+ * command line, and a command line that does not match all fall back to
415
+ * `adopt`, so an uncertain answer never costs a process its life.
416
+ *
417
+ * `isOwnerAlive` is the caller's definition of a legitimate owner. These
418
+ * workers have exactly one, the daemon, so passing a plain liveness probe
419
+ * would let a recycled owner PID keep a stranded worker looking owned forever.
420
+ * The embed worker passes a liveness probe instead, because two processes may
421
+ * each legitimately run one of its workers.
422
+ */
423
+ export function decideWorkerSlot(
424
+ status: WorkerProcessStatus,
425
+ row: Pick<ProcessTableRow, "pid" | "ppid" | "command"> | null,
426
+ signature: readonly string[],
427
+ selfPid: number,
428
+ isOwnerAlive: (pid: number) => boolean,
429
+ pid1OwnsWorkers: boolean,
430
+ ): WorkerSlotDecision {
431
+ if (status.status !== "running" || status.pid == null) {
432
+ return { action: "spawn" };
433
+ }
434
+ if (!row) {
435
+ return { action: "adopt", pid: status.pid };
436
+ }
437
+ if (!matchesSignature(row.command, signature)) {
438
+ return { action: "adopt", pid: status.pid };
439
+ }
440
+ const ownership = classifyWorkerOwnership(
441
+ row,
442
+ selfPid,
443
+ isOwnerAlive,
444
+ pid1OwnsWorkers,
445
+ );
446
+ return ownership === "orphan"
447
+ ? { action: "reclaim", pid: status.pid }
448
+ : { action: "adopt", pid: status.pid };
449
+ }
450
+
451
+ /**
452
+ * The slot decision for the worker at `pidPath`, made without yielding.
453
+ *
454
+ * Callers spawn inside their own synchronous prefix, so this stays sync: an
455
+ * `await` here would let a caller's next tick run before the child exists.
456
+ */
457
+ function inspectWorkerSlot(
458
+ pidPath: string,
459
+ signature: readonly string[],
460
+ ): WorkerSlotDecision {
461
+ const status = probeWorkerPidFile(pidPath);
462
+ if (status.status !== "running" || status.pid == null) {
463
+ return { action: "spawn" };
464
+ }
465
+
466
+ // The worker's row, its owner's identity, and what PID 1 is all come from
467
+ // one snapshot. Reading the table is a subprocess on most platforms, so
468
+ // asking three separate times would be three of them per worker.
469
+ let table: readonly ProcessTableRow[];
470
+ try {
471
+ table = listProcessTable();
472
+ } catch {
473
+ table = [];
474
+ }
475
+ const commandOf = (pid: number): string | null =>
476
+ table.find((row) => row.pid === pid)?.command ?? null;
477
+
478
+ const row = table.find((entry) => entry.pid === status.pid) ?? null;
479
+ if (row && !matchesSignature(row.command, signature)) {
480
+ log.info(
481
+ { pid: row.pid, pidPath },
482
+ "Worker PID file names a live process this runtime does not recognise as its worker; reusing it rather than signalling it",
483
+ );
484
+ }
485
+
486
+ return decideWorkerSlot(
487
+ status,
488
+ row,
489
+ signature,
490
+ process.pid,
491
+ (pid) => isDaemonCommand(commandOf(pid)),
492
+ pid1OwnsWorkers(commandOf(1)),
493
+ );
494
+ }
495
+
496
+ /**
497
+ * Stop the orphan holding the slot, then report a PID to adopt instead of
498
+ * spawning: either the orphan itself when it could not be stopped, or a
499
+ * replacement a concurrent launcher brought up while we waited.
500
+ */
501
+ async function reclaimWorkerSlot(
502
+ pid: number,
503
+ pidPath: string,
504
+ workerLabel: string,
505
+ signature: readonly string[],
506
+ ): Promise<number | null> {
507
+ if (!(await stopOrphanedWorker(pid, pidPath, workerLabel, signature))) {
508
+ // Still running and beyond our reach. One stale worker beats two live ones.
509
+ return pid;
510
+ }
511
+ const replacement = probeWorkerPidFile(pidPath);
512
+ return replacement.status === "running" && replacement.pid != null
513
+ ? replacement.pid
514
+ : null;
515
+ }
516
+
517
+ /**
518
+ * Spawn a worker entry script as a background process, as a child of this
519
+ * process, and wait for it to report readiness by writing its PID file. The
520
+ * child is `unref`'d, so the spawning process never blocks on it.
201
521
  *
202
522
  * If a worker is already running (per the PID file), returns its PID with
203
523
  * `alreadyRunning: true` rather than spawning a second one. Throws
@@ -218,11 +538,22 @@ export async function spawnWorkerProcess(args: {
218
538
  const opts = args.options ?? {};
219
539
  const pidWaitTimeoutMs = opts.pidWaitTimeoutMs ?? PID_FILE_WAIT_TIMEOUT_MS;
220
540
  const pidPollIntervalMs = opts.pidPollIntervalMs ?? PID_FILE_POLL_INTERVAL_MS;
221
- const detached = opts.detached ?? true;
222
541
 
223
- const current = probeWorkerPidFile(args.pidPath);
224
- if (current.status === "running" && current.pid != null) {
225
- return { pid: current.pid, alreadyRunning: true };
542
+ const signature = workerKindSignature(args.entry, args.packagedEntry);
543
+ const decision = inspectWorkerSlot(args.pidPath, signature);
544
+ if (decision.action === "adopt") {
545
+ return { pid: decision.pid, alreadyRunning: true };
546
+ }
547
+ if (decision.action === "reclaim") {
548
+ const adopted = await reclaimWorkerSlot(
549
+ decision.pid,
550
+ args.pidPath,
551
+ args.workerLabel,
552
+ signature,
553
+ );
554
+ if (adopted != null) {
555
+ return { pid: adopted, alreadyRunning: true };
556
+ }
226
557
  }
227
558
 
228
559
  // Pipe the worker's stderr into the same daily log file the daemon writes
@@ -252,7 +583,9 @@ export async function spawnWorkerProcess(args: {
252
583
  cmd: resolveWorkerCommand(args.entry, args.packagedEntry),
253
584
  env: workerMemoryEnv(),
254
585
  stdio: ["ignore", "ignore", stderrFd],
255
- detached,
586
+ // Every worker is a direct child of the daemon. That parentage is the
587
+ // ownership record `classifyWorkerOwnership` reads, so it is not optional.
588
+ detached: false,
256
589
  windowsHide: true,
257
590
  });
258
591
 
@@ -319,23 +652,28 @@ export function stopWorkerProcess(pidPath: string): WorkerProcessStatus {
319
652
  // ---------------------------------------------------------------------------
320
653
 
321
654
  /**
322
- * Remove the worker PID file only while it still names this process. A
323
- * successor worker overwrites the file with its own PID at startup, so an
324
- * exiting predecessor that unlinked unconditionally would delete the
325
- * successor's entry — the liveness supervisor would then respawn a
326
- * duplicate and orphan the successor.
655
+ * Remove `pidPath` only while it still names `pid`.
656
+ *
657
+ * A successor worker overwrites the file with its own PID at startup, so an
658
+ * unconditional unlink would delete the successor's entry: the liveness
659
+ * supervisor would then respawn a duplicate and orphan the successor.
327
660
  */
328
- export function cleanupWorkerPidFile(pidPath: string): void {
661
+ export function unlinkPidFileIfNames(pidPath: string, pid: number): void {
329
662
  try {
330
663
  const raw = readFileSync(pidPath, "utf-8").trim();
331
- if (parseInt(raw, 10) === process.pid) {
664
+ if (parseInt(raw, 10) === pid) {
332
665
  unlinkSync(pidPath);
333
666
  }
334
667
  } catch {
335
- // best-effort — a missing or unreadable file needs no cleanup
668
+ // best-effort: a missing or unreadable file needs no cleanup
336
669
  }
337
670
  }
338
671
 
672
+ /** Release this process's own PID file on the way out. */
673
+ export function cleanupWorkerPidFile(pidPath: string): void {
674
+ unlinkPidFileIfNames(pidPath, process.pid);
675
+ }
676
+
339
677
  /**
340
678
  * Watch the worker PID file and evict this process when the file stops
341
679
  * naming it. The PID file is a worker's sole tracking handle: stop
@@ -15,6 +15,7 @@ import { getConfig } from "../config/loader.js";
15
15
  import { isFileUnheld } from "../util/file-use.js";
16
16
  import { getLogger } from "../util/logger.js";
17
17
  import { Mutex } from "../util/mutex.js";
18
+ import { addToPathEnv, getExtraToolPathDirs } from "../util/platform.js";
18
19
  import { PromiseGuard } from "../util/promise-guard.js";
19
20
 
20
21
  const execFileAsync = promisify(execFile);
@@ -41,18 +42,7 @@ function cleanGitEnv(workspaceDir: string): Record<string, string> {
41
42
  }
42
43
  env.GIT_CEILING_DIRECTORIES = workspaceDir;
43
44
 
44
- const home = process.env.HOME ?? "";
45
- const extraDirs = [
46
- "/opt/homebrew/bin",
47
- "/usr/local/bin",
48
- `${home}/.local/bin`,
49
- ];
50
- const currentPath = env.PATH ?? "";
51
- const pathDirs = currentPath.split(":");
52
- const missing = extraDirs.filter((d) => !pathDirs.includes(d));
53
- if (missing.length > 0) {
54
- env.PATH = [...missing, currentPath].filter(Boolean).join(":");
55
- }
45
+ addToPathEnv(env, getExtraToolPathDirs());
56
46
 
57
47
  return env;
58
48
  }
@@ -1814,6 +1804,7 @@ export class WorkspaceGitService {
1814
1804
  timeout: timeoutMs,
1815
1805
  env: { ...cleanGitEnv(this.workspaceDir), ...options?.env },
1816
1806
  signal: options?.signal,
1807
+ windowsHide: true,
1817
1808
  });
1818
1809
  return { stdout, stderr };
1819
1810
  } catch (err) {
@@ -1859,6 +1850,7 @@ export class WorkspaceGitService {
1859
1850
  cwd: this.workspaceDir,
1860
1851
  env: { ...cleanGitEnv(this.workspaceDir), ...options?.env },
1861
1852
  signal: options?.signal,
1853
+ windowsHide: true,
1862
1854
  });
1863
1855
 
1864
1856
  // Swallow EPIPE from a child that exits without reading stdin; the
@@ -242,6 +242,7 @@ export const appDirRenameMigration: WorkspaceMigration = {
242
242
  cwd: appsDir,
243
243
  stdio: "ignore",
244
244
  timeout: 10_000,
245
+ windowsHide: true,
245
246
  },
246
247
  );
247
248
  }