@vellumai/assistant 0.12.2 → 0.12.3-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 (298) hide show
  1. package/.env.example +2 -0
  2. package/AGENTS.md +1 -1
  3. package/docs/architecture/integrations.md +7 -0
  4. package/docs/architecture/memory.md +17 -2
  5. package/docs/credential-execution-service.md +1 -1
  6. package/docs/desktop-browser-cli.md +7 -3
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  9. package/node_modules/@vellumai/environments/src/shell.test.ts +21 -0
  10. package/node_modules/@vellumai/environments/src/shell.ts +24 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  13. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +8 -2
  14. package/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  15. package/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  16. package/openapi.yaml +54 -5
  17. package/package.json +1 -1
  18. package/scripts/smoke-desktop-browser-cli.ts +1 -0
  19. package/scripts/sync-llm-catalog.ts +3 -0
  20. package/scripts/voice-ttft-spike.ts +2 -2
  21. package/src/__tests__/agent-loop.test.ts +259 -0
  22. package/src/__tests__/agent-wake-delegation-prompt.test.ts +64 -2
  23. package/src/__tests__/anthropic-provider.test.ts +55 -0
  24. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  25. package/src/__tests__/attachments-store.test.ts +22 -3
  26. package/src/__tests__/channel-approval.test.ts +9 -14
  27. package/src/__tests__/channel-reply-delivery.test.ts +56 -0
  28. package/src/__tests__/chat-credential-redaction.test.ts +23 -0
  29. package/src/__tests__/computer-use-screenshot-attachments.test.ts +411 -0
  30. package/src/__tests__/computer-use-screenshot-selection.test.ts +50 -0
  31. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  32. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  33. package/src/__tests__/conversation-agent-loop.test.ts +257 -6
  34. package/src/__tests__/conversation-attachments.test.ts +106 -0
  35. package/src/__tests__/conversation-fork-crud.test.ts +191 -0
  36. package/src/__tests__/conversation-rendered-delegation-state.test.ts +276 -0
  37. package/src/__tests__/credential-execution-client.test.ts +70 -53
  38. package/src/__tests__/db-conversation-tool-surface.test.ts +221 -0
  39. package/src/__tests__/events-tail-route.test.ts +33 -0
  40. package/src/__tests__/history-repair.test.ts +53 -0
  41. package/src/__tests__/inference-profile-session-handler.test.ts +26 -0
  42. package/src/__tests__/list-messages-tool-merge.test.ts +85 -1
  43. package/src/__tests__/llm-catalog-parity.test.ts +27 -4
  44. package/src/__tests__/oauth-apps-routes.test.ts +1 -0
  45. package/src/__tests__/oauth-commands-routes.test.ts +114 -101
  46. package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
  47. package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
  48. package/src/__tests__/oauth-providers-routes.test.ts +2 -0
  49. package/src/__tests__/persist-media-references.test.ts +50 -0
  50. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  51. package/src/__tests__/run-conversation-turn-persistence.test.ts +138 -1
  52. package/src/__tests__/schedule-routes.test.ts +20 -0
  53. package/src/__tests__/scheduler-result-notification.test.ts +23 -4
  54. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  55. package/src/__tests__/secret-routes-platform-proxy.test.ts +86 -31
  56. package/src/__tests__/secret-routes-scrub.test.ts +22 -27
  57. package/src/__tests__/secret-scanner.test.ts +20 -0
  58. package/src/__tests__/secure-keys-managed-failover.test.ts +8 -0
  59. package/src/__tests__/secure-keys.test.ts +7 -3
  60. package/src/__tests__/server-tool-pairing.test.ts +107 -0
  61. package/src/__tests__/skills.test.ts +5 -4
  62. package/src/__tests__/subagent-tool-gate-mode.test.ts +226 -0
  63. package/src/__tests__/terminal-tools.test.ts +8 -0
  64. package/src/__tests__/tool-result-follow-up.test.ts +99 -0
  65. package/src/__tests__/tool-result-metadata-plumbing.test.ts +63 -0
  66. package/src/__tests__/unicode.test.ts +36 -0
  67. package/src/agent/loop.ts +37 -12
  68. package/src/agent/tool-result-follow-up.ts +70 -0
  69. package/src/api/attachment-provenance.test.ts +68 -0
  70. package/src/api/computer-use-tool.test.ts +55 -0
  71. package/src/api/computer-use-tool.ts +27 -0
  72. package/src/api/events/assistant-outbound-attachment.ts +3 -0
  73. package/src/api/events/desktop-activity-changed.ts +10 -0
  74. package/src/api/events/question-request.ts +1 -0
  75. package/src/api/index.ts +11 -0
  76. package/src/api/responses/conversation-message.ts +3 -0
  77. package/src/approvals/approval-primitive.ts +5 -2
  78. package/src/approvals/scoped-approval-grants.ts +6 -2
  79. package/src/browser/virtual-desktop-target.ts +1 -2
  80. package/src/cli/commands/__tests__/cli-test-harness.ts +21 -3
  81. package/src/cli/commands/__tests__/plugins.test.ts +20 -1
  82. package/src/cli/commands/__tests__/schedules.test.ts +14 -0
  83. package/src/cli/commands/bash.help.ts +4 -3
  84. package/src/cli/commands/browser.help.ts +5 -1
  85. package/src/cli/commands/credentials.help.ts +3 -3
  86. package/src/cli/commands/oauth/index.help.ts +10 -0
  87. package/src/cli/commands/oauth/providers.ts +12 -0
  88. package/src/cli/commands/oauth/request.test.ts +179 -2
  89. package/src/cli/commands/oauth/request.ts +41 -10
  90. package/src/cli/commands/plugins.ts +12 -5
  91. package/src/cli/commands/schedules.ts +2 -0
  92. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  93. package/src/cli/lib/__tests__/install-from-github.test.ts +41 -0
  94. package/src/cli/lib/__tests__/local-plugin-upgrade.test.ts +133 -51
  95. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +8 -2
  96. package/src/cli/lib/bundled-marketplace.json +855 -0
  97. package/src/cli/lib/bundled-plugin-packages.json +783 -1
  98. package/src/cli/lib/inspect-plugin.ts +11 -4
  99. package/src/cli/lib/upgrade-plugin.ts +19 -11
  100. package/src/cli/output.ts +18 -4
  101. package/src/config/__tests__/default-provider.test.ts +4 -0
  102. package/src/config/bundled-skills/schedule/SKILL.md +6 -6
  103. package/src/config/loader.ts +9 -17
  104. package/src/config/platform-identity.ts +8 -8
  105. package/src/config/profile-text-generation.test.ts +51 -0
  106. package/src/config/profile-text-generation.ts +51 -0
  107. package/src/config/schemas/__tests__/memory-v3.test.ts +12 -0
  108. package/src/config/schemas/llm.ts +7 -2
  109. package/src/config/schemas/mcp.ts +5 -1
  110. package/src/config/schemas/memory-v3.ts +12 -0
  111. package/src/credential-execution/ces-runtime.ts +2 -2
  112. package/src/credential-execution/executable-discovery.ts +36 -134
  113. package/src/credential-execution/process-manager.test.ts +20 -24
  114. package/src/credential-execution/process-manager.ts +24 -19
  115. package/src/daemon/__tests__/conversation-tool-setup.test.ts +43 -0
  116. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +36 -0
  117. package/src/daemon/assistant-attachments.ts +20 -12
  118. package/src/daemon/chat-credential-redaction.ts +16 -1
  119. package/src/daemon/conversation-agent-loop-handlers.ts +74 -3
  120. package/src/daemon/conversation-agent-loop.ts +12 -0
  121. package/src/daemon/conversation-attachments.ts +93 -12
  122. package/src/daemon/conversation-tool-setup.ts +71 -3
  123. package/src/daemon/conversation-turn-finalize.ts +26 -13
  124. package/src/daemon/conversation.ts +72 -17
  125. package/src/daemon/daemon-control.ts +3 -7
  126. package/src/daemon/lifecycle.ts +4 -4
  127. package/src/daemon/mcp-reload-service.ts +1 -1
  128. package/src/daemon/message-types/shared.ts +1 -0
  129. package/src/daemon/orphan-reaper.ts +4 -3
  130. package/src/daemon/persist-media-references.ts +20 -3
  131. package/src/daemon/tool-setup-types.ts +6 -0
  132. package/src/daemon/wake-conversation-ops.ts +50 -15
  133. package/src/desktop/desktop-automation-lease.test.ts +230 -0
  134. package/src/desktop/desktop-automation-lease.ts +90 -6
  135. package/src/desktop/desktop-help.ts +15 -0
  136. package/src/desktop/desktop-stream-bridge.test.ts +65 -10
  137. package/src/desktop/desktop-stream-bridge.ts +3 -3
  138. package/src/desktop/desktop-wallpaper-renderer.ts +132 -0
  139. package/src/desktop/desktop-wallpaper-worker.ts +17 -0
  140. package/src/desktop/desktop-wallpaper.test.ts +48 -7
  141. package/src/desktop/desktop-wallpaper.ts +34 -102
  142. package/src/desktop/virtual-desktop-feature.ts +1 -1
  143. package/src/mcp/__tests__/manager-tool-caps.test.ts +111 -0
  144. package/src/mcp/__tests__/startup.test.ts +30 -7
  145. package/src/mcp/__tests__/tool-caps.test.ts +107 -0
  146. package/src/mcp/manager.ts +168 -101
  147. package/src/mcp/startup.ts +33 -13
  148. package/src/mcp/tool-caps.ts +176 -0
  149. package/src/messaging/provider-message-metadata.ts +3 -3
  150. package/src/notifications/__tests__/copy-composer.test.ts +70 -0
  151. package/src/notifications/copy-composer.ts +11 -3
  152. package/src/notifications/schedule-result-producer.ts +4 -6
  153. package/src/oauth/AGENTS.md +2 -0
  154. package/src/oauth/__tests__/identity-verifier.test.ts +36 -1
  155. package/src/oauth/identity-verifier.ts +24 -0
  156. package/src/oauth/oauth-store.ts +11 -1
  157. package/src/oauth/provider-serializer.ts +1 -0
  158. package/src/oauth/seed-providers.ts +12 -0
  159. package/src/permissions/confirmation-guardian-request.test.ts +16 -0
  160. package/src/permissions/confirmation-guardian-request.ts +2 -3
  161. package/src/permissions/question-prompter.test.ts +31 -0
  162. package/src/permissions/question-prompter.ts +2 -0
  163. package/src/persistence/attachments-store.ts +84 -44
  164. package/src/persistence/conversation-crud.ts +111 -20
  165. package/src/persistence/conversation-plugin-facade.ts +30 -0
  166. package/src/persistence/conversation-tool-surface.ts +135 -0
  167. package/src/persistence/conversation-types.test.ts +32 -0
  168. package/src/persistence/conversation-types.ts +26 -4
  169. package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +78 -0
  170. package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +29 -0
  171. package/src/persistence/migrations/379-oauth-providers-response-ok-field.test.ts +92 -0
  172. package/src/persistence/migrations/379-oauth-providers-response-ok-field.ts +32 -0
  173. package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.test.ts +90 -0
  174. package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.ts +34 -0
  175. package/src/persistence/schema/conversation-tool-surfaces.ts +32 -0
  176. package/src/persistence/schema/index.ts +1 -0
  177. package/src/persistence/schema/oauth.ts +1 -0
  178. package/src/persistence/steps.ts +24 -3
  179. package/src/plugin-api/conversation-turn.ts +31 -7
  180. package/src/plugin-api/index.ts +11 -1
  181. package/src/plugin-api/plugin-channel-turn-trust.test.ts +133 -0
  182. package/src/plugin-api/plugin-channel-turn-trust.ts +71 -0
  183. package/src/plugins/defaults/memory/AGENTS.md +39 -5
  184. package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +365 -0
  185. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +43 -0
  186. package/src/plugins/defaults/memory/__tests__/conversation-memory-purge.test.ts +1 -0
  187. package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +2 -0
  188. package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +17 -0
  189. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +74 -0
  190. package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +161 -0
  191. package/src/plugins/defaults/memory/__tests__/relocated-memory-test-rows.ts +10 -0
  192. package/src/plugins/defaults/memory/buffer-file.ts +354 -0
  193. package/src/plugins/defaults/memory/buffer-format.ts +40 -0
  194. package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -2
  195. package/src/plugins/defaults/memory/context-search/format.ts +2 -1
  196. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -1
  197. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +2 -1
  198. package/src/plugins/defaults/memory/conversation-memory-purge.ts +4 -0
  199. package/src/plugins/defaults/memory/graph/capability-seed.ts +1 -2
  200. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -42
  201. package/src/plugins/defaults/memory/host-utils.ts +0 -10
  202. package/src/plugins/defaults/memory/injectors.ts +4 -3
  203. package/src/plugins/defaults/memory/memory-retrospective-job.ts +72 -183
  204. package/src/plugins/defaults/memory/memory-run-evidence.ts +213 -0
  205. package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +1 -1
  206. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +407 -99
  207. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +10 -0
  208. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +107 -7
  209. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +307 -86
  210. package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +34 -0
  211. package/src/plugins/defaults/memory/substrate/page-index.ts +2 -1
  212. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +89 -49
  213. package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
  214. package/src/plugins/defaults/memory/tools.ts +1 -1
  215. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
  216. package/src/plugins/defaults/memory/v1/graph/extraction.ts +2 -1
  217. package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
  218. package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +5 -0
  219. package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +5 -2
  220. package/src/plugins/defaults/memory/v2/reranker.ts +2 -1
  221. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +81 -1
  222. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +87 -0
  223. package/src/plugins/defaults/memory/v3/__tests__/plugin-schema.test.ts +12 -0
  224. package/src/plugins/defaults/memory/v3/__tests__/pool-log-store.test.ts +183 -4
  225. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +103 -2
  226. package/src/plugins/defaults/memory/v3/card.ts +2 -1
  227. package/src/plugins/defaults/memory/v3/injector.ts +212 -178
  228. package/src/plugins/defaults/memory/v3/orchestrate.ts +112 -25
  229. package/src/plugins/defaults/memory/v3/plugin-schema.ts +54 -4
  230. package/src/plugins/defaults/memory/v3/pool-log-store.ts +253 -1
  231. package/src/plugins/defaults/memory/v3/pool-select.test.ts +35 -2
  232. package/src/plugins/defaults/memory/v3/pool-select.ts +31 -19
  233. package/src/plugins/defaults/memory/v3/sections.ts +2 -1
  234. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +41 -14
  235. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +4 -1
  236. package/src/plugins/defaults/tool-result-truncate/terminal.ts +1 -46
  237. package/src/prompts/__tests__/parallel-tasks-section.test.ts +25 -0
  238. package/src/prompts/delegation-gate.ts +57 -0
  239. package/src/prompts/system-prompt.ts +14 -32
  240. package/src/providers/inference/adapter-factory.ts +6 -0
  241. package/src/providers/jev/client.test.ts +260 -0
  242. package/src/providers/jev/client.ts +518 -0
  243. package/src/providers/model-catalog.ts +58 -3
  244. package/src/providers/server-tool-pairing.ts +16 -7
  245. package/src/runtime/AGENTS.md +2 -2
  246. package/src/runtime/__tests__/agent-wake.test.ts +94 -1
  247. package/src/runtime/agent-wake.ts +31 -4
  248. package/src/runtime/guardian-action-service.ts +2 -17
  249. package/src/runtime/guardian-reply-router.ts +1 -8
  250. package/src/runtime/http-server.ts +2 -2
  251. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +89 -0
  252. package/src/runtime/migrations/vbundle-import-policy.ts +33 -11
  253. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +24 -2
  254. package/src/runtime/routes/channel-route-shared.ts +1 -9
  255. package/src/runtime/routes/conversation-routes.ts +29 -4
  256. package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
  257. package/src/runtime/routes/desktop-setup-routes.ts +8 -5
  258. package/src/runtime/routes/guardian-approval-interception.ts +24 -0
  259. package/src/runtime/routes/inbound-message-handler.ts +2 -3
  260. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
  261. package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -4
  262. package/src/runtime/routes/inference-profile-session-handler.ts +11 -1
  263. package/src/runtime/routes/inference-profiles-routes.ts +9 -0
  264. package/src/runtime/routes/oauth-commands-routes.ts +57 -54
  265. package/src/runtime/routes/oauth-providers.ts +4 -0
  266. package/src/runtime/routes/oauth-request-hints.test.ts +246 -0
  267. package/src/runtime/routes/oauth-request-hints.ts +149 -0
  268. package/src/runtime/routes/schedule-routes.ts +2 -0
  269. package/src/runtime/routes/secret-routes.ts +99 -119
  270. package/src/schedule/__tests__/worker-mcp-bootstrap.test.ts +107 -0
  271. package/src/schedule/__tests__/worker-mcp-readiness.test.ts +173 -0
  272. package/src/schedule/__tests__/worker-mcp-tools.test.ts +13 -47
  273. package/src/schedule/run-script.ts +2 -2
  274. package/src/schedule/scheduler.ts +47 -11
  275. package/src/schedule/tool-surface-readiness.ts +65 -0
  276. package/src/schedule/worker-mcp.ts +85 -0
  277. package/src/schedule/worker.ts +9 -31
  278. package/src/security/secure-keys.ts +60 -48
  279. package/src/tools/ask-question/ask-question-tool.test.ts +167 -5
  280. package/src/tools/ask-question/ask-question-tool.ts +119 -23
  281. package/src/tools/browser/browser-execution.ts +7 -1
  282. package/src/tools/host-terminal/host-shell.ts +12 -6
  283. package/src/tools/shared/filesystem/file-ops-service.ts +1 -31
  284. package/src/tools/shared/shell-output.test.ts +10 -0
  285. package/src/tools/shared/shell-output.ts +14 -2
  286. package/src/tools/skills/sandbox-runner.ts +13 -2
  287. package/src/tools/skills/scaffold-managed.ts +2 -1
  288. package/src/tools/terminal/__tests__/safe-env.test.ts +33 -4
  289. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +0 -14
  290. package/src/tools/terminal/safe-env.ts +35 -24
  291. package/src/tools/terminal/sanitized-bash.ts +15 -2
  292. package/src/tools/terminal/shell-launch.test.ts +162 -0
  293. package/src/tools/terminal/shell.test.ts +29 -0
  294. package/src/tools/terminal/shell.ts +13 -7
  295. package/src/util/browser-human-verification.ts +14 -0
  296. package/src/util/host-process.test.ts +17 -1
  297. package/src/util/host-process.ts +24 -0
  298. package/src/util/unicode.ts +29 -0
