@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
@@ -157,6 +157,45 @@ export function isSystemCardMetadata(
157
157
  return metadata?.messageKind === SYSTEM_CARD_MESSAGE_KIND;
158
158
  }
159
159
 
160
+ /**
161
+ * Marker for a turn whose whole reply was the `<no_response/>` sentinel: the
162
+ * assistant deliberately chose silence. The row's content keeps the raw
163
+ * sentinel (the model reads its own convention back from history); clients
164
+ * switch on the marker to render a quiet standalone notice and to resolve
165
+ * their pending-response state, instead of showing the sentinel text or
166
+ * waiting forever for a reply that was never coming.
167
+ */
168
+ export const NO_RESPONSE_MESSAGE_KIND = "no_response";
169
+
170
+ /**
171
+ * Shared predicate for the deliberate-silence marker, mirroring
172
+ * {@link isSystemCardMetadata} so display merging, transcript rendering, and
173
+ * turn grouping cannot drift.
174
+ */
175
+ export function isNoResponseMetadata(
176
+ metadata: Record<string, unknown> | null | undefined,
177
+ ): boolean {
178
+ return metadata?.messageKind === NO_RESPONSE_MESSAGE_KIND;
179
+ }
180
+
181
+ /**
182
+ * Marker for an assistant-authored reaction row: the assistant reacted with
183
+ * an emoji, and the row records it durably. The reaction fact itself lives
184
+ * in the row's `providerMeta.reaction` envelope, same as inbound reaction
185
+ * rows; this kind carries the display and grouping semantics, keeping the
186
+ * row a standalone turn (never merged into adjacent assistant speech, where
187
+ * consolidation would fold its sentinel text into a bubble and drop the
188
+ * envelope from the wire).
189
+ */
190
+ export const REACTION_MESSAGE_KIND = "reaction";
191
+
192
+ /** Shared predicate for the assistant-reaction marker. */
193
+ export function isReactionMessageMetadata(
194
+ metadata: Record<string, unknown> | null | undefined,
195
+ ): boolean {
196
+ return metadata?.messageKind === REACTION_MESSAGE_KIND;
197
+ }
198
+
160
199
  /**
161
200
  * True when a role-`"user"` row is internal scaffolding rather than a person's
162
201
  * prompt: a daemon-injected run lifecycle notification (subagent
@@ -227,6 +266,76 @@ export function isVoiceSessionUserMessage(
227
266
  return metadata?.voiceSessionTurn === true;
228
267
  }
229
268
 
269
+ /**
270
+ * Metadata key naming which of a row's attachments arrived as ambient camera
271
+ * frames rather than files the user picked. Exported so the SQL prefilter that
272
+ * finds candidate rows and the reader below name the same key.
273
+ */
274
+ export const SIGHT_FRAME_ATTACHMENT_IDS_KEY = "sightFrameAttachmentIds";
275
+
276
+ /**
277
+ * The row's attachments that arrived as ambient camera frames, by attachment
278
+ * id. A user attachment carries no metadata of its own, so the marking lives on
279
+ * the message and refers to the attachments by id.
280
+ *
281
+ * Tolerant of any metadata shape: a row without the key, or one whose value is
282
+ * not an array of ids, yields nothing. Retention consumers therefore treat an
283
+ * untagged conversation exactly like one that predates the tag.
284
+ */
285
+ export function sightFrameAttachmentIdsFromMetadata(
286
+ metadata: Record<string, unknown> | null | undefined,
287
+ ): string[] {
288
+ const ids = metadata?.[SIGHT_FRAME_ATTACHMENT_IDS_KEY];
289
+ if (!Array.isArray(ids)) {
290
+ return [];
291
+ }
292
+ return ids.filter(
293
+ (id): id is string => typeof id === "string" && id.length > 0,
294
+ );
295
+ }
296
+
297
+ /**
298
+ * True when a stored `messages.metadata` column belongs to a standalone
299
+ * ambient camera keep: a row the call's own gate sampled, which no one spoke.
300
+ *
301
+ * The raw-column counterpart to {@link sightFrameAttachmentIdsFromMetadata},
302
+ * for consumers holding the JSON string a row was written with rather than a
303
+ * parsed bag: the memory rebuild's re-embed scan and the retrospective's
304
+ * accounting both ask this question of rows they never otherwise decode. It
305
+ * lives here, beside the key and the parser, so the two cannot drift.
306
+ *
307
+ * BOTH halves are required, and the tag alone is not enough. A genuine spoken
308
+ * turn carries the same tag whenever a parked frame rode it (see
309
+ * `calls/voice-session-bridge.ts`), which in camera mode is every turn the
310
+ * user speaks, so keying on the tag would read real speech as machine
311
+ * output. `scripted` is what separates them: the standalone persist sets it
312
+ * (`live-voice/live-voice-photo.ts`), while a spoken turn leaves it absent
313
+ * and the persist stamps the `false` default durably. The pair is the
314
+ * signature of a keep; `scripted` on its own belongs to every auto-sent row,
315
+ * onboarding prompts included, which are none of this predicate's business.
316
+ *
317
+ * Absent or unparseable metadata is not a keep. Both callers use this to hold
318
+ * keeps BACK from work, so a row whose marking cannot be read is treated as
319
+ * ordinary content and still gets indexed or reviewed, which is the direction
320
+ * that loses nothing.
321
+ */
322
+ export function messageMetadataIsAmbientSightKeep(
323
+ metadata: string | null,
324
+ ): boolean {
325
+ if (!metadata) {
326
+ return false;
327
+ }
328
+ try {
329
+ const parsed = JSON.parse(metadata) as Record<string, unknown>;
330
+ return (
331
+ parsed.scripted === true &&
332
+ sightFrameAttachmentIdsFromMetadata(parsed).length > 0
333
+ );
334
+ } catch {
335
+ return false;
336
+ }
337
+ }
338
+
230
339
  /**
231
340
  * True when the row that opened the turn was sent from a desktop app, on that
232
341
  * row's own evidence.
@@ -1,8 +1,9 @@
1
- import { existsSync, realpathSync } from "node:fs";
1
+ import { existsSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { basename, dirname, join, resolve, sep } from "node:path";
3
+ import { join, sep } from "node:path";
4
4
  import { Database } from "bun:sqlite";
5
5
 
6
+ import { canonicalizePathThroughExistingParent } from "@vellumai/environments/test-path-guard";
6
7
  import { drizzle } from "drizzle-orm/bun-sqlite";
7
8
 
8
9
  import { getLogsDbPath } from "../util/logs-db-path.js";
@@ -15,25 +16,6 @@ import { wrapSqliteForSlowQueryLogging } from "./slow-query-log.js";
15
16
 
16
17
  export type DrizzleDb = ReturnType<typeof drizzle<typeof schema>>;
17
18
 
18
- function canonicalizePathThroughExistingParent(path: string): string {
19
- const resolvedPath = resolve(path);
20
- const pendingSegments: string[] = [];
21
- let currentPath = resolvedPath;
22
-
23
- while (true) {
24
- try {
25
- return resolve(realpathSync(currentPath), ...pendingSegments.reverse());
26
- } catch {
27
- const parentPath = dirname(currentPath);
28
- if (parentPath === currentPath) {
29
- return resolvedPath;
30
- }
31
- pendingSegments.push(basename(currentPath));
32
- currentPath = parentPath;
33
- }
34
- }
35
- }
36
-
37
19
  /**
38
20
  * Guard against opening a real workspace database during tests. Shared by
39
21
  * every connection opener (main, logs, memory) so a misconfigured test run
@@ -51,12 +51,12 @@ const SLACK_LEGACY_THREAD_EVIDENCE_BATCH_SIZE = 50;
51
51
  const SLACK_LEGACY_THREAD_EVIDENCE_MAX_SCAN = 500;
52
52
 
53
53
  /**
54
- * Rows examined when locating a Slack message the assistant itself posted.
54
+ * Rows examined when locating a channel message the assistant itself posted.
55
55
  * Bounded on purpose: the scan runs on the inbound path while the gateway
56
56
  * waits for its ack, and reactions land on recent messages, so a cap costs
57
57
  * almost no recall and keeps the cost flat as the database grows.
58
58
  */
