@vellumai/assistant 0.11.2 → 0.11.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 (709) hide show
  1. package/ARCHITECTURE.md +58 -3
  2. package/docs/trusted-contact-access.md +9 -5
  3. package/docs/vellum-doctor.md +195 -0
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +39 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +10 -0
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
  7. package/node_modules/@vellumai/ces-client/src/index.ts +1 -0
  8. package/node_modules/@vellumai/ces-client/src/rpc-client.ts +19 -2
  9. package/node_modules/@vellumai/environments/package.json +2 -1
  10. package/node_modules/@vellumai/environments/src/__tests__/cloud-assistant-hub-url.test.ts +38 -0
  11. package/node_modules/@vellumai/environments/src/__tests__/install-layout.test.ts +98 -0
  12. package/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +5 -5
  13. package/node_modules/@vellumai/environments/src/index.ts +17 -6
  14. package/node_modules/@vellumai/environments/src/install-layout.ts +49 -0
  15. package/node_modules/@vellumai/environments/src/seeds.ts +29 -0
  16. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +39 -0
  17. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +10 -0
  18. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
  19. package/node_modules/@vellumai/gateway-client/src/index.ts +4 -0
  20. package/node_modules/@vellumai/gateway-client/src/verification-session-contract.ts +91 -6
  21. package/node_modules/@vellumai/ipc-server-utils/src/endpoint.test.ts +36 -0
  22. package/node_modules/@vellumai/ipc-server-utils/src/endpoint.ts +142 -0
  23. package/node_modules/@vellumai/ipc-server-utils/src/index.ts +2 -0
  24. package/node_modules/@vellumai/ipc-server-utils/src/listen-options.ts +3 -0
  25. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +15 -1
  26. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +17 -2
  27. package/node_modules/@vellumai/service-contracts/src/channels.ts +39 -0
  28. package/node_modules/@vellumai/service-contracts/src/ingress.ts +10 -0
  29. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
  30. package/node_modules/@vellumai/slack-text/src/index.ts +16 -2
  31. package/openapi.yaml +589 -30
  32. package/package.json +1 -1
  33. package/src/__tests__/agent-image-optimize.test.ts +4 -4
  34. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +113 -66
  35. package/src/__tests__/agent-loop-override-profile.test.ts +38 -48
  36. package/src/__tests__/agent-loop-subagent-billing-headers.test.ts +166 -0
  37. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +1 -0
  38. package/src/__tests__/agent-wake-override-profile.test.ts +1 -0
  39. package/src/__tests__/answered-question-persistence.test.ts +240 -0
  40. package/src/__tests__/anthropic-provider.test.ts +165 -30
  41. package/src/__tests__/app-control-flow.test.ts +9 -8
  42. package/src/__tests__/assistant-event-hub.test.ts +48 -0
  43. package/src/__tests__/attachments-store-heic-normalize.test.ts +16 -12
  44. package/src/__tests__/attachments-store.test.ts +130 -68
  45. package/src/__tests__/attachments.test.ts +36 -36
  46. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  47. package/src/__tests__/call-start-guardian-guard.test.ts +18 -4
  48. package/src/__tests__/canned-reply-release.test.ts +86 -0
  49. package/src/__tests__/channel-guardian.test.ts +59 -29
  50. package/src/__tests__/channel-readiness-telegram-remote.test.ts +154 -0
  51. package/src/__tests__/channel-setup-panel-ack.test.ts +6 -5
  52. package/src/__tests__/client-os-metadata-persistence.test.ts +103 -4
  53. package/src/__tests__/compactor-image-manifest-trust.test.ts +1 -1
  54. package/src/__tests__/consult-deadline.test.ts +65 -0
  55. package/src/__tests__/conversation-agent-loop.test.ts +438 -2
  56. package/src/__tests__/conversation-disk-view-integration.test.ts +5 -1
  57. package/src/__tests__/conversation-disk-view.test.ts +4 -4
  58. package/src/__tests__/conversation-error.test.ts +22 -3
  59. package/src/__tests__/conversation-fork-crud.test.ts +5 -1
  60. package/src/__tests__/conversation-fork-referential.test.ts +352 -0
  61. package/src/__tests__/conversation-fork-retrospective.test.ts +5 -1
  62. package/src/__tests__/conversation-group-tools.test.ts +32 -2
  63. package/src/__tests__/conversation-inference-profile-route.test.ts +27 -1
  64. package/src/__tests__/conversation-initial-prompt.test.ts +51 -22
  65. package/src/__tests__/conversation-key-store-bootstrap-cleanup.test.ts +119 -0
  66. package/src/__tests__/conversation-key-store-origin.test.ts +105 -0
  67. package/src/__tests__/conversation-list-recency-ordering.test.ts +104 -0
  68. package/src/__tests__/conversation-origin-at-creation.test.ts +153 -0
  69. package/src/__tests__/conversation-origin-channel-filter.test.ts +95 -0
  70. package/src/__tests__/conversation-placement-promotion.test.ts +172 -0
  71. package/src/__tests__/conversation-queue.test.ts +265 -1
  72. package/src/__tests__/conversation-retry-route.test.ts +3 -1
  73. package/src/__tests__/conversation-store-ephemeral.test.ts +4 -0
  74. package/src/__tests__/conversation-store.test.ts +26 -10
  75. package/src/__tests__/conversation-summarize-route.test.ts +1 -1
  76. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +15 -6
  77. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -5
  78. package/src/__tests__/conversation-surfaces-app-control.test.ts +12 -11
  79. package/src/__tests__/conversation-surfaces-app-open.test.ts +11 -5
  80. package/src/__tests__/conversation-surfaces-data-persist.test.ts +5 -4
  81. package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +967 -0
  82. package/src/__tests__/conversation-surfaces-persisted-info.test.ts +312 -0
  83. package/src/__tests__/conversation-surfaces-queued-emit.test.ts +6 -5
  84. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +12 -8
  85. package/src/__tests__/conversation-surfaces-standalone.test.ts +21 -11
  86. package/src/__tests__/conversation-surfaces-state-update.test.ts +7 -11
  87. package/src/__tests__/conversation-surfaces-table-action.test.ts +7 -6
  88. package/src/__tests__/conversation-surfaces-task-progress.test.ts +11 -6
  89. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +9 -6
  90. package/src/__tests__/conversation-tool-setup-attribution.test.ts +70 -7
  91. package/src/__tests__/conversation-wait-for-idle.test.ts +6 -11
  92. package/src/__tests__/credential-prompt-route.test.ts +57 -7
  93. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  94. package/src/__tests__/cu-unified-flow.test.ts +89 -28
  95. package/src/__tests__/custom-group-listing-visibility.test.ts +1 -1
  96. package/src/__tests__/db-schedule-syntax-migration.test.ts +4 -1
  97. package/src/__tests__/discord-access-request-privacy.test.ts +366 -0
  98. package/src/__tests__/discord-requester-notice-privacy.test.ts +87 -0
  99. package/src/__tests__/disk-pressure-guard.test.ts +55 -55
  100. package/src/__tests__/disk-pressure-routes.test.ts +10 -10
  101. package/src/__tests__/disk-pressure-tools.test.ts +2 -2
  102. package/src/__tests__/disk-usage.test.ts +35 -20
  103. package/src/__tests__/document-sync-tags.test.ts +379 -0
  104. package/src/__tests__/document-update-default-surface.test.ts +28 -0
  105. package/src/__tests__/dynamic-skill-background-guard.test.ts +2 -2
  106. package/src/__tests__/emit-signal-routing-intent.test.ts +3 -4
  107. package/src/__tests__/ensure-conversation-exists.test.ts +8 -5
  108. package/src/__tests__/events-dev-bypass-actor.test.ts +183 -1
  109. package/src/__tests__/evict-conversations-for-reload.test.ts +90 -0
  110. package/src/__tests__/file-list-tool.test.ts +12 -10
  111. package/src/__tests__/file-ops-service.test.ts +151 -44
  112. package/src/__tests__/file-read-container-boundary.test.ts +137 -0
  113. package/src/__tests__/file-read-tool.test.ts +21 -9
  114. package/src/__tests__/file-url-path-guard.test.ts +83 -0
  115. package/src/__tests__/filesystem-tools.test.ts +82 -66
  116. package/src/__tests__/guardian-outbound-http.test.ts +8 -8
  117. package/src/__tests__/helpers/gateway-verification-sessions-stub.ts +14 -1
  118. package/src/__tests__/helpers/hub-clients.ts +42 -0
  119. package/src/__tests__/helpers/mock-conversation.ts +46 -0
  120. package/src/__tests__/helpers/verification-sessions-ipc-sim.ts +61 -57
  121. package/src/__tests__/history-repair.test.ts +112 -12
  122. package/src/__tests__/host-bash-proxy.test.ts +10 -5
  123. package/src/__tests__/host-bash-routes.test.ts +67 -0
  124. package/src/__tests__/host-browser-proxy.test.ts +2 -2
  125. package/src/__tests__/host-cu-proxy.test.ts +3 -7
  126. package/src/__tests__/host-file-proxy.test.ts +3 -7
  127. package/src/__tests__/host-file-read-tool.test.ts +5 -1
  128. package/src/__tests__/host-transfer-proxy-targeted.test.ts +6 -4
  129. package/src/__tests__/http-conversation-lineage.test.ts +2 -5
  130. package/src/__tests__/http-user-message-parity.test.ts +44 -0
  131. package/src/__tests__/identity-routes.test.ts +13 -13
  132. package/src/__tests__/image-conversion.test.ts +29 -26
  133. package/src/__tests__/image-source-path-reinject.test.ts +2 -2
  134. package/src/__tests__/inference-profile-session-handler.test.ts +95 -0
  135. package/src/__tests__/inference-profile-session-ipc.test.ts +25 -0
  136. package/src/__tests__/introduction-card-resolver.test.ts +6 -1
  137. package/src/__tests__/list-messages-attachments.test.ts +56 -32
  138. package/src/__tests__/list-messages-background-tool-completion.test.ts +4 -4
  139. package/src/__tests__/list-messages-client-message-id.test.ts +2 -2
  140. package/src/__tests__/list-messages-hidden-metadata.test.ts +16 -16
  141. package/src/__tests__/list-messages-page-latest.test.ts +61 -61
  142. package/src/__tests__/list-messages-provider-error.test.ts +6 -6
  143. package/src/__tests__/list-messages-queued.test.ts +49 -6
  144. package/src/__tests__/list-messages-system-card.test.ts +4 -4
  145. package/src/__tests__/list-messages-tool-merge.test.ts +9 -9
  146. package/src/__tests__/llm-resolver-override-or-default.test.ts +141 -0
  147. package/src/__tests__/llm-usage-store.test.ts +55 -0
  148. package/src/__tests__/manual-token-reconciliation.test.ts +45 -0
  149. package/src/__tests__/mcp-tool-annotations-risk.test.ts +133 -0
  150. package/src/__tests__/media-stream-server-integration.test.ts +36 -0
  151. package/src/__tests__/migration-export-http.test.ts +1 -1
  152. package/src/__tests__/migration-import-commit-http.test.ts +1 -1
  153. package/src/__tests__/migration-import-preflight-http.test.ts +1 -1
  154. package/src/__tests__/migration-validate-http.test.ts +1 -1
  155. package/src/__tests__/mtime-cache.test.ts +65 -3
  156. package/src/__tests__/notification-platform-adapter.test.ts +104 -0
  157. package/src/__tests__/notification-schedule-notify-dedup.test.ts +1 -1
  158. package/src/__tests__/oauth-provider-profiles.test.ts +4 -2
  159. package/src/__tests__/oauth-provider-serializer.test.ts +17 -0
  160. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  161. package/src/__tests__/openai-responses-prompt-cache.test.ts +5 -2
  162. package/src/__tests__/path-policy.test.ts +164 -1
  163. package/src/__tests__/persist-media-references.test.ts +2 -2
  164. package/src/__tests__/persist-unsendable-image-downscale.test.ts +5 -5
  165. package/src/__tests__/persist-unsendable-image.test.ts +23 -23
  166. package/src/__tests__/persona-resolver.test.ts +154 -0
  167. package/src/__tests__/plugin-api-webhook-url.test.ts +170 -0
  168. package/src/__tests__/plugin-import-boundary-guard.test.ts +6 -0
  169. package/src/__tests__/processing-flag-persist-failure.test.ts +15 -10
  170. package/src/__tests__/profile-availability-incomplete.test.ts +113 -0
  171. package/src/__tests__/profiler-routes.test.ts +13 -13
  172. package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +593 -0
  173. package/src/__tests__/proxy-approval-callback.test.ts +3 -3
  174. package/src/__tests__/queued-message-cancel-and-steer.test.ts +189 -0
  175. package/src/__tests__/queued-message-steer-actor-scoping.test.ts +164 -0
  176. package/src/__tests__/resolve-trust-class.test.ts +20 -0
  177. package/src/__tests__/run-conversation-turn-persistence.test.ts +1 -1
  178. package/src/__tests__/run-due-schedules.test.ts +174 -0
  179. package/src/__tests__/runtime-attachment-metadata.test.ts +15 -7
  180. package/src/__tests__/scaffold-managed-skill-tool.test.ts +166 -0
  181. package/src/__tests__/schedule-routes.test.ts +730 -8
  182. package/src/__tests__/schedule-store.test.ts +540 -0
  183. package/src/__tests__/schedule-tools.test.ts +103 -0
  184. package/src/__tests__/scheduler-wake.test.ts +5 -0
  185. package/src/__tests__/secret-routes-platform-proxy.test.ts +29 -1
  186. package/src/__tests__/secure-keys.test.ts +91 -0
  187. package/src/__tests__/server-history-render.test.ts +97 -7
  188. package/src/__tests__/size-guard.test.ts +8 -8
  189. package/src/__tests__/skill-docs-sync-guard.test.ts +3 -2
  190. package/src/__tests__/skill-tool-factory.test.ts +247 -0
  191. package/src/__tests__/skills-install-staging.test.ts +15 -6
  192. package/src/__tests__/slack-edit-ordering-characterization.test.ts +103 -0
  193. package/src/__tests__/slack-inbound-verification.test.ts +4 -1
  194. package/src/__tests__/slack-mention-binding-invariance.test.ts +103 -0
  195. package/src/__tests__/slack-mention-provider-leak-audit.test.ts +135 -0
  196. package/src/__tests__/starter-task-flow.test.ts +5 -4
  197. package/src/__tests__/steer-on-enqueue-question.test.ts +3 -2
  198. package/src/__tests__/stt-language-catalog-parity.test.ts +38 -0
  199. package/src/__tests__/subagent-allowlist-validation.test.ts +26 -4
  200. package/src/__tests__/subagent-cron-run-attribution.test.ts +301 -0
  201. package/src/__tests__/subagent-disposal.test.ts +10 -0
  202. package/src/__tests__/subagent-fork-notifications.test.ts +10 -0
  203. package/src/__tests__/subagent-fork-prompt-role.test.ts +85 -11
  204. package/src/__tests__/subagent-fork-spawn.test.ts +15 -7
  205. package/src/__tests__/subagent-manager-notify.test.ts +148 -0
  206. package/src/__tests__/subagent-notify-parent.test.ts +1 -1
  207. package/src/__tests__/subagent-persistence.test.ts +141 -0
  208. package/src/__tests__/subagent-role-registry.test.ts +172 -63
  209. package/src/__tests__/subagent-role-resolution.test.ts +88 -0
  210. package/src/__tests__/subagent-spawn-and-await.test.ts +48 -0
  211. package/src/__tests__/subagent-spawn-tool-fork.test.ts +111 -10
  212. package/src/__tests__/subagent-terminal-message.test.ts +88 -1
  213. package/src/__tests__/subagent-tool-gate-mode.test.ts +225 -10
  214. package/src/__tests__/subagent-tools.test.ts +1901 -108
  215. package/src/__tests__/surface-completion-compaction-boundary.test.ts +124 -0
  216. package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +298 -0
  217. package/src/__tests__/system-prompt.test.ts +82 -7
  218. package/src/__tests__/tool-result-spool.test.ts +74 -18
  219. package/src/__tests__/tool-side-effects-documents.test.ts +174 -0
  220. package/src/__tests__/tool-side-effects-hook-failures.test.ts +129 -0
  221. package/src/__tests__/tools-audio-read.test.ts +10 -10
  222. package/src/__tests__/tools-get-route.test.ts +85 -12
  223. package/src/__tests__/tools-list-cli.test.ts +171 -0
  224. package/src/__tests__/top-level-renderer.test.ts +54 -1
  225. package/src/__tests__/trusted-contact-multichannel.test.ts +4 -1
  226. package/src/__tests__/turn-tail-chain.test.ts +135 -0
  227. package/src/__tests__/ui-choice-copy-surfaces.test.ts +6 -4
  228. package/src/__tests__/ui-visual-surface.test.ts +5 -4
  229. package/src/__tests__/ui-work-result-surface.test.ts +5 -4
  230. package/src/__tests__/unified-turn-context-visible-app.test.ts +7 -8
  231. package/src/__tests__/usage-routes.test.ts +62 -0
  232. package/src/__tests__/verification-outbound-delivery.test.ts +176 -0
  233. package/src/__tests__/visible-app-context.test.ts +20 -4
  234. package/src/__tests__/web-search-history.test.ts +96 -0
  235. package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +6 -6
  236. package/src/__tests__/workspace-migration-140-repair-seed-pinned-memory-v3-live.test.ts +330 -0
  237. package/src/acp/failure-error.ts +3 -5
  238. package/src/agent/attachments.ts +26 -24
  239. package/src/agent/history-repair/history-repair.ts +27 -26
  240. package/src/agent/image-optimize.ts +6 -6
  241. package/src/agent/loop.ts +40 -22
  242. package/src/agent/message-types.ts +3 -3
  243. package/src/api/constants/document-tools.ts +55 -0
  244. package/src/api/events/question-answered.ts +53 -0
  245. package/src/api/events/tool-result.ts +7 -0
  246. package/src/api/index.ts +13 -0
  247. package/src/api/responses/conversation-message.ts +11 -0
  248. package/src/api/surfaces-oauth-connect.test.ts +96 -0
  249. package/src/api/surfaces.ts +36 -8
  250. package/src/approvals/guardian-channel-delivery.ts +83 -0
  251. package/src/approvals/guardian-expiry-notifier.ts +13 -11
  252. package/src/approvals/guardian-request-resolvers.ts +216 -132
  253. package/src/calls/__tests__/voice-session-bridge.test.ts +50 -0
  254. package/src/calls/media-stream-server.ts +9 -1
  255. package/src/calls/voice-session-bridge.ts +38 -0
  256. package/src/channels/__tests__/gateway-verification-sessions.test.ts +3 -3
  257. package/src/channels/config.ts +7 -2
  258. package/src/channels/gateway-verification-sessions.ts +7 -2
  259. package/src/channels/types.ts +7 -1
  260. package/src/cli/commands/__tests__/cache.test.ts +7 -4
  261. package/src/cli/commands/__tests__/cli-test-harness.ts +82 -0
  262. package/src/cli/commands/__tests__/conversations-wake.test.ts +51 -6
  263. package/src/cli/commands/__tests__/inference-models.test.ts +14 -0
  264. package/src/cli/commands/__tests__/inference-profiles.test.ts +44 -1
  265. package/src/cli/commands/__tests__/inference-providers.test.ts +7 -0
  266. package/src/cli/commands/__tests__/plugins.test.ts +692 -0
  267. package/src/cli/commands/__tests__/schedules.test.ts +136 -37
  268. package/src/cli/commands/conversations.ts +7 -3
  269. package/src/cli/commands/credentials.help.ts +7 -0
  270. package/src/cli/commands/inference-profiles.ts +9 -3
  271. package/src/cli/commands/inference.help.ts +16 -2
  272. package/src/cli/commands/mcp.help.ts +4 -2
  273. package/src/cli/commands/memory/memory-retrospective.ts +18 -0
  274. package/src/cli/commands/notifications.ts +3 -6
  275. package/src/cli/commands/oauth/connect-surface-guidance.test.ts +16 -0
  276. package/src/cli/commands/oauth/connect-surface-guidance.ts +25 -7
  277. package/src/cli/commands/oauth/connect.test.ts +26 -1
  278. package/src/cli/commands/oauth/connect.ts +4 -1
  279. package/src/cli/commands/oauth/index.help.ts +3 -1
  280. package/src/cli/commands/oauth/providers.ts +3 -0
  281. package/src/cli/commands/plugins.help.ts +25 -2
  282. package/src/cli/commands/plugins.ts +289 -10
  283. package/src/cli/commands/schedules.help.ts +14 -12
  284. package/src/cli/commands/schedules.ts +19 -5
  285. package/src/cli/commands/tools.help.ts +4 -2
  286. package/src/cli/commands/tools.ts +38 -0
  287. package/src/cli/lib/__tests__/inspect-plugin.test.ts +1 -0
  288. package/src/cli/lib/__tests__/install-from-github.test.ts +79 -0
  289. package/src/cli/lib/__tests__/install-from-platform.test.ts +47 -1
  290. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +86 -7
  291. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +122 -0
  292. package/src/cli/lib/install-from-github.ts +61 -0
  293. package/src/cli/lib/install-from-platform.ts +13 -2
  294. package/src/cli/lib/plugin-surfaces.ts +124 -4
  295. package/src/cli/lib/upgrade-plugin.ts +18 -0
  296. package/src/cli.ts +1 -1
  297. package/src/config/__tests__/deployment-context-defaults.test.ts +73 -0
  298. package/src/config/__tests__/memory-retrospective-schema.test.ts +40 -0
  299. package/src/config/__tests__/plugin-updates-schema.test.ts +43 -0
  300. package/src/config/__tests__/profile-tool-support.test.ts +131 -0
  301. package/src/config/bundled-skills/AGENTS.md +1 -1
  302. package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
  303. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +3 -3
  304. package/src/config/bundled-skills/schedule/SKILL.md +5 -1
  305. package/src/config/bundled-skills/schedule/TOOLS.json +2 -2
  306. package/src/config/bundled-skills/settings/TOOLS.json +2 -2
  307. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +1 -0
  308. package/src/config/bundled-skills/subagent/SKILL.md +53 -22
  309. package/src/config/bundled-skills/subagent/TOOLS.json +14 -13
  310. package/src/config/bundled-skills/visualize/SKILL.md +1 -1
  311. package/src/config/default-profile-catalog.ts +6 -2
  312. package/src/config/feature-flag-registry.json +25 -17
  313. package/src/config/llm-resolver.ts +45 -15
  314. package/src/config/loader.ts +70 -1
  315. package/src/config/profile-tool-support.ts +110 -0
  316. package/src/config/sanitize-for-transfer.ts +11 -0
  317. package/src/config/schema.ts +4 -0
  318. package/src/config/schemas/__tests__/stt.test.ts +20 -5
  319. package/src/config/schemas/channels.ts +13 -0
  320. package/src/config/schemas/memory-retrospective.ts +25 -0
  321. package/src/config/schemas/plugin-updates.ts +55 -0
  322. package/src/config/schemas/services.ts +8 -4
  323. package/src/config/schemas/stt.ts +17 -5
  324. package/src/contacts/__tests__/guardian-delivery-reader.test.ts +16 -0
  325. package/src/contacts/guardian-delivery-reader.ts +29 -2
  326. package/src/context/compactor.ts +4 -4
  327. package/src/context/post-turn-tool-result-truncation.ts +32 -16
  328. package/src/context/strip-injections.ts +42 -0
  329. package/src/context/tool-result-spool.ts +32 -18
  330. package/src/credential-execution/ces-runtime.ts +3 -0
  331. package/src/credential-execution/client.ts +22 -1
  332. package/src/credential-execution/process-manager.test.ts +99 -0
  333. package/src/credential-execution/process-manager.ts +62 -16
  334. package/src/daemon/__tests__/abort-null-controller.test.ts +141 -5
  335. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +8 -12
  336. package/src/daemon/__tests__/install-assistant-command.test.ts +234 -0
  337. package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +2 -4
  338. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +20 -29
  339. package/src/daemon/channel-ui-capability.ts +4 -4
  340. package/src/daemon/conversation-agent-loop-handlers.ts +117 -61
  341. package/src/daemon/conversation-agent-loop.ts +296 -145
  342. package/src/daemon/conversation-attachments.ts +1 -1
  343. package/src/daemon/conversation-error.ts +8 -1
  344. package/src/daemon/conversation-initial-prompt.ts +16 -38
  345. package/src/daemon/conversation-lifecycle.ts +154 -32
  346. package/src/daemon/conversation-messaging.ts +85 -42
  347. package/src/daemon/conversation-process.ts +76 -28
  348. package/src/daemon/conversation-runtime-assembly.ts +9 -4
  349. package/src/daemon/conversation-store.ts +52 -3
  350. package/src/daemon/conversation-surface-state.ts +9 -0
  351. package/src/daemon/conversation-surfaces.ts +403 -210
  352. package/src/daemon/conversation-tool-setup.ts +56 -7
  353. package/src/daemon/conversation-turn-finalize.ts +134 -89
  354. package/src/daemon/conversation.ts +105 -27
  355. package/src/daemon/disk-pressure-guard-lifecycle.ts +2 -2
  356. package/src/daemon/disk-pressure-guard.ts +3 -3
  357. package/src/daemon/doordash-steps.ts +5 -5
  358. package/src/daemon/external-plugins-bootstrap.ts +7 -1
  359. package/src/daemon/handlers/config-channels.ts +48 -5
  360. package/src/daemon/handlers/conversation-history.ts +1 -1
  361. package/src/daemon/handlers/conversations.ts +62 -17
  362. package/src/daemon/handlers/shared.ts +12 -0
  363. package/src/daemon/handlers/skills.ts +11 -1
  364. package/src/daemon/identity-helpers.ts +38 -10
  365. package/src/daemon/install-assistant-command.ts +322 -0
  366. package/src/daemon/lifecycle.ts +14 -3
  367. package/src/daemon/message-types/conversations.ts +6 -7
  368. package/src/daemon/message-types/sync.ts +1 -0
  369. package/src/daemon/persist-media-references.ts +23 -14
  370. package/src/daemon/process-message.ts +6 -6
  371. package/src/daemon/tool-setup-types.ts +19 -122
  372. package/src/daemon/tool-side-effects.ts +17 -94
  373. package/src/daemon/trust-context.ts +14 -6
  374. package/src/daemon/turn-tail-chain.ts +73 -0
  375. package/src/daemon/web-search-history.ts +47 -33
  376. package/src/heartbeat/heartbeat-service.ts +6 -6
  377. package/src/ipc/__tests__/socket-path.test.ts +21 -0
  378. package/src/ipc/assistant-server.ts +17 -10
  379. package/src/ipc/cli-client.ts +10 -1
  380. package/src/ipc/routes/__tests__/documents-sync-ipc-routes.test.ts +56 -0
  381. package/src/ipc/routes/documents-sync-ipc-routes.ts +42 -0
  382. package/src/ipc/socket-cleanup.ts +9 -7
  383. package/src/ipc/socket-path.ts +4 -96
  384. package/src/live-voice/__tests__/live-voice-archive.test.ts +16 -16
  385. package/src/live-voice/__tests__/live-voice-attach-image.test.ts +209 -0
  386. package/src/live-voice/__tests__/live-voice-connection.test.ts +20 -0
  387. package/src/live-voice/__tests__/live-voice-integration.test.ts +7 -5
  388. package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +265 -0
  389. package/src/live-voice/__tests__/live-voice-stt.test.ts +6 -1
  390. package/src/live-voice/__tests__/live-voice-vad.test.ts +35 -15
  391. package/src/live-voice/__tests__/protocol.test.ts +52 -0
  392. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +3 -0
  393. package/src/live-voice/live-voice-archive.ts +13 -13
  394. package/src/live-voice/live-voice-connection.ts +7 -1
  395. package/src/live-voice/live-voice-photo.ts +169 -0
  396. package/src/live-voice/live-voice-session.ts +108 -4
  397. package/src/live-voice/protocol.ts +89 -1
  398. package/src/mcp/client.ts +10 -0
  399. package/src/messaging/providers/__tests__/callback-routing.test.ts +5 -1
  400. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +66 -4
  401. package/src/messaging/providers/callback-routing.ts +1 -0
  402. package/src/messaging/providers/discord/api.ts +312 -0
  403. package/src/messaging/providers/discord/dm-delivery.test.ts +204 -0
  404. package/src/messaging/providers/discord/render.test.ts +204 -0
  405. package/src/messaging/providers/discord/render.ts +195 -0
  406. package/src/messaging/providers/discord/send.test.ts +179 -0
  407. package/src/messaging/providers/discord/send.ts +212 -0
  408. package/src/messaging/providers/discord/transport.ts +81 -0
  409. package/src/messaging/providers/index.ts +3 -1
  410. package/src/messaging/providers/retry-policy.test.ts +247 -0
  411. package/src/messaging/providers/retry-policy.ts +206 -0
  412. package/src/messaging/providers/slack/mention-source.test.ts +285 -0
  413. package/src/messaging/providers/slack/mention-source.ts +287 -0
  414. package/src/messaging/providers/telegram-bot/api.ts +47 -130
  415. package/src/messaging/providers/whatsapp/api.ts +47 -136
  416. package/src/monitoring/__tests__/db-integrity-sample.test.ts +4 -1
  417. package/src/monitoring/__tests__/plugin-auto-update.test.ts +332 -0
  418. package/src/monitoring/db-integrity-sample.ts +6 -1
  419. package/src/monitoring/plugin-auto-update.ts +359 -0
  420. package/src/monitoring/resource-sampler.ts +21 -5
  421. package/src/monitoring/worker.ts +13 -0
  422. package/src/notifications/__tests__/assistant-reply-producer.test.ts +375 -2
  423. package/src/notifications/__tests__/broadcaster.test.ts +37 -1
  424. package/src/notifications/__tests__/copy-composer.test.ts +165 -0
  425. package/src/notifications/__tests__/emit-signal-pipeline-failure.test.ts +205 -0
  426. package/src/notifications/__tests__/notification-utils.test.ts +249 -0
  427. package/src/notifications/adapters/platform.ts +31 -2
  428. package/src/notifications/assistant-reply-producer.ts +79 -32
  429. package/src/notifications/broadcaster.ts +30 -10
  430. package/src/notifications/copy-composer.ts +69 -0
  431. package/src/notifications/emit-signal.ts +82 -15
  432. package/src/notifications/events-store.ts +10 -2
  433. package/src/notifications/home-feed-side-effect.ts +6 -3
  434. package/src/notifications/notification-utils.ts +173 -5
  435. package/src/notifications/signal.ts +12 -0
  436. package/src/oauth/byo-connection.ts +6 -1
  437. package/src/oauth/credential-token-resolver.ts +3 -10
  438. package/src/oauth/manual-token-connection.ts +29 -64
  439. package/src/oauth/manual-token-providers.ts +62 -0
  440. package/src/oauth/provider-serializer.ts +13 -0
  441. package/src/oauth/seed-providers.ts +56 -8
  442. package/src/onboarding/onboarding-events-store.ts +91 -1
  443. package/src/permissions/question-prompter.test.ts +4 -0
  444. package/src/permissions/question-prompter.ts +21 -4
  445. package/src/persistence/__tests__/conversation-lineage.test.ts +248 -0
  446. package/src/persistence/attachments-store.ts +15 -15
  447. package/src/persistence/conversation-attention-store.ts +30 -10
  448. package/src/persistence/conversation-bootstrap.ts +17 -0
  449. package/src/persistence/conversation-crud.ts +363 -97
  450. package/src/persistence/conversation-group-migration.ts +1 -1
  451. package/src/persistence/conversation-key-store.ts +38 -10
  452. package/src/persistence/conversation-lineage.ts +226 -0
  453. package/src/persistence/conversation-plugin-facade.ts +28 -3
  454. package/src/persistence/conversation-queries.ts +207 -33
  455. package/src/persistence/conversation-types.ts +107 -1
  456. package/src/persistence/jobs-store.ts +32 -8
  457. package/src/persistence/llm-usage-store.ts +22 -0
  458. package/src/persistence/migrations/361-normalize-managed-connection-rows.test.ts +168 -0
  459. package/src/persistence/migrations/361-normalize-managed-connection-rows.ts +76 -0
  460. package/src/persistence/migrations/362-add-conversation-subagent-kind.test.ts +187 -0
  461. package/src/persistence/migrations/362-add-conversation-subagent-kind.ts +93 -0
  462. package/src/persistence/migrations/363-backfill-schedule-inference-profile.test.ts +229 -0
  463. package/src/persistence/migrations/363-backfill-schedule-inference-profile.ts +127 -0
  464. package/src/persistence/migrations/364-add-schedule-source-key.test.ts +120 -0
  465. package/src/persistence/migrations/364-add-schedule-source-key.ts +43 -0
  466. package/src/persistence/migrations/365-add-conversation-fork-strategy.ts +27 -0
  467. package/src/persistence/schema/conversations.ts +32 -0
  468. package/src/persistence/schema/infrastructure.ts +3 -0
  469. package/src/persistence/steps.ts +30 -0
  470. package/src/persistence/subagent-store.ts +148 -6
  471. package/src/platform/managed-speech.test.ts +20 -0
  472. package/src/platform/managed-speech.ts +9 -0
  473. package/src/plugin-api/index.ts +8 -0
  474. package/src/plugin-api/webhook-url.ts +151 -0
  475. package/src/plugins/__tests__/installed-plugin-dirs.test.ts +82 -0
  476. package/src/plugins/__tests__/source-fingerprint.test.ts +5 -1
  477. package/src/plugins/collect-source-versions.ts +4 -27
  478. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +5 -5
  479. package/src/plugins/defaults/exploration-drift/package.json +1 -1
  480. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +4 -2
  481. package/src/plugins/defaults/image-recovery/recover.ts +67 -57
  482. package/src/plugins/defaults/index.ts +1 -1
  483. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +64 -18
  484. package/src/plugins/defaults/memory/__tests__/jobs-worker-outcome-contract.test.ts +242 -0
  485. package/src/plugins/defaults/memory/__tests__/jobs-worker-retrospective-sweep.test.ts +24 -1
  486. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +48 -3
  487. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +511 -19
  488. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +39 -3
  489. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +493 -0
  490. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +5 -4
  491. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +5 -1
  492. package/src/plugins/defaults/memory/job-handlers.ts +87 -5
  493. package/src/plugins/defaults/memory/jobs-worker.ts +60 -6
  494. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +11 -0
  495. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +29 -0
  496. package/src/plugins/defaults/memory/memory-retrospective-job.ts +342 -60
  497. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +23 -3
  498. package/src/plugins/defaults/memory/substrate/consolidation-lock.ts +9 -4
  499. package/src/plugins/defaults/memory/substrate/sweep-job.ts +7 -0
  500. package/src/plugins/defaults/memory/v1/graph/injection.ts +1 -1
  501. package/src/plugins/defaults/memory/v2/router.ts +8 -0
  502. package/src/plugins/defaults/turn-context/unified-turn-context.ts +1 -2
  503. package/src/plugins/external-plugin-loader.ts +17 -8
  504. package/src/plugins/installed-plugin-dirs.ts +101 -0
  505. package/src/plugins/mtime-cache.ts +34 -22
  506. package/src/plugins/types.ts +69 -8
  507. package/src/prompts/persona-resolver.ts +6 -2
  508. package/src/prompts/system-prompt.ts +72 -17
  509. package/src/providers/__tests__/inference.test.ts +40 -0
  510. package/src/providers/__tests__/preflight-resolved-config.test.ts +58 -1
  511. package/src/providers/__tests__/retry-callsite.test.ts +67 -0
  512. package/src/providers/__tests__/vellum-connection-routing.test.ts +42 -11
  513. package/src/providers/anthropic/client.ts +112 -95
  514. package/src/providers/connection-resolution.ts +35 -1
  515. package/src/providers/gemini/client.ts +5 -5
  516. package/src/providers/inference/adapter-factory.ts +16 -10
  517. package/src/providers/inference/auth.ts +16 -0
  518. package/src/providers/inference/connection-availability.ts +164 -12
  519. package/src/providers/inference/connections.ts +25 -1
  520. package/src/providers/media-resolve.ts +21 -11
  521. package/src/providers/model-catalog.ts +15 -0
  522. package/src/providers/openai/chat-completions-provider.ts +4 -4
  523. package/src/providers/openai/responses-provider.ts +12 -25
  524. package/src/providers/registry.ts +9 -4
  525. package/src/providers/retry.ts +25 -0
  526. package/src/providers/server-tool-pairing.ts +134 -0
  527. package/src/providers/speech-to-text/__tests__/resolve.test.ts +187 -6
  528. package/src/providers/speech-to-text/deepgram.ts +1 -0
  529. package/src/providers/speech-to-text/resolve.ts +80 -10
  530. package/src/providers/speech-to-text/vellum-managed.ts +7 -1
  531. package/src/providers/speech-to-text/vellum-speech-relay-connection.ts +3 -1
  532. package/src/providers/types.ts +26 -6
  533. package/src/providers/vellum-model-routing.ts +6 -7
  534. package/src/runtime/AGENTS.md +2 -0
  535. package/src/runtime/__tests__/agent-wake.test.ts +16 -16
  536. package/src/runtime/__tests__/background-job-runner.test.ts +0 -144
  537. package/src/runtime/__tests__/desktop-presence.test.ts +162 -0
  538. package/src/runtime/agent-wake.ts +63 -24
  539. package/src/runtime/assistant-event-hub.ts +87 -12
  540. package/src/runtime/auth/__tests__/guard-tests.test.ts +2 -0
  541. package/src/runtime/auth/__tests__/scopes.test.ts +20 -0
  542. package/src/runtime/auth/same-actor.ts +56 -8
  543. package/src/runtime/auth/scopes.ts +12 -0
  544. package/src/runtime/auth/types.ts +2 -0
  545. package/src/runtime/background-job-runner.ts +5 -27
  546. package/src/runtime/channel-readiness-service.ts +59 -3
  547. package/src/runtime/channel-readiness-types.ts +18 -0
  548. package/src/runtime/desktop-presence.ts +69 -0
  549. package/src/runtime/http-server.ts +12 -0
  550. package/src/runtime/local-actor-identity.ts +11 -9
  551. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -0
  552. package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
  553. package/src/runtime/routes/__tests__/client-routes.test.ts +186 -14
  554. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +547 -1
  555. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +162 -7
  556. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +14 -10
  557. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +340 -3
  558. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +28 -1
  559. package/src/runtime/routes/__tests__/plugins-routes.test.ts +173 -1
  560. package/src/runtime/routes/__tests__/question-routes.test.ts +64 -0
  561. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +20 -0
  562. package/src/runtime/routes/__tests__/skill-history-route.test.ts +116 -0
  563. package/src/runtime/routes/__tests__/wake-conversation-routes.test.ts +60 -2
  564. package/src/runtime/routes/archive-utils.ts +16 -12
  565. package/src/runtime/routes/attachment-routes.ts +13 -6
  566. package/src/runtime/routes/canned-reply-release.ts +54 -0
  567. package/src/runtime/routes/channel-verification-routes.ts +2 -27
  568. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +5 -0
  569. package/src/runtime/routes/client-routes.ts +114 -2
  570. package/src/runtime/routes/conversation-cli-routes.ts +10 -12
  571. package/src/runtime/routes/conversation-list-routes.ts +59 -10
  572. package/src/runtime/routes/conversation-management-routes.ts +47 -18
  573. package/src/runtime/routes/conversation-query-routes.ts +32 -53
  574. package/src/runtime/routes/conversation-routes.ts +350 -293
  575. package/src/runtime/routes/credential-prompt-routes.ts +23 -2
  576. package/src/runtime/routes/debug-routes.ts +4 -1
  577. package/src/runtime/routes/default-provider-routes.ts +7 -11
  578. package/src/runtime/routes/documents-routes.ts +44 -1
  579. package/src/runtime/routes/events-routes.ts +28 -1
  580. package/src/runtime/routes/host-app-control-routes.ts +2 -0
  581. package/src/runtime/routes/host-bash-routes.ts +2 -0
  582. package/src/runtime/routes/host-browser-routes.ts +2 -0
  583. package/src/runtime/routes/host-cu-routes.ts +2 -0
  584. package/src/runtime/routes/host-file-routes.ts +2 -0
  585. package/src/runtime/routes/host-transfer-routes.ts +4 -0
  586. package/src/runtime/routes/identity-routes.ts +6 -6
  587. package/src/runtime/routes/inbound-message-handler.ts +5 -5
  588. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +7 -3
  589. package/src/runtime/routes/inbound-stages/background-dispatch.ts +14 -0
  590. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -2
  591. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +14 -11
  592. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -17
  593. package/src/runtime/routes/inference-profile-availability-guard.ts +171 -0
  594. package/src/runtime/routes/inference-profile-session-handler.ts +26 -2
  595. package/src/runtime/routes/inference-profile-session-routes.ts +2 -1
  596. package/src/runtime/routes/inference-profiles-routes.ts +152 -73
  597. package/src/runtime/routes/inference-provider-connection-routes.ts +0 -16
  598. package/src/runtime/routes/llm-call-sites-routes.ts +13 -0
  599. package/src/runtime/routes/log-export-routes.ts +1 -1
  600. package/src/runtime/routes/notification-routes.ts +0 -14
  601. package/src/runtime/routes/oauth-providers.ts +1 -0
  602. package/src/runtime/routes/plugins-routes.ts +106 -1
  603. package/src/runtime/routes/profiler-routes.ts +4 -2
  604. package/src/runtime/routes/question-routes.ts +28 -10
  605. package/src/runtime/routes/retrospective-routes.ts +9 -3
  606. package/src/runtime/routes/schedule-routes.ts +210 -12
  607. package/src/runtime/routes/secret-routes.ts +40 -23
  608. package/src/runtime/routes/settings-routes.ts +41 -13
  609. package/src/runtime/routes/skills-routes.ts +79 -0
  610. package/src/runtime/routes/ui-snapshot-routes.ts +1 -0
  611. package/src/runtime/routes/wake-conversation-routes.ts +23 -0
  612. package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
  613. package/src/runtime/services/conversation-serializer.ts +24 -9
  614. package/src/runtime/sync/documents-sidecar-publish.test.ts +101 -0
  615. package/src/runtime/sync/resource-sync-events.ts +79 -1
  616. package/src/runtime/sync/worker-daemon-notify.test.ts +34 -0
  617. package/src/runtime/sync/worker-daemon-notify.ts +44 -6
  618. package/src/runtime/verification-outbound-actions.ts +250 -222
  619. package/src/runtime/verification-templates.ts +41 -30
  620. package/src/schedule/__tests__/plugin-schedule-declarations.test.ts +614 -0
  621. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1066 -0
  622. package/src/schedule/inference-profile.ts +64 -3
  623. package/src/schedule/plugin-schedule-declarations.ts +518 -0
  624. package/src/schedule/plugin-schedule-reconciler.ts +605 -0
  625. package/src/schedule/plugin-schedules-gate.ts +23 -0
  626. package/src/schedule/schedule-store.ts +520 -26
  627. package/src/schedule/scheduler.ts +38 -0
  628. package/src/security/credential-backend.ts +51 -4
  629. package/src/security/encrypted-store.ts +64 -42
  630. package/src/security/secure-keys.ts +55 -7
  631. package/src/skills/catalog-install.ts +20 -7
  632. package/src/skills/inline-command-runner.ts +2 -7
  633. package/src/skills/skill-history.test.ts +269 -0
  634. package/src/skills/skill-history.ts +250 -0
  635. package/src/skills/skillssh-registry.ts +1 -1
  636. package/src/stt/daemon-batch-transcriber.ts +8 -6
  637. package/src/subagent/__tests__/consult-prompt.test.ts +25 -0
  638. package/src/subagent/consult-prompt.ts +3 -1
  639. package/src/subagent/consult-transcript.ts +1 -1
  640. package/src/subagent/manager.ts +295 -42
  641. package/src/subagent/progress-events.ts +35 -0
  642. package/src/subagent/role-resolution.ts +88 -0
  643. package/src/subagent/types.ts +302 -67
  644. package/src/subagent/validate-allowlists.ts +3 -4
  645. package/src/telegram/__tests__/webhook-health.test.ts +73 -1
  646. package/src/telegram/webhook-health.ts +101 -3
  647. package/src/telemetry/__tests__/telemetry-event-fixtures.ts +2 -0
  648. package/src/telemetry/activation-funnel.ts +6 -1
  649. package/src/telemetry/live-voice-funnel.ts +75 -0
  650. package/src/telemetry/telemetry-event-sources.ts +6 -0
  651. package/src/telemetry/telemetry-wire-source.json +1 -1
  652. package/src/telemetry/telemetry-wire.generated.ts +2 -0
  653. package/src/telemetry/types.ts +23 -4
  654. package/src/telemetry/usage-telemetry-reporter.test.ts +4 -0
  655. package/src/tools/__tests__/subagent-workflow-tool-input-schemas.test.ts +1 -1
  656. package/src/tools/ask-question/ask-question-tool.test.ts +118 -3
  657. package/src/tools/ask-question/ask-question-tool.ts +56 -2
  658. package/src/tools/browser/browser-execution.ts +1 -1
  659. package/src/tools/calls/call-start.ts +8 -5
  660. package/src/tools/conversation-groups/move_to_group.ts +10 -12
  661. package/src/tools/document/document-tool.ts +11 -4
  662. package/src/tools/executor.ts +7 -6
  663. package/src/tools/filesystem/edit.ts +1 -1
  664. package/src/tools/filesystem/list.ts +1 -1
  665. package/src/tools/filesystem/read.ts +7 -13
  666. package/src/tools/filesystem/search.ts +23 -21
  667. package/src/tools/filesystem/write.ts +4 -1
  668. package/src/tools/host-filesystem/edit.ts +7 -2
  669. package/src/tools/host-filesystem/read.ts +21 -8
  670. package/src/tools/host-filesystem/write.ts +10 -2
  671. package/src/tools/host-terminal/host-shell.ts +2 -2
  672. package/src/tools/mcp/mcp-tool-factory.ts +25 -1
  673. package/src/tools/permission-checker.ts +8 -3
  674. package/src/tools/schedule/delete.ts +13 -1
  675. package/src/tools/schedule/list.ts +14 -3
  676. package/src/tools/schedule/update.ts +56 -33
  677. package/src/tools/shared/filesystem/audio-read.ts +6 -4
  678. package/src/tools/shared/filesystem/file-ops-service.ts +176 -116
  679. package/src/tools/shared/filesystem/image-read.ts +12 -12
  680. package/src/tools/shared/filesystem/path-policy.ts +70 -5
  681. package/src/tools/shared/filesystem/size-guard.ts +6 -6
  682. package/src/tools/shared/input-misuse.ts +72 -0
  683. package/src/tools/skills/scaffold-managed.ts +82 -0
  684. package/src/tools/skills/skill-tool-factory.ts +14 -1
  685. package/src/tools/subagent/consult-deadline.ts +12 -7
  686. package/src/tools/subagent/message.ts +3 -1
  687. package/src/tools/subagent/read.ts +66 -4
  688. package/src/tools/subagent/spawn.ts +500 -78
  689. package/src/tools/types.ts +22 -3
  690. package/src/tools/ui-surface/surface-shape-docs.ts +1 -1
  691. package/src/tools/ui-surface/visual-validation.ts +2 -0
  692. package/src/usage/__tests__/subagent-attribution.test.ts +109 -0
  693. package/src/usage/subagent-attribution.ts +108 -0
  694. package/src/util/__tests__/sqlite-retry.test.ts +112 -0
  695. package/src/util/abort-reasons.ts +16 -0
  696. package/src/util/ansi.ts +44 -0
  697. package/src/util/disk-usage.ts +17 -13
  698. package/src/util/image-conversion.ts +20 -13
  699. package/src/util/schedule-source-key.ts +23 -0
  700. package/src/util/worker-process.ts +7 -1
  701. package/src/workspace/git-service.ts +33 -9
  702. package/src/workspace/migrations/140-repair-seed-pinned-memory-v3-live.ts +260 -0
  703. package/src/workspace/migrations/141-stt-english-default-to-multilingual.ts +111 -0
  704. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +126 -0
  705. package/src/workspace/migrations/registry.ts +4 -0
  706. package/src/workspace/top-level-renderer.ts +15 -2
  707. package/src/__tests__/tool-side-effects-slack-dm.test.ts +0 -357
  708. package/src/daemon/install-symlink.ts +0 -207
  709. package/src/notifications/deferred-emit.ts +0 -147