@@ -213,6 +213,7 @@ const scheduleSchema = z.object({
213
213
  mode: z.enum(["notify", "execute", "script", "wake", "workflow"]),
214
214
  status: z.enum(["active", "firing", "fired", "cancelled"]),
215
215
  routingIntent: z.enum(["single_channel", "multi_channel", "all_channels"]),
216
+ quiet: z.boolean(),
216
217
  reuseConversation: z.boolean(),
217
218
  wakeConversationId: z.string().nullable(),
218
219
  workflowName: z.string().nullable(),
@@ -381,6 +382,7 @@ function serializeSchedule(
381
382
  mode: j.mode,
382
383
  status: j.status,
383
384
  routingIntent: j.routingIntent,
385
+ quiet: j.quiet,
384
386
  reuseConversation: j.reuseConversation,
385
387
  wakeConversationId: j.wakeConversationId,
386
388
  workflowName: j.workflowName,
@@ -16,10 +16,7 @@ import {
16
16
  } from "../../acp/acp-credentials.js";
17
17
  import {
18
18
  getPlatformAssistantId,
19
- setPlatformAssistantId,
20
19
  setPlatformBaseUrl,
21
- setPlatformOrganizationId,
22
- setPlatformUserId,
23
20
  } from "../../config/env.js";
24
21
  import { getConfig } from "../../config/loader.js";
25
22
  import { maybeDefaultSpeechToManaged } from "../../config/managed-speech-defaults.js";
@@ -41,6 +38,7 @@ import {
41
38
  refreshProvidersAfterSecretChange,
42
39
  refreshProvidersForRotatedCredential,
43
40
  } from "../../providers/inference/credential-rotation.js";
41
+ import { validateJevApiKey } from "../../providers/jev/client.js";
44
42
  import { validateMinimaxApiKey } from "../../providers/minimax/client.js";
45
43
  import { validateOpenAIApiKey } from "../../providers/openai/client.js";
46
44
  import { validatePoolsideApiKey } from "../../providers/poolside/client.js";
@@ -79,6 +77,48 @@ const log = getLogger("runtime-http");
79
77
  const CES_READY_POLL_INTERVAL_MS = 500;
80
78
  const CES_READY_POLL_TIMEOUT_MS = 30_000;
81
79
 
80
+ const WRITABLE_VELLUM_CREDENTIAL_FIELDS = [
81
+ "assistant_api_key",
82
+ "webhook_secret",
83
+ "platform_base_url",
84
+ ] as const;
85
+
86
+ function isWritableVellumCredentialField(field: string): boolean {
87
+ return (WRITABLE_VELLUM_CREDENTIAL_FIELDS as readonly string[]).includes(
88
+ field,
89
+ );
90
+ }
91
+
92
+ async function persistCredential(
93
+ service: string,
94
+ field: string,
95
+ value: string,
96
+ ): Promise<void> {
97
+ const key = credentialKey(service, field);
98
+ const stored = await setSecureKeyAsync(key, value);
99
+ if (!stored) {
100
+ throw new InternalError(
101
+ `Failed to store credential in secure storage (backend: ${getActiveBackendName()})`,
102
+ );
103
+ }
104
+ if (!isNonSecretPlatformField(service, field)) {
105
+ try {
106
+ const scrubbed = await scrubStoredCredentialFromTranscripts(value);
107
+ log.info(
108
+ { service, field, ...scrubbed },
109
+ "Credential stored; scrubbed value from recent transcripts",
110
+ );
111
+ } catch (err) {
112
+ log.warn(
113
+ { err, service, field },
114
+ "Credential stored, but transcript scrub failed",
115
+ );
116
+ }
117
+ }
118
+ upsertCredentialMetadata(service, field, {});
119
+ await syncManualTokenConnection(service);
120
+ }
121
+
82
122
  let apiKeyGeneration = 0;
83
123
 
84
124
  async function queueApiKeyPropagation(
@@ -263,6 +303,15 @@ async function handleAddSecret({ body }: RouteHandlerArgs) {
263
303
  );
264
304
  return { success: false, error: validation.reason };
265
305
  }
306
+ } else if (name === "jev") {
307
+ const validation = await validateJevApiKey(value);
308
+ if (!validation.valid) {
309
+ log.warn(
310
+ { provider: name, reason: validation.reason },
311
+ "API key validation failed",
312
+ );
313
+ return { success: false, error: validation.reason };
314
+ }
266
315
  }
267
316
 
268
317
  const stored = await setSecureKeyAsync(
@@ -307,10 +356,50 @@ async function handleAddSecret({ body }: RouteHandlerArgs) {
307
356
  'For credential type, name must be in "service:field" format (e.g. "github:api_token")',
308
357
  );
309
358
  }
310
- assertMetadataWritable();
311
359
  const service = name.slice(0, colonIdx);
312
360
  const field = name.slice(colonIdx + 1);
313
361
 
362
+ if (service === "vellum") {
363
+ if (!isWritableVellumCredentialField(field)) {
364
+ throw new BadRequestError(
365
+ `Unknown vellum credential: ${field}. Allowed fields: ${WRITABLE_VELLUM_CREDENTIAL_FIELDS.join(", ")}`,
366
+ );
367
+ }
368
+ assertMetadataWritable();
369
+ await persistCredential(service, field, value);
370
+ if (field === "platform_base_url") {
371
+ setPlatformBaseUrl(value);
372
+ }
373
+ if (isPlatformManagedCredential(service, field)) {
374
+ await refreshProvidersAfterSecretChange();
375
+ // Close the first-boot race where the startup capability seed ran before
376
+ // the managed embedding credential was provisioned, leaving skill/CLI
377
+ // pages unseeded until restart. Detached, must not block the response.
378
+ void maybeReseedCapabilitiesAfterManagedCredential(getConfig());
379
+ if (field === "assistant_api_key") {
380
+ await notifyCesOfAssistantApiKeyUpdate(value, getCesClient());
381
+ }
382
+ } else {
383
+ await refreshProvidersForRotatedCredential(service, field);
384
+ }
385
+ if (field === "assistant_api_key" || field === "platform_base_url") {
386
+ // Managed-speech availability needs the API key, the assistant ID,
387
+ // and the base URL. Fire on each write that completes a piece of
388
+ // that set; the hook no-ops until the connection is complete.
389
+ // Detached, must not block the response.
390
+ void maybeDefaultSpeechToManaged();
391
+ // Same last-write-wins shape: the startup syncs no-op before live
392
+ // registration, so re-enqueue them here. Both dedup and no-op until
393
+ // the client and assistant id exist.
394
+ syncWorkspaceIdentityToPlatform();
395
+ syncAvatarToPlatform();
396
+ }
397
+ log.info({ service, field }, "Credential added via HTTP");
398
+ return { success: true, type, name };
399
+ }
400
+
401
+ assertMetadataWritable();
402
+
314
403
  // Reject an Anthropic API key pasted into the ACP OAuth-token field (a 401
315
404
  // footgun) as a 400 rather than letting it persist and fail at runtime.
316
405
  if (service === ACP_SERVICE) {
@@ -324,104 +413,8 @@ async function handleAddSecret({ body }: RouteHandlerArgs) {
324
413
  }
325
414
  }
326
415
 
327
- const key = credentialKey(service, field);
328
-
329
- const TRIMMED_IDENTITY_FIELDS = new Set([
330
- "platform_assistant_id",
331
- "platform_organization_id",
332
- "platform_user_id",
333
- ]);
334
- const isTrimmedIdentity =
335
- service === "vellum" && TRIMMED_IDENTITY_FIELDS.has(field);
336
- const effectiveValue = isTrimmedIdentity ? value.trim() : value;
337
-
338
- if (isTrimmedIdentity && effectiveValue === "") {
339
- const deleteResult = await deleteSecureKeyAsync(key);
340
- if (deleteResult === "error") {
341
- throw new InternalError(
342
- `Failed to delete stale credential from secure storage: ${service}:${field}`,
343
- );
344
- }
345
- if (field === "platform_assistant_id") {
346
- setPlatformAssistantId(undefined);
347
- } else if (field === "platform_organization_id") {
348
- setPlatformOrganizationId(undefined);
349
- } else if (field === "platform_user_id") {
350
- setPlatformUserId(undefined);
351
- }
352
- deleteCredentialMetadata(service, field);
353
- } else {
354
- const stored = await setSecureKeyAsync(key, effectiveValue);
355
- if (!stored) {
356
- throw new InternalError(
357
- `Failed to store credential in secure storage (backend: ${getActiveBackendName()})`,
358
- );
359
- }
360
- if (!isNonSecretPlatformField(service, field)) {
361
- // Same seam as the api_key branch: the scrub runs immediately after
362
- // the secure-store write, before side effects that can throw. The
363
- // value IS stored at this point; the scrub is best-effort hygiene
364
- // and must stay invisible to the caller. Counts only — never the
365
- // value.
366
- try {
367
- const scrubbed =
368
- await scrubStoredCredentialFromTranscripts(effectiveValue);
369
- log.info(
370
- { service, field, ...scrubbed },
371
- "Credential stored; scrubbed value from recent transcripts",
372
- );
373
- } catch (err) {
374
- log.warn(
375
- { err, service, field },
376
- "Credential stored, but transcript scrub failed",
377
- );
378
- }
379
- }
380
- upsertCredentialMetadata(service, field, {});
381
- await syncManualTokenConnection(service);
382
- if (service === "vellum" && field === "platform_base_url") {
383
- setPlatformBaseUrl(effectiveValue);
384
- }
385
- if (service === "vellum" && field === "platform_assistant_id") {
386
- setPlatformAssistantId(effectiveValue || undefined);
387
- }
388
- if (service === "vellum" && field === "platform_organization_id") {
389
- setPlatformOrganizationId(effectiveValue || undefined);
390
- }
391
- if (service === "vellum" && field === "platform_user_id") {
392
- setPlatformUserId(effectiveValue || undefined);
393
- }
394
- }
395
- if (isPlatformManagedCredential(service, field)) {
396
- await refreshProvidersAfterSecretChange();
397
- // Close the first-boot race where the startup capability seed ran before
398
- // the managed embedding credential was provisioned, leaving skill/CLI
399
- // pages unseeded until restart. Detached — must not block the response.
400
- void maybeReseedCapabilitiesAfterManagedCredential(getConfig());
401
- if (service === "vellum" && field === "assistant_api_key") {
402
- await notifyCesOfAssistantApiKeyUpdate(value, getCesClient());
403
- }
404
- } else if (!isTrimmedIdentity) {
405
- await refreshProvidersForRotatedCredential(service, field);
406
- }
407
- if (
408
- service === "vellum" &&
409
- (field === "assistant_api_key" ||
410
- field === "platform_assistant_id" ||
411
- field === "platform_base_url")
412
- ) {
413
- // Managed-speech availability needs the API key, the assistant ID,
414
- // and the base URL, and the CLI connect path stores all three
415
- // concurrently — fire on each so the last write to land triggers the
416
- // defaulting; the hook no-ops until the connection is complete.
417
- // Detached — must not block the response.
418
- void maybeDefaultSpeechToManaged();
419
- // Same last-write-wins shape: the startup syncs no-op before live
420
- // registration, so re-enqueue them here. Both dedup and no-op until
421
- // the client and assistant id exist.
422
- syncWorkspaceIdentityToPlatform();
423
- syncAvatarToPlatform();
424
- }
416
+ await persistCredential(service, field, value);
417
+ await refreshProvidersForRotatedCredential(service, field);
425
418
  log.info({ service, field }, "Credential added via HTTP");
426
419
  return { success: true, type, name };
427
420
  }
@@ -464,8 +457,7 @@ async function handleReadSecret({ body }: RouteHandlerArgs) {
464
457
  try {
465
458
  let accountKey: string;
466
459
  let prefetchedResult:
467
- | Awaited<ReturnType<typeof getSecureKeyResultAsync>>
468
- | undefined;
460
+ Awaited<ReturnType<typeof getSecureKeyResultAsync>> | undefined;
469
461
 
470
462
  if (type === "api_key") {
471
463
  if (
@@ -609,6 +601,9 @@ async function handleDeleteSecret({ body }: RouteHandlerArgs) {
609
601
  );
610
602
  }
611
603
  const service = name.slice(0, colonIdx);
604
+ if (service === "vellum") {
605
+ throw new BadRequestError("Vellum credentials cannot be deleted");
606
+ }
612
607
  const field = name.slice(colonIdx + 1);
613
608
  assertMetadataWritable();
614
609
  const key = credentialKey(service, field);
@@ -624,21 +619,6 @@ async function handleDeleteSecret({ body }: RouteHandlerArgs) {
624
619
  );
625
620
  }
626
621
  deleteCredentialMetadata(service, field);
627
- if (service === "vellum" && field === "platform_base_url") {
628
- setPlatformBaseUrl(undefined);
629
- }
630
- if (service === "vellum" && field === "platform_assistant_id") {
631
- setPlatformAssistantId(undefined);
632
- }
633
- if (service === "vellum" && field === "platform_organization_id") {
634
- setPlatformOrganizationId(undefined);
635
- }
636
- if (service === "vellum" && field === "platform_user_id") {
637
- setPlatformUserId(undefined);
638
- }
639
- if (isPlatformManagedCredential(service, field)) {
640
- await refreshProvidersAfterSecretChange();
641
- }
642
622
  invalidateConnectionsAfterCredentialDelete(affectedConnections);
643
623
  log.info({ service, field }, "Credential deleted via HTTP");
644
624
  return { success: true, type, name };
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Timing coverage for the schedule-worker MCP bootstrap and reload helpers.
3
+ *
4
+ * The worker entrypoint cannot be imported (it calls process.exit). These
5
+ * tests drive the extracted helpers with a delayed start so a due schedule
6
+ * cannot observe a ready surface before registration settles.
7
+ */
8
+
9
+ import { afterEach, describe, expect, test } from "bun:test";
10
+
11
+ import { EMPTY_MCP_STARTUP_SNAPSHOT } from "../../mcp/tool-caps.js";
12
+ import {
13
+ getScheduleToolSurfaceState,
14
+ isScheduleToolSurfaceReady,
15
+ resetScheduleToolSurfaceReadinessForTests,
16
+ } from "../tool-surface-readiness.js";
17
+ import {
18
+ bootstrapScheduleWorkerMcp,
19
+ reloadScheduleWorkerMcp,
20
+ } from "../worker-mcp.js";
21
+
22
+ function deferred<T>(): {
23
+ promise: Promise<T>;
24
+ resolve: (value: T) => void;
25
+ } {
26
+ let resolve!: (value: T) => void;
27
+ const promise = new Promise<T>((res) => {
28
+ resolve = res;
29
+ });
30
+ return { promise, resolve };
31
+ }
32
+
33
+ afterEach(() => {
34
+ resetScheduleToolSurfaceReadinessForTests();
35
+ });
36
+
37
+ describe("bootstrapScheduleWorkerMcp", () => {
38
+ test("stays unready past the grace period until start settles", async () => {
39
+ const gate = deferred<typeof EMPTY_MCP_STARTUP_SNAPSHOT>();
40
+ const snapshot = {
41
+ ...EMPTY_MCP_STARTUP_SNAPSHOT,
42
+ configuredServerCount: 8,
43
+ connectedServerCount: 8,
44
+ registeredToolCount: 3,
45
+ };
46
+
47
+ const boot = bootstrapScheduleWorkerMcp({
48
+ start: () => gate.promise,
49
+ graceMs: 20,
50
+ });
51
+ await boot;
52
+
53
+ expect(isScheduleToolSurfaceReady()).toBe(false);
54
+ expect(getScheduleToolSurfaceState()).toBe("starting");
55
+
56
+ gate.resolve(snapshot);
57
+ await new Promise((resolve) => setTimeout(resolve, 0));
58
+
59
+ expect(isScheduleToolSurfaceReady()).toBe(true);
60
+ expect(getScheduleToolSurfaceState()).toBe("ready");
61
+ });
62
+
63
+ test("marks ready when start finishes before the grace deadline", async () => {
64
+ await bootstrapScheduleWorkerMcp({
65
+ start: async () => ({
66
+ ...EMPTY_MCP_STARTUP_SNAPSHOT,
67
+ registeredToolCount: 2,
68
+ }),
69
+ graceMs: 50,
70
+ });
71
+
72
+ expect(isScheduleToolSurfaceReady()).toBe(true);
73
+ });
74
+ });
75
+
76
+ describe("reloadScheduleWorkerMcp", () => {
77
+ test("is not ready while reload is in flight", async () => {
78
+ const gate = deferred<typeof EMPTY_MCP_STARTUP_SNAPSHOT>();
79
+ const reloading = reloadScheduleWorkerMcp({
80
+ restart: () => gate.promise,
81
+ });
82
+
83
+ expect(getScheduleToolSurfaceState()).toBe("reloading");
84
+ expect(isScheduleToolSurfaceReady()).toBe(false);
85
+
86
+ gate.resolve({
87
+ ...EMPTY_MCP_STARTUP_SNAPSHOT,
88
+ registeredToolCount: 4,
89
+ });
90
+ await reloading;
91
+
92
+ expect(isScheduleToolSurfaceReady()).toBe(true);
93
+ });
94
+
95
+ test("failed reload does not stay in reloading", async () => {
96
+ await expect(
97
+ reloadScheduleWorkerMcp({
98
+ restart: async () => {
99
+ throw new Error("reconnect failed");
100
+ },
101
+ }),
102
+ ).rejects.toThrow("reconnect failed");
103
+
104
+ expect(getScheduleToolSurfaceState()).toBe("ready");
105
+ expect(isScheduleToolSurfaceReady()).toBe(true);
106
+ });
107
+ });
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Scheduler gate for tool-backed schedules while the MCP surface is not ready.
3
+ *
4
+ * A due execute schedule must not record success while worker MCP
5
+ * initialization is still incomplete. Once ready, the expected MCP tool
6
+ * has to be present in the registry the turn would use.
7
+ */
8
+
9
+ import { afterEach, describe, expect, mock, test } from "bun:test";
10
+
11
+ import { getDb } from "../../persistence/db-connection.js";
12
+ import { initializeDb } from "../../persistence/db-init.js";
13
+ import { RiskLevel } from "../../tools/tool-types.js";
14
+ import { createSchedule } from "../schedule-store.js";
15
+ import {
16
+ markScheduleToolSurfaceStarting,
17
+ resetScheduleToolSurfaceReadinessForTests,
18
+ } from "../tool-surface-readiness.js";
19
+
20
+ const processedPrompts: string[] = [];
21
+ mock.module("../../runtime/background-job-runner.js", () => ({
22
+ runBackgroundJob: async (opts: { prompt: string }) => {
23
+ const { getMcpToolDefinitions } = await import("../../tools/registry.js");
24
+ capturedMcpTools = getMcpToolDefinitions().map((tool) => tool.name);
25
+ processedPrompts.push(opts.prompt);
26
+ return { conversationId: "conv-xyz", ok: true };
27
+ },
28
+ }));
29
+
30
+ mock.module("../../daemon/process-message.js", () => ({
31
+ processMessage: async () => ({ messageId: "msg-xyz" }),
32
+ processMessageInBackground: async () => {},
33
+ }));
34
+
35
+ mock.module("../../background-wake/publisher.js", () => ({
36
+ refreshBackgroundWakeIntent: () => {},
37
+ }));
38
+
39
+ mock.module("../../daemon/disk-pressure-background-gate.js", () => ({
40
+ checkDiskPressureBackgroundGate: () => ({
41
+ action: "allow",
42
+ status: {
43
+ enabled: false,
44
+ state: "disabled",
45
+ locked: false,
46
+ acknowledged: false,
47
+ overrideActive: false,
48
+ effectivelyLocked: false,
49
+ lockId: null,
50
+ usagePercent: null,
51
+ thresholdPercent: 95,
52
+ path: null,
53
+ lastCheckedAt: null,
54
+ blockedCapabilities: [],
55
+ error: null,
56
+ },
57
+ }),
58
+ diskPressureBackgroundSkipLogFields: () => ({}),
59
+ shouldLogDiskPressureBackgroundSkip: () => false,
60
+ }));
61
+
62
+ const mockEmitNotificationSignal = mock(() => Promise.resolve());
63
+ mock.module("../../notifications/emit-signal.js", () => ({
64
+ emitNotificationSignal: mockEmitNotificationSignal,
65
+ }));
66
+
67
+ const { runDueSchedulesOnce } = await import("../scheduler.js");
68
+ const { __resetRegistryForTesting, getTool, registerMcpTools } =
69
+ await import("../../tools/registry.js");
70
+
71
+ await initializeDb();
72
+
73
+ let capturedMcpTools: string[] = [];
74
+
75
+ function rawDb(): import("bun:sqlite").Database {
76
+ return (getDb() as unknown as { $client: import("bun:sqlite").Database })
77
+ .$client;
78
+ }
79
+
80
+ afterEach(() => {
81
+ resetScheduleToolSurfaceReadinessForTests();
82
+ processedPrompts.length = 0;
83
+ capturedMcpTools = [];
84
+ mockEmitNotificationSignal.mockClear();
85
+ __resetRegistryForTesting();
86
+ const db = getDb();
87
+ db.run("DELETE FROM cron_runs");
88
+ db.run("DELETE FROM cron_jobs");
89
+ });
90
+
91
+ describe("automatic schedule MCP readiness", () => {
92
+ test("defers a due execute schedule while MCP is still starting", async () => {
93
+ markScheduleToolSurfaceStarting(8);
94
+ const job = await createSchedule({
95
+ name: "Friday tagging",
96
+ cronExpression: "* * * * *",
97
+ message: "Tag new Reader documents",
98
+ mode: "execute",
99
+ });
100
+ rawDb().run("UPDATE cron_jobs SET next_run_at = ?", [Date.now() - 1000]);
101
+
102
+ const result = await runDueSchedulesOnce();
103
+
104
+ expect(result.claimed).toBe(1);
105
+ expect(result.skipped).toBe(1);
106
+ expect(result.completed).toBe(0);
107
+ expect(processedPrompts).toEqual([]);
108
+ const runs = rawDb()
109
+ .query("SELECT status FROM cron_runs WHERE job_id = ?")
110
+ .all(job.id) as Array<{ status: string }>;
111
+ expect(runs).toEqual([]);
112
+ const row = rawDb()
113
+ .query("SELECT status, enabled FROM cron_jobs WHERE id = ?")
114
+ .get(job.id) as { status: string; enabled: number };
115
+ expect(row.status).toBe("active");
116
+ expect(row.enabled).toBe(1);
117
+ });
118
+
119
+ test("still fires notify and script schedules while MCP is starting", async () => {
120
+ markScheduleToolSurfaceStarting(8);
121
+ await createSchedule({
122
+ name: "Ping",
123
+ cronExpression: "* * * * *",
124
+ message: "ping",
125
+ mode: "notify",
126
+ });
127
+ await createSchedule({
128
+ name: "Script",
129
+ cronExpression: "* * * * *",
130
+ message: "run script",
131
+ mode: "script",
132
+ script: "true",
133
+ });
134
+ rawDb().run("UPDATE cron_jobs SET next_run_at = ?", [Date.now() - 1000]);
135
+
136
+ const result = await runDueSchedulesOnce();
137
+
138
+ expect(result.completed).toBe(2);
139
+ expect(mockEmitNotificationSignal).toHaveBeenCalledTimes(1);
140
+ });
141
+
142
+ test("an automatic execute run sees the registered MCP tool once ready", async () => {
143
+ registerMcpTools("reader", [
144
+ {
145
+ name: "mcp__reader__list_documents",
146
+ description: "List documents",
147
+ defaultRiskLevel: RiskLevel.Low,
148
+ executionTarget: "sandbox",
149
+ input_schema: { type: "object", properties: {}, required: [] },
150
+ category: "",
151
+ async execute() {
152
+ return { content: "ok", isError: false };
153
+ },
154
+ },
155
+ ]);
156
+ resetScheduleToolSurfaceReadinessForTests();
157
+
158
+ await createSchedule({
159
+ name: "Saturday digest",
160
+ cronExpression: "* * * * *",
161
+ message: "Build the Reader digest",
162
+ mode: "execute",
163
+ });
164
+ rawDb().run("UPDATE cron_jobs SET next_run_at = ?", [Date.now() - 1000]);
165
+
166
+ const result = await runDueSchedulesOnce();
167
+
168
+ expect(result.completed).toBe(1);
169
+ expect(processedPrompts).toEqual(["Build the Reader digest"]);
170
+ expect(capturedMcpTools).toContain("mcp__reader__list_documents");
171
+ expect(getTool("mcp__reader__list_documents")).toBeDefined();
172
+ });
173
+ });
@@ -1,38 +1,19 @@
1
1
  /**
2
2
  * Guard over the schedule worker entrypoint's MCP bootstrap.
3
3
  *
4
- * The tool registry is process-local and MCP tools reach it only by connecting
5
- * to each server and listing what it offers. `initializeTools()` does not do
6
- * that: it loads core built-ins and workspace tools from disk. So a worker
7
- * that stops connecting fails every `mcp__*` call an execute-mode schedule
8
- * makes as "Unknown tool", while `assistant tools list` reads the daemon's
9
- * registry over IPC and reports the same tool as registered. The mismatch is
10
- * what makes the failure hard to place, so it is worth a guard.
11
- *
12
- * The worker is also long-lived, so the set it connects at startup goes stale
13
- * as soon as the config moves. It watches for the daemon's reload signal to
14
- * rebuild, which is covered here too.
15
- *
16
- * The entrypoint installs signal handlers and calls `process.exit`, so it
17
- * cannot be imported into a test process; these assertions read its source the
18
- * way `worker-feature-flags.test.ts` does. The behavior of the steps
19
- * themselves is covered in `src/mcp/__tests__/startup.test.ts` and
20
- * `src/mcp/__tests__/reload-signal.test.ts`.
4
+ * Behavioral coverage lives in worker-mcp-readiness.test.ts. This file only
5
+ * asserts the process entrypoint still calls the extracted helpers before
6
+ * the first tick, because the entrypoint installs signal handlers and
7
+ * calls process.exit and cannot be imported into a test process.
21
8
  */
22
9
 
23
10
  import { readFileSync } from "node:fs";
24
11
  import { join } from "node:path";
25
12
  import { describe, expect, test } from "bun:test";
26
13
 
27
- const MCP_CALL = "startConfiguredMcpServers(";
28
-
29
- /** The bounded wait that keeps unreachable servers off the critical path. */
30
- const MCP_GRACE = "MCP_STARTUP_GRACE_MS";
31
-
32
- /** The reload watcher that keeps the connected set current. */
14
+ const MCP_BOOTSTRAP = "bootstrapScheduleWorkerMcp(";
15
+ const MCP_RELOAD = "reloadScheduleWorkerMcp(";
33
16
  const MCP_WATCH = "watchForMcpReload(";
34
-
35
- /** The worker's first call that can execute a schedule. */
36
17
  const WORK_START = "void tick()";
37
18
 
38
19
  function readWorkerSource(): string {
@@ -40,37 +21,22 @@ function readWorkerSource(): string {
40
21
  }
41
22
 
42
23
  describe("schedule worker MCP tools", () => {
43
- test("connects its own MCP servers before the first schedule tick", () => {
24
+ test("bootstraps its own MCP servers before the first schedule tick", () => {
44
25
  const source = readWorkerSource();
45
- const mcpAt = source.indexOf(MCP_CALL);
26
+ const mcpAt = source.indexOf(MCP_BOOTSTRAP);
46
27
  const workAt = source.indexOf(WORK_START);
47
28
 
48
29
  expect(
49
30
  mcpAt,
50
- `The schedule worker must call \`${MCP_CALL})\` at startup. This ` +
31
+ `The schedule worker must call \`${MCP_BOOTSTRAP})\` at startup. This ` +
51
32
  "process hosts execute-mode schedule turns, and its tool registry is " +
52
33
  "its own: without the call it has no mcp__* tools at all.",
53
34
  ).toBeGreaterThanOrEqual(0);
54
35
  expect(workAt).toBeGreaterThanOrEqual(0);
55
36
  expect(
56
37
  mcpAt < workAt,
57
- "The connect starts and is waited on before the first tick, so a " +
58
- "schedule that fires immediately runs with the tools it was written " +
59
- "against. The wait is bounded (see below), so a server that never " +
60
- "answers delays it rather than blocking it.",
61
- ).toBe(true);
62
- });
63
-
64
- test("bounds the startup wait so unrelated schedules still fire", () => {
65
- const source = readWorkerSource();
66
-
67
- expect(
68
- source.includes(MCP_GRACE),
69
- "`McpServerManager.start()` walks servers one at a time and allows " +
70
- "each 30s to connect and 30s more to list its tools. An unbounded " +
71
- "wait lets a few unreachable servers hold every due schedule, " +
72
- "including the notify and script ones that never touch MCP, while " +
73
- "the PID file already reports this worker as running.",
38
+ "The connect starts and is waited on before the first tick. Tool-backed " +
39
+ "schedules stay deferred until that surface is marked ready.",
74
40
  ).toBe(true);
75
41
  });
76
42
 
@@ -78,11 +44,11 @@ describe("schedule worker MCP tools", () => {
78
44
  const source = readWorkerSource();
79
45
 
80
46
  expect(
81
- source.includes(MCP_WATCH),
47
+ source.includes(MCP_WATCH) && source.includes(MCP_RELOAD),
82
48
  "This process is long-lived and holds its own connections. Without " +
83
49
  "the watcher, a server disabled or removed in the config stays " +
84
50
  "registered and callable by background schedules until the worker " +
85
- "restarts.",
51
+ "restarts. Reload must mark the tool surface unready until it settles.",
86
52
  ).toBe(true);
87
53
  });
88
54
  });