@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,220 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ buildCheckinDescription,
5
+ buildCheckinTitle,
6
+ type BusyInterval,
7
+ checkinAvailabilityWindow,
8
+ chooseCheckinSlot,
9
+ extractBusyFromEvents,
10
+ findFirstOpenSlot,
11
+ type GcalEvent,
12
+ tomorrowInTimeZone,
13
+ zonedWallTimeToUtcMs,
14
+ } from "./checkin-event.js";
15
+
16
+ const TZ = "America/New_York";
17
+ const MIN = 60 * 1000;
18
+ const HOUR = 60 * MIN;
19
+
20
+ // A fixed "now": 2024-01-15 18:00 UTC (= 1pm EST). Tomorrow is 2024-01-16.
21
+ const NOW = Date.parse("2024-01-15T18:00:00Z");
22
+
23
+ /** Helper: epoch ms for tomorrow (2024-01-16) at a given EST hour. */
24
+ function estTomorrow(hour: number, minute = 0): number {
25
+ return zonedWallTimeToUtcMs(2024, 1, 16, hour, minute, TZ);
26
+ }
27
+
28
+ describe("buildCheckinTitle", () => {
29
+ test("both names", () => {
30
+ expect(
31
+ buildCheckinTitle({ userName: "Alex", assistantName: "Jarvis" }),
32
+ ).toBe("Alex <> Jarvis: Day 2 Check-in");
33
+ });
34
+ test("user only", () => {
35
+ expect(buildCheckinTitle({ userName: "Alex" })).toBe(
36
+ "Alex: Day 2 Check-in",
37
+ );
38
+ });
39
+ test("assistant only", () => {
40
+ expect(buildCheckinTitle({ assistantName: "Jarvis" })).toBe(
41
+ "Jarvis: Day 2 Check-in",
42
+ );
43
+ });
44
+ test("neither (blank strings dropped)", () => {
45
+ expect(buildCheckinTitle({ userName: " ", assistantName: "" })).toBe(
46
+ "Day 2 Check-in",
47
+ );
48
+ });
49
+ });
50
+
51
+ describe("buildCheckinDescription", () => {
52
+ test("embeds the uuid + fixed first-week prompt in the CTA link", () => {
53
+ const html = buildCheckinDescription("uuid-123");
54
+ expect(html).toContain(
55
+ "https://www.vellum.ai/assistant/conversations/uuid-123?prompt=What%20would%20you%20recommend",
56
+ );
57
+ // Only sanitization-safe tags; the CTA is a bold link, not a styled button.
58
+ expect(html).toContain("<a href=");
59
+ expect(html).toContain("<strong>");
60
+ expect(html).not.toContain("style=");
61
+ });
62
+ });
63
+
64
+ describe("findFirstOpenSlot", () => {
65
+ const windowStart = 0;
66
+ const windowEnd = 60 * MIN;
67
+ const dur = 15 * MIN;
68
+
69
+ test("empty calendar → start of window", () => {
70
+ expect(findFirstOpenSlot(windowStart, windowEnd, [], dur)).toBe(
71
+ windowStart,
72
+ );
73
+ });
74
+
75
+ test("busy at start → first gap after it", () => {
76
+ const busy: BusyInterval[] = [{ start: 0, end: 20 * MIN }];
77
+ expect(findFirstOpenSlot(windowStart, windowEnd, busy, dur)).toBe(20 * MIN);
78
+ });
79
+
80
+ test("finds a gap between two meetings", () => {
81
+ const busy: BusyInterval[] = [
82
+ { start: 0, end: 10 * MIN },
83
+ { start: 30 * MIN, end: 60 * MIN },
84
+ ];
85
+ // gap [10,30) is 20min >= 15min → slot at 10min.
86
+ expect(findFirstOpenSlot(windowStart, windowEnd, busy, dur)).toBe(10 * MIN);
87
+ });
88
+
89
+ test("skips a gap too small to fit", () => {
90
+ const busy: BusyInterval[] = [
91
+ { start: 0, end: 10 * MIN },
92
+ { start: 20 * MIN, end: 60 * MIN }, // gap [10,20) only 10min
93
+ ];
94
+ expect(findFirstOpenSlot(windowStart, windowEnd, busy, dur)).toBeNull();
95
+ });
96
+
97
+ test("fully booked → null", () => {
98
+ const busy: BusyInterval[] = [{ start: -MIN, end: windowEnd + MIN }];
99
+ expect(findFirstOpenSlot(windowStart, windowEnd, busy, dur)).toBeNull();
100
+ });
101
+
102
+ test("ignores out-of-window busy intervals", () => {
103
+ const busy: BusyInterval[] = [{ start: -2 * HOUR, end: -HOUR }];
104
+ expect(findFirstOpenSlot(windowStart, windowEnd, busy, dur)).toBe(
105
+ windowStart,
106
+ );
107
+ });
108
+ });
109
+
110
+ describe("tomorrowInTimeZone", () => {
111
+ test("advances to the next local calendar day", () => {
112
+ expect(tomorrowInTimeZone(NOW, TZ)).toEqual({
113
+ year: 2024,
114
+ month: 1,
115
+ day: 16,
116
+ });
117
+ });
118
+
119
+ test("late-evening local time still maps to the correct next day", () => {
120
+ // 2024-01-16 02:00 UTC = 2024-01-15 21:00 EST → tomorrow is the 16th.
121
+ const lateLocal = Date.parse("2024-01-16T02:00:00Z");
122
+ expect(tomorrowInTimeZone(lateLocal, TZ)).toEqual({
123
+ year: 2024,
124
+ month: 1,
125
+ day: 16,
126
+ });
127
+ });
128
+ });
129
+
130
+ describe("chooseCheckinSlot", () => {
131
+ test("empty calendar → 12:00 (start of primary window)", () => {
132
+ const slot = chooseCheckinSlot(NOW, TZ, []);
133
+ expect(slot.window).toBe("primary");
134
+ expect(slot.startMs).toBe(estTomorrow(12));
135
+ expect(slot.endMs).toBe(estTomorrow(12, 15));
136
+ });
137
+
138
+ test("noon booked → next open slot inside the primary window", () => {
139
+ const busy: BusyInterval[] = [
140
+ { start: estTomorrow(12), end: estTomorrow(13) },
141
+ ];
142
+ const slot = chooseCheckinSlot(NOW, TZ, busy);
143
+ expect(slot.window).toBe("primary");
144
+ expect(slot.startMs).toBe(estTomorrow(13));
145
+ });
146
+
147
+ test("primary window full → widens to 8am–8pm and takes earliest slot", () => {
148
+ // Block all of 12pm–5pm; leave the morning open.
149
+ const busy: BusyInterval[] = [
150
+ { start: estTomorrow(12), end: estTomorrow(17) },
151
+ ];
152
+ const slot = chooseCheckinSlot(NOW, TZ, busy);
153
+ expect(slot.window).toBe("wide");
154
+ // Earliest free slot in 8am–8pm is 8:00am.
155
+ expect(slot.startMs).toBe(estTomorrow(8));
156
+ });
157
+
158
+ test("entire 8am–8pm full → fallback books at noon anyway", () => {
159
+ const busy: BusyInterval[] = [
160
+ { start: estTomorrow(8), end: estTomorrow(20) },
161
+ ];
162
+ const slot = chooseCheckinSlot(NOW, TZ, busy);
163
+ expect(slot.window).toBe("fallback");
164
+ expect(slot.startMs).toBe(estTomorrow(12));
165
+ });
166
+ });
167
+
168
+ describe("extractBusyFromEvents", () => {
169
+ const a = "2024-01-16T13:00:00Z";
170
+ const b = "2024-01-16T13:30:00Z";
171
+
172
+ test("timed event → busy interval", () => {
173
+ const events: GcalEvent[] = [
174
+ { start: { dateTime: a }, end: { dateTime: b } },
175
+ ];
176
+ expect(extractBusyFromEvents(events)).toEqual([
177
+ { start: Date.parse(a), end: Date.parse(b) },
178
+ ]);
179
+ });
180
+
181
+ test("skips cancelled, transparent, all-day, and declined events", () => {
182
+ const events: GcalEvent[] = [
183
+ { status: "cancelled", start: { dateTime: a }, end: { dateTime: b } },
184
+ {
185
+ transparency: "transparent",
186
+ start: { dateTime: a },
187
+ end: { dateTime: b },
188
+ },
189
+ { start: { date: "2024-01-16" }, end: { date: "2024-01-17" } },
190
+ {
191
+ start: { dateTime: a },
192
+ end: { dateTime: b },
193
+ attendees: [{ self: true, responseStatus: "declined" }],
194
+ },
195
+ ];
196
+ expect(extractBusyFromEvents(events)).toEqual([]);
197
+ });
198
+
199
+ test("keeps an accepted event even with another attendee declined", () => {
200
+ const events: GcalEvent[] = [
201
+ {
202
+ start: { dateTime: a },
203
+ end: { dateTime: b },
204
+ attendees: [
205
+ { self: true, responseStatus: "accepted" },
206
+ { responseStatus: "declined" },
207
+ ],
208
+ },
209
+ ];
210
+ expect(extractBusyFromEvents(events)).toHaveLength(1);
211
+ });
212
+ });
213
+
214
+ describe("checkinAvailabilityWindow", () => {
215
+ test("spans the full 8am–8pm fallback window tomorrow", () => {
216
+ const { timeMinMs, timeMaxMs } = checkinAvailabilityWindow(NOW, TZ);
217
+ expect(timeMinMs).toBe(estTomorrow(8));
218
+ expect(timeMaxMs).toBe(estTomorrow(20));
219
+ });
220
+ });
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Pure helpers for the onboarding "Day 2 Check-in" meeting: choosing the slot
3
+ * and building the event's title and HTML description.
4
+ *
5
+ * The check-in is booked into the first open 15-minute slot in the user's local
6
+ * afternoon tomorrow. The title uses fixed typography and the description uses a
7
+ * sanitization-safe HTML body (Google Calendar strips most styling) with
8
+ * default first-run substitutions and a deep-link CTA back into the app.
9
+ *
10
+ * Everything here is side-effect-free and timezone-pure so it can be unit
11
+ * tested without a calendar connection; the orchestration that talks to Google
12
+ * lives in `schedule-checkin.ts`.
13
+ */
14
+
15
+ /** Length of the check-in meeting. */
16
+ export const CHECKIN_DURATION_MINUTES = 15;
17
+
18
+ /**
19
+ * Primary booking window in the user's local clock: the first open 15-minute
20
+ * slot between 12pm and 5pm tomorrow.
21
+ */
22
+ export const PRIMARY_WINDOW = { startHour: 12, endHour: 17 } as const;
23
+
24
+ /**
25
+ * Fallback window when the primary 12pm–5pm window is fully booked: widen on
26
+ * both ends to 8am–8pm and take the earliest open slot there.
27
+ */
28
+ export const WIDE_WINDOW = { startHour: 8, endHour: 20 } as const;
29
+
30
+ /** The fixed, single-encoded first-week prompt carried by the CTA deep link. */
31
+ const CTA_ENCODED_PROMPT =
32
+ "What%20would%20you%20recommend%20I%20tackle%20first%20this%20week%3F%20Propose%20it%20but%20wait%20for%20my%20go-ahead%20before%20doing%20anything";
33
+
34
+ export interface CheckinNames {
35
+ /** The user's collected name. Blank/omitted → dropped from the title. */
36
+ userName?: string;
37
+ /** The assistant's display name. Blank/omitted → dropped from the title. */
38
+ assistantName?: string;
39
+ }
40
+
41
+ /**
42
+ * Build the check-in event title. Locked typography, four cases:
43
+ * both names → `{me} <> {you}: Day 2 Check-in`
44
+ * me only → `{me}: Day 2 Check-in`
45
+ * you only → `{you}: Day 2 Check-in`
46
+ * neither → `Day 2 Check-in`
47
+ */
48
+ export function buildCheckinTitle({
49
+ userName,
50
+ assistantName,
51
+ }: CheckinNames): string {
52
+ const me = userName?.trim();
53
+ const you = assistantName?.trim();
54
+ if (me && you) return `${me} <> ${you}: Day 2 Check-in`;
55
+ if (me) return `${me}: Day 2 Check-in`;
56
+ if (you) return `${you}: Day 2 Check-in`;
57
+ return "Day 2 Check-in";
58
+ }
59
+
60
+ /**
61
+ * Build the HTML event description with the default first-run substitutions.
62
+ *
63
+ * Google Calendar strips nearly all styling, so the body relies only on the
64
+ * tags that survive (`<p>`, `<strong>`, `<a>`, emoji) and makes the CTA a bold
65
+ * link rather than a styled button. The deep link opens a fresh conversation
66
+ * (`uuid`) pre-seeded with the first-week prompt.
67
+ */
68
+ export function buildCheckinDescription(uuid: string): string {
69
+ const href = `https://www.vellum.ai/assistant/conversations/${uuid}?prompt=${CTA_ENCODED_PROMPT}`;
70
+ return [
71
+ "<p>👋 <strong>Hi, it was great to meet you properly.</strong></p>",
72
+ "<p>You just set me up, and I've already started learning <strong>what you're working on</strong>. This 15 minutes is the natural place to put that to work. I'll walk you through one thing I'd like to do for you this week.</p>",
73
+ `<p><a href="${href}"><strong>Let's go →</strong></a></p>`,
74
+ "<p>Click the link and we'll get started.</p>",
75
+ ].join("\n");
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Timezone math
80
+ //
81
+ // We resolve wall-clock windows ("tomorrow 12:00 in America/New_York") to
82
+ // absolute instants using Intl, so DST transitions and arbitrary offsets are
83
+ // handled without a date library.
84
+ // ---------------------------------------------------------------------------
85
+
86
+ /**
87
+ * Offset (local − UTC, in ms) that `timeZone` has at the given instant.
88
+ * Derived by formatting the instant in the zone and diffing the wall clock.
89
+ */
90
+ function tzOffsetMs(utcMs: number, timeZone: string): number {
91
+ const dtf = new Intl.DateTimeFormat("en-US", {
92
+ timeZone,
93
+ hour12: false,
94
+ year: "numeric",
95
+ month: "2-digit",
96
+ day: "2-digit",
97
+ hour: "2-digit",
98
+ minute: "2-digit",
99
+ second: "2-digit",
100
+ });
101
+ const map: Record<string, string> = {};
102
+ for (const part of dtf.formatToParts(new Date(utcMs))) {
103
+ if (part.type !== "literal") map[part.type] = part.value;
104
+ }
105
+ // `hourCycle: h23` can emit "24" for midnight on some runtimes — normalize.
106
+ let hour = Number(map.hour);
107
+ if (hour === 24) hour = 0;
108
+ const asUtc = Date.UTC(
109
+ Number(map.year),
110
+ Number(map.month) - 1,
111
+ Number(map.day),
112
+ hour,
113
+ Number(map.minute),
114
+ Number(map.second),
115
+ );
116
+ return asUtc - utcMs;
117
+ }
118
+
119
+ /**
120
+ * Convert a wall-clock time in `timeZone` to an absolute epoch-ms instant.
121
+ * Refines once to settle DST boundaries where the naive offset guess differs
122
+ * from the offset that actually applies at the resolved instant.
123
+ */
124
+ export function zonedWallTimeToUtcMs(
125
+ year: number,
126
+ month: number,
127
+ day: number,
128
+ hour: number,
129
+ minute: number,
130
+ timeZone: string,
131
+ ): number {
132
+ const guess = Date.UTC(year, month - 1, day, hour, minute);
133
+ const off1 = tzOffsetMs(guess, timeZone);
134
+ let utc = guess - off1;
135
+ const off2 = tzOffsetMs(utc, timeZone);
136
+ if (off2 !== off1) utc = guess - off2;
137
+ return utc;
138
+ }
139
+
140
+ /** Calendar date (1-based month) that is "tomorrow" in `timeZone` at `nowMs`. */
141
+ export function tomorrowInTimeZone(
142
+ nowMs: number,
143
+ timeZone: string,
144
+ ): { year: number; month: number; day: number } {
145
+ const dtf = new Intl.DateTimeFormat("en-US", {
146
+ timeZone,
147
+ year: "numeric",
148
+ month: "2-digit",
149
+ day: "2-digit",
150
+ });
151
+ const map: Record<string, string> = {};
152
+ for (const part of dtf.formatToParts(new Date(nowMs))) {
153
+ if (part.type !== "literal") map[part.type] = part.value;
154
+ }
155
+ // Add a day via UTC arithmetic (handles month/year rollover), then read the
156
+ // wall-clock date back out — we only care about Y/M/D, not the instant.
157
+ const tomorrow = new Date(
158
+ Date.UTC(Number(map.year), Number(map.month) - 1, Number(map.day)) +
159
+ 24 * 60 * 60 * 1000,
160
+ );
161
+ return {
162
+ year: tomorrow.getUTCFullYear(),
163
+ month: tomorrow.getUTCMonth() + 1,
164
+ day: tomorrow.getUTCDate(),
165
+ };
166
+ }
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // Slot finding
170
+ // ---------------------------------------------------------------------------
171
+
172
+ /** A half-open busy interval `[start, end)` in epoch ms. */
173
+ export interface BusyInterval {
174
+ start: number;
175
+ end: number;
176
+ }
177
+
178
+ /** Subset of a Google Calendar event needed to decide whether it blocks time. */
179
+ export interface GcalEvent {
180
+ status?: string;
181
+ transparency?: string;
182
+ start?: { dateTime?: string; date?: string };
183
+ end?: { dateTime?: string; date?: string };
184
+ attendees?: Array<{ self?: boolean; responseStatus?: string }>;
185
+ }
186
+
187
+ /**
188
+ * Derive busy intervals (epoch ms) from listed calendar events, approximating
189
+ * freeBusy semantics under the calendar.events scope (which authorizes
190
+ * events.list/insert but not freeBusy.query). Skips events that don't occupy
191
+ * the user's time: cancelled, transparent ("free"), all-day (date-only), and
192
+ * ones the user has declined.
193
+ */
194
+ export function extractBusyFromEvents(events: GcalEvent[]): BusyInterval[] {
195
+ const intervals: BusyInterval[] = [];
196
+ for (const event of events) {
197
+ if (event.status === "cancelled") continue;
198
+ if (event.transparency === "transparent") continue;
199
+ // All-day events (date-only) don't block a 15-minute afternoon slot.
200
+ if (!event.start?.dateTime || !event.end?.dateTime) continue;
201
+ const self = event.attendees?.find((a) => a.self);
202
+ if (self?.responseStatus === "declined") continue;
203
+
204
+ const start = Date.parse(event.start.dateTime);
205
+ const end = Date.parse(event.end.dateTime);
206
+ if (Number.isFinite(start) && Number.isFinite(end) && end > start) {
207
+ intervals.push({ start, end });
208
+ }
209
+ }
210
+ return intervals;
211
+ }
212
+
213
+ /**
214
+ * Earliest start (epoch ms) of a free `durationMs` slot inside
215
+ * `[windowStart, windowEnd)`, or `null` if the window is fully booked.
216
+ *
217
+ * Walks the busy intervals in order, advancing a cursor past each overlap; the
218
+ * first gap wide enough to fit the duration wins. This returns the *first*
219
+ * open slot, which is exactly the "first open time slot in the window"
220
+ * behavior the onboarding flow wants.
221
+ */
222
+ export function findFirstOpenSlot(
223
+ windowStart: number,
224
+ windowEnd: number,
225
+ busy: BusyInterval[],
226
+ durationMs: number,
227
+ ): number | null {
228
+ const relevant = busy
229
+ .filter((b) => b.end > windowStart && b.start < windowEnd)
230
+ .sort((a, b) => a.start - b.start);
231
+
232
+ let cursor = windowStart;
233
+ for (const interval of relevant) {
234
+ if (interval.start - cursor >= durationMs) {
235
+ return cursor;
236
+ }
237
+ if (interval.end > cursor) {
238
+ cursor = interval.end;
239
+ }
240
+ if (cursor + durationMs > windowEnd) {
241
+ return null;
242
+ }
243
+ }
244
+ return cursor + durationMs <= windowEnd ? cursor : null;
245
+ }
246
+
247
+ export interface ChosenSlot {
248
+ /** Slot start, epoch ms. */
249
+ startMs: number;
250
+ /** Slot end, epoch ms. */
251
+ endMs: number;
252
+ /** Which window the slot came from — drives logging/telemetry only. */
253
+ window: "primary" | "wide" | "fallback";
254
+ }
255
+
256
+ /**
257
+ * Pick the check-in slot for tomorrow in `timeZone` given the busy intervals.
258
+ *
259
+ * 1. First open 15-min slot in 12pm–5pm.
260
+ * 2. If that window is full, widen to 8am–8pm and take the earliest open slot.
261
+ * 3. If even that is full (pathological), fall back to 12:00 so a reminder
262
+ * still lands — better an overlapping check-in than none.
263
+ */
264
+ export function chooseCheckinSlot(
265
+ nowMs: number,
266
+ timeZone: string,
267
+ busy: BusyInterval[],
268
+ ): ChosenSlot {
269
+ const { year, month, day } = tomorrowInTimeZone(nowMs, timeZone);
270
+ const durationMs = CHECKIN_DURATION_MINUTES * 60 * 1000;
271
+
272
+ const at = (hour: number) =>
273
+ zonedWallTimeToUtcMs(year, month, day, hour, 0, timeZone);
274
+
275
+ const primaryStart = at(PRIMARY_WINDOW.startHour);
276
+ const primaryEnd = at(PRIMARY_WINDOW.endHour);
277
+ const wideStart = at(WIDE_WINDOW.startHour);
278
+ const wideEnd = at(WIDE_WINDOW.endHour);
279
+
280
+ const primary = findFirstOpenSlot(primaryStart, primaryEnd, busy, durationMs);
281
+ if (primary !== null) {
282
+ return { startMs: primary, endMs: primary + durationMs, window: "primary" };
283
+ }
284
+
285
+ const wide = findFirstOpenSlot(wideStart, wideEnd, busy, durationMs);
286
+ if (wide !== null) {
287
+ return { startMs: wide, endMs: wide + durationMs, window: "wide" };
288
+ }
289
+
290
+ return {
291
+ startMs: primaryStart,
292
+ endMs: primaryStart + durationMs,
293
+ window: "fallback",
294
+ };
295
+ }
296
+
297
+ /** Window covering both the primary and fallback ranges — the availability query span. */
298
+ export function checkinAvailabilityWindow(
299
+ nowMs: number,
300
+ timeZone: string,
301
+ ): { timeMinMs: number; timeMaxMs: number } {
302
+ const { year, month, day } = tomorrowInTimeZone(nowMs, timeZone);
303
+ return {
304
+ timeMinMs: zonedWallTimeToUtcMs(
305
+ year,
306
+ month,
307
+ day,
308
+ WIDE_WINDOW.startHour,
309
+ 0,
310
+ timeZone,
311
+ ),
312
+ timeMaxMs: zonedWallTimeToUtcMs(
313
+ year,
314
+ month,
315
+ day,
316
+ WIDE_WINDOW.endHour,
317
+ 0,
318
+ timeZone,
319
+ ),
320
+ };
321
+ }