59
- const SLACK_OUTBOUND_TS_MAX_SCAN = 400;
59
+ const OUTBOUND_MESSAGE_ID_MAX_SCAN = 400;
60
60
 
61
61
  /**
62
62
  * Channels where an inbound thread id scopes the conversation: a Slack thread
@@ -295,8 +295,8 @@ export function findInboundEvent(
295
295
  }
296
296
 
297
297
  /**
298
- * The conversation holding the message with this provider id, found by reading
299
- * the metadata the assistant's own posts carry.
298
+ * The conversation holding the message with this provider id, found by
299
+ * reading the metadata the assistant's own posts carry, on any channel.
300
300
  *
301
301
  * Reads through `readProviderMetadata`, so it matches any channel that
302
302
  * describes its rows in the neutral shape as well as Slack's own envelope.
@@ -305,16 +305,18 @@ export function findInboundEvent(
305
305
  * event. It cannot see what the assistant posted, because an outbound reply
306
306
  * opens no inbound event, so a reaction on the assistant's own message needs
307
307
  * this. The search is confined to conversations already bound to the same
308
- * Slack channel and to the most recent {@link SLACK_OUTBOUND_TS_MAX_SCAN}
309
- * rows among them; beyond that it gives up and the caller drops the
310
- * annotation, which is the same outcome as never finding it at all.
308
+ * channel address and to the most recent
309
+ * {@link OUTBOUND_MESSAGE_ID_MAX_SCAN} rows among them; beyond that it gives
310
+ * up and the caller drops the annotation, which is the same outcome as never
311
+ * finding it at all.
311
312
  */
