@vellumai/assistant 0.11.3 → 0.11.4-staging.2

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 (341) hide show
  1. package/ARCHITECTURE.md +11 -6
  2. package/docs/architecture/memory.md +11 -0
  3. package/docs/architecture/turn-actor.md +70 -0
  4. package/docs/flux-turn-detection-spike.md +243 -0
  5. package/docs/stt-provider-onboarding.md +3 -1
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  8. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
  9. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  10. package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  11. package/openapi.yaml +139 -37
  12. package/package.json +1 -1
  13. package/scripts/voice-ttft-spike.ts +3 -3
  14. package/src/__tests__/app-compiler.test.ts +38 -3
  15. package/src/__tests__/attachments-store.test.ts +21 -12
  16. package/src/__tests__/byok-default-profile-ensure.test.ts +17 -0
  17. package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
  18. package/src/__tests__/call-site-routing-provider.test.ts +1 -1
  19. package/src/__tests__/channel-availability-routes.test.ts +14 -1
  20. package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
  21. package/src/__tests__/channel-delivery-store.test.ts +14 -14
  22. package/src/__tests__/config-loader-backfill.test.ts +3 -3
  23. package/src/__tests__/config-schema.test.ts +25 -10
  24. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
  26. package/src/__tests__/conversation-agent-loop.test.ts +28 -20
  27. package/src/__tests__/conversation-attention-store.test.ts +63 -0
  28. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
  29. package/src/__tests__/conversation-fork-crud.test.ts +69 -0
  30. package/src/__tests__/conversation-fork-referential.test.ts +67 -0
  31. package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
  32. package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
  33. package/src/__tests__/conversation-queue.test.ts +177 -4
  34. package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
  35. package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
  36. package/src/__tests__/credential-prompt-route.test.ts +7 -10
  37. package/src/__tests__/custom-profile-ensure.test.ts +5 -1
  38. package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
  39. package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
  40. package/src/__tests__/document-append-idempotency.test.ts +233 -0
  41. package/src/__tests__/edit-propagation.test.ts +0 -7
  42. package/src/__tests__/helpers/mock-actor-context.ts +49 -0
  43. package/src/__tests__/helpers/mock-conversation.ts +13 -1
  44. package/src/__tests__/injector-chain.test.ts +63 -41
  45. package/src/__tests__/injector-disk-pressure.test.ts +11 -23
  46. package/src/__tests__/llm-context-resolution.test.ts +73 -1
  47. package/src/__tests__/llm-schema.test.ts +5 -2
  48. package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
  49. package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
  50. package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
  51. package/src/__tests__/mtime-cache.test.ts +1 -1
  52. package/src/__tests__/non-member-access-request.test.ts +0 -20
  53. package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
  54. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
  55. package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
  56. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
  57. package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
  58. package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
  59. package/src/__tests__/scoped-approval-grants.test.ts +11 -6
  60. package/src/__tests__/secret-ingress-channel.test.ts +0 -1
  61. package/src/__tests__/skills.test.ts +32 -0
  62. package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
  63. package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
  64. package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
  65. package/src/__tests__/turn-events-store.test.ts +43 -0
  66. package/src/__tests__/ui-shape-teaching.test.ts +33 -0
  67. package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
  68. package/src/__tests__/user-plugin-loader.test.ts +1 -1
  69. package/src/__tests__/visible-app-context.test.ts +16 -9
  70. package/src/__tests__/voice-config-update.test.ts +40 -0
  71. package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
  72. package/src/__tests__/worker-plugin-surface.test.ts +77 -0
  73. package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
  74. package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
  75. package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
  76. package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
  77. package/src/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -0
  78. package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
  79. package/src/acp/__tests__/auth-required.test.ts +161 -0
  80. package/src/acp/acp-claude-oauth.ts +19 -2
  81. package/src/acp/agent-process.test.ts +100 -0
  82. package/src/acp/agent-process.ts +29 -26
  83. package/src/acp/auth-required.ts +102 -0
  84. package/src/acp/session-manager.test.ts +119 -0
  85. package/src/acp/session-manager.ts +68 -2
  86. package/src/api/events/acp-auth-required.ts +55 -0
  87. package/src/api/index.ts +7 -0
  88. package/src/api/surfaces.ts +7 -3
  89. package/src/apps/app-store.ts +3 -0
  90. package/src/bundler/package-resolver.ts +2 -30
  91. package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
  92. package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
  93. package/src/calls/call-controller.ts +19 -3
  94. package/src/calls/call-setup-flow.ts +0 -1
  95. package/src/calls/media-stream-stt-session.ts +15 -0
  96. package/src/calls/voice-session-bridge.ts +71 -16
  97. package/src/calls/voice-triage-escalate.ts +104 -2
  98. package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
  99. package/src/channels/config.ts +13 -0
  100. package/src/channels/plugin-channel-declarations.ts +108 -0
  101. package/src/channels/types.ts +30 -0
  102. package/src/cli/AGENTS.md +5 -2
  103. package/src/cli/commands/credentials.help.ts +2 -2
  104. package/src/cli/commands/inference-providers.ts +1 -1
  105. package/src/cli/commands/mcp.help.ts +13 -4
  106. package/src/cli/commands/mcp.ts +9 -0
  107. package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
  108. package/src/cli/commands/memory/index.help.ts +43 -1
  109. package/src/cli/commands/memory/memory-v3.ts +64 -0
  110. package/src/cli/commands/stt.help.ts +27 -2
  111. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
  112. package/src/cli/lib/bundled-marketplace.json +13 -0
  113. package/src/cli/lib/upgrade-plugin.ts +42 -0
  114. package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
  115. package/src/config/__tests__/default-provider.test.ts +6 -1
  116. package/src/config/__tests__/profile-materialization.test.ts +75 -19
  117. package/src/config/bundled-skills/acp/SKILL.md +6 -7
  118. package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
  119. package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
  120. package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
  121. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  122. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.test.ts +65 -0
  123. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
  124. package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
  125. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
  126. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
  127. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
  128. package/src/config/call-site-defaults.ts +4 -5
  129. package/src/config/default-profile-catalog.ts +83 -12
  130. package/src/config/default-profile-names.ts +4 -1
  131. package/src/config/default-provider-resolution.ts +4 -0
  132. package/src/config/llm-context-resolution.ts +11 -3
  133. package/src/config/llm-resolver.ts +28 -1
  134. package/src/config/profile-materialization.ts +70 -22
  135. package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
  136. package/src/config/schemas/call-site-catalog.ts +4 -4
  137. package/src/config/schemas/live-voice.ts +57 -23
  138. package/src/config/schemas/llm.ts +59 -32
  139. package/src/config/schemas/mcp.ts +23 -0
  140. package/src/config/schemas/plugin-updates.ts +6 -2
  141. package/src/config/schemas/stt.ts +1 -0
  142. package/src/context/outbound-sanitize.ts +96 -1
  143. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
  144. package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
  145. package/src/daemon/conversation-agent-loop.ts +17 -6
  146. package/src/daemon/conversation-messaging.ts +5 -1
  147. package/src/daemon/conversation-notifiers.ts +9 -1
  148. package/src/daemon/conversation-process.ts +36 -6
  149. package/src/daemon/conversation-runtime-assembly.ts +3 -4
  150. package/src/daemon/conversation-surfaces.ts +27 -5
  151. package/src/daemon/conversation-tool-setup.ts +1 -2
  152. package/src/daemon/conversation.ts +48 -0
  153. package/src/daemon/interactive-turn-sender.ts +59 -0
  154. package/src/daemon/mcp-reload-service.ts +36 -6
  155. package/src/daemon/process-message.ts +24 -24
  156. package/src/daemon/providers-setup.ts +6 -3
  157. package/src/daemon/trust-context-types.ts +29 -0
  158. package/src/daemon/wake-conversation-ops.ts +3 -2
  159. package/src/documents/document-store.ts +138 -5
  160. package/src/hooks/hook-loader.ts +3 -3
  161. package/src/hooks/registry.ts +50 -6
  162. package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
  163. package/src/inbound/oauth-callback-url.ts +61 -0
  164. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
  165. package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
  166. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
  167. package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
  168. package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
  169. package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
  170. package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
  171. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
  172. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
  173. package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
  174. package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
  175. package/src/live-voice/live-voice-archive.ts +2 -0
  176. package/src/live-voice/live-voice-metrics.ts +57 -32
  177. package/src/live-voice/live-voice-photo.ts +1 -2
  178. package/src/live-voice/live-voice-session.ts +535 -314
  179. package/src/live-voice/progress-narration.ts +277 -0
  180. package/src/live-voice/protocol.ts +21 -1
  181. package/src/mcp/__tests__/effective-config.test.ts +238 -0
  182. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
  183. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
  184. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
  185. package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
  186. package/src/mcp/client.ts +16 -11
  187. package/src/mcp/effective-config.ts +113 -0
  188. package/src/mcp/manager.ts +11 -6
  189. package/src/mcp/mcp-auth-orchestrator.ts +13 -22
  190. package/src/mcp/mcp-oauth-provider.ts +205 -240
  191. package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
  192. package/src/monitoring/plugin-auto-update.ts +128 -24
  193. package/src/notifications/signal.ts +1 -0
  194. package/src/permissions/confirmation-guardian-request.test.ts +15 -11
  195. package/src/permissions/confirmation-guardian-request.ts +2 -2
  196. package/src/permissions/question-guardian-request.test.ts +14 -6
  197. package/src/permissions/question-guardian-request.ts +1 -2
  198. package/src/persistence/attachments-store.ts +8 -1
  199. package/src/persistence/bookmark-crud.ts +3 -7
  200. package/src/persistence/conversation-attention-store.ts +16 -45
  201. package/src/persistence/conversation-crud.ts +33 -4
  202. package/src/persistence/conversation-lineage.ts +9 -0
  203. package/src/persistence/conversation-queries.ts +108 -41
  204. package/src/persistence/delivery-crud.ts +38 -29
  205. package/src/persistence/external-conversation-store.ts +32 -4
  206. package/src/persistence/llm-request-log-store.ts +4 -10
  207. package/src/persistence/llm-usage-store.ts +8 -3
  208. package/src/persistence/message-reads.test.ts +197 -0
  209. package/src/persistence/message-reads.ts +211 -0
  210. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
  211. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
  212. package/src/persistence/real-user-turn-filter.ts +27 -3
  213. package/src/persistence/steps.ts +9 -0
  214. package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
  215. package/src/plugin-api/conversation-turn.ts +168 -5
  216. package/src/plugin-api/index.ts +21 -5
  217. package/src/plugin-api/vision-support.test.ts +1 -1
  218. package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
  219. package/src/plugins/defaults/memory/AGENTS.md +4 -0
  220. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
  221. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
  224. package/src/plugins/defaults/memory/buffer-format.ts +165 -0
  225. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
  226. package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
  227. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
  228. package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
  229. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
  230. package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
  231. package/src/plugins/defaults/memory/indexer.ts +3 -1
  232. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
  233. package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
  234. package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
  235. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
  236. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
  237. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
  238. package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
  239. package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
  240. package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
  241. package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
  242. package/src/plugins/defaults/memory/v3/prune.ts +2 -0
  243. package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
  244. package/src/plugins/defaults/memory/worker.ts +6 -3
  245. package/src/plugins/external-plugin-loader.ts +47 -0
  246. package/src/plugins/mcp-servers.ts +361 -0
  247. package/src/plugins/mtime-cache.ts +23 -49
  248. package/src/plugins/worker-plugin-surface.ts +33 -0
  249. package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
  250. package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
  251. package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
  252. package/src/providers/__tests__/retry-callsite.test.ts +5 -2
  253. package/src/providers/call-site-routing.ts +30 -3
  254. package/src/providers/connection-resolution.ts +194 -11
  255. package/src/providers/inference/auth.ts +6 -0
  256. package/src/providers/inference/connection-availability.ts +24 -2
  257. package/src/providers/inference/connections.ts +2 -0
  258. package/src/providers/model-catalog.ts +3 -3
  259. package/src/providers/model-intents.ts +28 -8
  260. package/src/providers/openai/chat-completions-provider.ts +5 -6
  261. package/src/providers/openai/codex-models.ts +2 -1
  262. package/src/providers/provider-send-message.ts +32 -3
  263. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
  264. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
  265. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
  266. package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
  267. package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
  268. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
  269. package/src/providers/speech-to-text/provider-catalog.ts +99 -8
  270. package/src/providers/speech-to-text/resolve.ts +25 -2
  271. package/src/routes/worker.ts +17 -5
  272. package/src/runtime/access-request-helper.ts +9 -12
  273. package/src/runtime/agent-wake.ts +3 -3
  274. package/src/runtime/pre-first-message-gate.ts +4 -0
  275. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
  276. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
  277. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
  278. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
  279. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
  280. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
  281. package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
  282. package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
  283. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
  284. package/src/runtime/routes/channel-availability-routes.ts +32 -14
  285. package/src/runtime/routes/channel-route-shared.ts +0 -6
  286. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
  287. package/src/runtime/routes/conversation-list-routes.ts +112 -1
  288. package/src/runtime/routes/conversation-query-routes.ts +40 -27
  289. package/src/runtime/routes/credential-prompt-routes.ts +4 -7
  290. package/src/runtime/routes/default-provider-routes.ts +15 -0
  291. package/src/runtime/routes/inbound-message-handler.ts +17 -41
  292. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
  293. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
  294. package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
  295. package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
  296. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
  297. package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
  298. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
  299. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
  300. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
  301. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
  302. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
  303. package/src/runtime/routes/inference-profiles-routes.ts +20 -11
  304. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
  305. package/src/runtime/routes/log-export-routes.ts +3 -0
  306. package/src/runtime/routes/mcp-auth-routes.ts +148 -57
  307. package/src/runtime/routes/plugins-routes.ts +21 -3
  308. package/src/runtime/routes/stt-routes.ts +31 -25
  309. package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
  310. package/src/runtime/routes/user-route-dispatcher.ts +39 -14
  311. package/src/runtime/routes/user-route-import.ts +108 -0
  312. package/src/schedule/worker.ts +6 -0
  313. package/src/security/oauth2.ts +6 -22
  314. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
  315. package/src/stt/__tests__/types.test.ts +94 -0
  316. package/src/stt/daemon-batch-transcriber.ts +10 -0
  317. package/src/stt/stt-stream-session.ts +8 -4
  318. package/src/stt/types.ts +103 -0
  319. package/src/subagent/manager.ts +1 -3
  320. package/src/subagent/types.ts +7 -6
  321. package/src/tools/acp/spawn.test.ts +97 -0
  322. package/src/tools/acp/spawn.ts +32 -0
  323. package/src/tools/document/document-tool.ts +12 -3
  324. package/src/tools/registry.ts +2 -1
  325. package/src/tools/ui-surface/surface-shape-docs.ts +11 -0
  326. package/src/tools/workflows/run-workflow.ts +1 -2
  327. package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
  328. package/src/tts/reasoning-tag-filter.ts +89 -0
  329. package/src/util/think-tag-stream.ts +95 -0
  330. package/src/workspace/byok-default-profile-ensure.ts +76 -24
  331. package/src/workspace/custom-profile-ensure.ts +4 -24
  332. package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
  333. package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
  334. package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
  335. package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
  336. package/src/workspace/migrations/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
  337. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
  338. package/src/workspace/migrations/registry.ts +10 -0
  339. package/src/workspace/provider-commit-message-generator.ts +7 -5
  340. package/src/live-voice/__tests__/front-decision.test.ts +0 -645
  341. package/src/live-voice/front-decision.ts +0 -476
