@vellumai/assistant 0.10.1 → 0.10.2-dev.202606241651.2d2b40d

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 (367) hide show
  1. package/docs/workspace-tools.md +42 -33
  2. package/eslint-rules/cli-no-daemon-internals.js +6 -0
  3. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +91 -0
  4. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +31 -0
  5. package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +48 -0
  6. package/node_modules/@vellumai/gateway-client/src/index.ts +14 -0
  7. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +17 -0
  8. package/openapi.yaml +74 -1
  9. package/package.json +1 -1
  10. package/scripts/test.sh +36 -15
  11. package/src/__tests__/actor-token-service.test.ts +36 -14
  12. package/src/__tests__/agent-loop-override-profile.test.ts +1 -0
  13. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  14. package/src/__tests__/agent-wake-override-profile.test.ts +2 -0
  15. package/src/__tests__/annotate-activity-metadata.test.ts +2 -0
  16. package/src/__tests__/annotate-risk-options.test.ts +2 -0
  17. package/src/__tests__/approval-cascade.test.ts +2 -0
  18. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -0
  19. package/src/__tests__/btw-routes.test.ts +2 -0
  20. package/src/__tests__/build-persisted-content.test.ts +2 -0
  21. package/src/__tests__/call-controller.test.ts +19 -0
  22. package/src/__tests__/channel-guardian.test.ts +94 -58
  23. package/src/__tests__/channel-reply-delivery.test.ts +2 -0
  24. package/src/__tests__/compaction-events.test.ts +2 -0
  25. package/src/__tests__/compaction.benchmark.test.ts +2 -0
  26. package/src/__tests__/compactor-call-site-logging.test.ts +2 -0
  27. package/src/__tests__/compactor-low-watermark-cut.test.ts +2 -0
  28. package/src/__tests__/compactor-preserved-tail-count.test.ts +2 -0
  29. package/src/__tests__/compactor-summary-call-truncation.test.ts +2 -0
  30. package/src/__tests__/compactor-web-search-strip.test.ts +2 -0
  31. package/src/__tests__/computer-use-tools.test.ts +13 -0
  32. package/src/__tests__/config-loader-backfill.test.ts +5 -1
  33. package/src/__tests__/config-schema.test.ts +1 -0
  34. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +31 -29
  35. package/src/__tests__/contacts-relay-reads.test.ts +13 -15
  36. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  37. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +2 -0
  38. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  39. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  40. package/src/__tests__/conversation-agent-loop.test.ts +7 -0
  41. package/src/__tests__/conversation-analysis-routes.test.ts +2 -0
  42. package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -0
  43. package/src/__tests__/conversation-confirmation-signals.test.ts +2 -0
  44. package/src/__tests__/conversation-history-web-search.test.ts +2 -0
  45. package/src/__tests__/conversation-load-history-repair.test.ts +2 -0
  46. package/src/__tests__/conversation-load-history-stripped.test.ts +2 -0
  47. package/src/__tests__/conversation-pairing.test.ts +2 -0
  48. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
  49. package/src/__tests__/conversation-process-callsite.test.ts +2 -0
  50. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  51. package/src/__tests__/conversation-queue.test.ts +91 -0
  52. package/src/__tests__/conversation-routes-guardian-reply.test.ts +14 -0
  53. package/src/__tests__/conversation-routes-slash-commands.test.ts +14 -0
  54. package/src/__tests__/conversation-slash-queue.test.ts +2 -0
  55. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  56. package/src/__tests__/conversation-speed-override.test.ts +2 -0
  57. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +65 -0
  58. package/src/__tests__/conversation-title-service.test.ts +2 -0
  59. package/src/__tests__/conversation-tool-setup-attribution.test.ts +47 -0
  60. package/src/__tests__/conversation-usage.test.ts +2 -0
  61. package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -0
  62. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  63. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  64. package/src/__tests__/credential-security-invariants.test.ts +0 -1
  65. package/src/__tests__/db-migration-rollback.test.ts +205 -171
  66. package/src/__tests__/db-test-helpers.ts +5 -4
  67. package/src/__tests__/deterministic-verification-control-plane.test.ts +4 -2
  68. package/src/__tests__/disk-pressure-guard.test.ts +41 -0
  69. package/src/__tests__/dm-persistence.test.ts +2 -0
  70. package/src/__tests__/emit-signal-routing-intent.test.ts +10 -5
  71. package/src/__tests__/events-dev-bypass-actor.test.ts +7 -1
  72. package/src/__tests__/filing-service.test.ts +2 -0
  73. package/src/__tests__/guardian-binding-drift-heal.test.ts +75 -10
  74. package/src/__tests__/guardian-dispatch.test.ts +95 -1
  75. package/src/__tests__/guardian-outbound-http.test.ts +13 -0
  76. package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
  77. package/src/__tests__/heartbeat-service.test.ts +2 -0
  78. package/src/__tests__/helpers/channel-test-adapter.ts +1 -7
  79. package/src/__tests__/host-app-control-routes.test.ts +24 -30
  80. package/src/__tests__/host-bash-routes.test.ts +31 -41
  81. package/src/__tests__/host-browser-routes.test.ts +26 -32
  82. package/src/__tests__/host-cu-proxy.test.ts +299 -0
  83. package/src/__tests__/host-cu-routes-targeted.test.ts +25 -33
  84. package/src/__tests__/host-file-routes-targeted.test.ts +40 -52
  85. package/src/__tests__/host-transfer-routes-targeted.test.ts +31 -43
  86. package/src/__tests__/http-user-message-parity.test.ts +167 -8
  87. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  88. package/src/__tests__/invite-redemption-service.test.ts +43 -0
  89. package/src/__tests__/llm-context-normalization.test.ts +105 -0
  90. package/src/__tests__/llm-usage-store.test.ts +25 -0
  91. package/src/__tests__/media-stream-server-integration.test.ts +127 -0
  92. package/src/__tests__/memory-retrieval-hook.test.ts +2 -0
  93. package/src/__tests__/messaging-send-tool.test.ts +2 -0
  94. package/src/__tests__/migration-import-from-url.test.ts +2 -2
  95. package/src/__tests__/native-web-search.test.ts +2 -0
  96. package/src/__tests__/non-member-access-request.test.ts +189 -17
  97. package/src/__tests__/notification-broadcaster.test.ts +4 -0
  98. package/src/__tests__/notification-decision-recipient-context.test.ts +33 -32
  99. package/src/__tests__/notification-deep-link.test.ts +6 -0
  100. package/src/__tests__/notification-guardian-path.test.ts +19 -0
  101. package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
  102. package/src/__tests__/pending-interactions-resolved-event.test.ts +7 -4
  103. package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
  104. package/src/__tests__/plugin-bootstrap.test.ts +3 -73
  105. package/src/__tests__/plugin-route-contribution.test.ts +4 -17
  106. package/src/__tests__/plugin-tool-contribution.test.ts +3 -18
  107. package/src/__tests__/plugin-types.test.ts +0 -2
  108. package/src/__tests__/process-message-background-slack.test.ts +2 -0
  109. package/src/__tests__/process-message-display-content.test.ts +2 -0
  110. package/src/__tests__/provider-usage-tracking.test.ts +39 -0
  111. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +2 -0
  112. package/src/__tests__/registry.test.ts +3 -0
  113. package/src/__tests__/relay-server.test.ts +694 -25
  114. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  115. package/src/__tests__/secret-ingress-http.test.ts +14 -0
  116. package/src/__tests__/send-endpoint-busy.test.ts +30 -8
  117. package/src/__tests__/skills.test.ts +44 -0
  118. package/src/__tests__/slack-inbound-verification.test.ts +47 -2
  119. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +102 -0
  120. package/src/__tests__/steer-on-enqueue-question.test.ts +181 -0
  121. package/src/__tests__/stt-hints.test.ts +44 -13
  122. package/src/__tests__/subagent-detail.test.ts +27 -0
  123. package/src/__tests__/subagent-disposal.test.ts +65 -0
  124. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  125. package/src/__tests__/subagent-spawn-tool-fork.test.ts +2 -0
  126. package/src/__tests__/subagent-tools.test.ts +2 -0
  127. package/src/__tests__/suggestion-routes.test.ts +2 -0
  128. package/src/__tests__/title-generate-hook.test.ts +2 -0
  129. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
  130. package/src/__tests__/tool-executor.test.ts +16 -11
  131. package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
  132. package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
  133. package/src/__tests__/tool-start-timestamp.test.ts +2 -0
  134. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +10 -10
  135. package/src/__tests__/twilio-routes.test.ts +96 -0
  136. package/src/__tests__/verification-control-plane-policy.test.ts +2 -0
  137. package/src/__tests__/web-search-backend-failure.test.ts +2 -0
  138. package/src/__tests__/workspace-tool-loader.test.ts +195 -2
  139. package/src/agent/loop-exclusive-tool.test.ts +150 -0
  140. package/src/agent/loop.ts +56 -0
  141. package/src/api/constants/sse-replay.ts +41 -0
  142. package/src/api/index.ts +6 -0
  143. package/src/api/responses/llm-request-log-entry.ts +25 -0
  144. package/src/api/responses/subagent-detail.ts +17 -0
  145. package/src/calls/__tests__/relay-setup-router.test.ts +262 -4
  146. package/src/calls/call-domain.ts +3 -3
  147. package/src/calls/guardian-dispatch.ts +10 -8
  148. package/src/calls/inbound-trust-reader.ts +17 -1
  149. package/src/calls/media-stream-server.ts +21 -0
  150. package/src/calls/relay-server.ts +167 -50
  151. package/src/calls/relay-setup-router.ts +37 -7
  152. package/src/calls/relay-verification.ts +4 -4
  153. package/src/calls/stt-hints.ts +9 -12
  154. package/src/calls/twilio-routes.ts +14 -4
  155. package/src/cli/commands/__tests__/cache.test.ts +8 -1
  156. package/src/cli/commands/cache.ts +194 -181
  157. package/src/cli/commands/db/__tests__/repair.test.ts +6 -5
  158. package/src/cli/commands/db/status.ts +37 -1
  159. package/src/cli/commands/mcp.ts +252 -218
  160. package/src/cli/commands/memory/__tests__/worker.test.ts +302 -0
  161. package/src/cli/commands/memory/index.ts +2 -0
  162. package/src/cli/commands/memory/worker.ts +175 -0
  163. package/src/cli/commands/plugins.ts +75 -3
  164. package/src/cli/lib/__tests__/install-from-github.test.ts +102 -0
  165. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +160 -1
  166. package/src/cli/lib/list-installed-plugins.ts +179 -1
  167. package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +143 -0
  168. package/src/config/bundled-skills/computer-use/TOOLS.json +6 -1
  169. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +27 -17
  170. package/src/config/bundled-skills/contacts/tools/contact-search.ts +13 -3
  171. package/src/config/feature-flag-registry.json +0 -8
  172. package/src/config/loader.ts +36 -5
  173. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  174. package/src/config/schemas/memory-lifecycle.ts +12 -0
  175. package/src/config/schemas/memory-v3.ts +7 -0
  176. package/src/config/schemas/memory.ts +4 -0
  177. package/src/config/schemas/timeouts.ts +8 -0
  178. package/src/config/seed-inference-profiles.ts +14 -5
  179. package/src/config/skills.ts +27 -5
  180. package/src/contacts/__tests__/guardian-delivery-reader.test.ts +312 -0
  181. package/src/contacts/contacts-write.ts +3 -0
  182. package/src/contacts/guardian-delivery-reader.ts +223 -0
  183. package/src/daemon/conversation-agent-loop.ts +9 -0
  184. package/src/daemon/conversation-process.ts +39 -17
  185. package/src/daemon/conversation-surfaces.ts +8 -0
  186. package/src/daemon/conversation-tool-setup.ts +49 -16
  187. package/src/daemon/conversation.ts +21 -2
  188. package/src/daemon/disk-pressure-guard.ts +12 -2
  189. package/src/daemon/event-loop-watchdog.ts +28 -1
  190. package/src/daemon/external-plugins-bootstrap.ts +4 -34
  191. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +25 -0
  192. package/src/daemon/handlers/__tests__/config-channels.test.ts +225 -0
  193. package/src/daemon/handlers/config-a2a.ts +6 -14
  194. package/src/daemon/handlers/config-channels.ts +78 -22
  195. package/src/daemon/handlers/conversations.ts +77 -0
  196. package/src/daemon/host-cu-proxy.ts +102 -11
  197. package/src/daemon/lifecycle.ts +4 -0
  198. package/src/daemon/memory-v2-startup.test.ts +72 -0
  199. package/src/daemon/memory-v2-startup.ts +87 -19
  200. package/src/daemon/server.ts +0 -4
  201. package/src/daemon/shutdown-handlers.ts +20 -0
  202. package/src/daemon/tool-setup-types.ts +9 -0
  203. package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
  204. package/src/ipc/assistant-server.ts +2 -2
  205. package/src/memory/__tests__/301-create-watchdog-events.test.ts +110 -0
  206. package/src/memory/__tests__/memory-retrospective-job.test.ts +8 -0
  207. package/src/memory/__tests__/prompt-override.test.ts +192 -0
  208. package/src/memory/__tests__/watchdog-events-store.test.ts +161 -0
  209. package/src/memory/conversation-crud.ts +38 -0
  210. package/src/memory/db-connection.ts +22 -3
  211. package/src/memory/db-init.ts +36 -502
  212. package/src/memory/db-singleton.ts +6 -4
  213. package/src/memory/jobs-worker.ts +58 -0
  214. package/src/memory/llm-usage-store.ts +48 -20
  215. package/src/memory/memory-retrospective-job.ts +9 -8
  216. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +13 -3
  217. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -27
  218. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +130 -56
  219. package/src/memory/migrations/300-add-processing-started-at.ts +30 -0
  220. package/src/memory/migrations/301-create-watchdog-events.ts +45 -0
  221. package/src/memory/migrations/__tests__/014-backfill-inbox-thread-state.test.ts +108 -0
  222. package/src/memory/migrations/__tests__/136-drop-assistant-id-columns.test.ts +82 -0
  223. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +224 -0
  224. package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
  225. package/src/memory/migrations/run-migrations.ts +90 -6
  226. package/src/memory/migrations/schema-introspection.ts +14 -0
  227. package/src/memory/migrations/validate-migration-state.ts +101 -66
  228. package/src/memory/prompt-override.ts +129 -0
  229. package/src/memory/schema/conversations.ts +9 -0
  230. package/src/memory/schema/infrastructure.ts +20 -0
  231. package/src/memory/steps.ts +573 -0
  232. package/src/memory/v2/__tests__/cli-command-store.test.ts +25 -0
  233. package/src/memory/v2/__tests__/skill-store.test.ts +80 -0
  234. package/src/memory/v2/cli-command-store.ts +75 -38
  235. package/src/memory/v2/prompts/consolidation.ts +13 -82
  236. package/src/memory/v2/prompts/router.ts +21 -93
  237. package/src/memory/v2/skill-store.ts +68 -31
  238. package/src/memory/watchdog-events-store.ts +87 -0
  239. package/src/memory/worker-control.ts +118 -0
  240. package/src/memory/worker-process.ts +72 -0
  241. package/src/notifications/__tests__/broadcaster.test.ts +16 -8
  242. package/src/notifications/__tests__/connected-channels.test.ts +114 -0
  243. package/src/notifications/__tests__/decision-engine.test.ts +78 -9
  244. package/src/notifications/__tests__/destination-resolver.test.ts +256 -0
  245. package/src/notifications/broadcaster.ts +8 -1
  246. package/src/notifications/decision-engine.ts +15 -7
  247. package/src/notifications/destination-resolver.ts +68 -24
  248. package/src/notifications/emit-signal.ts +39 -14
  249. package/src/onboarding/checkin-event.test.ts +220 -0
  250. package/src/onboarding/checkin-event.ts +321 -0
  251. package/src/onboarding/schedule-checkin.ts +190 -0
  252. package/src/permissions/question-prompter.test.ts +1 -1
  253. package/src/permissions/question-prompter.ts +7 -4
  254. package/src/plugin-api/index.ts +6 -6
  255. package/src/plugin-api/types.ts +3 -5
  256. package/src/plugin-api/vision-support.test.ts +28 -4
  257. package/src/plugin-api/vision-support.ts +66 -31
  258. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +161 -0
  259. package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +106 -0
  260. package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +60 -0
  261. package/src/plugins/defaults/advisor/consult.ts +110 -6
  262. package/src/plugins/defaults/advisor/context-pack.ts +288 -0
  263. package/src/plugins/defaults/advisor/steering.ts +14 -2
  264. package/src/plugins/defaults/advisor/tools/advisor.ts +32 -5
  265. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +47 -7
  266. package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +10 -11
  267. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +12 -20
  268. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +42 -11
  269. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +11 -2
  270. package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +146 -0
  271. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +29 -1
  272. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +8 -1
  273. package/src/plugins/mtime-cache.ts +7 -2
  274. package/src/plugins/types.ts +0 -2
  275. package/src/providers/anthropic/client.ts +5 -0
  276. package/src/providers/call-site-routing.ts +4 -0
  277. package/src/providers/model-catalog.ts +16 -0
  278. package/src/providers/openai/responses-provider.ts +5 -0
  279. package/src/providers/openrouter/client.ts +5 -0
  280. package/src/providers/provider-send-message.ts +4 -0
  281. package/src/providers/ratelimit.ts +4 -0
  282. package/src/providers/retry.ts +4 -0
  283. package/src/providers/types.ts +9 -0
  284. package/src/providers/usage-tracking.ts +4 -0
  285. package/src/runtime/__tests__/channel-verification-service.test.ts +133 -0
  286. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +181 -0
  287. package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +66 -0
  288. package/src/runtime/__tests__/local-principal-trust.test.ts +164 -0
  289. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +335 -3
  290. package/src/runtime/access-request-helper.ts +19 -39
  291. package/src/runtime/actor-trust-resolver.ts +2 -2
  292. package/src/runtime/anchored-guardian.test.ts +156 -0
  293. package/src/runtime/anchored-guardian.ts +135 -0
  294. package/src/runtime/assistant-event-hub.ts +1 -1
  295. package/src/runtime/assistant-stream-state.ts +9 -2
  296. package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +99 -0
  297. package/src/runtime/auth/require-bound-guardian.ts +21 -11
  298. package/src/runtime/channel-verification-service.ts +56 -31
  299. package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
  300. package/src/runtime/guardian-vellum-migration.ts +66 -7
  301. package/src/runtime/invite-redemption-service.ts +50 -18
  302. package/src/runtime/local-actor-identity.ts +76 -11
  303. package/src/runtime/local-principal-trust.ts +52 -0
  304. package/src/runtime/pending-interactions.ts +11 -1
  305. package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +56 -5
  306. package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
  307. package/src/runtime/routes/__tests__/contact-routes.test.ts +212 -0
  308. package/src/runtime/routes/__tests__/global-search-routes.test.ts +93 -0
  309. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +215 -1
  310. package/src/runtime/routes/browser-routes.ts +1 -1
  311. package/src/runtime/routes/channel-verification-routes.ts +3 -3
  312. package/src/runtime/routes/contact-routes.ts +8 -32
  313. package/src/runtime/routes/conversation-cli-routes.ts +4 -5
  314. package/src/runtime/routes/conversation-list-routes.ts +4 -7
  315. package/src/runtime/routes/conversation-routes.ts +74 -81
  316. package/src/runtime/routes/events-routes.ts +2 -2
  317. package/src/runtime/routes/global-search-routes.ts +3 -1
  318. package/src/runtime/routes/guardian-action-routes.ts +4 -5
  319. package/src/runtime/routes/host-app-control-routes.ts +5 -4
  320. package/src/runtime/routes/host-bash-routes.ts +5 -4
  321. package/src/runtime/routes/host-browser-routes.ts +9 -11
  322. package/src/runtime/routes/host-cu-routes.ts +5 -4
  323. package/src/runtime/routes/host-file-routes.ts +5 -4
  324. package/src/runtime/routes/host-transfer-routes.ts +6 -6
  325. package/src/runtime/routes/http-adapter.ts +1 -1
  326. package/src/runtime/routes/identity-routes.ts +3 -2
  327. package/src/runtime/routes/inbound-message-handler.ts +5 -5
  328. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +97 -5
  329. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +61 -49
  330. package/src/runtime/routes/inbound-stages/background-dispatch.ts +16 -4
  331. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
  332. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +21 -8
  333. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -3
  334. package/src/runtime/routes/index.ts +2 -0
  335. package/src/runtime/routes/llm-context-normalization.ts +71 -0
  336. package/src/runtime/routes/mcp-auth-routes.ts +38 -15
  337. package/src/runtime/routes/migration-rollback-routes.ts +4 -3
  338. package/src/runtime/routes/migration-routes.ts +4 -1
  339. package/src/runtime/routes/onboarding-checkin-routes.ts +86 -0
  340. package/src/runtime/routes/subagents-routes.ts +5 -0
  341. package/src/runtime/routes/surface-action-routes.ts +51 -55
  342. package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
  343. package/src/runtime/services/conversation-serializer.ts +7 -9
  344. package/src/runtime/tool-grant-request-helper.ts +3 -3
  345. package/src/runtime/trust-verdict-consumer.ts +85 -9
  346. package/src/runtime/verification-outbound-actions.ts +18 -18
  347. package/src/signals/user-message.ts +16 -0
  348. package/src/subagent/manager.ts +9 -0
  349. package/src/telemetry/types.ts +34 -1
  350. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  351. package/src/telemetry/usage-telemetry-reporter.ts +87 -3
  352. package/src/tools/ask-question/ask-question-tool.test.ts +29 -0
  353. package/src/tools/ask-question/ask-question-tool.ts +13 -0
  354. package/src/tools/computer-use/definitions.ts +8 -2
  355. package/src/tools/executor.ts +4 -4
  356. package/src/tools/registry.ts +18 -0
  357. package/src/tools/tool-approval-handler.ts +1 -1
  358. package/src/tools/tool-defaults.ts +9 -2
  359. package/src/tools/types.ts +17 -2
  360. package/src/tools/workspace-tools/loader.ts +348 -244
  361. package/src/util/platform.ts +5 -0
  362. package/src/util/telemetry-db-path.ts +24 -0
  363. package/src/workspace/migrations/017-seed-persona-dirs.ts +3 -34
  364. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +3 -24
  365. package/src/__tests__/workspace-tools-watcher-flag.test.ts +0 -70
  366. package/src/daemon/workspace-tools-watcher.ts +0 -328
  367. package/src/memory/migrations/registry.ts +0 -573