312
- export function findSlackConversationByMessageTs(
313
+ export function findConversationByProviderMessageId(
314
+ sourceChannel: string,
313
315
  externalChatId: string,
314
- channelTs: string,
316
+ providerMessageId: string,
315
317
  ): string | null {
316
318
  const db = getDb();
317
- const keyPrefix = `${CONVERSATION_KEY_SCOPE}:slack:${externalChatId}`;
319
+ const keyPrefix = `${CONVERSATION_KEY_SCOPE}:${sourceChannel}:${externalChatId}`;
318
320
  const rows = db
319
321
  .select({
320
322
  conversationId: messages.conversationId,
@@ -338,14 +340,15 @@ export function findSlackConversationByMessageTs(
338
340
  ),
339
341
  )
340
342
  .orderBy(desc(messages.createdAt))
341
- .limit(SLACK_OUTBOUND_TS_MAX_SCAN)
343
+ .limit(OUTBOUND_MESSAGE_ID_MAX_SCAN)
342
344
  .all();
343
345
 
344
346
  for (const row of rows) {
345
347
  const meta = readProviderMetadata(row.metadata, { allowFlatLegacy: true });
346
348
  if (
347
349
  meta?.conversationExternalId === externalChatId &&
348
- meta.messageId === channelTs
350
+ (meta.messageId === providerMessageId ||
351
+ meta.additionalMessageIds?.includes(providerMessageId))
349
352
  ) {
350
353
  return row.conversationId;
351
354
  }
@@ -480,6 +483,33 @@ export function findMessageBySourceId(
480
483
  return { messageId: row.messageId, conversationId: row.conversationId };
481
484
  }
482
485
 
486
+ /**
487
+ * Whether any inbound-event row exists for this source message, linked or
488
+ * not. Discriminates "never ingested" from "ingested, link still landing":
489
+ * only the second justifies waiting out the link race, so a delete or edit
490
+ * for a message the daemon never saw can return immediately instead of
491
+ * holding its conversation's serialized lane through the retry window.
492
+ */
493
+ export function hasInboundEventForSource(
494
+ sourceChannel: string,
495
+ externalChatId: string,
496
+ sourceMessageId: string,
497
+ ): boolean {
498
+ const db = getDb();
499
+ const row = db
500
+ .select({ id: channelInboundEvents.id })
501
+ .from(channelInboundEvents)
502
+ .where(
503
+ and(
504
+ eq(channelInboundEvents.sourceChannel, sourceChannel),
505
+ eq(channelInboundEvents.externalChatId, externalChatId),
506
+ eq(channelInboundEvents.sourceMessageId, sourceMessageId),
507
+ ),
508
+ )
509
+ .get();
510
+ return row !== undefined;
511
+ }
512
+
483
513
  /**
484
514
  * Reference to the most recent inbound channel event for a conversation:
485
515
  * the external chat plus the channel-native message identifiers needed to
@@ -18,8 +18,10 @@ import { getEmbedWorkerPidPath } from "../../../util/platform.js";
18
18
  import {
19
19
  classifyWorkerOwnership,
20
20
  listWorkerProcesses,
21
- LocalEmbeddingBackend,
22
- } from "../embedding-local.js";
21
+ } from "../../../util/worker-ownership.js";
22
+ import { ORPHAN_STOP_TIMEOUT_MS } from "../../../util/worker-process.js";
23
+ import { EMBEDDING_SHUTDOWN_BUDGET_MS } from "../embedding-backend.js";
24
+ import { LocalEmbeddingBackend } from "../embedding-local.js";
23
25
 
24
26
  /** Reach past `private`, which is compile-time only, so tests drive real state. */
25
27
  type Internals = any;
@@ -753,3 +755,14 @@ describe("model matching", () => {
753
755
  ).toEqual([proc.pid]);
754
756
  });
755
757
  });
758
+
759
+ // The daemon's orphan reclaim SIGTERMs a stale memory worker and waits
760
+ // ORPHAN_STOP_TIMEOUT_MS before SIGKILL. That worker's shutdown reaps the ONNX
761
+ // subprocess it owns, bounded at EMBEDDING_SHUTDOWN_BUDGET_MS + 1s. If the
762
+ // reclaim ceiling does not outlast the reap, reclaiming kills the worker
763
+ // mid-reap and strands the subprocess, which is the JARVIS-1125 failure.
764
+ test("orphan reclaim outlasts the embedding reap it must not truncate", () => {
765
+ expect(ORPHAN_STOP_TIMEOUT_MS).toBeGreaterThan(
766
+ EMBEDDING_SHUTDOWN_BUDGET_MS + 1_000,
767
+ );
768
+ });
@@ -13,13 +13,15 @@ import {
13
13
  getEmbeddingModelsDir,
14
14
  getEmbedWorkerPidPath,
15
15
  } from "../../util/platform.js";
16
- import {
17
- listProcessTable,
18
- readRawProcessCommand,
19
- } from "../../util/process-table.js";
16
+ import { isProcessAlive } from "../../util/process-liveness.js";
20
17
  import { PromiseGuard } from "../../util/promise-guard.js";
21
18
  import { workerComputeEnv } from "../../util/worker-compute.js";
22
19
  import { workerMemoryEnv } from "../../util/worker-memory.js";
20
+ import {
21
+ classifyWorkerOwnership,
22
+ listWorkerProcesses,
23
+ pid1OwnsWorkers,
24
+ } from "../../util/worker-ownership.js";
23
25
  import { EmbeddingRuntimeManager } from "./embedding-runtime-manager.js";
24
26
  import {
25
27
  type EmbeddingBackend,
@@ -87,113 +89,6 @@ async function didSettle(
87
89
  return result !== timeout;
88
90
  }
89
91
 
90
- /** Whether a PID names a live process. */
91
- function isProcessAlive(pid: number): boolean {
92
- try {
93
- // Signal 0 probes for liveness without delivering a signal.
94
- process.kill(pid, 0);
95
- return true;
96
- } catch {
97
- return false;
98
- }
99
- }
100
-
101
- interface WorkerProcess {
102
- pid: number;
103
- ppid: number;
104
- }
105
-
106
- /**
107
- * What this process may do with an embed worker it found in the process table.
108
- *
109
- * - `reclaim`: parented to us, a worker we started and lost track of. Reaping
110
- * it is what enforces one live worker per owning process.
111
- * - `orphan`: reparented to init, or its owner is gone. Nobody else will ever
112
- * clean it up.
113
- * - `foreign`: owned by another live process. The memory-worker process runs
114
- * its own backend against this same workspace and is entitled to its own
115
- * worker; signalling it is what made the two replace each other in a loop.
116
- *
117
- * A parent of PID 1 is ambiguous, and resolving it wrongly in either direction
118
- * costs something real. `docker-entrypoint.sh` execs the daemon, so PID 1 can
119
- * BE the daemon and its child a healthy sibling's worker. Under `docker run
120
- * --init`, and on any host, PID 1 is an init process instead, so the same
121
- * parentage means the owner died and the worker needs reclaiming. Callers
122
- * therefore pass what PID 1 actually is rather than inferring it from whether
123
- * the deployment is containerized, which is true in both container shapes.
124
- */
125
- export type WorkerOwnership = "reclaim" | "orphan" | "foreign";
126
-
127
- export function classifyWorkerOwnership(
128
- worker: WorkerProcess,
129
- selfPid: number,
130
- isOwnerAlive: (pid: number) => boolean,
131
- pid1OwnsWorkers: boolean,
132
- ): WorkerOwnership {
133
- if (worker.ppid === selfPid) {
134
- return "reclaim";
135
- }
136
- if (worker.ppid <= 1) {
137
- return pid1OwnsWorkers ? "foreign" : "orphan";
138
- }
139
- if (!isOwnerAlive(worker.ppid)) {
140
- return "orphan";
141
- }
142
- return "foreign";
143
- }
144
-
145
- /** Entrypoint the daemon is exec'd with, including inside a container. */
146
- const DAEMON_ENTRYPOINT_MARKER = "daemon/main";
147
-
148
- /**
149
- * Whether PID 1 is an assistant daemon rather than an init process.
150
- *
151
- * True only where the daemon was exec'd as PID 1 (`docker-entrypoint.sh`),
152
- * which is what makes a worker parented to 1 a live sibling's rather than an
153
- * orphan. Under `docker run --init` PID 1 is docker-init and under launchd or
154
- * systemd it is the system init, so both answer false and PID-1 orphans stay
155
- * reclaimable. Unreadable means false, which keeps the reclaiming behaviour.
156
- */
157
- function pid1OwnsEmbedWorkers(): boolean {
158
- if (process.pid === 1) {
159
- return true;
160
- }
161
- return readRawProcessCommand(1)?.includes(DAEMON_ENTRYPOINT_MARKER) ?? false;
162
- }
163
-
164
- /**
165
- * Live embed-worker processes for this workspace, as `(pid, ppid)` pairs.
166
- *
167
- * Ownership of an embed worker is recorded by the OS process tree, not by the
168
- * PID file: a worker's parent IS its owner. The process table is therefore the
169
- * authoritative answer to "whose worker is this", and unlike the PID file it
170
- * survives a crash and cannot be overwritten by a second owner. That matters
171
- * because the daemon and the memory-worker process legitimately run one worker
172
- * each against the same workspace.
173
- *
174
- * Matches on the absolute worker script path, which lives under THIS
175
- * workspace's embedding-models directory and is therefore unique per assistant
176
- * instance, so a sibling instance's workers never match. Raw command lines are
177
- * read for matching only, never stored or logged: process arguments can carry
178
- * secrets (see the redaction note in `util/process-tree.ts`).
179
- */
180
- export function listWorkerProcesses(
181
- workerPath: string,
182
- model?: string,
183
- ): WorkerProcess[] {
184
- try {
185
- return listProcessTable()
186
- .filter(
187
- (row) =>
188
- row.command.includes(workerPath) &&
189
- (!model || row.command.split(/\s+/).includes(model)),
190
- )
191
- .map(({ pid, ppid }) => ({ pid, ppid }));
192
- } catch {
193
- return [];
194
- }
195
- }
196
-
197
92
  /**
198
93
  * Local embedding backend using @huggingface/transformers (ONNX Runtime).
199
94
  * Runs BAAI/bge-small-en-v1.5 locally — no API calls, no network required.
@@ -874,7 +769,7 @@ export class LocalEmbeddingBackend implements EmbeddingBackend {
874
769
  worker,
875
770
  process.pid,
876
771
  isProcessAlive,
877
- pid1OwnsEmbedWorkers(),
772
+ pid1OwnsWorkers(),
878
773
  );
879
774
  if (ownership === "foreign") {
880
775
  continue;
@@ -51,7 +51,9 @@ export function resolveQdrantReleaseAsset(
51
51
  target = "x86_64-unknown-linux-musl";
52
52
  } else if (os === "linux" && cpu === "arm64") {
53
53
  target = "aarch64-unknown-linux-musl";
54
- } else if (os === "win32" && cpu === "x64") {
54
+ } else if (os === "win32" && (cpu === "x64" || cpu === "arm64")) {
55
+ // Qdrant ships no arm64 Windows build; Windows 11 on ARM runs the x64
56
+ // binary via transparent emulation.
55
57
  return {
56
58
  binaryName: "qdrant.exe",
57
59
  filename: "qdrant-x86_64-pc-windows-msvc.zip",
@@ -286,6 +288,7 @@ export class QdrantManager {
286
288
  cmd: ["tar", "xzf", tmpTar, "-C", binDir, release.binaryName],
287
289
  stdout: "ignore",
288
290
  stderr: "pipe",
291
+ windowsHide: true,
289
292
  });
290
293
  await proc.exited;
291
294
  if (proc.exitCode !== 0) {
@@ -346,7 +346,7 @@ export function recordRequestLog(
346
346
  // plumb call sites through one site at a time.
347
347
  callSite: callSite ?? null,
348
348
  // JSON first-token latency waterfall, supplied by `handleUsage` for
349
- // main-agent calls. NULL for failed/non-instrumented call paths.
349
+ // instrumented calls. NULL for failed/non-instrumented call paths.
350
350
  latencyBreakdown: latencyBreakdown ?? null,
351
351
  });
352
352
  return id;
@@ -0,0 +1,36 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { getSqliteFrom } from "../db-connection.js";
3
+
4
+ const TABLE = "acp_session_history";
5
+ const COLUMN = "auth_error_code";
6
+
7
+ /**
8
+ * Add a nullable `auth_error_code TEXT` column to `acp_session_history`.
9
+ *
10
+ * Names the credential failure that ended a run, so a client that reopens the
11
+ * conversation can re-raise the inline "Connect Claude Code" card for it. The
12
+ * row is the right home for that: it already carries the parent conversation
13
+ * and the spawning tool call the card anchors to, nothing rewrites it the way
14
+ * a turn's finalize rewrites message content, and it outlives the daemon
15
+ * process, so retirement does not depend on anything held in memory.
16
+ *
17
+ * `NULL` for rows persisted before this migration ran, and for every run that
18
+ * ended for any other reason.
19
+ *
20
+ * Idempotent: the PRAGMA guard makes re-running a no-op once the column
21
+ * exists.
22
+ */
23
+ export function migrateAcpSessionHistoryAuthErrorCode(
24
+ database: DrizzleDb,
25
+ ): void {
26
+ const raw = getSqliteFrom(database);
27
+
28
+ const columns = raw.query(`PRAGMA table_info(${TABLE})`).all() as Array<{
29
+ name: string;
30
+ }>;
31
+ const columnNames = new Set(columns.map((c) => c.name));
32
+
33
+ if (!columnNames.has(COLUMN)) {
34
+ raw.exec(`ALTER TABLE ${TABLE} ADD COLUMN ${COLUMN} TEXT`);
35
+ }
36
+ }
@@ -0,0 +1,40 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { getSqliteFrom } from "../db-connection.js";
3
+
4
+ const TABLE = "acp_session_history";
5
+ const COLUMN = "auth_error_credential";
6
+
7
+ /**
8
+ * Add a nullable `auth_error_credential TEXT` column to `acp_session_history`.
9
+ *
10
+ * Holds a digest of the Claude token the run was using when Claude refused it,
11
+ * beside the `auth_error_code` that names the refusal. Together they let a
12
+ * marker answer for itself whether it is still worth showing: a client asks
13
+ * whether the credential named here is still the one a spawn would resolve,
14
+ * rather than relying on a sweep having run at the right moment to clear it.
15
+ *
16
+ * A digest, never the token. It only ever has to be compared for equality, and
17
+ * this outlives the run that produced it.
18
+ *
19
+ * `NULL` for rows written before this migration ran, and for every run that
20
+ * ended for any other reason. A marker with no credential named is served
21
+ * rather than hidden, since an unknown credential is no evidence the failure
22
+ * was repaired.
23
+ *
24
+ * Idempotent: the PRAGMA guard makes re-running a no-op once the column
25
+ * exists.
26
+ */
27
+ export function migrateAcpSessionHistoryAuthErrorCredential(
28
+ database: DrizzleDb,
29
+ ): void {
30
+ const raw = getSqliteFrom(database);
31
+
32
+ const columns = raw.query(`PRAGMA table_info(${TABLE})`).all() as Array<{
33
+ name: string;
34
+ }>;
35
+ const columnNames = new Set(columns.map((c) => c.name));
36
+
37
+ if (!columnNames.has(COLUMN)) {
38
+ raw.exec(`ALTER TABLE ${TABLE} ADD COLUMN ${COLUMN} TEXT`);
39
+ }
40
+ }
@@ -0,0 +1,34 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { getSqliteFrom } from "../db-connection.js";
3
+
4
+ const TABLE = "acp_refused_credentials";
5
+
6
+ /**
7
+ * Create `acp_refused_credentials`: which Claude tokens Claude has refused.
8
+ *
9
+ * Its own table rather than a column on `acp_session_history`, because the two
10
+ * answer different questions with different lifetimes. A marker says "show a
11
+ * card for this run" and is fair game for the user to delete along with the
12
+ * run. This says "do not resolve this token again", and clearing session
13
+ * history must not change which credential a spawn selects: a configured
14
+ * `CLAUDE_CODE_OAUTH_TOKEN` lives in config, so forgetting that Claude refused
15
+ * it lets it win over the vault replacement again and reopen the Connect loop.
16
+ *
17
+ * Digests, never tokens. Equality is the only operation, and these records
18
+ * deliberately outlive the runs that produced them.
19
+ *
20
+ * Retained rather than consumed: a revoked value the user never removes from
21
+ * config is resolved by every later spawn too, so a one-shot only spares the
22
+ * first. Keeping them is safe because a value the user actually fixes hashes
23
+ * differently and was never recorded.
24
+ *
25
+ * Idempotent via IF NOT EXISTS.
26
+ */
27
+ export function migrateCreateAcpRefusedCredentials(database: DrizzleDb): void {
28
+ getSqliteFrom(database).exec(
29
+ `CREATE TABLE IF NOT EXISTS ${TABLE} (
30
+ digest TEXT PRIMARY KEY,
31
+ refused_at INTEGER NOT NULL
32
+ )`,
33
+ );
34
+ }
@@ -0,0 +1,27 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { getSqliteFrom } from "../db-connection.js";
3
+
4
+ const INDEX = "idx_acp_session_history_auth_marker";
5
+
6
+ /**
7
+ * Index the credential-failure markers a conversation carries.
8
+ *
9
+ * Every snapshot refetch asks the same question: the newest marked runs in one
10
+ * conversation. The existing indexes cover the parent and the timestamp
11
+ * separately, so that question meant gathering a conversation's rows through
12
+ * one and then sorting them, work that grows with how many runs it has had
13
+ * rather than with the handful the answer needs.
14
+ *
15
+ * Partial, on the marker predicate, so the index holds only rows that carry a
16
+ * failure. Conversations that have never had one contribute nothing to it, and
17
+ * the ordered columns let the scan stop as soon as it has enough.
18
+ *
19
+ * Idempotent via IF NOT EXISTS.
20
+ */
21
+ export function migrateAcpAuthMarkerIndex(database: DrizzleDb): void {
22
+ getSqliteFrom(database).exec(
23
+ `CREATE INDEX IF NOT EXISTS ${INDEX}
24
+ ON acp_session_history (parent_conversation_id, started_at DESC)
25
+ WHERE auth_error_code IS NOT NULL`,
26
+ );
27
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Migration 370 adds the nullable `auth_error_code` column that carries a
3
+ * run's credential failure across a reload, and is idempotent so a repair flow
4
+ * can re-run it.
5
+ */
6
+
7
+ import { Database } from "bun:sqlite";
8
+ import { describe, expect, test } from "bun:test";
9
+
10
+ import { drizzle } from "drizzle-orm/bun-sqlite";
11
+
12
+ import { getSqliteFrom } from "../../db-connection.js";
13
+ import * as schema from "../../schema.js";
14
+ import { migrateAcpSessionHistoryAuthErrorCode } from "../370-acp-session-history-auth-error-code.js";
15
+
16
+ function createTestDb() {
17
+ const sqlite = new Database(":memory:");
18
+ sqlite.exec("PRAGMA journal_mode=WAL");
19
+ sqlite.exec(
20
+ "CREATE TABLE acp_session_history (id TEXT PRIMARY KEY, status TEXT)",
21
+ );
22
+ return drizzle(sqlite, { schema });
23
+ }
24
+
25
+ function columnNames(db: ReturnType<typeof createTestDb>): string[] {
26
+ const rows = getSqliteFrom(db)
27
+ .prepare("PRAGMA table_info(acp_session_history)")
28
+ .all() as { name: string }[];
29
+ return rows.map((row) => row.name);
30
+ }
31
+
32
+ describe("migrateAcpSessionHistoryAuthErrorCode", () => {
33
+ test("adds the column when it is missing", () => {
34
+ const db = createTestDb();
35
+
36
+ migrateAcpSessionHistoryAuthErrorCode(db);
37
+
38
+ expect(columnNames(db)).toContain("auth_error_code");
39
+ });
40
+
41
+ test("re-running is a no-op", () => {
42
+ const db = createTestDb();
43
+
44
+ migrateAcpSessionHistoryAuthErrorCode(db);
45
+ migrateAcpSessionHistoryAuthErrorCode(db);
46
+
47
+ expect(columnNames(db)).toContain("auth_error_code");
48
+ });
49
+
50
+ test("leaves existing rows null rather than inventing a failure", () => {
51
+ const db = createTestDb();
52
+ getSqliteFrom(db).exec(
53
+ "INSERT INTO acp_session_history (id, status) VALUES ('run-1', 'completed')",
54
+ );
55
+
56
+ migrateAcpSessionHistoryAuthErrorCode(db);
57
+
58
+ const row = getSqliteFrom(db)
59
+ .prepare(
60
+ "SELECT auth_error_code FROM acp_session_history WHERE id = 'run-1'",
61
+ )
62
+ .get() as { auth_error_code: string | null };
63
+ expect(row.auth_error_code).toBeNull();
64
+ });
65
+ });