@@ -825,6 +825,38 @@ describe("always-candidate frontmatter parsing", () => {
825
825
  .map(({ id, chars }) => `${id}: ${chars}`);
826
826
  expect(overrun).toEqual([]);
827
827
  });
828
+
829
+ test("the bundled acp skill's card fits the default budget", async () => {
830
+ // `acp` owns Claude Code and Codex setup, sign-in, and delegation, and the
831
+ // terms that route those turns to it live in this card. The card is
832
+ // hard-truncated at the default budget, so an overrun drops the trailing
833
+ // hints and the whole avoid-when list from what the selector sees.
834
+ const { DEFAULT_CARD_CHARS, buildSkillContent } =
835
+ await import("../plugins/defaults/memory/substrate/skill-content.js");
836
+ const acp = loadSkillCatalog().find((skill) => skill.id === "acp");
837
+ expect(acp).toBeDefined();
838
+
839
+ // A card that overran is cut to exactly the budget, so staying under it is
840
+ // what proves nothing was dropped. `avoid-when` is appended last and is
841
+ // therefore the first content a truncation eats.
842
+ const card = buildSkillContent(acp!);
843
+ expect(card.length).toBeLessThan(DEFAULT_CARD_CHARS);
844
+ expect(card).toContain("Avoid when:");
845
+ });
846
+
847
+ test("the bundled acp card carries its setup and sign-in vocabulary", async () => {
848
+ // A "configure Claude Code" or "let's do the auth" turn only reaches this
849
+ // skill if the card actually says it covers setup and authentication;
850
+ // delegation-only wording routes those turns to a terminal skill instead.
851
+ const { buildSkillContent } =
852
+ await import("../plugins/defaults/memory/substrate/skill-content.js");
853
+ const acp = loadSkillCatalog().find((skill) => skill.id === "acp");
854
+ const card = buildSkillContent(acp!).toLowerCase();
855
+
856
+ for (const term of ["set up", "install", "authenticate", "connect"]) {
857
+ expect(card).toContain(term);
858
+ }
859
+ });
828
860
  });