@@ -0,0 +1,1066 @@
1
+ /**
2
+ * Tests for `reconcilePluginSchedules()`.
3
+ *
4
+ * Fixture plugin directories are written under the (per-test-process)
5
+ * workspace plugins dir and converged against the real schedule store; the
6
+ * only stubs are the notification emitter (recorded for assertions) and the
7
+ * background-wake publisher (kept out for hermeticity, mirroring
8
+ * schedule-store.test.ts).
9
+ */
10
+
11
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
12
+ import { dirname, join } from "node:path";
13
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
14
+
15
+ mock.module("../../background-wake/publisher.js", () => ({
16
+ refreshBackgroundWakeIntent: () => {},
17
+ }));
18
+
19
+ const emittedSignals: Array<Record<string, unknown>> = [];
20
+
21
+ /** When set, merged into the next emit results (e.g. a failure reason). */
22
+ let emitResultOverride: Record<string, unknown> | null = null;
23
+
24
+ /**
25
+ * When set, an emit records its call and then parks on this promise instead
26
+ * of resolving, so a test can hold one attempt open across further passes.
27
+ */
28
+ let emitGate: Promise<void> | null = null;
29
+
30
+ mock.module("../../notifications/emit-signal.js", () => ({
31
+ emitNotificationSignal: async (params: Record<string, unknown>) => {
32
+ emittedSignals.push(params);
33
+ if (emitGate) {
34
+ await emitGate;
35
+ }
36
+ return {
37
+ signalId: "test-signal",
38
+ deduplicated: false,
39
+ dispatched: true,
40
+ reason: "test",
41
+ deliveryResults: [],
42
+ pipelineFailed: false,
43
+ ...(emitResultOverride ?? {}),
44
+ };
45
+ },
46
+ }));
47
+
48
+ import { setOverridesForTesting } from "../../__tests__/feature-flag-test-helpers.js";
49
+ import { getDb } from "../../persistence/db-connection.js";
50
+ import { initializeDb } from "../../persistence/db-init.js";
51
+ import { getWorkspacePluginsDir } from "../../util/platform.js";
52
+ import {
53
+ reconcilePluginSchedules,
54
+ resetDefinitionErrorEmitGuardForTests,
55
+ } from "../plugin-schedule-reconciler.js";
56
+ import {
57
+ claimDueSchedules,
58
+ createSchedule,
59
+ createScheduleRun,
60
+ listDeclaredSchedules,
61
+ setUserEnabled,
62
+ } from "../schedule-store.js";
63
+
64
+ await initializeDb();
65
+
66
+ const pluginsDir = getWorkspacePluginsDir();
67
+
68
+ function getRawDb(): import("bun:sqlite").Database {
69
+ return (getDb() as unknown as { $client: import("bun:sqlite").Database })
70
+ .$client;
71
+ }
72
+
73
+ function rawJob(id: string): Record<string, unknown> {
74
+ return getRawDb()
75
+ .query("SELECT * FROM cron_jobs WHERE id = ?")
76
+ .get(id) as Record<string, unknown>;
77
+ }
78
+
79
+ /**
80
+ * Write a plugin fixture: a `package.json` plus the given files under
81
+ * `schedules/` (paths relative to the schedules dir).
82
+ */
83
+ function writePlugin(name: string, files: Record<string, string>): string {
84
+ const dir = join(pluginsDir, name);
85
+ mkdirSync(dir, { recursive: true });
86
+ writeFileSync(
87
+ join(dir, "package.json"),
88
+ JSON.stringify({ name, version: "1.0.0" }),
89
+ );
90
+ for (const [rel, content] of Object.entries(files)) {
91
+ const abs = join(dir, "schedules", rel);
92
+ mkdirSync(dirname(abs), { recursive: true });
93
+ writeFileSync(abs, content);
94
+ }
95
+ return dir;
96
+ }
97
+
98
+ /**
99
+ * A `digest` declaration: a `config.json` holding the given config plus an
100
+ * `index.md` carrying the prompt body.
101
+ */
102
+ function digestFiles(
103
+ config: Record<string, unknown>,
104
+ body = "Summarize the day.",
105
+ ): Record<string, string> {
106
+ return {
107
+ "digest/config.json": JSON.stringify(config),
108
+ "digest/index.md": `${body}\n`,
109
+ };
110
+ }
111
+
112
+ /** The every-minute digest most cases converge. */
113
+ function digest(body = "Summarize the day."): Record<string, string> {
114
+ return digestFiles(
115
+ { expression: "* * * * *", description: "Daily digest" },
116
+ body,
117
+ );
118
+ }
119
+
120
+ /**
121
+ * A digest declaration whose recurrence has nothing left to fire: five daily
122
+ * occurrences, all of them in 2020. Well-formed, just finished.
123
+ */
124
+ function endedDigest(): Record<string, string> {
125
+ return digestFiles({
126
+ expression: "DTSTART:20200101T090000Z\nRRULE:FREQ=DAILY;COUNT=5",
127
+ expression_syntax: "rrule",
128
+ });
129
+ }
130
+
131
+ /**
132
+ * The same five daily occurrences, shipped switched off and starting at the
133
+ * given DTSTART. A declaration with `enabled: false` inserts its row unarmed:
134
+ * the clock is zeroed, no run sits behind it, and no user choice is recorded.
135
+ */
136
+ function disabledDigest(dtstart: string): Record<string, string> {
137
+ return digestFiles({
138
+ expression: `DTSTART:${dtstart}\nRRULE:FREQ=DAILY;COUNT=5`,
139
+ expression_syntax: "rrule",
140
+ enabled: false,
141
+ });
142
+ }
143
+
144
+ /** A digest declaration the parser refuses: its config.json is not JSON. */
145
+ function brokenDigest(): Record<string, string> {
146
+ return {
147
+ "digest/config.json": "{not json",
148
+ "digest/index.md": "Summarize the day.\n",
149
+ };
150
+ }
151
+
152
+ /** A script declaration: the engine runs its `index.sh` by path. */
153
+ function scriptSync(body = "echo synced"): Record<string, string> {
154
+ return {
155
+ "sync/config.json": JSON.stringify({ expression: "* * * * *" }),
156
+ "sync/index.sh": `#!/bin/sh\n${body}\n`,
157
+ };
158
+ }
159
+
160
+ /** The same script declaration with a config.json the parser refuses. */
161
+ function brokenScriptSync(body = "echo unreviewed"): Record<string, string> {
162
+ return {
163
+ "sync/config.json": "{not json",
164
+ "sync/index.sh": `#!/bin/sh\n${body}\n`,
165
+ };
166
+ }
167
+
168
+ const DIGEST_KEY = "plugin:news/digest";
169
+
170
+ /** Reset the store, the fixture plugins dir, and the emit doubles. */
171
+ function resetReconcilerFixtures(): void {
172
+ getDb().run("DELETE FROM cron_runs");
173
+ getDb().run("DELETE FROM cron_jobs");
174
+ rmSync(pluginsDir, { recursive: true, force: true });
175
+ emittedSignals.length = 0;
176
+ emitResultOverride = null;
177
+ emitGate = null;
178
+ resetDefinitionErrorEmitGuardForTests();
179
+ }
180
+
181
+ describe("reconcilePluginSchedules", () => {
182
+ beforeEach(() => {
183
+ resetReconcilerFixtures();
184
+ // The feature ships off, so every case below states the flag it runs
185
+ // under rather than inheriting the registry default.
186
+ setOverridesForTesting({ "plugin-schedules": true });
187
+ });
188
+
189
+ /** Let a released emit's continuation, `.then`, and `.finally` all run. */
190
+ async function settleEmits(): Promise<void> {
191
+ await new Promise((resolve) => setTimeout(resolve, 0));
192
+ }
193
+
194
+ test("converges execute and script declarations into rows the engine claims", async () => {
195
+ writePlugin("news", {
196
+ ...digest(),
197
+ "sync/config.json": JSON.stringify({ expression: "0 */2 * * *" }),
198
+ "sync/index.sh": "#!/bin/sh\necho synced\n",
199
+ });
200
+
201
+ await reconcilePluginSchedules();
202
+
203
+ const rows = listDeclaredSchedules();
204
+ expect(rows).toHaveLength(2);
205
+
206
+ const digestRow = rows.find((r) => r.sourceKey === DIGEST_KEY)!;
207
+ expect(digestRow.mode).toBe("execute");
208
+ expect(digestRow.message).toBe("Summarize the day.");
209
+ expect(digestRow.description).toBe("Daily digest");
210
+ expect(digestRow.enabled).toBe(true);
211
+ expect(digestRow.userEnabled).toBeNull();
212
+ expect(digestRow.definitionHash).toMatch(/^[0-9a-f]{64}$/);
213
+
214
+ const sync = rows.find((r) => r.sourceKey === "plugin:news/sync")!;
215
+ expect(sync.mode).toBe("script");
216
+ expect(sync.script).toContain("index.sh");
217
+ expect(sync.message).toBe("");
218
+
219
+ // Every newly created armed row announces itself, deduped by hash.
220
+ const declared = emittedSignals.filter(
221
+ (s) => s.sourceEventName === "schedule.declared",
222
+ );
223
+ expect(declared).toHaveLength(2);
224
+ const digestDeclared = declared.find(
225
+ (s) => s.sourceContextId === DIGEST_KEY,
226
+ )!;
227
+ expect(digestDeclared.dedupeKey).toBe(
228
+ `schedule-declared:${DIGEST_KEY}:${digestRow.definitionHash}`,
229
+ );
230
+ expect(digestDeclared.contextPayload).toEqual({
231
+ pluginName: "news",
232
+ scheduleName: "digest",
233
+ sourceKey: DIGEST_KEY,
234
+ cadence: "* * * * *",
235
+ });
236
+
237
+ // The untouched engine picks the row up: an every-minute cron is always
238
+ // due within a two-minute claim horizon.
239
+ const claimed = await claimDueSchedules(Date.now() + 120_000);
240
+ expect(claimed.map((j) => j.id)).toContain(digestRow.id);
241
+ });
242
+
243
+ test("a repeat pass with unchanged declarations is a no-op", async () => {
244
+ writePlugin("news", digest());
245
+ await reconcilePluginSchedules();
246
+ const row = listDeclaredSchedules()[0]!;
247
+ const before = rawJob(row.id);
248
+ emittedSignals.length = 0;
249
+
250
+ await reconcilePluginSchedules();
251
+
252
+ expect(rawJob(row.id)).toEqual(before);
253
+ expect(emittedSignals).toHaveLength(0);
254
+ });
255
+
256
+ test("an upgrade updates by hash and emits definition_changed for the armed row", async () => {
257
+ writePlugin("news", digest());
258
+ await reconcilePluginSchedules();
259
+ const created = listDeclaredSchedules()[0]!;
260
+ emittedSignals.length = 0;
261
+
262
+ writePlugin("news", digest("Summarize the WEEK."));
263
+ await reconcilePluginSchedules();
264
+
265
+ const updated = listDeclaredSchedules()[0]!;
266
+ expect(updated.id).toBe(created.id);
267
+ expect(updated.message).toBe("Summarize the WEEK.");
268
+ expect(updated.definitionHash).not.toBe(created.definitionHash);
269
+
270
+ expect(emittedSignals).toHaveLength(1);
271
+ const signal = emittedSignals[0]!;
272
+ expect(signal.sourceEventName).toBe("schedule.definition_changed");
273
+ expect(signal.dedupeKey).toBe(
274
+ `schedule-definition-changed:${DIGEST_KEY}:${updated.definitionHash}`,
275
+ );
276
+ expect(signal.contextPayload).toEqual({
277
+ pluginName: "news",
278
+ scheduleName: "digest",
279
+ sourceKey: DIGEST_KEY,
280
+ });
281
+ });
282
+
283
+ test("uninstall disarms without deleting rows or runs; reinstall re-links and re-arms", async () => {
284
+ writePlugin("news", digest());
285
+ await reconcilePluginSchedules();
286
+ const created = listDeclaredSchedules()[0]!;
287
+ await createScheduleRun(created.id, "conv-1");
288
+
289
+ rmSync(join(pluginsDir, "news"), { recursive: true, force: true });
290
+ await reconcilePluginSchedules();
291
+
292
+ const disarmed = listDeclaredSchedules()[0]!;
293
+ expect(disarmed.id).toBe(created.id);
294
+ expect(disarmed.enabled).toBe(false);
295
+ expect(disarmed.sourceKey).toBe(DIGEST_KEY);
296
+ const runs = getRawDb()
297
+ .query("SELECT COUNT(*) AS n FROM cron_runs WHERE job_id = ?")
298
+ .get(created.id) as { n: number };
299
+ expect(runs.n).toBe(1);
300
+
301
+ emittedSignals.length = 0;
302
+ writePlugin("news", digest());
303
+ await reconcilePluginSchedules();
304
+
305
+ const relinked = listDeclaredSchedules();
306
+ expect(relinked).toHaveLength(1);
307
+ expect(relinked[0]!.id).toBe(created.id);
308
+ expect(relinked[0]!.enabled).toBe(true);
309
+ expect(relinked[0]!.nextRunAt).toBeGreaterThan(0);
310
+ // Re-linking an existing row is not a first arming: no arrival signal.
311
+ expect(
312
+ emittedSignals.filter((s) => s.sourceEventName === "schedule.declared"),
313
+ ).toHaveLength(0);
314
+ });
315
+
316
+ test("user_enabled survives upgrades and disarm/re-arm cycles", async () => {
317
+ writePlugin("news", digest());
318
+ await reconcilePluginSchedules();
319
+ const created = listDeclaredSchedules()[0]!;
320
+
321
+ await setUserEnabled(created.id, false);
322
+ emittedSignals.length = 0;
323
+
324
+ // Upgrade: definition columns update, the user override keeps the row
325
+ // disabled, and no definition_changed emits for an unarmed row.
326
+ writePlugin("news", digest("Summarize the WEEK."));
327
+ await reconcilePluginSchedules();
328
+ let row = listDeclaredSchedules()[0]!;
329
+ expect(row.message).toBe("Summarize the WEEK.");
330
+ expect(row.enabled).toBe(false);
331
+ expect(row.userEnabled).toBe(false);
332
+ expect(emittedSignals).toHaveLength(0);
333
+
334
+ // Disarm (uninstall) then re-arm (reinstall): the override still wins.
335
+ rmSync(join(pluginsDir, "news"), { recursive: true, force: true });
336
+ await reconcilePluginSchedules();
337
+ writePlugin("news", digest("Summarize the WEEK."));
338
+ await reconcilePluginSchedules();
339
+ row = listDeclaredSchedules()[0]!;
340
+ expect(row.id).toBe(created.id);
341
+ expect(row.enabled).toBe(false);
342
+ expect(row.userEnabled).toBe(false);
343
+ });
344
+
345
+ test("an upgrade that flips a disarmed declaration to enabled arms it and notifies", async () => {
346
+ writePlugin(
347
+ "news",
348
+ digestFiles({ expression: "* * * * *", enabled: false }),
349
+ );
350
+ await reconcilePluginSchedules();
351
+ expect(listDeclaredSchedules()[0]!.enabled).toBe(false);
352
+ expect(emittedSignals).toHaveLength(0);
353
+
354
+ writePlugin("news", digest());
355
+ await reconcilePluginSchedules();
356
+
357
+ const armed = listDeclaredSchedules()[0]!;
358
+ expect(armed.enabled).toBe(true);
359
+ expect(emittedSignals).toHaveLength(1);
360
+ const signal = emittedSignals[0]!;
361
+ expect(signal.sourceEventName).toBe("schedule.declared");
362
+ expect(signal.dedupeKey).toBe(
363
+ `schedule-declared:${DIGEST_KEY}:${armed.definitionHash}`,
364
+ );
365
+ });
366
+
367
+ test("a user override enables a declaration shipped disabled", async () => {
368
+ writePlugin(
369
+ "news",
370
+ digestFiles({ expression: "* * * * *", enabled: false }),
371
+ );
372
+ await reconcilePluginSchedules();
373
+ const created = listDeclaredSchedules()[0]!;
374
+ expect(created.enabled).toBe(false);
375
+
376
+ await setUserEnabled(created.id, true);
377
+ await reconcilePluginSchedules();
378
+
379
+ const row = listDeclaredSchedules()[0]!;
380
+ expect(row.enabled).toBe(true);
381
+ expect(row.userEnabled).toBe(true);
382
+ });
383
+
384
+ test("disabling a plugin pauses its schedules; re-enabling restores them", async () => {
385
+ const dir = writePlugin("news", digest());
386
+ await reconcilePluginSchedules();
387
+ const created = listDeclaredSchedules()[0]!;
388
+
389
+ writeFileSync(join(dir, ".disabled"), "");
390
+ await reconcilePluginSchedules();
391
+ expect(listDeclaredSchedules()[0]!.enabled).toBe(false);
392
+
393
+ rmSync(join(dir, ".disabled"));
394
+ await reconcilePluginSchedules();
395
+ const restored = listDeclaredSchedules()[0]!;
396
+ expect(restored.id).toBe(created.id);
397
+ expect(restored.enabled).toBe(true);
398
+ expect(restored.userEnabled).toBeNull();
399
+ });
400
+
401
+ test("a parse error keeps the last-good execute row running and emits a deduped notification", async () => {
402
+ writePlugin("news", digest());
403
+ await reconcilePluginSchedules();
404
+ const created = listDeclaredSchedules()[0]!;
405
+ const before = rawJob(created.id);
406
+ emittedSignals.length = 0;
407
+
408
+ // Break the declaration: a config.json the parser cannot read.
409
+ writePlugin("news", brokenDigest());
410
+ await reconcilePluginSchedules();
411
+
412
+ // The row fires the message it was armed with, so nothing unvalidated
413
+ // runs while it stays armed.
414
+ expect(rawJob(created.id)).toEqual(before);
415
+ expect(emittedSignals).toHaveLength(1);
416
+ const signal = emittedSignals[0]!;
417
+ expect(signal.sourceEventName).toBe("schedule.definition_error");
418
+ const day = new Date().toISOString().slice(0, 10);
419
+ expect(signal.dedupeKey).toBe(
420
+ `schedule-definition-error:${DIGEST_KEY}:${day}`,
421
+ );
422
+ const payload = signal.contextPayload as Record<string, unknown>;
423
+ expect(payload.pluginName).toBe("news");
424
+ expect(payload.scheduleName).toBe("digest");
425
+ expect(payload.sourceKey).toBe(DIGEST_KEY);
426
+ expect(typeof payload.reason).toBe("string");
427
+ expect(payload.paused).toBe(false);
428
+ });
429
+
430
+ test("a parse error disarms a script row, which re-arms once the declaration parses", async () => {
431
+ writePlugin("news", scriptSync());
432
+ await reconcilePluginSchedules();
433
+ const created = listDeclaredSchedules()[0]!;
434
+ expect(created.mode).toBe("script");
435
+ expect(created.enabled).toBe(true);
436
+ emittedSignals.length = 0;
437
+
438
+ // A script row runs its entrypoint by path, so an upgrade the parser
439
+ // refuses would otherwise fire the unreviewed index.sh next to it.
440
+ writePlugin("news", brokenScriptSync());
441
+ await reconcilePluginSchedules();
442
+
443
+ const paused = listDeclaredSchedules()[0]!;
444
+ expect(paused.id).toBe(created.id);
445
+ expect(paused.enabled).toBe(false);
446
+ expect(emittedSignals).toHaveLength(1);
447
+ const signal = emittedSignals[0]!;
448
+ expect(signal.sourceEventName).toBe("schedule.definition_error");
449
+ const payload = signal.contextPayload as Record<string, unknown>;
450
+ expect(payload.sourceKey).toBe("plugin:news/sync");
451
+ expect(payload.paused).toBe(true);
452
+
453
+ // Fixing the declaration re-arms the same row on the next pass.
454
+ writePlugin("news", scriptSync());
455
+ await reconcilePluginSchedules();
456
+
457
+ const rearmed = listDeclaredSchedules()[0]!;
458
+ expect(rearmed.id).toBe(created.id);
459
+ expect(rearmed.enabled).toBe(true);
460
+ expect(rearmed.nextRunAt).toBeGreaterThan(0);
461
+ });
462
+
463
+ test("a script row the user turned off reports no pause when its declaration breaks", async () => {
464
+ writePlugin("news", scriptSync());
465
+ await reconcilePluginSchedules();
466
+ const created = listDeclaredSchedules()[0]!;
467
+ await setUserEnabled(created.id, false);
468
+ emittedSignals.length = 0;
469
+
470
+ // The row is already off and stays off, so promising the user it resumes
471
+ // once the declaration loads again would be a lie.
472
+ writePlugin("news", brokenScriptSync());
473
+ await reconcilePluginSchedules();
474
+
475
+ expect(listDeclaredSchedules()[0]!.enabled).toBe(false);
476
+ expect(emittedSignals).toHaveLength(1);
477
+ const signal = emittedSignals[0]!;
478
+ expect(signal.sourceEventName).toBe("schedule.definition_error");
479
+ const payload = signal.contextPayload as Record<string, unknown>;
480
+ expect(payload.sourceKey).toBe("plugin:news/sync");
481
+ expect(payload.paused).toBe(false);
482
+ });
483
+
484
+ test("a user's off choice survives a script row's disarm and re-arm", async () => {
485
+ writePlugin("news", scriptSync());
486
+ await reconcilePluginSchedules();
487
+ const created = listDeclaredSchedules()[0]!;
488
+ await setUserEnabled(created.id, false);
489
+
490
+ writePlugin("news", brokenScriptSync());
491
+ await reconcilePluginSchedules();
492
+ writePlugin("news", scriptSync());
493
+ await reconcilePluginSchedules();
494
+
495
+ const row = listDeclaredSchedules()[0]!;
496
+ expect(row.enabled).toBe(false);
497
+ expect(row.userEnabled).toBe(false);
498
+ });
499
+
500
+ test("an ended recurrence keeps a script row on its last-good definition", async () => {
501
+ writePlugin("news", scriptSync());
502
+ await reconcilePluginSchedules();
503
+ const created = listDeclaredSchedules()[0]!;
504
+ const before = rawJob(created.id);
505
+ emittedSignals.length = 0;
506
+
507
+ // A recurrence running out is not a rewrite of the entrypoint, so the
508
+ // last-good handling every mode shares still applies.
509
+ writePlugin("news", {
510
+ "sync/config.json": JSON.stringify({
511
+ expression: "DTSTART:20200101T090000Z\nRRULE:FREQ=DAILY;COUNT=5",
512
+ expression_syntax: "rrule",
513
+ }),
514
+ "sync/index.sh": "#!/bin/sh\necho synced\n",
515
+ });
516
+ await reconcilePluginSchedules();
517
+
518
+ expect(rawJob(created.id)).toEqual(before);
519
+ expect(emittedSignals).toHaveLength(1);
520
+ expect(emittedSignals[0]!.sourceEventName).toBe(
521
+ "schedule.definition_error",
522
+ );
523
+ });
524
+
525
+ test("engine-latched rows are left untouched by definition changes", async () => {
526
+ writePlugin("news", digest());
527
+ await reconcilePluginSchedules();
528
+ const created = listDeclaredSchedules()[0]!;
529
+ emittedSignals.length = 0;
530
+
531
+ // Recurrence exhaustion latch: the claim path disables the row, zeroes
532
+ // nextRunAt, and stamps lastRunAt in one write.
533
+ getRawDb().run(
534
+ "UPDATE cron_jobs SET enabled = 0, next_run_at = 0, last_run_at = ? WHERE id = ?",
535
+ [Date.now(), created.id],
536
+ );
537
+ const latched = rawJob(created.id);
538
+
539
+ writePlugin("news", digest("Summarize the WEEK."));
540
+ await reconcilePluginSchedules();
541
+
542
+ expect(rawJob(created.id)).toEqual(latched);
543
+ expect(emittedSignals).toHaveLength(0);
544
+ });
545
+
546
+ test("imperative rows are byte-identical across passes", async () => {
547
+ const imperative = await createSchedule({
548
+ name: "Mine",
549
+ cronExpression: "0 8 * * *",
550
+ message: "hello",
551
+ syntax: "cron",
552
+ });
553
+ const before = rawJob(imperative.id);
554
+
555
+ writePlugin("news", digest());
556
+ await reconcilePluginSchedules();
557
+ rmSync(join(pluginsDir, "news"), { recursive: true, force: true });
558
+ await reconcilePluginSchedules();
559
+
560
+ expect(rawJob(imperative.id)).toEqual(before);
561
+ });
562
+
563
+ test("concurrent reconciles serialize into one row per declaration", async () => {
564
+ writePlugin("news", digest());
565
+
566
+ await Promise.all([reconcilePluginSchedules(), reconcilePluginSchedules()]);
567
+
568
+ const rows = listDeclaredSchedules();
569
+ expect(rows).toHaveLength(1);
570
+ expect(rows[0]!.sourceKey).toBe(DIGEST_KEY);
571
+ });
572
+
573
+ test("a manifest-rejected plugin disarms its schedules and surfaces the failure", async () => {
574
+ const dir = writePlugin("news", digest());
575
+ await reconcilePluginSchedules();
576
+ const created = listDeclaredSchedules()[0]!;
577
+ expect(created.enabled).toBe(true);
578
+ emittedSignals.length = 0;
579
+
580
+ // A package.json the loader's schema rejects (empty name): the runtime
581
+ // refuses to bring such a plugin up, so its schedules must disarm too.
582
+ writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "" }));
583
+ await reconcilePluginSchedules();
584
+
585
+ const disarmed = listDeclaredSchedules()[0]!;
586
+ expect(disarmed.id).toBe(created.id);
587
+ expect(disarmed.enabled).toBe(false);
588
+ expect(emittedSignals).toHaveLength(1);
589
+ const signal = emittedSignals[0]!;
590
+ expect(signal.sourceEventName).toBe("schedule.definition_error");
591
+ const payload = signal.contextPayload as Record<string, unknown>;
592
+ expect(payload.pluginName).toBe("news");
593
+ expect(payload.scheduleName).toBe("digest");
594
+ expect(payload.sourceKey).toBe(DIGEST_KEY);
595
+ expect(payload.reason).toContain("package.json");
596
+ // The pass took an armed row off, so the notification says so.
597
+ expect(payload.paused).toBe(true);
598
+
599
+ // The failure surfaces once per day, not per pass.
600
+ await reconcilePluginSchedules();
601
+ expect(emittedSignals).toHaveLength(1);
602
+
603
+ // Restoring a valid manifest re-arms the row on the next pass.
604
+ writeFileSync(
605
+ join(dir, "package.json"),
606
+ JSON.stringify({ name: "news", version: "1.0.0" }),
607
+ );
608
+ await reconcilePluginSchedules();
609
+ expect(listDeclaredSchedules()[0]!.enabled).toBe(true);
610
+ });
611
+
612
+ test("a plugin with an unparseable package.json creates no rows and surfaces the failure", async () => {
613
+ const dir = writePlugin("news", digest());
614
+ writeFileSync(join(dir, "package.json"), "{not json");
615
+
616
+ await reconcilePluginSchedules();
617
+
618
+ expect(listDeclaredSchedules()).toHaveLength(0);
619
+ expect(emittedSignals).toHaveLength(1);
620
+ expect(emittedSignals[0]!.sourceEventName).toBe(
621
+ "schedule.definition_error",
622
+ );
623
+ // Nothing was ever armed, so nothing was paused.
624
+ const payload = emittedSignals[0]!.contextPayload as Record<
625
+ string,
626
+ unknown
627
+ >;
628
+ expect(payload.paused).toBe(false);
629
+ });
630
+
631
+ test("a broken manifest on a plugin without schedules stays silent", async () => {
632
+ const dir = join(pluginsDir, "quiet");
633
+ mkdirSync(dir, { recursive: true });
634
+ writeFileSync(join(dir, "package.json"), "{not json");
635
+
636
+ await reconcilePluginSchedules();
637
+
638
+ expect(listDeclaredSchedules()).toHaveLength(0);
639
+ expect(emittedSignals).toHaveLength(0);
640
+ });
641
+
642
+ test("an upgrade that adds a schedule arms it and emits schedule.declared", async () => {
643
+ writePlugin("news", digest());
644
+ await reconcilePluginSchedules();
645
+ emittedSignals.length = 0;
646
+
647
+ // The upgraded revision ships an additional declaration alongside the
648
+ // unchanged digest.
649
+ writePlugin("news", {
650
+ "sync/config.json": JSON.stringify({ expression: "0 */2 * * *" }),
651
+ "sync/index.sh": "#!/bin/sh\necho synced\n",
652
+ });
653
+ await reconcilePluginSchedules();
654
+
655
+ const sync = listDeclaredSchedules().find(
656
+ (r) => r.sourceKey === "plugin:news/sync",
657
+ )!;
658
+ expect(sync.enabled).toBe(true);
659
+ expect(emittedSignals).toHaveLength(1);
660
+ const signal = emittedSignals[0]!;
661
+ expect(signal.sourceEventName).toBe("schedule.declared");
662
+ expect(signal.dedupeKey).toBe(
663
+ `schedule-declared:plugin:news/sync:${sync.definitionHash}`,
664
+ );
665
+ const payload = signal.contextPayload as Record<string, unknown>;
666
+ expect(payload.pluginName).toBe("news");
667
+ expect(payload.scheduleName).toBe("sync");
668
+ expect(payload.cadence).toBe("0 */2 * * *");
669
+ });
670
+
671
+ test("a declaration shipped disabled creates its row silently", async () => {
672
+ writePlugin(
673
+ "news",
674
+ digestFiles({ expression: "* * * * *", enabled: false }),
675
+ );
676
+
677
+ await reconcilePluginSchedules();
678
+
679
+ expect(listDeclaredSchedules()[0]!.enabled).toBe(false);
680
+ expect(emittedSignals).toHaveLength(0);
681
+ });
682
+
683
+ test("a persistently broken declaration emits its error once, not per pass", async () => {
684
+ writePlugin("news", brokenDigest());
685
+
686
+ await reconcilePluginSchedules();
687
+ expect(emittedSignals).toHaveLength(1);
688
+ expect(emittedSignals[0]!.sourceEventName).toBe(
689
+ "schedule.definition_error",
690
+ );
691
+
692
+ await reconcilePluginSchedules();
693
+ await reconcilePluginSchedules();
694
+
695
+ expect(emittedSignals).toHaveLength(1);
696
+ });
697
+
698
+ test("a pipeline-failed error emit does not latch the day guard; the next pass retries", async () => {
699
+ writePlugin("news", brokenDigest());
700
+ emitResultOverride = {
701
+ dispatched: false,
702
+ pipelineFailed: true,
703
+ reason: "Signal pipeline failed: transient outage",
704
+ };
705
+
706
+ await reconcilePluginSchedules();
707
+ expect(emittedSignals).toHaveLength(1);
708
+
709
+ emitResultOverride = null;
710
+ await reconcilePluginSchedules();
711
+ expect(emittedSignals).toHaveLength(2);
712
+
713
+ // The completed retry latched the guard, so further passes stay quiet.
714
+ await reconcilePluginSchedules();
715
+ expect(emittedSignals).toHaveLength(2);
716
+ });
717
+
718
+ test("an error emit still in flight is not duplicated by the next pass", async () => {
719
+ writePlugin("news", brokenDigest());
720
+ let releaseGate!: () => void;
721
+ emitGate = new Promise<void>((resolve) => {
722
+ releaseGate = resolve;
723
+ });
724
+ const errors = () =>
725
+ emittedSignals.filter(
726
+ (s) => s.sourceEventName === "schedule.definition_error",
727
+ );
728
+
729
+ // The first pass starts the emit and returns without awaiting it.
730
+ await reconcilePluginSchedules();
731
+ expect(errors()).toHaveLength(1);
732
+
733
+ // A second attempt while the first is still evaluating would hit
734
+ // event-store dedupe and latch the day guard on that verdict, silencing
735
+ // the retry the first attempt still needs if it goes on to fail.
736
+ await reconcilePluginSchedules();
737
+ expect(errors()).toHaveLength(1);
738
+
739
+ // The held attempt fails, so the day guard stays unlatched.
740
+ emitResultOverride = {
741
+ dispatched: false,
742
+ pipelineFailed: true,
743
+ reason: "Signal pipeline failed: transient outage",
744
+ };
745
+ emitGate = null;
746
+ releaseGate();
747
+ await settleEmits();
748
+ expect(errors()).toHaveLength(1);
749
+
750
+ emitResultOverride = null;
751
+ await reconcilePluginSchedules();
752
+ expect(errors()).toHaveLength(2);
753
+
754
+ // Verdict reached: the day guard latches and further passes stay quiet.
755
+ await reconcilePluginSchedules();
756
+ expect(errors()).toHaveLength(2);
757
+ });
758
+
759
+ test("a pipeline-failed declared emit is retried on later passes until a verdict", async () => {
760
+ writePlugin("news", digest());
761
+ emitResultOverride = {
762
+ dispatched: false,
763
+ pipelineFailed: true,
764
+ reason: "Signal pipeline failed: transient outage",
765
+ };
766
+ const declared = () =>
767
+ emittedSignals.filter((s) => s.sourceEventName === "schedule.declared");
768
+
769
+ await reconcilePluginSchedules();
770
+ expect(declared()).toHaveLength(1);
771
+
772
+ // The row now exists with the current hash, so without the pending
773
+ // retry this pass would be silent and the consent notification lost.
774
+ emitResultOverride = null;
775
+ await reconcilePluginSchedules();
776
+ expect(declared()).toHaveLength(2);
777
+
778
+ // Verdict reached: further passes stay quiet.
779
+ await reconcilePluginSchedules();
780
+ expect(declared()).toHaveLength(2);
781
+ });
782
+
783
+ test("a declared emit still in flight is not duplicated by the next pass", async () => {
784
+ writePlugin("news", digest());
785
+ let releaseGate!: () => void;
786
+ emitGate = new Promise<void>((resolve) => {
787
+ releaseGate = resolve;
788
+ });
789
+ const declared = () =>
790
+ emittedSignals.filter((s) => s.sourceEventName === "schedule.declared");
791
+
792
+ // The first pass starts the emit and returns without awaiting it.
793
+ await reconcilePluginSchedules();
794
+ expect(declared()).toHaveLength(1);
795
+
796
+ // A second attempt while the first is still evaluating would hit
797
+ // event-store dedupe, resolve as a verdict of its own, and clear the
798
+ // pending marker the first attempt still owns.
799
+ await reconcilePluginSchedules();
800
+ expect(declared()).toHaveLength(1);
801
+
802
+ // The held attempt fails, so the marker it owns survives.
803
+ emitResultOverride = {
804
+ dispatched: false,
805
+ pipelineFailed: true,
806
+ reason: "Signal pipeline failed: transient outage",
807
+ };
808
+ emitGate = null;
809
+ releaseGate();
810
+ await settleEmits();
811
+ expect(declared()).toHaveLength(1);
812
+
813
+ emitResultOverride = null;
814
+ await reconcilePluginSchedules();
815
+ expect(declared()).toHaveLength(2);
816
+
817
+ // Verdict reached: further passes stay quiet.
818
+ await reconcilePluginSchedules();
819
+ expect(declared()).toHaveLength(2);
820
+ });
821
+
822
+ test("a pipeline-failed definition_changed emit is retried on later passes until a verdict", async () => {
823
+ writePlugin("news", digest());
824
+ await reconcilePluginSchedules();
825
+ emittedSignals.length = 0;
826
+ const changed = () =>
827
+ emittedSignals.filter(
828
+ (s) => s.sourceEventName === "schedule.definition_changed",
829
+ );
830
+
831
+ emitResultOverride = {
832
+ dispatched: false,
833
+ pipelineFailed: true,
834
+ reason: "Signal pipeline failed: transient outage",
835
+ };
836
+ writePlugin("news", digest("Summarize the WEEK."));
837
+ await reconcilePluginSchedules();
838
+ expect(changed()).toHaveLength(1);
839
+
840
+ // The new hash is already stored, so without the pending retry this pass
841
+ // would be silent and the change notice lost.
842
+ emitResultOverride = null;
843
+ await reconcilePluginSchedules();
844
+ expect(changed()).toHaveLength(2);
845
+
846
+ // Verdict reached: further passes stay quiet.
847
+ await reconcilePluginSchedules();
848
+ expect(changed()).toHaveLength(2);
849
+ });
850
+
851
+ test("a completed bounded recurrence stays silent across passes", async () => {
852
+ writePlugin("news", digest());
853
+ await reconcilePluginSchedules();
854
+ const created = listDeclaredSchedules()[0]!;
855
+
856
+ // The engine latches an exhausted recurrence in one write: disabled,
857
+ // next run zeroed, last run stamped.
858
+ getRawDb().run(
859
+ "UPDATE cron_jobs SET enabled = 0, next_run_at = 0, last_run_at = ? WHERE id = ?",
860
+ [Date.now(), created.id],
861
+ );
862
+ const latched = rawJob(created.id);
863
+ emittedSignals.length = 0;
864
+
865
+ // The declaration is unchanged in spirit: its last occurrence simply
866
+ // passed, so the parser now reports it as ended.
867
+ writePlugin("news", endedDigest());
868
+ await reconcilePluginSchedules();
869
+ await reconcilePluginSchedules();
870
+
871
+ expect(rawJob(created.id)).toEqual(latched);
872
+ expect(emittedSignals).toHaveLength(0);
873
+ });
874
+
875
+ test("an ended recurrence on a still-armed row surfaces the error", async () => {
876
+ writePlugin("news", digest());
877
+ await reconcilePluginSchedules();
878
+ expect(listDeclaredSchedules()[0]!.enabled).toBe(true);
879
+ emittedSignals.length = 0;
880
+
881
+ // The armed row expects more firings; an upgrade that ends the
882
+ // recurrence stops them, which the user has to hear about.
883
+ writePlugin("news", endedDigest());
884
+ await reconcilePluginSchedules();
885
+
886
+ expect(emittedSignals).toHaveLength(1);
887
+ expect(emittedSignals[0]!.sourceEventName).toBe(
888
+ "schedule.definition_error",
889
+ );
890
+ });
891
+
892
+ test("an ended recurrence on a row this pass disarmed still surfaces", async () => {
893
+ const dir = writePlugin("news", digest());
894
+ await reconcilePluginSchedules();
895
+ const created = listDeclaredSchedules()[0]!;
896
+
897
+ // Disabling the plugin disarms the row without the engine latching it:
898
+ // `enabled` goes false, the run clock is untouched.
899
+ writeFileSync(join(dir, ".disabled"), "");
900
+ await reconcilePluginSchedules();
901
+ const disarmed = listDeclaredSchedules()[0]!;
902
+ expect(disarmed.enabled).toBe(false);
903
+ expect(disarmed.lastRunAt).toBeNull();
904
+ expect(disarmed.userEnabled).toBeNull();
905
+ emittedSignals.length = 0;
906
+
907
+ // The recurrence runs out while the plugin is off. Re-enabling brings back
908
+ // a schedule that can never fire, so the user has to hear about it.
909
+ writePlugin("news", endedDigest());
910
+ rmSync(join(dir, ".disabled"));
911
+ await reconcilePluginSchedules();
912
+
913
+ expect(listDeclaredSchedules()[0]!.id).toBe(created.id);
914
+ expect(emittedSignals).toHaveLength(1);
915
+ expect(emittedSignals[0]!.sourceEventName).toBe(
916
+ "schedule.definition_error",
917
+ );
918
+ });
919
+
920
+ test("a user-disabled row keeps an ended recurrence quiet", async () => {
921
+ writePlugin("news", digest());
922
+ await reconcilePluginSchedules();
923
+ const created = listDeclaredSchedules()[0]!;
924
+ await setUserEnabled(created.id, false);
925
+ emittedSignals.length = 0;
926
+
927
+ // The user turned the schedule off, so its recurrence running out costs
928
+ // no firing they expected.
929
+ writePlugin("news", endedDigest());
930
+ await reconcilePluginSchedules();
931
+
932
+ expect(emittedSignals).toHaveLength(0);
933
+ });
934
+
935
+ test("a declaration that shipped disabled keeps an ended recurrence quiet", async () => {
936
+ writePlugin("news", disabledDigest("20990101T090000Z"));
937
+ await reconcilePluginSchedules();
938
+
939
+ const created = listDeclaredSchedules()[0]!;
940
+ expect(created.enabled).toBe(false);
941
+ expect(created.nextRunAt).toBe(0);
942
+ expect(created.lastRunAt).toBeNull();
943
+ expect(created.userEnabled).toBeNull();
944
+ emittedSignals.length = 0;
945
+
946
+ // The row was never armed, so the recurrence running out costs no firing
947
+ // the user was going to get. Telling them every day about a schedule they
948
+ // never turned on is noise.
949
+ writePlugin("news", disabledDigest("20200101T090000Z"));
950
+ await reconcilePluginSchedules();
951
+ await reconcilePluginSchedules();
952
+
953
+ expect(emittedSignals).toHaveLength(0);
954
+ });
955
+
956
+ test("a freshly installed declaration that is already expired errors", async () => {
957
+ writePlugin("news", endedDigest());
958
+
959
+ await reconcilePluginSchedules();
960
+
961
+ expect(listDeclaredSchedules()).toHaveLength(0);
962
+ expect(emittedSignals).toHaveLength(1);
963
+ expect(emittedSignals[0]!.sourceEventName).toBe(
964
+ "schedule.definition_error",
965
+ );
966
+ const payload = emittedSignals[0]!.contextPayload as Record<
967
+ string,
968
+ unknown
969
+ >;
970
+ expect(payload.reason).toContain("no upcoming occurrences");
971
+ });
972
+
973
+ test("a suppressed error emit still latches the day guard", async () => {
974
+ writePlugin("news", brokenDigest());
975
+ emitResultOverride = {
976
+ dispatched: false,
977
+ pipelineFailed: false,
978
+ reason: "Signal blocked by deterministic checks: quiet hours",
979
+ };
980
+
981
+ await reconcilePluginSchedules();
982
+ await reconcilePluginSchedules();
983
+
984
+ expect(emittedSignals).toHaveLength(1);
985
+ });
986
+ });
987
+
988
+ // ── Feature-flag kill switch ────────────────────────────────────────────
989
+
990
+ describe("reconcilePluginSchedules under the plugin-schedules flag", () => {
991
+ beforeEach(() => {
992
+ resetReconcilerFixtures();
993
+ });
994
+
995
+ test("a flag-off pass disarms armed declared rows and creates nothing", async () => {
996
+ setOverridesForTesting({ "plugin-schedules": true });
997
+ writePlugin("news", digest());
998
+ await reconcilePluginSchedules();
999
+ const armed = listDeclaredSchedules();
1000
+ expect(armed).toHaveLength(1);
1001
+ expect(armed[0]!.enabled).toBe(true);
1002
+ emittedSignals.length = 0;
1003
+
1004
+ setOverridesForTesting({ "plugin-schedules": false });
1005
+ // A second plugin lands while the feature is off, so the pass has both a
1006
+ // row to disarm and a declaration it must refuse to pick up.
1007
+ writePlugin("weather", {
1008
+ "forecast/config.json": JSON.stringify({ expression: "* * * * *" }),
1009
+ "forecast/index.md": "Forecast the day.\n",
1010
+ });
1011
+ await reconcilePluginSchedules();
1012
+
1013
+ const rows = listDeclaredSchedules();
1014
+ expect(rows).toHaveLength(1);
1015
+ expect(rows[0]!.id).toBe(armed[0]!.id);
1016
+ expect(rows[0]!.enabled).toBe(false);
1017
+ expect(emittedSignals).toHaveLength(0);
1018
+ });
1019
+
1020
+ test("a flag-off pass emits nothing for a broken declaration", async () => {
1021
+ setOverridesForTesting({ "plugin-schedules": false });
1022
+ writePlugin("news", brokenDigest());
1023
+
1024
+ await reconcilePluginSchedules();
1025
+
1026
+ expect(listDeclaredSchedules()).toHaveLength(0);
1027
+ expect(emittedSignals).toHaveLength(0);
1028
+ });
1029
+
1030
+ test("turning the flag back on re-arms the rows it disarmed", async () => {
1031
+ setOverridesForTesting({ "plugin-schedules": true });
1032
+ writePlugin("news", digest());
1033
+ await reconcilePluginSchedules();
1034
+ const created = listDeclaredSchedules()[0]!;
1035
+
1036
+ setOverridesForTesting({ "plugin-schedules": false });
1037
+ await reconcilePluginSchedules();
1038
+ expect(listDeclaredSchedules()[0]!.enabled).toBe(false);
1039
+
1040
+ setOverridesForTesting({ "plugin-schedules": true });
1041
+ await reconcilePluginSchedules();
1042
+
1043
+ const rearmed = listDeclaredSchedules();
1044
+ expect(rearmed).toHaveLength(1);
1045
+ expect(rearmed[0]!.id).toBe(created.id);
1046
+ expect(rearmed[0]!.enabled).toBe(true);
1047
+ expect(rearmed[0]!.definitionHash).toBe(created.definitionHash);
1048
+ });
1049
+
1050
+ test("a user's off choice survives the flag going off and back on", async () => {
1051
+ setOverridesForTesting({ "plugin-schedules": true });
1052
+ writePlugin("news", digest());
1053
+ await reconcilePluginSchedules();
1054
+ const created = listDeclaredSchedules()[0]!;
1055
+ await setUserEnabled(created.id, false);
1056
+
1057
+ setOverridesForTesting({ "plugin-schedules": false });
1058
+ await reconcilePluginSchedules();
1059
+ setOverridesForTesting({ "plugin-schedules": true });
1060
+ await reconcilePluginSchedules();
1061
+
1062
+ const row = listDeclaredSchedules()[0]!;
1063
+ expect(row.userEnabled).toBe(false);
1064
+ expect(row.enabled).toBe(false);
1065
+ });
1066
+ });