@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,7 +19,7 @@ import {
19
19
  ServicesSchema,
20
20
  } from "../../config/schemas/services.js";
21
21
  import type { OAuthConnectionRequest } from "../../oauth/connection.js";
22
- import { jsonSafeOAuthBody } from "../../oauth/connection.js";
22
+ import { isBinaryOAuthBody, jsonSafeOAuthBody } from "../../oauth/connection.js";
23
23
  import {
24
24
  resolveOAuthConnection,
25
25
  type ResolveOAuthConnectionOptions,
@@ -42,6 +42,7 @@ import { matchHostPattern } from "../../tools/credentials/host-pattern-match.js"
42
42
  import { getLogger } from "../../util/logger.js";
43
43
  import {
44
44
  findContentTypeHeader,
45
+ parseRequestBodyBytes,
45
46
  parseRequestBodyData,
46
47
  } from "../../util/oauth-request-body.js";
47
48
  import { LOCAL_PRINCIPALS } from "../auth/route-policy.js";
@@ -729,7 +730,8 @@ async function handleToken({ body = {} }: RouteHandlerArgs) {
729
730
  /**
730
731
  * Resolve a raw `data` string into a request body. A non-JSON Content-Type
731
732
  * keeps the payload as the caller's exact string so multipart and other
732
- * byte-sensitive payloads survive; files are read as UTF-8 text.
733
+ * byte-sensitive payloads survive. Files are read as raw bytes: valid UTF-8
734
+ * stays text, and anything else stays a Buffer.
733
735
  */
734
736
  function readBodyData(data: string, contentType: string | undefined): unknown {
735
737
  if (data === "@-") {
@@ -744,7 +746,7 @@ function readBodyData(data: string, contentType: string | undefined): unknown {
744
746
 
745
747
  if (data.startsWith("@")) {
746
748
  const filePath = data.slice(1);
747
- return parseRequestBodyData(readFileSync(filePath, "utf-8"), contentType);
749
+ return parseRequestBodyBytes(readFileSync(filePath), contentType);
748
750
  }
749
751
 
750
752
  return parseRequestBodyData(data, contentType);
@@ -760,6 +762,8 @@ export async function handleRequest({ body = {} }: RouteHandlerArgs) {
760
762
  parsed_data?: unknown;
761
763
  /** Raw data string (for direct API callers, not the CLI). */
762
764
  data?: string;
765
+ /** When set to base64, `parsed_data` / `data` is a base64 string of raw bytes. */
766
+ body_encoding?: "base64";
763
767
  force_get?: boolean;
764
768
  head?: boolean;
765
769
  account?: string;
@@ -864,7 +868,15 @@ export async function handleRequest({ body = {} }: RouteHandlerArgs) {
864
868
  : undefined;
865
869
 
866
870
  if (resolvedData !== undefined) {
867
- const rawBody = resolvedData;
871
+ let rawBody = resolvedData;
872
+ if (b.body_encoding === "base64") {
873
+ if (typeof rawBody !== "string") {
874
+ throw new BadRequestError(
875
+ "body_encoding=base64 requires a string body",
876
+ );
877
+ }
878
+ rawBody = Buffer.from(rawBody, "base64");
879
+ }
868
880
 
869
881
  if (b.force_get) {
870
882
  if (typeof rawBody === "string") {
@@ -882,7 +894,8 @@ export async function handleRequest({ body = {} }: RouteHandlerArgs) {
882
894
  } else if (
883
895
  rawBody !== null &&
884
896
  typeof rawBody === "object" &&
885
- !Array.isArray(rawBody)
897
+ !Array.isArray(rawBody) &&
898
+ !isBinaryOAuthBody(rawBody)
886
899
  ) {
887
900
  for (const [key, value] of Object.entries(
888
901
  rawBody as Record<string, unknown>,
@@ -50,7 +50,7 @@ const statusResponseSchema = z.object({
50
50
  async function startRouteHostWorker() {
51
51
  let result: { pid: number; alreadyRunning: boolean };
52
52
  try {
53
- result = await spawnRouteHostWorkerProcess({ detached: false });
53
+ result = await spawnRouteHostWorkerProcess();
54
54
  } catch (err) {
55
55
  const message =
56
56
  err instanceof RouteHostSpawnError || err instanceof Error
@@ -50,7 +50,7 @@ const statusResponseSchema = z.object({
50
50
  async function startScheduleWorker() {
51
51
  let result: { pid: number; alreadyRunning: boolean };
52
52
  try {
53
- result = await spawnScheduleWorkerProcess({ detached: false });
53
+ result = await spawnScheduleWorkerProcess();
54
54
  } catch (err) {
55
55
  const message =
56
56
  err instanceof ScheduleWorkerSpawnError || err instanceof Error
@@ -11,6 +11,8 @@
11
11
  * - Notification routing goes through emitNotificationSignal().
12
12
  */
13
13
 
14
+ import { isGuardianRequestExpired } from "@vellumai/gateway-client";
15
+
14
16
  import {
15
17
  createGuardianRequest,
16
18
  listGuardianRequestsOrEmpty,
@@ -124,8 +126,10 @@ export async function createOrReuseToolGrantRequest(
124
126
  kind: "tool_grant_request",
125
127
  toolName,
126
128
  });
129
+ // Past-deadline rows never dedupe: see the access-request helper's note.
127
130
  const dedupeMatch = existing.find(
128
131
  (r) =>
132
+ !isGuardianRequestExpired(r) &&
129
133
  r.inputDigest === inputDigest &&
130
134
  r.guardianExternalUserId === binding.guardianExternalUserId,
131
135
  );
@@ -66,6 +66,7 @@ export async function runScript(
66
66
  detached: true,
67
67
  stdout: "pipe",
68
68
  stderr: "pipe",
69
+ windowsHide: true,
69
70
  env: {
70
71
  ...buildSanitizedEnv(),
71
72
  // __SCHEDULE_ID lets a saved command find its own dir; __SCHEDULE_RUN_ID
@@ -360,7 +360,7 @@ export function startScheduler(): SchedulerHandle {
360
360
  scheduleWorkerSupervisor = createWorkerSupervisor({
361
361
  label: "Schedule worker",
362
362
  probe: probeScheduleWorker,
363
- respawn: () => spawnScheduleWorkerProcess({ detached: false }),
363
+ respawn: () => spawnScheduleWorkerProcess(),
364
364
  isSuppressed: isScheduleWorkerAdministrativelyStopped,
365
365
  killChild: (pid) => {
366
366
  try {
@@ -119,13 +119,13 @@ export function stopScheduleWorkerProcess(): WorkerProcessStatus {
119
119
 
120
120
  /**
121
121
  * Daemon-lifecycle entry point: spawn the schedule worker as a child of the
122
- * daemon (`detached: false`, so it appears in `assistant ps` and is torn down
122
+ * daemon (so it appears in the daemon's process tree and is torn down
123
123
  * on shutdown). Fire-and-forget — a worker failure must never block boot. A
124
124
  * worker that comes up late is still the desired sole schedule runner
125
125
  * (`terminateOnTimeout` is deliberately not set).
126
126
  */
127
127
  export function startScheduleWorker(): void {
128
- void spawnScheduleWorkerProcess({ detached: false })
128
+ void spawnScheduleWorkerProcess()
129
129
  .then((r) =>
130
130
  log.info(
131
131
  { pid: r.pid, alreadyRunning: r.alreadyRunning },
@@ -20,6 +20,7 @@ import { createHash, randomBytes } from "node:crypto";
20
20
  import { createServer, type Server } from "node:http";
21
21
 
22
22
  import { getIsPlatform } from "../config/env-registry.js";
23
+ import { parseScopeList } from "../oauth/scope-utils.js";
23
24
  import { getLogger } from "../util/logger.js";
24
25
  import { renderOAuthCompletionPage as renderLoopbackPage } from "./oauth-completion-page.js";
25
26
 
@@ -249,19 +250,9 @@ export async function exchangeCodeForTokens(
249
250
  (tokenData.token_type as string | undefined),
250
251
  };
251
252
 
252
- // Defensive split: providers (e.g. GitHub, Slack) may return comma-separated
253
- // scopes in token responses regardless of the scope_separator used to join
254
- // outbound authorize URLs, so we tolerate both spaces and commas here. When
255
- // a provider explicitly configures a non-default separator (e.g. Linear uses
256
- // ","), we honor that to keep symmetric round-tripping of configured scopes.
257
- const splitPattern =
258
- config.scopeSeparator === " " ? /[ ,]/ : config.scopeSeparator;
259
253
  const grantedScopes =
260
254
  typeof tokens.scope === "string"
261
- ? tokens.scope
262
- .split(splitPattern)
263
- .map((s) => s.trim())
264
- .filter(Boolean)
255
+ ? parseScopeList(tokens.scope, config.scopeSeparator)
265
256
  : [...config.scopes];
266
257
 
267
258
  return { tokens, grantedScopes, rawTokenResponse: tokenData };
@@ -60,6 +60,7 @@ export type {
60
60
  CredentialListResult,
61
61
  DeleteResult,
62
62
  } from "./credential-backend.js";
63
+ import { ACP_OAUTH_TOKEN_FIELD, ACP_SERVICE } from "../acp/acp-credentials.js";
63
64
 
64
65
  /**
65
66
  * Re-export shared-package secure-key abstractions so downstream consumers
@@ -673,8 +674,40 @@ export async function getSecureKeyAsync(
673
674
  }
674
675
 
675
676
  /**
676
- * Store a secret in secure storage. Writes to exactly one backend —
677
- * no dual-writing. Forces a CES reconnection when the backend is dead —
677
+ * Post-write side effects keyed on which credential was actually stored.
678
+ *
679
+ * Every path that lands a credential runs through this, single writes and bulk
680
+ * restores alike, so a behaviour that has to follow a credential does not
681
+ * depend on a list of call sites that drifts as write paths are added.
682
+ *
683
+ * Deliberately not where a Connect card is retired. A card is retired by the
684
+ * marker it came from no longer matching the credential a spawn would resolve,
685
+ * which is a comparison made when the marker is read. Nothing here has to fire
686
+ * at the right moment for that to happen, so this is a notification rather
687
+ * than a mechanism, and a failure costs freshness rather than correctness.
688
+ *
689
+ * Imported on demand and only on a match, so the ACP module graph stays out of
690
+ * this module's load path.
691
+ */
692
+ async function onCredentialsWritten(accounts: string[]): Promise<void> {
693
+ if (!accounts.includes(credentialKey(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD))) {
694
+ return;
695
+ }
696
+ try {
697
+ // The Connect flow repairs the spawn policy *after* this write, so a token
698
+ // whose read is still denied here is retried by `storeAcpClaudeToken` once
699
+ // that repair lands. This pass covers every other writer.
700
+ const { notifyAcpConnectRetired } =
701
+ await import("../acp/acp-claude-oauth.js");
702
+ await notifyAcpConnectRetired();
703
+ } catch (err) {
704
+ log.warn({ err }, "ACP Connect card notification failed after a write");
705
+ }
706
+ }
707
+
708
+ /**
709
+ * Store a secret in secure storage. Writes to exactly one backend, with no
710
+ * dual-writing. Forces a CES reconnection when the backend is dead, because
678
711
  * callers (e.g. the post-login platform credential push) do not retry a
679
712
  * failed write.
680
713
  */
@@ -682,10 +715,10 @@ export async function setSecureKeyAsync(
682
715
  account: string,
683
716
  value: string,
684
717
  ): Promise<boolean> {
685
- return withCredentialTimeout(async () => {
718
+ const ok = await withCredentialTimeout(async () => {
686
719
  const backend = await resolveBackendAsync({ forceReconnect: true });
687
- const ok = await backend.set(account, value);
688
- if (!ok) {
720
+ const stored = await backend.set(account, value);
721
+ if (!stored) {
689
722
  log.warn(
690
723
  { account, backend: backend.name },
691
724
  "Credential backend set failed",
@@ -693,9 +726,19 @@ export async function setSecureKeyAsync(
693
726
  } else {
694
727
  log.info({ account, backend: backend.name }, "Credential stored");
695
728
  }
696
- updateCesHttpReachability(backend, !ok);
697
- return ok;
729
+ updateCesHttpReachability(backend, !stored);
730
+ return stored;
698
731
  }, false);
732
+ // Detached, because it neither decides the result nor gates it. Under the
733
+ // deadline its cost could turn a stored credential into a reported failure;
734
+ // awaited after it, the cost merely held the caller at "signing in" while a
735
+ // scan and a broadcast finished. Nothing downstream depends on it having
736
+ // run: a card is retired by the marker comparison at read time, and the
737
+ // credential prompt re-checks the registry entry before honouring it.
738
+ if (ok) {
739
+ void onCredentialsWritten([account]);
740
+ }
741
+ return ok;
699
742
  }
700
743
 
701
744
  /**
@@ -726,7 +769,7 @@ export async function deleteSecureKeyAsync(
726
769
  export async function bulkSetSecureKeysAsync(
727
770
  credentials: Array<{ account: string; value: string }>,
728
771
  ): Promise<Array<{ account: string; ok: boolean }>> {
729
- return withCredentialTimeout(
772
+ const results = await withCredentialTimeout(
730
773
  async () => {
731
774
  const backend = await resolveBackendAsync({ forceReconnect: true });
732
775
  let results: Array<{ account: string; ok: boolean }>;
@@ -760,6 +803,10 @@ export async function bulkSetSecureKeysAsync(
760
803
  },
761
804
  credentials.map((c) => ({ account: c.account, ok: false })),
762
805
  );
806
+ // Same as the single-write path: detached, so a bundle's notifications
807
+ // neither decide nor delay its result.
808
+ void onCredentialsWritten(results.filter((r) => r.ok).map((r) => r.account));
809
+ return results;
763
810
  }
764
811
 
765
812
  // ---------------------------------------------------------------------------
@@ -16,8 +16,9 @@ import { gunzipSync } from "node:zlib";
16
16
 
17
17
  import { getPlatformBaseUrl } from "../config/env.js";
18
18
  import { loadSkillCatalog } from "../config/skills.js";
19
+ import { isBunVirtualPath } from "../util/bundled-asset.js";
19
20
  import { getLogger } from "../util/logger.js";
20
- import { getWorkspaceSkillsDir } from "../util/platform.js";
21
+ import { addToPathEnv, getWorkspaceSkillsDir } from "../util/platform.js";
21
22
  import { computeSkillHash, writeInstallMeta } from "./install-meta.js";
22
23
  import {
23
24
  isSkillCompatibleWithPlatform,
@@ -67,7 +68,7 @@ export interface CatalogSkill {
67
68
  export function getRepoSkillsDir(): string | undefined {
68
69
  const importDir = import.meta.dir;
69
70
 
70
- if (importDir.startsWith("/$bunfs/")) {
71
+ if (isBunVirtualPath(importDir)) {
71
72
  const execDir = dirname(process.execPath);
72
73
  // macOS .app bundle: binary in Contents/MacOS/, resources in Contents/Resources/
73
74
  const resourcesPath = join(
@@ -415,12 +416,14 @@ export async function installSkillDependenciesIfPresent(
415
416
  if (!existsSync(join(skillDir, "package.json"))) {
416
417
  return;
417
418
  }
418
- const bunPath = `${homedir()}/.bun/bin`;
419
+ const env = { ...process.env };
420
+ addToPathEnv(env, [join(homedir(), ".bun", "bin")]);
419
421
  await new Promise<void>((resolve, reject) => {
420
422
  const child = spawn("bun", ["install"], {
421
423
  cwd: skillDir,
422
424
  stdio: "inherit",
423
- env: { ...process.env, PATH: `${bunPath}:${process.env.PATH}` },
425
+ env,
426
+ windowsHide: true,
424
427
  });
425
428
  child.on("error", reject);
426
429
  child.on("close", (code) => {
@@ -195,6 +195,7 @@ async function runClawhub(
195
195
  env: { ...process.env, CLAWHUB_DISABLE_TELEMETRY: "1" },
196
196
  stdout: "pipe",
197
197
  stderr: "pipe",
198
+ windowsHide: true,
198
199
  });
199
200
 
200
201
  let timer: ReturnType<typeof setTimeout>;
@@ -125,6 +125,7 @@ export async function runInlineCommand(
125
125
  cwd: workingDir,
126
126
  env,
127
127
  stdio: ["ignore", "pipe", "ignore"],
128
+ windowsHide: true,
128
129
  });
129
130
  } catch (err) {
130
131
  const message = err instanceof Error ? err.message : String(err);
@@ -0,0 +1,108 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ NOISE_FLOOR_BLOCK_COUNT,
5
+ NOISE_FLOOR_BLOCK_MS,
6
+ RoomNoiseFloor,
7
+ } from "../room-noise-floor.js";
8
+
9
+ /** Audio needed before the estimator will report anything at all. */
10
+ const FULL_WINDOW_MS = NOISE_FLOOR_BLOCK_MS * NOISE_FLOOR_BLOCK_COUNT;
11
+
12
+ /** Feed `ms` of audio at a steady amplitude, in 50ms chunks like the client. */
13
+ function feed(floor: RoomNoiseFloor, amplitude: number, ms: number): void {
14
+ for (let elapsed = 0; elapsed < ms; elapsed += 50) {
15
+ floor.observe(amplitude, 50);
16
+ }
17
+ }
18
+
19
+ describe("RoomNoiseFloor", () => {
20
+ test("has no opinion until the window is full", () => {
21
+ const floor = new RoomNoiseFloor();
22
+ feed(floor, 300, FULL_WINDOW_MS - NOISE_FLOOR_BLOCK_MS);
23
+
24
+ // A partial window would report the loudest thing it has heard as the room.
25
+ expect(floor.floor).toBeNull();
26
+
27
+ feed(floor, 300, NOISE_FLOOR_BLOCK_MS);
28
+ expect(floor.floor).toBeCloseTo(300, 5);
29
+ });
30
+
31
+ test("ignores intermittent speech and keeps the quiet second", () => {
32
+ const floor = new RoomNoiseFloor();
33
+ // Someone talking over a 250-level room, with one pause in the window.
34
+ feed(floor, 2_400, FULL_WINDOW_MS - NOISE_FLOOR_BLOCK_MS);
35
+ feed(floor, 250, NOISE_FLOOR_BLOCK_MS);
36
+
37
+ // The minimum picks the second nobody spoke in, not the average of the ten.
38
+ expect(floor.floor).toBeCloseTo(250, 5);
39
+ });
40
+
41
+ test("a block averages across syllable gaps rather than latching onto one", () => {
42
+ const floor = new RoomNoiseFloor();
43
+ // Speech with brief pauses inside it: alternating loud and room-level
44
+ // chunks throughout. A per-chunk minimum would call this room 250.
45
+ for (let elapsed = 0; elapsed < FULL_WINDOW_MS; elapsed += 100) {
46
+ floor.observe(2_400, 50);
47
+ floor.observe(250, 50);
48
+ }
49
+
50
+ expect(floor.floor).toBeCloseTo(1_325, 5);
51
+ });
52
+
53
+ test("forgets a room it has left, once the window has rolled", () => {
54
+ const floor = new RoomNoiseFloor();
55
+ feed(floor, 200, FULL_WINDOW_MS);
56
+ expect(floor.floor).toBeCloseTo(200, 5);
57
+
58
+ // Walk somewhere loud and stay there past the retention window.
59
+ feed(floor, 900, FULL_WINDOW_MS);
60
+
61
+ expect(floor.floor).toBeCloseTo(900, 5);
62
+ });
63
+
64
+ test("weights by duration so an odd-sized tail cannot skew a block", () => {
65
+ const floor = new RoomNoiseFloor();
66
+ for (let block = 0; block < NOISE_FLOOR_BLOCK_COUNT; block += 1) {
67
+ floor.observe(100, NOISE_FLOOR_BLOCK_MS - 10);
68
+ floor.observe(10_000, 10);
69
+ }
70
+
71
+ // 10ms of a loud tail moves a 1s block by ~1%, not by half.
72
+ expect(floor.floor).toBeCloseTo(
73
+ (100 * (NOISE_FLOOR_BLOCK_MS - 10) + 10_000 * 10) / NOISE_FLOOR_BLOCK_MS,
74
+ 5,
75
+ );
76
+ });
77
+
78
+ test("interrupt() drops the partial block without losing completed ones", () => {
79
+ const floor = new RoomNoiseFloor();
80
+ feed(floor, 300, FULL_WINDOW_MS);
81
+ // Half a block of near-silence, then playback starts and observation stops.
82
+ feed(floor, 10, NOISE_FLOOR_BLOCK_MS / 2);
83
+ floor.interrupt();
84
+ // Resume after playback; the two halves must not merge into one block that
85
+ // measures neither side of the gap.
86
+ feed(floor, 300, NOISE_FLOOR_BLOCK_MS);
87
+
88
+ expect(floor.floor).toBeCloseTo(300, 5);
89
+ });
90
+
91
+ test("ignores non-positive durations and non-finite amplitudes", () => {
92
+ const floor = new RoomNoiseFloor();
93
+ floor.observe(500, 0);
94
+ floor.observe(500, -50);
95
+ floor.observe(Number.NaN, 50);
96
+
97
+ feed(floor, 300, FULL_WINDOW_MS);
98
+ expect(floor.floor).toBeCloseTo(300, 5);
99
+ });
100
+
101
+ test("reset() clears completed blocks too", () => {
102
+ const floor = new RoomNoiseFloor();
103
+ feed(floor, 300, FULL_WINDOW_MS);
104
+ floor.reset();
105
+
106
+ expect(floor.floor).toBeNull();
107
+ });
108
+ });
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Rolling estimate of a room's continuous background noise level, on the same
3
+ * 16-bit mean-absolute-amplitude scale as `speech-energy.ts`.
4
+ *
5
+ * ## Why an estimate is needed at all
6
+ *
7
+ * `detectPcm16SpeechActivity` compares a chunk against a fixed absolute
8
+ * threshold. A constant works only if every room delivers roughly the same
9
+ * silence, and rooms do not: a quiet home office and a cafe differ by more
10
+ * than the gap between silence and speech. Set the constant for the cafe and a
11
+ * soft talker is never heard; set it for the office and the cafe's background
12
+ * reads as continuous speech, which in a live-voice session means room noise
13
+ * repeatedly cancels the assistant mid-reply.
14
+ *
15
+ * ## Why the minimum of block means, and not an average
16
+ *
17
+ * The hard part is estimating background noise from a signal that also
18
+ * contains the user. Averaging fails: it folds the user's own speech into the
19
+ * floor, so the floor climbs while they talk and the gate goes deaf exactly
20
+ * when someone is using it. An envelope with a slow time constant fails the
21
+ * same way for the same reason, because "a person talking" and "a noisier
22
+ * room" both last about as long as each other, and a time constant cannot
23
+ * separate two things that differ only in level.
24
+ *
25
+ * What does separate them is that **speech is intermittent and room noise is
26
+ * continuous.** Any ten seconds of a real conversation contains at least one
27
+ * second in which nobody is speaking, and in that second the microphone hears
28
+ * the room and nothing else. So: average within a one-second block, then take
29
+ * the *minimum* across the retained blocks. Block-averaging first matters,
30
+ * because a minimum taken over raw chunks would latch onto the quietest 50 ms
31
+ * gap between two syllables, which is a pause inside speech, not the room.
32
+ *
33
+ * The estimate degrades honestly rather than dangerously. Ten unbroken seconds
34
+ * of speech (a monologue with no breath) raises the floor, because there was
35
+ * genuinely no quiet second to measure; it recovers on the first pause. The
36
+ * caller bounds what a raised floor is allowed to do.
37
+ *
38
+ * Pure and transport-neutral: amplitudes in, an estimate out, no session or
39
+ * provider state.
40
+ */
41
+
42
+ /** Audio duration averaged into one block before it joins the estimate. */
43
+ export const NOISE_FLOOR_BLOCK_MS = 1_000;
44
+
45
+ /**
46
+ * Completed blocks retained. Ten seconds is the shortest window that reliably
47
+ * contains a conversational pause, and short enough that walking somewhere
48
+ * quieter is reflected within a turn or two.
49
+ */
50
+ export const NOISE_FLOOR_BLOCK_COUNT = 10;
51
+
52
+ /**
53
+ * Tracks the quietest recent second of microphone audio.
54
+ *
55
+ * Feed it every chunk's mean amplitude while the near end is *not* playing
56
+ * audio back; feeding it during playback would teach it the assistant's own
57
+ * echo. Call {@link interrupt} when observation stops for that reason, so a
58
+ * block never averages across the gap.
59
+ */
60
+ export class RoomNoiseFloor {
61
+ private readonly blockMs: number;
62
+ private readonly blockCount: number;
63
+ /** Completed block means, oldest first, at most `blockCount` of them. */
64
+ private readonly blocks: number[] = [];
65
+ /** Duration-weighted amplitude sum of the block being filled. */
66
+ private pendingWeightedSum = 0;
67
+ private pendingMs = 0;
68
+
69
+ constructor(
70
+ blockMs: number = NOISE_FLOOR_BLOCK_MS,
71
+ blockCount: number = NOISE_FLOOR_BLOCK_COUNT,
72
+ ) {
73
+ this.blockMs = blockMs;
74
+ this.blockCount = blockCount;
75
+ }
76
+
77
+ /**
78
+ * Fold one chunk's mean amplitude into the estimate.
79
+ *
80
+ * Weighted by duration so an irregular chunk size cannot skew a block: the
81
+ * client's framing is its own business, and a 10 ms tail must not count as
82
+ * much as the 50 ms frame before it.
83
+ */
84
+ observe(meanAmplitude: number, durationMs: number): void {
85
+ if (!(durationMs > 0) || !Number.isFinite(meanAmplitude)) {
86
+ return;
87
+ }
88
+ this.pendingWeightedSum += meanAmplitude * durationMs;
89
+ this.pendingMs += durationMs;
90
+ if (this.pendingMs < this.blockMs) {
91
+ return;
92
+ }
93
+ this.blocks.push(this.pendingWeightedSum / this.pendingMs);
94
+ if (this.blocks.length > this.blockCount) {
95
+ this.blocks.shift();
96
+ }
97
+ this.pendingWeightedSum = 0;
98
+ this.pendingMs = 0;
99
+ }
100
+
101
+ /**
102
+ * Drop the partially-filled block, keeping everything already completed.
103
+ *
104
+ * Call this when observation is about to pause (assistant playback starting)
105
+ * or resume after a gap. Without it a single block could average a half
106
+ * second of room from before the gap with a half second from after it, which
107
+ * is a measurement of neither.
108
+ */
109
+ interrupt(): void {
110
+ this.pendingWeightedSum = 0;
111
+ this.pendingMs = 0;
112
+ }
113
+
114
+ /**
115
+ * The quietest retained second, or `null` until the window is **full**.
116
+ *
117
+ * Waiting for a full window is what makes the minimum safe. Over ten seconds
118
+ * the claim "at least one of these seconds contains no speech" holds; over
119
+ * one second it does not, and a partial window would happily report the first
120
+ * second of a sentence as the room's noise floor. That over-estimate is the
121
+ * dangerous direction: it raises the gate against the very voice it measured.
122
+ *
123
+ * `null` means "no opinion yet" and callers must fall back to their
124
+ * configured constant rather than treating it as a floor of zero. Since only
125
+ * non-playback audio is observed, the window fills with accumulated listening
126
+ * time rather than wall clock, which in a real session is a turn or two.
127
+ */
128
+ get floor(): number | null {
129
+ if (this.blocks.length < this.blockCount) {
130
+ return null;
131
+ }
132
+ return Math.min(...this.blocks);
133
+ }
134
+
135
+ /** Forget everything, e.g. when the input route changes under the session. */
136
+ reset(): void {
137
+ this.blocks.length = 0;
138
+ this.interrupt();
139
+ }
140
+ }
@@ -9,7 +9,9 @@
9
9
  * pulling in the conversation/agent-loop core.
10
10
  *
11
11
  * Delivery targets the parent's in-process `Conversation` via the conversation
12
- * registry: a notification is injected only when the parent is live here.
12
+ * registry: a notification is injected only when the parent is live here. A
13
+ * stale idle parent is rebuilt through `getOrCreateConversation` first so the
14
+ * injected turn uses the current provider, prompt, and credentials.
13
15
  */
14
16
 
15
17
  import {
@@ -36,14 +38,58 @@ export function injectMessageIntoParent(
36
38
  message: string,
37
39
  metadata?: Record<string, unknown>,
38
40
  ): void {
39
- const parentConversation = findConversation(parentConversationId);
40
- if (!parentConversation) {
41
+ const existing = findConversation(parentConversationId);
42
+ if (!existing) {
41
43
  log.warn(
42
44
  { parentConversationId },
43
45
  "Subagent notification target parent conversation not found",
44
46
  );
45
47
  return;
46
48
  }
49
+ // A reload can mark the parent stale while children still run. Terminal
50
+ // injection is the first parent turn after the last child settles, so
51
+ // rebuild first when the instance is idle. Otherwise the completion turn
52
+ // would run on the construction-time provider, prompt, and credentials.
53
+ if (existing.isStale() && !existing.hasInFlightWork()) {
54
+ void import("../daemon/conversation-store.js")
55
+ .then(({ getOrCreateConversation }) =>
56
+ getOrCreateConversation(parentConversationId),
57
+ )
58
+ .then((parent) =>
59
+ deliverToParent(parent, parentConversationId, message, metadata),
60
+ )
61
+ .catch((err) => {
62
+ log.error(
63
+ { parentConversationId, err },
64
+ "Failed to rebuild stale parent before subagent notification",
65
+ );
66
+ });
67
+ return;
68
+ }
69
+ deliverToParent(existing, parentConversationId, message, metadata);
70
+ }
71
+
72
+ function deliverToParent(
73
+ parentConversation: {
74
+ enqueueMessage: (options: {
75
+ content: string;
76
+ metadata?: Record<string, unknown>;
77
+ isInteractive: boolean;
78
+ }) => { queued: boolean; rejected?: boolean };
79
+ persistUserMessage: (options: {
80
+ content: string;
81
+ metadata?: Record<string, unknown>;
82
+ }) => Promise<{ id: string }>;
83
+ runAgentLoop: (
84
+ message: string,
85
+ messageId: string,
86
+ options: { isInteractive: boolean },
87
+ ) => Promise<unknown>;
88
+ },
89
+ parentConversationId: string,
90
+ message: string,
91
+ metadata?: Record<string, unknown>,
92
+ ): void {
47
93
  // Machine-injected with no human asserted present, so the notification
48
94
  // turn runs non-interactive; it still streams to whoever is watching
49
95
  // through the parent's sink.
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import { askQuestionTool } from "../ask-question/ask-question-tool.js";
4
+ import { reactToMessageTool } from "../channel/react-to-message.js";
4
5
  import { fileEditTool } from "../filesystem/edit.js";
5
6
  import { fileListTool } from "../filesystem/list.js";
6
7
  import { fileReadTool } from "../filesystem/read.js";
@@ -140,6 +141,7 @@ describe("derived input_schema", () => {
140
141
  hostShellTool,
141
142
  requestSystemPermissionTool,
142
143
  notifyParentTool,
144
+ reactToMessageTool,
143
145
  ];
144
146
 
145
147
  test("every registered schema belongs to a tool whose input_schema is derived from it", () => {