829
861
 
830
862
  describe("bundled skill categories", () => {
@@ -59,7 +59,6 @@ async function applyEdit(eventId: string, content: string): Promise<void> {
59
59
  conversationExternalId: CHANNEL,
60
60
  externalMessageId: eventId,
61
61
  sourceMessageId: ORIGINAL_TS,
62
- canonicalAssistantId: "self",
63
62
  assistantId: "self",
64
63
  content,
65
64
  });
@@ -184,8 +184,11 @@ import {
184
184
  clearConversations,
185
185
  setConversation,
186
186
  } from "../daemon/conversation-registry.js";
187
+ import type { TrustContext } from "../daemon/trust-context-types.js";
187
188
  import { CallSiteRoutingProvider } from "../providers/call-site-routing.js";
188
189
  import { SubagentManager } from "../subagent/manager.js";
190
+ import { mockAuthContext } from "./helpers/mock-actor-context.js";
191
+ import { asConversation } from "./helpers/mock-conversation.js";
189
192
 
190
193
  // Seed the workspace `llm` config per-case. The real loader schema-merges the
191
194
  // raw fragment over defaults exactly as the code path reads it.
@@ -304,26 +307,29 @@ describe("SubagentManager — provider call-site routing", () => {
304
307
  test("copies parent guardian and auth context into spawned conversation", async () => {
305
308
  setLlmConfig(makeLlmFixture());
306
309
 
307
- const parentTrustContext = {
310
+ const parentTrustContext: TrustContext = {
308
311
  sourceChannel: "vellum",
309
312
  trustClass: "guardian",
310
313
  guardianPrincipalId: "guardian-1",
311
314
  guardianExternalUserId: "guardian-1",
312
315
  };
313
- const parentAuthContext = {
316
+ const parentAuthContext = mockAuthContext({
314
317
  subject: "local:self:parent-perms",
315
318
  actorPrincipalId: "guardian-1",
316
- };
319
+ });
317
320
 
318
321
  capturedConversations.length = 0;
319
322
  clearConversations();
320
323
  const manager = new SubagentManager();
321
- setConversation("parent-perms", {
322
- trustContext: parentTrustContext,
323
- getAuthContext: () => parentAuthContext,
324
- assistantId: "self",
325
- getCurrentSystemPrompt: () => "parent system",
326
- } as any);
324
+ setConversation(
325
+ "parent-perms",
326
+ asConversation({
327
+ trustContext: parentTrustContext,
328
+ getAuthContext: () => parentAuthContext,
329
+ assistantId: "self",
330
+ getCurrentSystemPrompt: () => "parent system",
331
+ }),
332
+ );
327
333
 
328
334
  await manager.spawn(
329
335
  {
@@ -354,11 +360,14 @@ describe("SubagentManager — provider call-site routing", () => {
354
360
  capturedConversations.length = 0;
355
361
  clearConversations();
356
362
  const manager = new SubagentManager();
357
- setConversation("parent-scoped", {
358
- enabledPlugins: parentScope,
359
- getAuthContext: () => undefined,
360
- getCurrentSystemPrompt: () => "parent system",
361
- } as any);
363
+ setConversation(
364
+ "parent-scoped",
365
+ asConversation({
366
+ enabledPlugins: parentScope,
367
+ getAuthContext: () => undefined,
368
+ getCurrentSystemPrompt: () => "parent system",
369
+ }),
370
+ );
362
371
 
363
372
  await manager.spawn(
364
373
  {
@@ -383,11 +392,14 @@ describe("SubagentManager — provider call-site routing", () => {
383
392
  capturedConversations.length = 0;
384
393
  clearConversations();
385
394
  const manager = new SubagentManager();
386
- setConversation("parent-unscoped", {
387
- enabledPlugins: null,
388
- getAuthContext: () => undefined,
389
- getCurrentSystemPrompt: () => "parent system",
390
- } as any);
395
+ setConversation(
396
+ "parent-unscoped",
397
+ asConversation({
398
+ enabledPlugins: null,
399
+ getAuthContext: () => undefined,
400
+ getCurrentSystemPrompt: () => "parent system",
401
+ }),
402
+ );
391
403
 
392
404
  await manager.spawn(
393
405
  {
@@ -207,6 +207,7 @@ import {
207
207
  setConversation,
208
208
  } from "../daemon/conversation-registry.js";
209
209
  import { SubagentAbortedError, SubagentManager } from "../subagent/manager.js";
210
+ import { asConversation } from "./helpers/mock-conversation.js";
210
211
 
211
212
  function makeConfig(overrides: Record<string, unknown> = {}) {
212
213
  return {
@@ -229,16 +230,19 @@ function registerFakeParent(parentConversationId: string): {
229
230
  enqueuedCount: () => number;
230
231
  } {
231
232
  let enqueued = 0;
232
- setConversation(parentConversationId, {
233
- // Accessors read by setUpSubagent when copying trust/auth context.
234
- trustContext: undefined,
235
- getAuthContext: () => undefined,
236
- assistantId: undefined,
237
- enqueueMessage: () => {
238
- enqueued += 1;
239
- return { rejected: false, queued: true };
240
- },
241
- } as never);
233
+ setConversation(
234
+ parentConversationId,
235
+ asConversation({
236
+ // Accessors read by setUpSubagent when copying trust/auth context.
237
+ trustContext: undefined,
238
+ getAuthContext: () => undefined,
239
+ assistantId: undefined,
240
+ enqueueMessage: () => {
241
+ enqueued += 1;
242
+ return { rejected: false, queued: true, requestId: "req-fake" };
243
+ },
244
+ }),
245
+ );
242
246
  return { enqueuedCount: () => enqueued };
243
247
  }
244
248
 
@@ -331,3 +331,46 @@ describe("queryUnreportedTurnEvents", () => {
331
331
  expect(() => stampTurnOutcome("no-such-message", "failed")).not.toThrow();
332
332
  });
333
333
  });
334
+
335
+ describe("unfinalized rows and turn accounting", () => {
336
+ test("an in-flight grouped tool-result row is not a real user turn", async () => {
337
+ const conv = createConversation({ conversationType: "standard" });
338
+ const first = await insertUserMessageAt(conv.id, "real question", 1000);
339
+ // The grouped tool-result row: user-role, reserved unfinalized while
340
+ // tool output streams, content a { ref } that does not contain the
341
+ // tool_result text the content exclusions match. Without the finalized
342
+ // predicate it counts as a phantom user turn until the fold.
343
+ const inflight = await insertUserMessageAt(
344
+ conv.id,
345
+ JSON.stringify({ ref: "conversations/conv/inflight/x.jsonl" }),
346
+ 2000,
347
+ );
348
+ getDb().run(`UPDATE messages SET finalized = 0 WHERE id = '${inflight}'`);
349
+ const second = await insertUserMessageAt(conv.id, "second question", 3000);
350
+
351
+ const events = queryUnreportedTurnEvents(0, undefined, 100);
352
+ const ids = events
353
+ .filter((e) => e.conversationId === conv.id)
354
+ .map((e) => e.id);
355
+
356
+ expect(ids).toEqual([first, second]);
357
+ const byId = Object.fromEntries(events.map((e) => [e.id, e]));
358
+ expect(byId[first].turnIndex).toBe(1);
359
+ expect(byId[second].turnIndex).toBe(2);
360
+ });
361
+
362
+ test("a persisted empty-display user row still counts as a turn", async () => {
363
+ const conv = createConversation({ conversationType: "standard" });
364
+ const real = await insertUserMessageAt(conv.id, "real question", 1000);
365
+ // A hidden send persists its user row with [] content (displayContent "")
366
+ // and still runs a turn, so [] rows must stay in turn accounting.
367
+ const hidden = await insertUserMessageAt(conv.id, "[]", 2000);
368
+
369
+ const events = queryUnreportedTurnEvents(0, undefined, 100);
370
+ const ids = events
371
+ .filter((e) => e.conversationId === conv.id)
372
+ .map((e) => e.id);
373
+
374
+ expect(ids).toEqual([real, hidden]);
375
+ });
376
+ });
@@ -5,6 +5,7 @@ import { uiShowTool } from "../tools/ui-surface/definitions.js";
5
5
  import {
6
6
  SURFACE_SHAPE_DOCS,
7
7
  SURFACE_TYPE_NAMES,
8
+ UI_SHOW_TYPE_DOCS,
8
9
  uiShowTeachingError,
9
10
  } from "../tools/ui-surface/surface-shape-docs.js";
10
11
 
@@ -290,6 +291,12 @@ describe("ui_show displayable payloads proxy through", () => {
290
291
  surfaceType: "channel_setup",
291
292
  input: { surface_type: "channel_setup", data: { channel: "telegram" } },
292
293
  },
294
+ {
295
+ // The picker reads its own voice and catalog, so an empty payload is
296
+ // the whole contract and there is no missing-content state to teach.
297
+ surfaceType: "voice_picker",
298
+ input: { surface_type: "voice_picker", data: {} },
299
+ },
293
300
  ];
294
301
 
295
302
  for (const { surfaceType, input } of cases) {
@@ -347,4 +354,30 @@ describe("uiShowTeachingError", () => {
347
354
  expect(uiShowTool.description).toContain(name);
348
355
  }
349
356
  });
357
+
358
+ test("voice_picker is a cold type that accepts an empty payload", () => {
359
+ expect(
360
+ uiShowTeachingError({ surface_type: "voice_picker", data: {} }),
361
+ ).toBeNull();
362
+ expect(UI_SHOW_TYPE_DOCS).toContain(
363
+ `voice_picker (${SURFACE_SHAPE_DOCS.voice_picker!.purpose})`,
364
+ );
365
+ expect(UI_SHOW_TYPE_DOCS).not.toContain("- voice_picker:");
366
+ });
367
+
368
+ test("voice_picker steering reaches the model through the cold index", () => {
369
+ // Asserted on the built description rather than the doc entry: a cold
370
+ // type's `shape` reaches the model only through a teaching error, and an
371
+ // empty payload has no missing-content state to raise one, so steering
372
+ // parked there would be dead.
373
+ for (const directive of [
374
+ "change, hear, or pick a voice",
375
+ "rather than describing voices in prose",
376
+ "attach no actions and never set await_action",
377
+ "during a live voice call answer in speech",
378
+ ]) {
379
+ expect(UI_SHOW_TYPE_DOCS).toContain(directive);
380
+ expect(uiShowTool.description).toContain(directive);
381
+ }
382
+ });
350
383
  });
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Tests for the `voice_picker` ui_show surface's "never blocks a turn"
3
+ * invariant.
4
+ *
5
+ * The card has no terminal action: it settles when the user picks a voice,
6
+ * with no button to click. `actions` and `await_action` are generic ui_show
7
+ * params though, so the model can attach them to any surface, and the web
8
+ * client latches `awaiting_user_input` on the presence of actions alone with
9
+ * nothing to clear it. The daemon resolver therefore strips both, and a
10
+ * pending picker never holds the one-interactive-surface lock.
11
+ */
12
+
13
+ import { describe, expect, test } from "bun:test";
14
+
15
+ import { UISurfaceShowEventSchema } from "../api/events/ui-surface-show.js";
16
+ import type { AssistantEvent } from "../api/index.js";
17
+ import type { Conversation } from "../daemon/conversation.js";
18
+ import {
19
+ createSurfaceMutex,
20
+ surfaceProxyResolver,
21
+ } from "../daemon/conversation-surfaces.js";
22
+ import type { SurfaceType } from "../daemon/message-protocol.js";
23
+ import { INTERACTIVE_SURFACE_TYPES } from "../daemon/message-protocol.js";
24
+ import { asConversation } from "./helpers/mock-conversation.js";
25
+
26
+ function makeContext(sent: AssistantEvent[] = []): Conversation {
27
+ return asConversation({
28
+ conversationId: "session-1",
29
+ sendToClient: (msg) => sent.push(msg),
30
+ pendingSurfaceActions: new Map<string, { surfaceType: SurfaceType }>(),
31
+ lastSurfaceAction: new Map<
32
+ string,
33
+ { actionId: string; data?: Record<string, unknown> }
34
+ >(),
35
+ surfaceState: new Map(),
36
+ surfaceUndoStacks: new Map<string, string[]>(),
37
+ accumulatedSurfaceState: new Map<string, Record<string, unknown>>(),
38
+ surfaceActionRequestIds: new Set<string>(),
39
+ currentTurnSurfaces: [],
40
+ isProcessing: () => false,
41
+ enqueueMessage: () => ({ queued: false, requestId: "req-1" }),
42
+ getQueueDepth: () => 0,
43
+ processMessage: async () => "ok",
44
+ withSurface: createSurfaceMutex(),
45
+ });
46
+ }
47
+
48
+ function showEvent(sent: AssistantEvent[]) {
49
+ return UISurfaceShowEventSchema.parse(
50
+ sent.find((msg) => msg.type === "ui_surface_show"),
51
+ );
52
+ }
53
+
54
+ describe("voice_picker never blocks a turn", () => {
55
+ test("shows with an empty payload and awaits nothing", async () => {
56
+ const sent: AssistantEvent[] = [];
57
+ const ctx = makeContext(sent);
58
+
59
+ const result = await surfaceProxyResolver(ctx, "ui_show", {
60
+ surface_type: "voice_picker",
61
+ data: {},
62
+ });
63
+
64
+ expect(result.isError).toBe(false);
65
+ expect(result.yieldToUser).toBeUndefined();
66
+
67
+ const parsed = showEvent(sent);
68
+ expect(parsed.surfaceType).toBe("voice_picker");
69
+ expect(parsed.actions).toBeUndefined();
70
+ expect(ctx.pendingSurfaceActions.has(parsed.surfaceId)).toBe(false);
71
+ });
72
+
73
+ test("strips actions the model attached", async () => {
74
+ const sent: AssistantEvent[] = [];
75
+ const ctx = makeContext(sent);
76
+
77
+ const result = await surfaceProxyResolver(ctx, "ui_show", {
78
+ surface_type: "voice_picker",
79
+ data: {},
80
+ actions: [{ id: "done", label: "Done", style: "primary" }],
81
+ });
82
+
83
+ expect(result.isError).toBe(false);
84
+ expect(result.yieldToUser).toBeUndefined();
85
+
86
+ const parsed = showEvent(sent);
87
+ expect(parsed.actions).toBeUndefined();
88
+ // The persisted copy must match the emitted one, or a history reseed would
89
+ // resurrect the button the client blocks on.
90
+ expect(ctx.currentTurnSurfaces[0]?.actions).toBeUndefined();
91
+ expect(ctx.pendingSurfaceActions.has(parsed.surfaceId)).toBe(false);
92
+ });
93
+
94
+ test("ignores an explicit await_action", async () => {
95
+ const sent: AssistantEvent[] = [];
96
+ const ctx = makeContext(sent);
97
+
98
+ const result = await surfaceProxyResolver(ctx, "ui_show", {
99
+ surface_type: "voice_picker",
100
+ data: {},
101
+ await_action: true,
102
+ });
103
+
104
+ expect(result.isError).toBe(false);
105
+ expect(result.yieldToUser).toBeUndefined();
106
+ expect(result.content).not.toContain("awaiting_user_action");
107
+ expect(ctx.pendingSurfaceActions.size).toBe(0);
108
+ });
109
+
110
+ test("a pending picker does not hold the one-interactive-surface lock", async () => {
111
+ const ctx = makeContext();
112
+ ctx.pendingSurfaceActions.set("surface-picker", {
113
+ surfaceType: "voice_picker",
114
+ });
115
+
116
+ const result = await surfaceProxyResolver(ctx, "ui_show", {
117
+ surface_type: "choice",
118
+ data: { options: [{ id: "a", title: "Option A" }] },
119
+ });
120
+
121
+ expect(result.isError).toBe(false);
122
+ expect(result.content).not.toContain("already awaiting user input");
123
+ });
124
+
125
+ test("voice_picker is not an interactive surface type", () => {
126
+ expect(INTERACTIVE_SURFACE_TYPES).not.toContain("voice_picker");
127
+ });
128
+ });
@@ -23,9 +23,9 @@ import { tmpdir } from "node:os";
23
23
  import { join } from "node:path";
24
24
  import { beforeEach, describe, expect, test } from "bun:test";
25
25
 
26
+ import { getUserHooksFor } from "../hooks/registry.js";
26
27
  import {
27
28
  getCachedUserTools,
28
- getUserHooksFor,
29
29
  resetPluginCacheForTests,
30
30
  } from "../plugins/mtime-cache.js";
31
31
  import { loadUserPlugins } from "../plugins/user-loader.js";
@@ -25,6 +25,7 @@ import {
25
25
  import { registerDefaultPluginInjectors } from "../plugins/defaults/index.js";
26
26
  import type { Message } from "../providers/types.js";
27
27
  import { getWorkspacePluginsDir } from "../util/platform.js";
28
+ import { asConversation } from "./helpers/mock-conversation.js";
28
29
 
29
30
  // The injector chain is registered by the daemon bootstrap in production; do
30
31
  // the same here so `applyRuntimeInjections` walks a non-empty chain.
@@ -151,15 +152,21 @@ describe("visible_app injection", () => {
151
152
  ];
152
153
 
153
154
  function seedConversation(currentTurnVisibleAppId: string | undefined): void {
154
- setConversation(CONVERSATION_ID, {
155
- conversationId: CONVERSATION_ID,
156
- workingDir: "/sandbox",
157
- workspaceTopLevelContext: "",
158
- workspaceTopLevelDirty: false,
159
- surfaceState: new Map(),
160
- currentTurnVisibleAppId,
161
- currentTurnTemporalSnapshot: { clientTimezone: null },
162
- } as never);
155
+ setConversation(
156
+ CONVERSATION_ID,
157
+ asConversation({
158
+ conversationId: CONVERSATION_ID,
159
+ workingDir: "/sandbox",
160
+ workspaceTopLevelContext: "",
161
+ workspaceTopLevelDirty: false,
162
+ surfaceState: new Map(),
163
+ currentTurnVisibleAppId,
164
+ currentTurnTemporalSnapshot: {
165
+ clientTimezone: null,
166
+ timeSinceLastMessage: null,
167
+ },
168
+ }),
169
+ );
163
170
  }
164
171
 
165
172
  afterEach(() => {
@@ -276,6 +276,46 @@ describe("voice_config_update — tts_voice_id (managed/vellum active)", () => {
276
276
  expect(result.content).not.toContain("broadcast");
277
277
  });
278
278
 
279
+ test("hints the inline picker for the next managed voice change", async () => {
280
+ makeVellumActive();
281
+
282
+ const result = await run(
283
+ { setting: "tts_voice_id", value: "aura-2-zeus-en" },
284
+ makeContext(),
285
+ );
286
+
287
+ expect(result.isError).toBe(false);
288
+ expect(result.content).toStartWith("Vellum managed voice updated to");
289
+ expect(result.content).toContain(
290
+ 'ui_show { surface_type: "voice_picker", data: {} }',
291
+ );
292
+ });
293
+
294
+ test("does not hint the picker when the voice is not managed", async () => {
295
+ writeConfig({ services: { tts: { provider: "elevenlabs" } } });
296
+ invalidateConfigCache();
297
+
298
+ const result = await run(
299
+ { setting: "tts_voice_id", value: "abc123" },
300
+ makeContext(),
301
+ );
302
+
303
+ expect(result.isError).toBe(false);
304
+ expect(result.content).not.toContain("voice_picker");
305
+ });
306
+
307
+ test("does not hint the picker for a non-voice setting", async () => {
308
+ makeVellumActive();
309
+
310
+ const result = await run(
311
+ { setting: "conversation_timeout", value: 30 },
312
+ makeContext(),
313
+ );
314
+
315
+ expect(result.isError).toBe(false);
316
+ expect(result.content).not.toContain("voice_picker");
317
+ });
318
+
279
319
  test("rejects a value with characters invalid for any voice id", async () => {
280
320
  makeVellumActive();
281
321
 
@@ -18,10 +18,18 @@ import { Glob } from "bun";
18
18
  * - start the PID-file identity guard — the PID file is a worker's sole
19
19
  * tracking handle, so a worker the file stops naming can never be
20
20
  * stopped externally and must evict itself.
21
+ *
22
+ * The subset that hosts real agent conversations
23
+ * ({@link CONVERSATION_RUNNING_ENTRYPOINTS}) must additionally register the
24
+ * default plugins' hooks and injectors, since hook dispatch is process-global
25
+ * and a worker that skips it runs conversations with no plugin behavior at all
26
+ * (no image captioning or vision-rejection recovery, no tool-result
27
+ * truncation, no runtime injections).
21
28
  */
22
29
 
23
30
  const DISABLE_CALL = "disableStreamSeqStamping()";
24
31
  const PID_GUARD_CALL = "startWorkerPidFileGuard(";
32
+ const PLUGIN_SURFACE_CALL = "registerWorkerPluginSurface(";
25
33
 
26
34
  /** Shape of a shutdown that schedules `process.exit` instead of calling it. */
27
35
  const DEFERRED_EXIT = ".finally(() => process.exit";
@@ -44,6 +52,16 @@ const WORK_START_MARKERS: Record<string, string> = {
44
52
  "routes/worker.ts": 'server.on("connection"',
45
53
  };
46
54
 
55
+ /**
56
+ * The entrypoints whose work wakes real agent conversations. `monitoring` (a
57
+ * resource sampler) and `routes` (an HTTP route host) run no conversations, so
58
+ * they carry no plugin surface.
59
+ */
60
+ const CONVERSATION_RUNNING_ENTRYPOINTS: ReadonlySet<string> = new Set([
61
+ "schedule/worker.ts",
62
+ "plugins/defaults/memory/worker.ts",
63
+ ]);
64
+
47
65
  function findWorkerEntrypoints(): string[] {
48
66
  const srcRoot = join(process.cwd(), "src");
49
67
  const glob = new Glob("**/worker.ts");
@@ -137,4 +155,40 @@ describe("worker entrypoint guards", () => {
137
155
  "worker superseded at startup before it runs orphaned work.",
138
156
  ).toEqual([]);
139
157
  });
158
+
159
+ test("the conversation-running entrypoints are all discovered workers", () => {
160
+ const discovered = new Set(findWorkerEntrypoints());
161
+ const unknown = [...CONVERSATION_RUNNING_ENTRYPOINTS].filter(
162
+ (file) => !discovered.has(file),
163
+ );
164
+ expect(
165
+ unknown,
166
+ "CONVERSATION_RUNNING_ENTRYPOINTS names an entrypoint the glob does " +
167
+ "not find — the worker moved or was renamed.",
168
+ ).toEqual([]);
169
+ });
170
+
171
+ test("every conversation-running entrypoint registers the default plugin surface before starting work", () => {
172
+ const offenders = [...CONVERSATION_RUNNING_ENTRYPOINTS]
173
+ .sort()
174
+ .filter((file) => {
175
+ const source = readFileSync(join(process.cwd(), "src", file), "utf8");
176
+ const marker = WORK_START_MARKERS[file];
177
+ if (marker == null) {
178
+ return true;
179
+ }
180
+ const registerAt = source.indexOf(PLUGIN_SURFACE_CALL);
181
+ const workAt = source.indexOf(marker);
182
+ return registerAt < 0 || workAt < 0 || registerAt > workAt;
183
+ });
184
+ expect(
185
+ offenders,
186
+ `A worker that wakes agent conversations must call ` +
187
+ `${PLUGIN_SURFACE_CALL}) before its work-start call (see ` +
188
+ "WORK_START_MARKERS). Hook dispatch is process-global, so without it " +
189
+ "the conversations this process runs get no default plugin behavior: " +
190
+ "no vision-rejection recovery, no tool-result truncation, no runtime " +
191
+ "injections.",
192
+ ).toEqual([]);
193
+ });
140
194
  });
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Tests for {@link registerWorkerPluginSurface}, the default-plugin surface
3
+ * that sidecar worker processes install at startup.
4
+ *
5
+ * A worker that wakes real agent conversations shares the daemon's
6
+ * process-global hook and injector registries, so registering the defaults is
7
+ * what gives those conversations the same behavior as daemon conversations.
8
+ * Registration must populate both tables and stay idempotent, and it must do so
9
+ * without running any plugin `init` hook (the memory plugin's init starts the
10
+ * memory jobs worker process itself).
11
+ */
12
+
13
+ import { beforeEach, describe, expect, test } from "bun:test";
14
+
15
+ import { getHookEntriesFor } from "../hooks/registry.js";
16
+ import {
17
+ clearInjectorRegistry,
18
+ getRegisteredInjectors,
19
+ } from "../plugins/injector-registry.js";
20
+ import { resetPluginRegistryForTests } from "../plugins/registry.js";
21
+ import { registerWorkerPluginSurface } from "../plugins/worker-plugin-surface.js";
22
+
23
+ async function hookOwnersFor(name: string): Promise<string[]> {
24
+ const entries = await getHookEntriesFor(name);
25
+ return entries.map((entry) => entry.owner.id);
26
+ }
27
+
28
+ describe("registerWorkerPluginSurface", () => {
29
+ beforeEach(() => {
30
+ resetPluginRegistryForTests();
31
+ clearInjectorRegistry();
32
+ });
33
+
34
+ test("serves the default plugins' hooks from the process-global registry", async () => {
35
+ expect(await hookOwnersFor("post-model-call")).not.toContain(
36
+ "default-image-fallback",
37
+ );
38
+
39
+ registerWorkerPluginSurface();
40
+
41
+ // The vision-rejection recovery that a worker without this surface never
42
+ // ran, plus an oversized-tool-result guard on the other hook surface.
43
+ expect(await hookOwnersFor("post-model-call")).toContain(
44
+ "default-image-fallback",
45
+ );
46
+ expect(await hookOwnersFor("post-tool-use")).toContain(
47
+ "default-tool-result-truncate",
48
+ );
49
+ });
50
+
51
+ test("registers the injector-only defaults' runtime injectors", () => {
52
+ expect(
53
+ getRegisteredInjectors().some((i) => i.name === "unified-turn-context"),
54
+ ).toBe(false);
55
+
56
+ registerWorkerPluginSurface();
57
+
58
+ // `default-turn-context` contributes no hooks, so its injections only reach
59
+ // a worker's conversations through the injector registry.
60
+ expect(
61
+ getRegisteredInjectors().some((i) => i.name === "unified-turn-context"),
62
+ ).toBe(true);
63
+ });
64
+
65
+ test("is idempotent: a second call neither throws nor duplicates", async () => {
66
+ registerWorkerPluginSurface();
67
+ const hooksAfterFirst = await hookOwnersFor("post-model-call");
68
+ const injectorsAfterFirst = getRegisteredInjectors().map((i) => i.name);
69
+
70
+ expect(() => registerWorkerPluginSurface()).not.toThrow();
71
+
72
+ expect(await hookOwnersFor("post-model-call")).toEqual(hooksAfterFirst);
73
+ expect(getRegisteredInjectors().map((i) => i.name)).toEqual(
74
+ injectorsAfterFirst,
75
+ );
76
+ });
77
+ });