@@ -0,0 +1,45 @@
1
+ import { Database } from "bun:sqlite";
2
+
3
+ import { getTelemetryDbPath } from "../../util/telemetry-db-path.js";
4
+ import {
5
+ type DrizzleDb,
6
+ getTelemetrySqlite,
7
+ } from "../db-connection.js";
8
+
9
+ /**
10
+ * Create the `watchdog_events` table on the dedicated telemetry database
11
+ * (`assistant-telemetry.db`), not the main DB. The migration runner passes the
12
+ * main DrizzleDb, but this table lives on the telemetry connection, so we open
13
+ * that connection and run DDL against it directly. The dedicated connection
14
+ * itself performs no DDL on open, so this migration owns the schema.
15
+ *
16
+ * Idempotent (`IF NOT EXISTS`). Writes are gated on `share_analytics`
17
+ * consent at the store level, so opted-out rows never exist and the
18
+ * reporter's standard 0 watermark default is safe. The index backs the
19
+ * telemetry reporter's compound `(created_at, id)` watermark cursor.
20
+ *
21
+ * The `mainDb` parameter is accepted to satisfy the migration-step signature but
22
+ * is not used — this migration operates exclusively on the telemetry DB.
23
+ */
24
+ export function createWatchdogEventsTable(_mainDb: DrizzleDb): void {
25
+ let raw: Database | null = getTelemetrySqlite();
26
+ if (!raw) {
27
+ // The dedicated connection failed to open (logged by openDedicatedDb).
28
+ // Fall back to opening the file directly so the migration still runs —
29
+ // the singleton will pick it up on a later access. This mirrors the
30
+ // fail-soft pattern of the other dedicated-DB migrations.
31
+ raw = new Database(getTelemetryDbPath());
32
+ }
33
+ raw.exec(/*sql*/ `
34
+ CREATE TABLE IF NOT EXISTS watchdog_events (
35
+ id TEXT PRIMARY KEY,
36
+ created_at INTEGER NOT NULL,
37
+ check_name TEXT NOT NULL,
38
+ value REAL,
39
+ detail TEXT
40
+ )
41
+ `);
42
+ raw.exec(
43
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_watchdog_events_created_at_id ON watchdog_events (created_at, id)`,
44
+ );
45
+ }
@@ -0,0 +1,108 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { describe, expect, test } from "bun:test";
3
+
4
+ import { drizzle } from "drizzle-orm/bun-sqlite";
5
+
6
+ import { getSqliteFrom } from "../../db-connection.js";
7
+ import * as schema from "../../schema.js";
8
+ import { migrateBackfillInboxThreadStateFromBindings } from "../014-backfill-inbox-thread-state.js";
9
+
10
+ function createTestDb() {
11
+ const sqlite = new Database(":memory:");
12
+ sqlite.exec("PRAGMA journal_mode=WAL");
13
+ return drizzle(sqlite, { schema });
14
+ }
15
+
16
+ function bootstrap(raw: Database, opts: { withAssistantId: boolean }): void {
17
+ raw.exec(/*sql*/ `
18
+ CREATE TABLE memory_checkpoints (
19
+ key TEXT PRIMARY KEY,
20
+ value TEXT NOT NULL,
21
+ updated_at INTEGER NOT NULL
22
+ )
23
+ `);
24
+ raw.exec(/*sql*/ `
25
+ CREATE TABLE external_conversation_bindings (
26
+ conversation_id TEXT PRIMARY KEY,
27
+ source_channel TEXT NOT NULL,
28
+ external_chat_id TEXT NOT NULL,
29
+ external_user_id TEXT,
30
+ display_name TEXT,
31
+ username TEXT,
32
+ last_inbound_at INTEGER,
33
+ last_outbound_at INTEGER,
34
+ created_at INTEGER NOT NULL,
35
+ updated_at INTEGER NOT NULL
36
+ )
37
+ `);
38
+ raw.exec(/*sql*/ `
39
+ CREATE TABLE assistant_inbox_thread_state (
40
+ conversation_id TEXT PRIMARY KEY,
41
+ ${opts.withAssistantId ? "assistant_id TEXT NOT NULL DEFAULT 'self'," : ""}
42
+ source_channel TEXT NOT NULL,
43
+ external_chat_id TEXT NOT NULL,
44
+ external_user_id TEXT,
45
+ display_name TEXT,
46
+ username TEXT,
47
+ last_inbound_at INTEGER,
48
+ last_outbound_at INTEGER,
49
+ last_message_at INTEGER,
50
+ unread_count INTEGER NOT NULL DEFAULT 0,
51
+ pending_escalation_count INTEGER NOT NULL DEFAULT 0,
52
+ has_pending_escalation INTEGER NOT NULL DEFAULT 0,
53
+ created_at INTEGER NOT NULL,
54
+ updated_at INTEGER NOT NULL
55
+ )
56
+ `);
57
+ raw.run(
58
+ `INSERT INTO external_conversation_bindings
59
+ (conversation_id, source_channel, external_chat_id, external_user_id,
60
+ display_name, username, last_inbound_at, last_outbound_at,
61
+ created_at, updated_at)
62
+ VALUES ('c1','telegram','chat-1','U-1','Name One','one',1000,2000,500,2500)`,
63
+ );
64
+ }
65
+
66
+ describe("migration 014 — backfill inbox thread state", () => {
67
+ test("seeds rows when assistant_id column is present", () => {
68
+ const db = createTestDb();
69
+ const raw = getSqliteFrom(db);
70
+ bootstrap(raw, { withAssistantId: true });
71
+
72
+ migrateBackfillInboxThreadStateFromBindings(db);
73
+
74
+ const row = raw
75
+ .query(
76
+ `SELECT assistant_id, last_message_at FROM assistant_inbox_thread_state WHERE conversation_id = 'c1'`,
77
+ )
78
+ .get() as { assistant_id: string; last_message_at: number } | undefined;
79
+ expect(row?.assistant_id).toBe("self");
80
+ expect(row?.last_message_at).toBe(2000);
81
+ });
82
+
83
+ test("seeds rows without throwing when assistant_id column was dropped", () => {
84
+ const db = createTestDb();
85
+ const raw = getSqliteFrom(db);
86
+ // Simulate a DB where drop-assistant-id-columns already removed the column
87
+ // but the backfill checkpoint was never written.
88
+ bootstrap(raw, { withAssistantId: false });
89
+
90
+ expect(() => migrateBackfillInboxThreadStateFromBindings(db)).not.toThrow();
91
+
92
+ const count = raw
93
+ .query(`SELECT COUNT(*) AS n FROM assistant_inbox_thread_state`)
94
+ .get() as { n: number };
95
+ expect(count.n).toBe(1);
96
+ });
97
+
98
+ test("is a no-op once the checkpoint is recorded", () => {
99
+ const db = createTestDb();
100
+ const raw = getSqliteFrom(db);
101
+ bootstrap(raw, { withAssistantId: false });
102
+
103
+ migrateBackfillInboxThreadStateFromBindings(db);
104
+ // Removing the source rows would surface any second-run insert attempt.
105
+ raw.exec(`DELETE FROM external_conversation_bindings`);
106
+ expect(() => migrateBackfillInboxThreadStateFromBindings(db)).not.toThrow();
107
+ });
108
+ });
@@ -0,0 +1,82 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { describe, expect, test } from "bun:test";
3
+
4
+ import { drizzle } from "drizzle-orm/bun-sqlite";
5
+
6
+ import { getSqliteFrom } from "../../db-connection.js";
7
+ import * as schema from "../../schema.js";
8
+ import { migrateDropAssistantIdColumns } from "../136-drop-assistant-id-columns.js";
9
+
10
+ function createTestDb() {
11
+ const sqlite = new Database(":memory:");
12
+ sqlite.exec("PRAGMA journal_mode=WAL");
13
+ return drizzle(sqlite, { schema });
14
+ }
15
+
16
+ function indexExists(raw: Database, name: string): boolean {
17
+ return !!raw
18
+ .prepare(`SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ?`)
19
+ .get(name);
20
+ }
21
+
22
+ /**
23
+ * Create a minimal assistant_inbox_thread_state carrying assistant_id plus its
24
+ * composite index, so the column-drop + index-recreate path has something to
25
+ * act on.
26
+ */
27
+ function createInboxThreadState(raw: Database): void {
28
+ raw.exec(/*sql*/ `
29
+ CREATE TABLE assistant_inbox_thread_state (
30
+ conversation_id TEXT PRIMARY KEY,
31
+ assistant_id TEXT NOT NULL DEFAULT 'self',
32
+ source_channel TEXT NOT NULL,
33
+ external_chat_id TEXT NOT NULL,
34
+ last_message_at INTEGER,
35
+ has_pending_escalation INTEGER NOT NULL DEFAULT 0
36
+ )
37
+ `);
38
+ raw.exec(/*sql*/ `
39
+ CREATE UNIQUE INDEX idx_inbox_thread_state_channel
40
+ ON assistant_inbox_thread_state(assistant_id, source_channel, external_chat_id)
41
+ `);
42
+ }
43
+
44
+ describe("migration 136 — drop assistant_id columns", () => {
45
+ test("drops the column and recreates the index without assistant_id", () => {
46
+ const db = createTestDb();
47
+ const raw = getSqliteFrom(db);
48
+ createInboxThreadState(raw);
49
+
50
+ migrateDropAssistantIdColumns(db);
51
+
52
+ const cols = (
53
+ raw.query(`PRAGMA table_info(assistant_inbox_thread_state)`).all() as {
54
+ name: string;
55
+ }[]
56
+ ).map((c) => c.name);
57
+ expect(cols).not.toContain("assistant_id");
58
+ expect(indexExists(raw, "idx_inbox_thread_state_channel")).toBe(true);
59
+ });
60
+
61
+ test("does not throw when some scoped tables were already dropped", () => {
62
+ const db = createTestDb();
63
+ const raw = getSqliteFrom(db);
64
+ // Only the inbox table exists — actor_token_records and friends were
65
+ // removed by an earlier migration. Index recreation must skip the missing
66
+ // tables instead of failing with "no such table".
67
+ createInboxThreadState(raw);
68
+
69
+ expect(() => migrateDropAssistantIdColumns(db)).not.toThrow();
70
+ // Sanity: the index targeting the missing table was not created.
71
+ expect(indexExists(raw, "idx_actor_tokens_active_device")).toBe(false);
72
+ });
73
+
74
+ test("is idempotent across repeated runs", () => {
75
+ const db = createTestDb();
76
+ const raw = getSqliteFrom(db);
77
+ createInboxThreadState(raw);
78
+
79
+ migrateDropAssistantIdColumns(db);
80
+ expect(() => migrateDropAssistantIdColumns(db)).not.toThrow();
81
+ });
82
+ });
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Tests for migration 209 — stripping thinking / redacted_thinking blocks from
3
+ * persisted assistant messages.
4
+ *
5
+ * The migration rewrites content entirely inside SQLite (JSON1), dispatched a
6
+ * rowid window at a time through `runAsyncSqlite`. These tests drive the step
7
+ * directly against a real DB and assert the at-rest content, idempotency,
8
+ * scoping (assistant rows only), tolerance of malformed content, and that the
9
+ * persisted rowid watermark is honored for resume.
10
+ */
11
+ import { describe, expect, test } from "bun:test";
12
+
13
+ const { getDb, getSqlite } = await import("../../db-connection.js");
14
+ const { initializeDb } = await import("../../db-init.js");
15
+ const { migrateStripThinkingFromConsolidated } =
16
+ await import("../209-strip-thinking-from-consolidated.js");
17
+
18
+ await initializeDb();
19
+
20
+ const CONV = "conv-209";
21
+ getSqlite()
22
+ .query(
23
+ `INSERT OR IGNORE INTO conversations (id, created_at, updated_at) VALUES (?, ?, ?)`,
24
+ )
25
+ .run(CONV, Date.now(), Date.now());
26
+
27
+ let seq = 0;
28
+ function insert(role: string, content: string): { id: string; rowid: number } {
29
+ const id = `m209-${seq++}`;
30
+ getSqlite()
31
+ .query(
32
+ `INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES (?, ?, ?, ?, ?)`,
33
+ )
34
+ .run(id, CONV, role, content, Date.now());
35
+ const rowid = (
36
+ getSqlite()
37
+ .query(`SELECT rowid AS r FROM messages WHERE id = ?`)
38
+ .get(id) as { r: number }
39
+ ).r;
40
+ return { id, rowid };
41
+ }
42
+
43
+ function content(id: string): string {
44
+ return (
45
+ getSqlite().query(`SELECT content FROM messages WHERE id = ?`).get(id) as {
46
+ content: string;
47
+ }
48
+ ).content;
49
+ }
50
+
51
+ function blocks(id: string): Array<Record<string, unknown>> {
52
+ return JSON.parse(content(id));
53
+ }
54
+
55
+ describe("migration 209 — strip thinking from consolidated assistant messages", () => {
56
+ test("strips thinking blocks but keeps text and tool_use, preserving order", async () => {
57
+ const { id } = insert(
58
+ "assistant",
59
+ JSON.stringify([
60
+ { type: "thinking", thinking: "secret", signature: "sig" },
61
+ { type: "text", text: "hello" },
62
+ { type: "tool_use", id: "t1", name: "x", input: { a: 1 } },
63
+ ]),
64
+ );
65
+
66
+ await migrateStripThinkingFromConsolidated(getDb());
67
+
68
+ expect(blocks(id)).toEqual([
69
+ { type: "text", text: "hello" },
70
+ { type: "tool_use", id: "t1", name: "x", input: { a: 1 } },
71
+ ]);
72
+ });
73
+
74
+ test("strips redacted_thinking blocks", async () => {
75
+ const { id } = insert(
76
+ "assistant",
77
+ JSON.stringify([
78
+ { type: "redacted_thinking", data: "blob" },
79
+ { type: "text", text: "world" },
80
+ ]),
81
+ );
82
+
83
+ await migrateStripThinkingFromConsolidated(getDb());
84
+
85
+ expect(blocks(id)).toEqual([{ type: "text", text: "world" }]);
86
+ });
87
+
88
+ test("all-thinking message becomes the null-byte placeholder sentinel", async () => {
89
+ const { id } = insert(
90
+ "assistant",
91
+ JSON.stringify([
92
+ { type: "thinking", thinking: "a", signature: "s1" },
93
+ { type: "redacted_thinking", data: "b" },
94
+ ]),
95
+ );
96
+
97
+ await migrateStripThinkingFromConsolidated(getDb());
98
+
99
+ const result = blocks(id);
100
+ expect(result).toHaveLength(1);
101
+ expect(result[0].type).toBe("text");
102
+ expect(result[0].text).toBe("\x00__PLACEHOLDER__[internal blocks omitted]");
103
+ // The leading byte must be a literal NUL, produced by SQLite's char(0).
104
+ expect((result[0].text as string).charCodeAt(0)).toBe(0);
105
+ });
106
+
107
+ test("preserves blocks with a missing/null type", async () => {
108
+ const { id } = insert(
109
+ "assistant",
110
+ JSON.stringify([
111
+ { type: "thinking", thinking: "x", signature: "s" },
112
+ { foo: "bar" },
113
+ { type: "text", text: "kept" },
114
+ ]),
115
+ );
116
+
117
+ await migrateStripThinkingFromConsolidated(getDb());
118
+
119
+ expect(blocks(id)).toEqual([
120
+ { foo: "bar" },
121
+ { type: "text", text: "kept" },
122
+ ]);
123
+ });
124
+
125
+ test("leaves thinking-free assistant messages untouched", async () => {
126
+ const original = JSON.stringify([
127
+ { type: "text", text: "I was thinking about lunch" },
128
+ ]);
129
+ const { id } = insert("assistant", original);
130
+
131
+ await migrateStripThinkingFromConsolidated(getDb());
132
+
133
+ // Substring 'thinking' appears in the text but no block is of thinking type.
134
+ expect(content(id)).toBe(original);
135
+ });
136
+
137
+ test("does not touch non-assistant roles", async () => {
138
+ const original = JSON.stringify([
139
+ { type: "thinking", thinking: "x", signature: "s" },
140
+ { type: "text", text: "u" },
141
+ ]);
142
+ const { id } = insert("user", original);
143
+
144
+ await migrateStripThinkingFromConsolidated(getDb());
145
+
146
+ expect(content(id)).toBe(original);
147
+ });
148
+
149
+ test("tolerates non-array and invalid JSON content", async () => {
150
+ const obj = insert(
151
+ "assistant",
152
+ JSON.stringify({ type: "thinking", thinking: "x" }),
153
+ );
154
+ const invalid = insert("assistant", "{not json with thinking");
155
+
156
+ await migrateStripThinkingFromConsolidated(getDb());
157
+
158
+ expect(content(obj.id)).toBe(
159
+ JSON.stringify({ type: "thinking", thinking: "x" }),
160
+ );
161
+ expect(content(invalid.id)).toBe("{not json with thinking");
162
+ });
163
+
164
+ test("is idempotent across repeated runs", async () => {
165
+ const { id } = insert(
166
+ "assistant",
167
+ JSON.stringify([
168
+ { type: "thinking", thinking: "x", signature: "s" },
169
+ { type: "text", text: "stable" },
170
+ ]),
171
+ );
172
+
173
+ await migrateStripThinkingFromConsolidated(getDb());
174
+ const once = content(id);
175
+ await migrateStripThinkingFromConsolidated(getDb());
176
+ const twice = content(id);
177
+
178
+ expect(twice).toBe(once);
179
+ expect(blocks(id)).toEqual([{ type: "text", text: "stable" }]);
180
+ });
181
+
182
+ test("honors the persisted rowid watermark and resumes above it", async () => {
183
+ const below = insert(
184
+ "assistant",
185
+ JSON.stringify([
186
+ { type: "thinking", thinking: "x", signature: "s" },
187
+ { type: "text", text: "below" },
188
+ ]),
189
+ );
190
+ const above = insert(
191
+ "assistant",
192
+ JSON.stringify([
193
+ { type: "thinking", thinking: "y", signature: "s" },
194
+ { type: "text", text: "above" },
195
+ ]),
196
+ );
197
+
198
+ // Pretend a prior run already swept through `below`'s rowid: the sweep must
199
+ // resume strictly above it, leaving `below` untouched and cleaning `above`.
200
+ getSqlite()
201
+ .query(
202
+ `INSERT OR REPLACE INTO memory_checkpoints (key, value, updated_at) VALUES (?, ?, ?)`,
203
+ )
204
+ .run(
205
+ "migration_209_strip_thinking_watermark",
206
+ String(below.rowid),
207
+ Date.now(),
208
+ );
209
+
210
+ await migrateStripThinkingFromConsolidated(getDb());
211
+
212
+ expect(blocks(below.id)).toEqual([
213
+ { type: "thinking", thinking: "x", signature: "s" },
214
+ { type: "text", text: "below" },
215
+ ]);
216
+ expect(blocks(above.id)).toEqual([{ type: "text", text: "above" }]);
217
+
218
+ // The watermark is cleared once the sweep reaches the end of the table.
219
+ const wm = getSqlite()
220
+ .query(`SELECT value FROM memory_checkpoints WHERE key = ?`)
221
+ .get("migration_209_strip_thinking_watermark");
222
+ expect(wm).toBeNull();
223
+ });
224
+ });
@@ -54,7 +54,7 @@ describe("runMigrationSteps — checkpointing", () => {
54
54
 
55
55
  test("records step completions in the shared memory_checkpoints ledger", async () => {
56
56
  /**
57
- * Step bookkeeping lives in the same memory_checkpoints table the registry
57
+ * Step bookkeeping lives in the same memory_checkpoints table the step runner
58
58
  * uses, under the `step:` namespace — one ledger for all applied state.
59
59
  */
60
60
 
@@ -110,7 +110,7 @@ describe("runMigrationSteps — checkpointing", () => {
110
110
  * before the loop so a migration interrupted mid-flight re-runs this boot.
111
111
  */
112
112
 
113
- // GIVEN a database with a stalled registry checkpoint left by a crash
113
+ // GIVEN a database with a stalled step checkpoint left by a crash
114
114
  const db = createTestDb();
115
115
  const raw = getSqliteFrom(db);
116
116
  raw.run(
@@ -4,10 +4,45 @@ import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";
4
4
 
5
5
  const log = getLogger("db-init");
6
6
 
7
+ /**
8
+ * Rollback metadata for a migration step that can be reversed.
9
+ *
10
+ * A single step may carry multiple rollback entries (e.g. migration 162 has
11
+ * two: one for the timestamp conversion and one for the table rebuild). Each
12
+ * entry has its own monotonic version number for rollback ordering and its
13
+ * own `down()` function.
14
+ */
15
+ export interface RollbackEntry {
16
+ /** Monotonic version used for rollback ordering. */
17
+ version: number;
18
+ /** Human-readable description for diagnostics. */
19
+ description: string;
20
+ /** Reverse the migration. Must be idempotent — safe to re-run. */
21
+ down: (database: DrizzleDb) => void;
22
+ }
23
+
24
+ /**
25
+ * Object form of a migration step, carrying optional rollback metadata and
26
+ * dependency declarations inline — no separate registry needed.
27
+ */
28
+ export interface MigrationStepObject {
29
+ /** Step name — used for `step:*` checkpoint key, logging, and skip logic. */
30
+ name: string;
31
+ /** Forward migration body. */
32
+ run: (database: DrizzleDb) => void | Promise<void>;
33
+ /** Step names that must complete before this step runs. */
34
+ dependsOn?: string[];
35
+ /** Rollback entries — present if this step has down() function(s). */
36
+ rollback?: RollbackEntry[];
37
+ }
38
+
7
39
  /**
8
40
  * A single forward migration step, identified for checkpointing and logging by
9
41
  * its `.name`. Anonymous steps (empty `.name`) cannot be tracked and always run.
10
42
  *
43
+ * Bare function steps are identified by `Function.name`. Object-form steps
44
+ * carry an explicit `name` plus optional `rollback` and `dependsOn` metadata.
45
+ *
11
46
  * A step may be synchronous or return a promise. The runner awaits an async
12
47
  * step to completion before checkpointing it and moving on, so ordering is
13
48
  * preserved exactly as for sync steps: step N+1 never starts — and is never
@@ -16,7 +51,56 @@ const log = getLogger("db-init");
16
51
  * thread between batches while still guaranteeing later migrations observe its
17
52
  * completed result.
18
53
  */
19
- export type MigrationStep = (database: DrizzleDb) => void | Promise<void>;
54
+ export type MigrationStep =
55
+ | MigrationStepObject
56
+ | ((database: DrizzleDb) => void | Promise<void>);
57
+
58
+ /**
59
+ * Normalize a {@link MigrationStep} (which may be a bare function or an object)
60
+ * into its object form so callers don't need to type-narrow.
61
+ */
62
+ export function normalizeStep(step: MigrationStep): MigrationStepObject {
63
+ if (typeof step === "function") {
64
+ return { name: step.name, run: step };
65
+ }
66
+ return step;
67
+ }
68
+
69
+ /**
70
+ * Extract the step name from either form of {@link MigrationStep}.
71
+ */
72
+ export function getStepName(step: MigrationStep): string {
73
+ return typeof step === "function" ? step.name : step.name;
74
+ }
75
+
76
+ /**
77
+ * Get the maximum rollback version across all steps that carry rollback
78
+ * metadata. Returns 0 if no steps have rollback entries.
79
+ */
80
+ export function getMaxRollbackVersion(steps: MigrationStep[]): number {
81
+ let max = 0;
82
+ for (const step of steps) {
83
+ if (typeof step === "function") continue;
84
+ if (!step.rollback) continue;
85
+ for (const entry of step.rollback) {
86
+ if (entry.version > max) max = entry.version;
87
+ }
88
+ }
89
+ return max;
90
+ }
91
+
92
+ /**
93
+ * Collect all step names that appear in the steps array (both bare-function
94
+ * and object-form). Used by {@link validateMigrationState} to detect
95
+ * checkpoints from a newer daemon version.
96
+ */
97
+ export function getKnownStepNames(steps: MigrationStep[]): Set<string> {
98
+ const names = new Set<string>();
99
+ for (const step of steps) {
100
+ names.add(getStepName(step));
101
+ }
102
+ return names;
103
+ }
20
104
 
21
105
  export interface MigrationRunResult {
22
106
  /** Steps that ran and completed successfully this boot. */
@@ -30,9 +114,8 @@ export interface MigrationRunResult {
30
114
  /**
31
115
  * Prefix under which forward-step completions are recorded in the shared
32
116
  * `memory_checkpoints` ledger. A distinct namespace keeps step bookkeeping from
33
- * colliding with registry checkpoint keys (`migration_*`, `backfill_*`,
34
- * `drop_*`) and is deliberately chosen so `validateMigrationState` does not
35
- * mistake a step record for an unknown registry migration.
117
+ * colliding with legacy checkpoint keys (`migration_*`, `backfill_*`,
118
+ * `drop_*`) left by older migration functions.
36
119
  */
37
120
  export const STEP_CHECKPOINT_PREFIX = "step:";
38
121
 
@@ -161,7 +244,8 @@ export async function runMigrationSteps(
161
244
  const ran: string[] = [];
162
245
 
163
246
  for (const step of steps) {
164
- const name = step.name;
247
+ const obj = normalizeStep(step);
248
+ const name = obj.name;
165
249
  const checkpointable = name !== "";
166
250
 
167
251
  if (checkpointable && applied.has(name)) {
@@ -175,7 +259,7 @@ export async function runMigrationSteps(
175
259
  markStarted.run(`${STEP_CHECKPOINT_PREFIX}${name}`, Date.now());
176
260
  }
177
261
  log.info({ migration: name }, `Starting migration: ${name}`);
178
- const result = step(database);
262
+ const result = obj.run(database);
179
263
  if (result instanceof Promise) {
180
264
  await result;
181
265
  }
@@ -16,3 +16,17 @@ export function tableHasColumn(
16
16
 
17
17
  return columns.some((column) => column.name === columnName);
18
18
  }
19
+
20
+ /**
21
+ * True when a base table with the given name exists. Useful for migrations
22
+ * that recreate indexes — `CREATE INDEX IF NOT EXISTS` guards against the
23
+ * index already existing, but still throws "no such table" if the underlying
24
+ * table was already dropped by an earlier migration.
25
+ */
26
+ export function tableExists(database: DrizzleDb, tableName: string): boolean {
27
+ const raw = getSqliteFrom(database);
28
+ const row = raw
29
+ .query(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?`)
30
+ .get(tableName);
31
+ return row != null;
32
+ }