@vellumai/assistant 0.8.12 → 0.9.0-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (442) hide show
  1. package/AGENTS.md +0 -14
  2. package/ARCHITECTURE.md +45 -45
  3. package/README.md +1 -1
  4. package/bun.lock +200 -154
  5. package/docs/architecture/integrations.md +3 -3
  6. package/docs/architecture/memory.md +2 -2
  7. package/docs/architecture/security.md +10 -10
  8. package/docs/runbook-trusted-contacts.md +12 -12
  9. package/docs/skills.md +6 -6
  10. package/docs/workflows-testing.md +221 -0
  11. package/docs/workflows.md +510 -0
  12. package/examples/plugins/echo/README.md +5 -5
  13. package/knip.json +2 -0
  14. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +105 -0
  15. package/node_modules/@vellumai/gateway-client/src/index.ts +12 -0
  16. package/openapi.yaml +7197 -5708
  17. package/package.json +8 -4
  18. package/scripts/generate-openapi.ts +66 -114
  19. package/src/__tests__/access-request-seed-content-blocks.test.ts +213 -0
  20. package/src/__tests__/adaptive-thinking-repair.test.ts +32 -3
  21. package/src/__tests__/agent-loop-output-hooks.test.ts +183 -0
  22. package/src/__tests__/agent-loop-regrowth-guard.test.ts +506 -0
  23. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  24. package/src/__tests__/agent-wake-override-profile.test.ts +77 -0
  25. package/src/__tests__/app-compiler.test.ts +7 -1
  26. package/src/__tests__/app-dir-path-guard.test.ts +27 -3
  27. package/src/__tests__/app-executors.test.ts +43 -0
  28. package/src/__tests__/approval-cascade.test.ts +0 -5
  29. package/src/__tests__/approval-routes-http.test.ts +91 -0
  30. package/src/__tests__/assistant-stream-state.test.ts +107 -0
  31. package/src/__tests__/browser-fill-credential.test.ts +3 -3
  32. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -1
  33. package/src/__tests__/compaction-events.test.ts +63 -7
  34. package/src/__tests__/compaction-trail-store.test.ts +74 -1
  35. package/src/__tests__/compaction.benchmark.test.ts +63 -41
  36. package/src/__tests__/compactor-low-watermark-cut.test.ts +349 -0
  37. package/src/__tests__/context-window-manager-compact-retry.test.ts +64 -0
  38. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -5
  39. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -5
  40. package/src/__tests__/conversation-history-web-search.test.ts +7 -0
  41. package/src/__tests__/conversation-process-callsite.test.ts +0 -5
  42. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -5
  43. package/src/__tests__/conversation-queue.test.ts +0 -5
  44. package/src/__tests__/conversation-slash-queue.test.ts +0 -5
  45. package/src/__tests__/conversation-slash-unknown.test.ts +0 -5
  46. package/src/__tests__/conversation-speed-override.test.ts +0 -5
  47. package/src/__tests__/conversation-surfaces-data-persist.test.ts +97 -0
  48. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  49. package/src/__tests__/conversation-usage.test.ts +2 -0
  50. package/src/__tests__/conversation-workspace-injection.test.ts +0 -5
  51. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -5
  52. package/src/__tests__/credential-broker-browser-fill.test.ts +2 -2
  53. package/src/__tests__/credential-broker-server-use.test.ts +2 -2
  54. package/src/__tests__/credential-broker.test.ts +1 -1
  55. package/src/__tests__/credential-prompt-route.test.ts +417 -0
  56. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  57. package/src/__tests__/credential-vault.test.ts +37 -0
  58. package/src/__tests__/db-schedule-syntax-migration.test.ts +24 -0
  59. package/src/__tests__/dynamic-page-surface.test.ts +219 -0
  60. package/src/__tests__/empty-state-greeting-cache.test.ts +94 -0
  61. package/src/__tests__/gateway-flag-listener.test.ts +24 -7
  62. package/src/__tests__/guardian-action-sweep.test.ts +56 -219
  63. package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
  64. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  65. package/src/__tests__/list-messages-hidden-metadata.test.ts +99 -0
  66. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +87 -1
  67. package/src/__tests__/llm-resolver.test.ts +115 -0
  68. package/src/__tests__/managed-profile-guard.test.ts +6 -5
  69. package/src/__tests__/max-tokens-continue-hook.test.ts +184 -0
  70. package/src/__tests__/media-generate-image.test.ts +20 -9
  71. package/src/__tests__/mock-gateway-ipc.ts +23 -0
  72. package/src/__tests__/model-intents.test.ts +1 -1
  73. package/src/__tests__/normalize-onboarding.test.ts +26 -0
  74. package/src/__tests__/notification-decision-strategy.test.ts +4 -2
  75. package/src/__tests__/notification-telegram-adapter.test.ts +21 -3
  76. package/src/__tests__/pending-interactions-resolved-event.test.ts +62 -0
  77. package/src/__tests__/post-turn-tool-result-truncation.test.ts +72 -18
  78. package/src/__tests__/require-fresh-approval.test.ts +425 -1
  79. package/src/__tests__/resolve-app-id.test.ts +56 -0
  80. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -0
  81. package/src/__tests__/schedule-routes-workflow-validation.test.ts +408 -0
  82. package/src/__tests__/schedule-routes.test.ts +257 -4
  83. package/src/__tests__/schedule-store.test.ts +60 -0
  84. package/src/__tests__/schedule-tools.test.ts +247 -2
  85. package/src/__tests__/skill-execute-input.test.ts +85 -0
  86. package/src/__tests__/skill-secret-handling-guard.test.ts +21 -20
  87. package/src/__tests__/skills.test.ts +3 -3
  88. package/src/__tests__/slack-app-setup-skill-regression.test.ts +1 -1
  89. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  90. package/src/__tests__/subagent-tool-gate-mode.test.ts +547 -0
  91. package/src/__tests__/system-prompt.test.ts +1 -1
  92. package/src/__tests__/task-progress-nudge-hook.test.ts +372 -0
  93. package/src/__tests__/task-scheduler.test.ts +299 -0
  94. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +209 -0
  95. package/src/__tests__/tool-result-spool.test.ts +3 -1
  96. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +181 -0
  97. package/src/__tests__/workspace-migration-103-upgrade-quality-profile-to-opus-4-8.test.ts +174 -0
  98. package/src/agent/compaction-circuit.ts +11 -0
  99. package/src/agent/loop.ts +181 -12
  100. package/src/api/constants/call-sites.ts +12 -0
  101. package/src/api/events/assistant-thinking-delta.ts +10 -0
  102. package/src/api/events/usage-update.ts +7 -0
  103. package/src/api/index.ts +4 -1
  104. package/src/api/responses/memory-v3-selection-log.ts +18 -11
  105. package/src/approvals/approval-primitive.ts +2 -2
  106. package/src/background-wake/background-wake-routes.test.ts +5 -2
  107. package/src/bundler/compiler-tools.ts +1 -1
  108. package/src/bundler/package-resolver.ts +0 -1
  109. package/src/calls/call-domain.ts +1 -1
  110. package/src/calls/guardian-action-sweep.ts +16 -93
  111. package/src/cli/AGENTS.md +4 -0
  112. package/src/cli/commands/__tests__/schedules.test.ts +430 -1
  113. package/src/cli/commands/credentials.ts +28 -24
  114. package/src/cli/commands/image-generation.ts +23 -9
  115. package/src/cli/commands/notifications.ts +1 -1
  116. package/src/cli/commands/plugins.ts +89 -46
  117. package/src/cli/commands/schedules.ts +384 -11
  118. package/src/cli/lib/__tests__/inspect-plugin.test.ts +69 -5
  119. package/src/cli/lib/__tests__/install-from-github.test.ts +15 -0
  120. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +81 -4
  121. package/src/cli/lib/inspect-plugin.ts +62 -1
  122. package/src/cli/lib/install-from-github.ts +52 -5
  123. package/src/cli/lib/upgrade-plugin.ts +18 -0
  124. package/src/config/__tests__/workflows-schema.test.ts +60 -0
  125. package/src/config/bundled-skills/acp/SKILL.md +2 -2
  126. package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
  127. package/src/config/bundled-skills/app-builder/tools/app-create.ts +6 -1
  128. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +7 -1
  129. package/src/config/bundled-skills/app-builder/tools/app-refresh.ts +7 -1
  130. package/src/config/bundled-skills/app-builder/tools/app-update.ts +10 -1
  131. package/src/config/bundled-skills/image-studio/SKILL.md +66 -19
  132. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -6
  133. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +22 -3
  134. package/src/config/bundled-skills/personal-page/SKILL.md +57 -0
  135. package/src/config/bundled-skills/personal-page/TOOLS.json +27 -0
  136. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +17 -0
  137. package/src/config/bundled-skills/schedule/SKILL.md +7 -2
  138. package/src/config/bundled-skills/schedule/TOOLS.json +48 -4
  139. package/src/config/bundled-skills/workflows/SKILL.md +214 -0
  140. package/src/config/bundled-skills/workflows/TOOLS.json +84 -0
  141. package/src/config/bundled-skills/workflows/tools/manage-workflows.ts +12 -0
  142. package/src/config/bundled-skills/workflows/tools/run-workflow.ts +12 -0
  143. package/src/config/bundled-tool-registry.ts +14 -2
  144. package/src/config/call-site-defaults.ts +5 -0
  145. package/src/config/feature-flag-registry.json +12 -4
  146. package/src/config/llm-context-resolution.ts +8 -0
  147. package/src/config/llm-resolver.ts +30 -0
  148. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +22 -0
  149. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +16 -0
  150. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +77 -0
  151. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +57 -0
  152. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +28 -0
  153. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +234 -0
  154. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +48 -0
  155. package/src/config/preloaded-apps/personal-page/src/components/media.ts +16 -0
  156. package/src/config/preloaded-apps/personal-page/src/index.html +20 -0
  157. package/src/config/preloaded-apps/personal-page/src/main.tsx +7 -0
  158. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +82 -0
  159. package/src/config/preloaded-apps/personal-page/src/styles.css +759 -0
  160. package/src/config/schema.ts +2 -0
  161. package/src/config/schemas/call-site-catalog.ts +7 -0
  162. package/src/config/schemas/heartbeat.ts +4 -1
  163. package/src/config/schemas/llm.ts +33 -26
  164. package/src/config/schemas/memory-retrospective.ts +19 -0
  165. package/src/config/schemas/platform.ts +8 -0
  166. package/src/config/schemas/services.ts +5 -2
  167. package/src/config/schemas/workflows.ts +42 -0
  168. package/src/config/skills.ts +3 -3
  169. package/src/context/compactor.ts +273 -39
  170. package/src/context/post-turn-tool-result-truncation.ts +23 -6
  171. package/src/context/tool-result-spool.ts +12 -17
  172. package/src/credential-execution/executable-discovery.ts +1 -1
  173. package/src/credential-execution/process-manager.ts +37 -3
  174. package/src/credential-execution/prompted-credential.ts +205 -0
  175. package/src/daemon/conversation-agent-loop-handlers.ts +14 -0
  176. package/src/daemon/conversation-process.ts +11 -2
  177. package/src/daemon/conversation-surfaces.ts +167 -3
  178. package/src/daemon/conversation-tool-setup.ts +103 -26
  179. package/src/daemon/conversation-usage.ts +2 -0
  180. package/src/daemon/conversation.ts +115 -11
  181. package/src/daemon/handlers/shared.ts +26 -14
  182. package/src/daemon/host-cu-proxy.ts +15 -12
  183. package/src/daemon/host-file-proxy.ts +15 -12
  184. package/src/daemon/host-transfer-proxy.ts +30 -24
  185. package/src/daemon/lifecycle.ts +40 -3
  186. package/src/daemon/message-protocol.ts +3 -0
  187. package/src/daemon/message-types/messages.ts +2 -10
  188. package/src/daemon/message-types/workflows.ts +49 -0
  189. package/src/daemon/parse-actual-tokens-from-error.test.ts +62 -1
  190. package/src/daemon/parse-actual-tokens-from-error.ts +43 -4
  191. package/src/daemon/process-message.ts +6 -0
  192. package/src/daemon/tool-setup-types.ts +57 -0
  193. package/src/daemon/wake-conversation-ops.ts +18 -0
  194. package/src/heartbeat/heartbeat-run-store.ts +8 -2
  195. package/src/home/feed-types.ts +1 -1
  196. package/src/ipc/gateway-flag-listener.ts +28 -6
  197. package/src/mcp/mcp-auth-state.ts +8 -20
  198. package/src/media/__tests__/image-models.test.ts +57 -0
  199. package/src/media/image-models.ts +66 -0
  200. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +38 -0
  201. package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +12 -2
  202. package/src/memory/__tests__/memory-retrospective-job.test.ts +911 -34
  203. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +227 -5
  204. package/src/memory/__tests__/memory-retrospective-state.test.ts +195 -0
  205. package/src/memory/__tests__/preloaded-apps.test.ts +85 -0
  206. package/src/memory/auto-analysis-enqueue.ts +14 -1
  207. package/src/memory/compaction-log-store-clickhouse.ts +6 -4
  208. package/src/memory/conversation-crud.ts +9 -2
  209. package/src/memory/conversation-disk-view.ts +1 -1
  210. package/src/memory/conversation-queries.ts +22 -7
  211. package/src/memory/db-init.ts +20 -0
  212. package/src/memory/db-maintenance.ts +16 -0
  213. package/src/memory/embedding-runtime-manager.ts +1 -1
  214. package/src/memory/llm-request-log-source-clickhouse.ts +112 -14
  215. package/src/memory/llm-request-log-source-local.ts +19 -1
  216. package/src/memory/llm-request-log-source.ts +35 -6
  217. package/src/memory/llm-request-log-store.ts +90 -2
  218. package/src/memory/memory-retrospective-constants.ts +9 -0
  219. package/src/memory/memory-retrospective-enqueue.ts +3 -6
  220. package/src/memory/memory-retrospective-fork-boundary.ts +94 -0
  221. package/src/memory/memory-retrospective-job.ts +500 -208
  222. package/src/memory/memory-retrospective-startup-cleanup.ts +97 -19
  223. package/src/memory/memory-retrospective-state.ts +85 -2
  224. package/src/memory/migrations/281-memory-retrospective-remembered-log.ts +40 -0
  225. package/src/memory/migrations/282-schedule-inference-profile.test.ts +77 -0
  226. package/src/memory/migrations/282-schedule-inference-profile.ts +26 -0
  227. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.test.ts +102 -0
  228. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.ts +53 -0
  229. package/src/memory/migrations/284-workflow-runs.ts +51 -0
  230. package/src/memory/migrations/285-schedule-workflow-mode.ts +26 -0
  231. package/src/memory/migrations/286-workflow-run-trust.ts +27 -0
  232. package/src/memory/migrations/287-conversation-origin-channel-index.ts +15 -0
  233. package/src/memory/migrations/288-backfill-origin-channel-from-bindings.ts +43 -0
  234. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +115 -0
  235. package/src/memory/migrations/290-schedule-capabilities.test.ts +77 -0
  236. package/src/memory/migrations/290-schedule-capabilities.ts +25 -0
  237. package/src/memory/migrations/__tests__/281-memory-retrospective-remembered-log.test.ts +96 -0
  238. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +571 -0
  239. package/src/memory/migrations/index.ts +10 -0
  240. package/src/memory/preloaded-apps.ts +116 -0
  241. package/src/memory/schema/infrastructure.ts +4 -0
  242. package/src/memory/schema/memory-core.ts +4 -0
  243. package/src/memory/v2/__tests__/concept-page-frontmatter-schema.test.ts +45 -0
  244. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +11 -7
  245. package/src/memory/v2/__tests__/page-store.test.ts +13 -2
  246. package/src/memory/v2/__tests__/qdrant.test.ts +24 -0
  247. package/src/memory/v2/frontmatter-sweep.ts +7 -6
  248. package/src/memory/v2/page-store.ts +4 -3
  249. package/src/memory/v2/qdrant.ts +42 -3
  250. package/src/memory/v2/types.ts +16 -10
  251. package/src/messaging/draft-store.ts +1 -1
  252. package/src/notifications/access-request-copy.ts +200 -113
  253. package/src/notifications/adapters/slack.ts +250 -111
  254. package/src/notifications/adapters/telegram.ts +7 -44
  255. package/src/notifications/approval-card-builder.ts +93 -0
  256. package/src/notifications/broadcaster.ts +74 -0
  257. package/src/notifications/conversation-pairing.ts +8 -6
  258. package/src/notifications/copy-composer.ts +32 -26
  259. package/src/notifications/decision-engine.ts +59 -7
  260. package/src/notifications/guardian-question-mode.ts +145 -155
  261. package/src/notifications/home-feed-side-effect.ts +28 -11
  262. package/src/notifications/notification-utils.ts +66 -0
  263. package/src/notifications/signal.ts +6 -0
  264. package/src/notifications/tool-approval-copy.ts +142 -0
  265. package/src/notifications/types.ts +19 -0
  266. package/src/permissions/threshold.ts +11 -0
  267. package/src/plugin-api/types.ts +16 -4
  268. package/src/plugins/defaults/compaction/window-manager.ts +44 -0
  269. package/src/plugins/defaults/index.ts +46 -0
  270. package/src/plugins/defaults/max-tokens-continue/continue-state-store.ts +53 -0
  271. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +80 -0
  272. package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +20 -0
  273. package/src/plugins/defaults/max-tokens-continue/package.json +14 -0
  274. package/src/plugins/defaults/memory-retrieval/hooks/__tests__/user-prompt-submit.test.ts +37 -0
  275. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +29 -1
  276. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +8 -3
  277. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +4 -2
  278. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +28 -18
  279. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +67 -0
  280. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +122 -22
  281. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +2 -0
  282. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +63 -1
  283. package/src/plugins/defaults/memory-v3-shadow/injector.ts +61 -18
  284. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +1 -1
  285. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +39 -10
  286. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +34 -1
  287. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +112 -47
  288. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +78 -15
  289. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +206 -0
  290. package/src/plugins/defaults/task-progress-nudge/package.json +15 -0
  291. package/src/prompts/__tests__/system-prompt.test.ts +100 -1
  292. package/src/prompts/__tests__/task-progress-hint-section.test.ts +5 -7
  293. package/src/prompts/normalize-onboarding.ts +2 -0
  294. package/src/prompts/persona-resolver.ts +3 -0
  295. package/src/prompts/system-prompt.ts +51 -2
  296. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +3 -1
  297. package/src/prompts/templates/system-sections.ts +8 -3
  298. package/src/providers/call-site-routing.ts +6 -3
  299. package/src/providers/fireworks/client.ts +3 -0
  300. package/src/providers/inference/auth.ts +52 -46
  301. package/src/providers/model-intents.ts +2 -2
  302. package/src/providers/openai/__tests__/coerce-object-args.test.ts +105 -0
  303. package/src/providers/openai/chat-completions-provider.ts +47 -9
  304. package/src/providers/openai/coerce-object-args.ts +104 -0
  305. package/src/providers/retry.ts +8 -5
  306. package/src/providers/types.ts +10 -0
  307. package/src/runtime/__tests__/agent-wake.test.ts +629 -7
  308. package/src/runtime/access-request-helper.ts +16 -9
  309. package/src/runtime/agent-wake.ts +302 -51
  310. package/src/runtime/assistant-stream-state.ts +141 -8
  311. package/src/runtime/background-job-runner.ts +9 -0
  312. package/src/runtime/channel-approval-types.ts +1 -0
  313. package/src/runtime/channel-invite-transports/telegram.ts +6 -5
  314. package/src/runtime/channel-invite-transports/voice.ts +2 -2
  315. package/src/runtime/channel-invite-types.ts +4 -2
  316. package/src/runtime/channel-retry-sweep.ts +19 -41
  317. package/src/runtime/finalize-event-delivery.ts +72 -0
  318. package/src/runtime/guardian-action-message-composer.ts +0 -54
  319. package/src/runtime/http-server.ts +6 -14
  320. package/src/runtime/http-types.ts +0 -1
  321. package/src/runtime/message-composer-types.ts +0 -9
  322. package/src/runtime/middleware/__tests__/rate-limiter.test.ts +63 -0
  323. package/src/runtime/middleware/auth.ts +27 -3
  324. package/src/runtime/middleware/rate-limiter.ts +28 -1
  325. package/src/runtime/migrations/vbundle-builder.ts +6 -5
  326. package/src/runtime/pending-interactions.ts +20 -1
  327. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +232 -173
  328. package/src/runtime/routes/__tests__/plugins-routes.test.ts +18 -0
  329. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +436 -0
  330. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +11 -0
  331. package/src/runtime/routes/approval-routes.ts +35 -8
  332. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -1
  333. package/src/runtime/routes/btw-routes.ts +37 -1
  334. package/src/runtime/routes/channel-delivery-routes.ts +11 -7
  335. package/src/runtime/routes/channel-route-definitions.ts +3 -0
  336. package/src/runtime/routes/channel-route-shared.ts +3 -1
  337. package/src/runtime/routes/consolidation-routes.ts +17 -13
  338. package/src/runtime/routes/conversation-compaction-routes.ts +159 -119
  339. package/src/runtime/routes/conversation-list-routes.ts +41 -4
  340. package/src/runtime/routes/conversation-query-routes.ts +196 -11
  341. package/src/runtime/routes/conversation-routes.ts +15 -1
  342. package/src/runtime/routes/credential-prompt-routes.ts +39 -17
  343. package/src/runtime/routes/empty-state-greeting-cache.ts +65 -0
  344. package/src/runtime/routes/heartbeat-routes.ts +18 -13
  345. package/src/runtime/routes/image-generation-routes.ts +20 -2
  346. package/src/runtime/routes/inbound-message-handler.ts +13 -12
  347. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +32 -31
  348. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +7 -5
  349. package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -21
  350. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +5 -5
  351. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +6 -15
  352. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +1 -1
  353. package/src/runtime/routes/index.ts +6 -0
  354. package/src/runtime/routes/log-export/AGENTS.md +1 -1
  355. package/src/runtime/routes/log-export/workspace-allowlist.ts +1 -1
  356. package/src/runtime/routes/migration-routes.ts +5 -9
  357. package/src/runtime/routes/plugins-routes.ts +26 -0
  358. package/src/runtime/routes/ps-routes.ts +10 -8
  359. package/src/runtime/routes/retrospective-routes.ts +235 -0
  360. package/src/runtime/routes/runs-pagination.ts +75 -0
  361. package/src/runtime/routes/schedule-routes.ts +367 -59
  362. package/src/runtime/routes/sounds-config-routes.ts +239 -0
  363. package/src/runtime/routes/surface-action-routes.ts +84 -4
  364. package/src/runtime/routes/workflow-routes.test.ts +372 -0
  365. package/src/runtime/routes/workflow-routes.ts +363 -0
  366. package/src/runtime/routes/workspace-routes.test.ts +61 -1
  367. package/src/runtime/routes/workspace-routes.ts +26 -1
  368. package/src/runtime/services/__tests__/analyze-conversation.test.ts +38 -0
  369. package/src/runtime/services/analyze-conversation.ts +26 -13
  370. package/src/schedule/inference-profile.ts +28 -0
  371. package/src/schedule/schedule-store.ts +152 -4
  372. package/src/schedule/scheduler-types.ts +6 -0
  373. package/src/schedule/scheduler.ts +96 -0
  374. package/src/security/secret-allowlist.ts +1 -1
  375. package/src/skills/path-classifier.ts +1 -1
  376. package/src/tools/apps/executors.ts +23 -0
  377. package/src/tools/apps/resolve-app-id.ts +42 -0
  378. package/src/tools/browser/browser-execution.ts +9 -11
  379. package/src/tools/credentials/broker.ts +4 -4
  380. package/src/tools/credentials/vault.ts +26 -137
  381. package/src/tools/executor.ts +69 -0
  382. package/src/tools/flag-gated-tools.test.ts +76 -0
  383. package/src/tools/permission-checker.ts +8 -1
  384. package/src/tools/registry.ts +51 -0
  385. package/src/tools/schedule/create.ts +77 -1
  386. package/src/tools/schedule/list.ts +1 -0
  387. package/src/tools/schedule/update.ts +73 -1
  388. package/src/tools/skills/execute.ts +56 -0
  389. package/src/tools/terminal/shell.ts +1 -1
  390. package/src/tools/ui-surface/definitions.ts +91 -2
  391. package/src/tools/workflows/manage-workflows.ts +183 -0
  392. package/src/tools/workflows/run-workflow.test.ts +442 -0
  393. package/src/tools/workflows/run-workflow.ts +88 -0
  394. package/src/types/onboarding-context.ts +2 -0
  395. package/src/usage/attribution.ts +24 -0
  396. package/src/util/canonicalize-identity.ts +12 -3
  397. package/src/util/platform.ts +17 -17
  398. package/src/watcher/__tests__/engine.test.ts +24 -0
  399. package/src/watcher/__tests__/telemetry.test.ts +135 -0
  400. package/src/watcher/engine.ts +7 -0
  401. package/src/watcher/telemetry.ts +74 -0
  402. package/src/workflows/capabilities.test.ts +365 -0
  403. package/src/workflows/capabilities.ts +359 -0
  404. package/src/workflows/deterministic-stringify.ts +27 -0
  405. package/src/workflows/engine-integration.test.ts +656 -0
  406. package/src/workflows/engine.test.ts +1144 -0
  407. package/src/workflows/engine.ts +1078 -0
  408. package/src/workflows/fanout-load.test.ts +168 -0
  409. package/src/workflows/journal-store.test.ts +369 -0
  410. package/src/workflows/journal-store.ts +470 -0
  411. package/src/workflows/leaf-runner.test.ts +704 -0
  412. package/src/workflows/leaf-runner.ts +589 -0
  413. package/src/workflows/library.test.ts +134 -0
  414. package/src/workflows/library.ts +124 -0
  415. package/src/workflows/run-manager.test.ts +711 -0
  416. package/src/workflows/run-manager.ts +593 -0
  417. package/src/workflows/sandbox-escape.test.ts +339 -0
  418. package/src/workflows/sandbox.test.ts +251 -0
  419. package/src/workflows/sandbox.ts +447 -0
  420. package/src/workspace/adaptive-thinking-repair.ts +33 -11
  421. package/src/workspace/migrations/021-move-signals-to-workspace.ts +1 -1
  422. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +1 -1
  423. package/src/workspace/migrations/026-backfill-install-meta.ts +1 -1
  424. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +2 -1
  425. package/src/workspace/migrations/031-drop-user-md.ts +1 -4
  426. package/src/workspace/migrations/048-remove-workspace-hooks.ts +1 -1
  427. package/src/workspace/migrations/056-release-notes-inference-profile-reordering.ts +5 -2
  428. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +1 -1
  429. package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +8 -2
  430. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +41 -14
  431. package/src/workspace/migrations/102-preserve-heartbeat-enabled-for-existing-workspaces.ts +69 -0
  432. package/src/workspace/migrations/103-upgrade-quality-profile-to-opus-4-8.ts +83 -0
  433. package/src/workspace/migrations/104-recheck-adaptive-thinking-model-implied-anthropic.ts +133 -0
  434. package/src/workspace/migrations/registry.ts +6 -0
  435. package/src/workspace/migrations/runner.ts +1 -1
  436. package/tsconfig.json +1 -1
  437. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -200
  438. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -579
  439. package/src/__tests__/guardian-action-store.test.ts +0 -106
  440. package/src/daemon/guardian-action-generators.ts +0 -71
  441. package/src/memory/guardian-action-store.ts +0 -484
  442. package/src/runtime/guardian-action-grant-minter.ts +0 -150
@@ -0,0 +1,571 @@
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 { migrateContactChannelsUniqueExtUser } from "../289-contact-channels-unique-ext-user.js";
9
+
10
+ interface IndexRow {
11
+ name: string;
12
+ unique: number;
13
+ }
14
+
15
+ interface ChannelRow {
16
+ id: string;
17
+ contact_id: string;
18
+ type: string;
19
+ address: string;
20
+ external_user_id: string | null;
21
+ status: string;
22
+ updated_at: number | null;
23
+ }
24
+
25
+ function createTestDb() {
26
+ const sqlite = new Database(":memory:");
27
+ sqlite.exec("PRAGMA journal_mode=WAL");
28
+ sqlite.exec("PRAGMA foreign_keys = ON");
29
+ return drizzle(sqlite, { schema });
30
+ }
31
+
32
+ /**
33
+ * Bootstrap minimal contact_channels + contacts tables with a case-sensitive
34
+ * unique index on (type, address) and a non-unique index on
35
+ * (type, external_user_id) — the pre-migration state.
36
+ */
37
+ function bootstrap(db: ReturnType<typeof createTestDb>): void {
38
+ const raw = getSqliteFrom(db);
39
+ raw.exec(/*sql*/ `
40
+ CREATE TABLE contacts (
41
+ id TEXT PRIMARY KEY,
42
+ display_name TEXT NOT NULL,
43
+ created_at INTEGER NOT NULL,
44
+ updated_at INTEGER NOT NULL,
45
+ role TEXT NOT NULL DEFAULT 'contact',
46
+ contact_type TEXT NOT NULL DEFAULT 'human'
47
+ )
48
+ `);
49
+ raw.exec(/*sql*/ `
50
+ CREATE TABLE contact_channels (
51
+ id TEXT PRIMARY KEY,
52
+ contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
53
+ type TEXT NOT NULL,
54
+ address TEXT NOT NULL,
55
+ is_primary INTEGER NOT NULL DEFAULT 0,
56
+ external_user_id TEXT,
57
+ external_chat_id TEXT,
58
+ status TEXT NOT NULL DEFAULT 'unverified',
59
+ policy TEXT NOT NULL DEFAULT 'allow',
60
+ interaction_count INTEGER NOT NULL DEFAULT 0,
61
+ updated_at INTEGER,
62
+ created_at INTEGER NOT NULL
63
+ )
64
+ `);
65
+ raw.exec(/*sql*/ `
66
+ CREATE UNIQUE INDEX idx_contact_channels_type_address
67
+ ON contact_channels(type, address)
68
+ `);
69
+ raw.exec(/*sql*/ `
70
+ CREATE INDEX idx_contact_channels_type_ext_user
71
+ ON contact_channels(type, external_user_id)
72
+ `);
73
+ }
74
+
75
+ function insertContact(raw: Database, id: string): void {
76
+ const now = Date.now();
77
+ raw
78
+ .query(
79
+ `INSERT INTO contacts (id, display_name, created_at, updated_at) VALUES (?, ?, ?, ?)`,
80
+ )
81
+ .run(id, `Contact ${id}`, now, now);
82
+ }
83
+
84
+ function insertChannel(
85
+ raw: Database,
86
+ opts: {
87
+ id: string;
88
+ contactId: string;
89
+ type: string;
90
+ address: string;
91
+ externalUserId: string | null;
92
+ status: string;
93
+ updatedAt: number;
94
+ },
95
+ ): void {
96
+ raw
97
+ .query(
98
+ `INSERT INTO contact_channels (id, contact_id, type, address, external_user_id, status, updated_at, created_at)
99
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
100
+ )
101
+ .run(
102
+ opts.id,
103
+ opts.contactId,
104
+ opts.type,
105
+ opts.address,
106
+ opts.externalUserId,
107
+ opts.status,
108
+ opts.updatedAt,
109
+ opts.updatedAt,
110
+ );
111
+ }
112
+
113
+ function getAllChannels(raw: Database): ChannelRow[] {
114
+ return raw
115
+ .query<ChannelRow, []>(`SELECT * FROM contact_channels ORDER BY id`)
116
+ .all();
117
+ }
118
+
119
+ function getIndexes(raw: Database): IndexRow[] {
120
+ return raw
121
+ .query<
122
+ IndexRow,
123
+ []
124
+ >(`SELECT name, "unique" FROM pragma_index_list('contact_channels')`)
125
+ .all();
126
+ }
127
+
128
+ describe("migration 287 — dedup case collisions + drop ext_user indexes", () => {
129
+ test("no-op on clean database (no duplicates), preserves original casing", () => {
130
+ const db = createTestDb();
131
+ const raw = getSqliteFrom(db);
132
+ bootstrap(db);
133
+
134
+ insertContact(raw, "c1");
135
+ insertChannel(raw, {
136
+ id: "ch1",
137
+ contactId: "c1",
138
+ type: "slack",
139
+ address: "U111",
140
+ externalUserId: "U111",
141
+ status: "active",
142
+ updatedAt: 1000,
143
+ });
144
+ insertChannel(raw, {
145
+ id: "ch2",
146
+ contactId: "c1",
147
+ type: "telegram",
148
+ address: "T222",
149
+ externalUserId: "T222",
150
+ status: "active",
151
+ updatedAt: 1000,
152
+ });
153
+
154
+ migrateContactChannelsUniqueExtUser(db);
155
+
156
+ const channels = getAllChannels(raw);
157
+ expect(channels).toHaveLength(2);
158
+ expect(channels.map((c) => c.id).sort()).toEqual(["ch1", "ch2"]);
159
+ // Addresses preserve original casing
160
+ expect(channels.find((c) => c.id === "ch1")!.address).toBe("U111");
161
+ expect(channels.find((c) => c.id === "ch2")!.address).toBe("T222");
162
+ });
163
+
164
+ test("preserves original Slack address casing", () => {
165
+ const db = createTestDb();
166
+ const raw = getSqliteFrom(db);
167
+ bootstrap(db);
168
+
169
+ insertContact(raw, "c1");
170
+ insertChannel(raw, {
171
+ id: "ch1",
172
+ contactId: "c1",
173
+ type: "slack",
174
+ address: "U12345ABC",
175
+ externalUserId: "U12345ABC",
176
+ status: "active",
177
+ updatedAt: 1000,
178
+ });
179
+
180
+ migrateContactChannelsUniqueExtUser(db);
181
+
182
+ const channels = getAllChannels(raw);
183
+ expect(channels).toHaveLength(1);
184
+ expect(channels[0]!.address).toBe("U12345ABC");
185
+ expect(channels[0]!.external_user_id).toBe("U12345ABC");
186
+ });
187
+
188
+ test("deduplicates historical case collisions (active wins over unverified)", () => {
189
+ const db = createTestDb();
190
+ const raw = getSqliteFrom(db);
191
+ bootstrap(db);
192
+
193
+ insertContact(raw, "c1");
194
+ insertContact(raw, "c2");
195
+
196
+ // Two Slack channels with different casing — historical inconsistency.
197
+ insertChannel(raw, {
198
+ id: "ch-upper",
199
+ contactId: "c1",
200
+ type: "slack",
201
+ address: "U12345",
202
+ externalUserId: "U12345",
203
+ status: "active",
204
+ updatedAt: 1000,
205
+ });
206
+ insertChannel(raw, {
207
+ id: "ch-lower",
208
+ contactId: "c2",
209
+ type: "slack",
210
+ address: "u12345",
211
+ externalUserId: "U12345",
212
+ status: "unverified",
213
+ updatedAt: 900,
214
+ });
215
+
216
+ migrateContactChannelsUniqueExtUser(db);
217
+
218
+ const channels = getAllChannels(raw);
219
+ // Only one survives — active wins over unverified
220
+ expect(channels).toHaveLength(1);
221
+ expect(channels[0]!.id).toBe("ch-upper");
222
+ expect(channels[0]!.address).toBe("U12345");
223
+ });
224
+
225
+ test("deduplicates rows keeping blocked over active (preserves deny state)", () => {
226
+ const db = createTestDb();
227
+ const raw = getSqliteFrom(db);
228
+ bootstrap(db);
229
+
230
+ insertContact(raw, "c1");
231
+ insertContact(raw, "c2");
232
+
233
+ insertChannel(raw, {
234
+ id: "ch-active",
235
+ contactId: "c1",
236
+ type: "slack",
237
+ address: "U789",
238
+ externalUserId: "U789",
239
+ status: "active",
240
+ updatedAt: 1000,
241
+ });
242
+ insertChannel(raw, {
243
+ id: "ch-blocked",
244
+ contactId: "c2",
245
+ type: "slack",
246
+ address: "u789",
247
+ externalUserId: "U789",
248
+ status: "blocked",
249
+ updatedAt: 800,
250
+ });
251
+
252
+ migrateContactChannelsUniqueExtUser(db);
253
+
254
+ const channels = getAllChannels(raw);
255
+ expect(channels).toHaveLength(1);
256
+ expect(channels[0]!.id).toBe("ch-blocked");
257
+ expect(channels[0]!.status).toBe("blocked");
258
+ });
259
+
260
+ test("deduplicates rows keeping most recent when same status", () => {
261
+ const db = createTestDb();
262
+ const raw = getSqliteFrom(db);
263
+ bootstrap(db);
264
+
265
+ insertContact(raw, "c1");
266
+ insertContact(raw, "c2");
267
+
268
+ insertChannel(raw, {
269
+ id: "ch-old",
270
+ contactId: "c1",
271
+ type: "slack",
272
+ address: "U456",
273
+ externalUserId: "U456",
274
+ status: "unverified",
275
+ updatedAt: 500,
276
+ });
277
+ insertChannel(raw, {
278
+ id: "ch-new",
279
+ contactId: "c2",
280
+ type: "slack",
281
+ address: "u456",
282
+ externalUserId: "U456",
283
+ status: "unverified",
284
+ updatedAt: 1000,
285
+ });
286
+
287
+ migrateContactChannelsUniqueExtUser(db);
288
+
289
+ const channels = getAllChannels(raw);
290
+ expect(channels).toHaveLength(1);
291
+ expect(channels[0]!.id).toBe("ch-new");
292
+ });
293
+
294
+ test("does not touch rows with different addresses on different types", () => {
295
+ const db = createTestDb();
296
+ const raw = getSqliteFrom(db);
297
+ bootstrap(db);
298
+
299
+ insertContact(raw, "c1");
300
+
301
+ insertChannel(raw, {
302
+ id: "ch-email-1",
303
+ contactId: "c1",
304
+ type: "email",
305
+ address: "user@example.com",
306
+ externalUserId: null,
307
+ status: "active",
308
+ updatedAt: 1000,
309
+ });
310
+ insertChannel(raw, {
311
+ id: "ch-email-2",
312
+ contactId: "c1",
313
+ type: "email",
314
+ address: "user2@example.com",
315
+ externalUserId: null,
316
+ status: "active",
317
+ updatedAt: 1000,
318
+ });
319
+
320
+ migrateContactChannelsUniqueExtUser(db);
321
+
322
+ const channels = getAllChannels(raw);
323
+ expect(channels).toHaveLength(2);
324
+ });
325
+
326
+ test("preserves external_user_id index (dropped by follow-up migration)", () => {
327
+ const db = createTestDb();
328
+ const raw = getSqliteFrom(db);
329
+ bootstrap(db);
330
+
331
+ const beforeIndexes = getIndexes(raw);
332
+ const oldIdx = beforeIndexes.find(
333
+ (i) => i.name === "idx_contact_channels_type_ext_user",
334
+ );
335
+ expect(oldIdx).toBeDefined();
336
+
337
+ migrateContactChannelsUniqueExtUser(db);
338
+
339
+ const afterIndexes = getIndexes(raw);
340
+ // Index is preserved — lookups still use externalUserId until PR 2
341
+ expect(
342
+ afterIndexes.find((i) => i.name === "idx_contact_channels_type_ext_user"),
343
+ ).toBeDefined();
344
+ });
345
+
346
+ test("preserves existing case-sensitive UNIQUE(type, address) index", () => {
347
+ const db = createTestDb();
348
+ const raw = getSqliteFrom(db);
349
+ bootstrap(db);
350
+
351
+ insertContact(raw, "c1");
352
+ insertChannel(raw, {
353
+ id: "ch1",
354
+ contactId: "c1",
355
+ type: "slack",
356
+ address: "U789",
357
+ externalUserId: "U789",
358
+ status: "active",
359
+ updatedAt: 1000,
360
+ });
361
+
362
+ migrateContactChannelsUniqueExtUser(db);
363
+
364
+ // The unique index on (type, address) still exists
365
+ const afterIndexes = getIndexes(raw);
366
+ const typeAddrIdx = afterIndexes.find(
367
+ (i) => i.name === "idx_contact_channels_type_address",
368
+ );
369
+ expect(typeAddrIdx).toBeDefined();
370
+ expect(typeAddrIdx!.unique).toBe(1);
371
+ });
372
+
373
+ test("same address on different channel types both survive", () => {
374
+ const db = createTestDb();
375
+ const raw = getSqliteFrom(db);
376
+ bootstrap(db);
377
+
378
+ insertContact(raw, "c1");
379
+ insertChannel(raw, {
380
+ id: "ch1",
381
+ contactId: "c1",
382
+ type: "slack",
383
+ address: "U111",
384
+ externalUserId: "U111",
385
+ status: "active",
386
+ updatedAt: 1000,
387
+ });
388
+ insertChannel(raw, {
389
+ id: "ch2",
390
+ contactId: "c1",
391
+ type: "telegram",
392
+ address: "U111",
393
+ externalUserId: "U111",
394
+ status: "active",
395
+ updatedAt: 1000,
396
+ });
397
+
398
+ migrateContactChannelsUniqueExtUser(db);
399
+
400
+ const channels = getAllChannels(raw);
401
+ expect(channels).toHaveLength(2);
402
+ });
403
+
404
+ test("does not normalize address casing (deferred to follow-up migration)", () => {
405
+ const db = createTestDb();
406
+ const raw = getSqliteFrom(db);
407
+ bootstrap(db);
408
+
409
+ insertContact(raw, "c1");
410
+ // Simulates old write path that lowercased the address
411
+ insertChannel(raw, {
412
+ id: "ch1",
413
+ contactId: "c1",
414
+ type: "slack",
415
+ address: "u12345abc",
416
+ externalUserId: "U12345ABC",
417
+ status: "active",
418
+ updatedAt: 1000,
419
+ });
420
+
421
+ migrateContactChannelsUniqueExtUser(db);
422
+
423
+ const channels = getAllChannels(raw);
424
+ expect(channels).toHaveLength(1);
425
+ // Address is NOT normalized — casing restoration deferred to later migration
426
+ expect(channels[0]!.address).toBe("u12345abc");
427
+ });
428
+
429
+ test("deduplicates rows sharing external_user_id but having different addresses", () => {
430
+ const db = createTestDb();
431
+ const raw = getSqliteFrom(db);
432
+ bootstrap(db);
433
+
434
+ insertContact(raw, "c1");
435
+ insertContact(raw, "c2");
436
+
437
+ // Two rows with different addresses but same external_user_id.
438
+ // Step 1 (address dedup) keeps both since addresses differ.
439
+ // Step 2 (external_user_id dedup) must reduce to one row to prevent
440
+ // step 3 from creating an address collision.
441
+ insertChannel(raw, {
442
+ id: "ch-old-addr",
443
+ contactId: "c1",
444
+ type: "slack",
445
+ address: "alice-old",
446
+ externalUserId: "U12345",
447
+ status: "unverified",
448
+ updatedAt: 500,
449
+ });
450
+ insertChannel(raw, {
451
+ id: "ch-correct",
452
+ contactId: "c2",
453
+ type: "slack",
454
+ address: "U12345",
455
+ externalUserId: "U12345",
456
+ status: "active",
457
+ updatedAt: 1000,
458
+ });
459
+
460
+ migrateContactChannelsUniqueExtUser(db);
461
+
462
+ const channels = getAllChannels(raw);
463
+ // Only one survives — active wins over unverified
464
+ expect(channels).toHaveLength(1);
465
+ expect(channels[0]!.id).toBe("ch-correct");
466
+ expect(channels[0]!.address).toBe("U12345");
467
+ });
468
+
469
+ test("cross-column collision: row with NULL ext_user_id removed as blocker", () => {
470
+ const db = createTestDb();
471
+ const raw = getSqliteFrom(db);
472
+ bootstrap(db);
473
+
474
+ insertContact(raw, "c1");
475
+ insertContact(raw, "c2");
476
+
477
+ // Row A: has external_user_id but stale lowercased address
478
+ insertChannel(raw, {
479
+ id: "ch-normalizer",
480
+ contactId: "c1",
481
+ type: "slack",
482
+ address: "old-handle",
483
+ externalUserId: "U12345",
484
+ status: "active",
485
+ updatedAt: 2000,
486
+ });
487
+
488
+ // Row B: occupies the target address with NULL external_user_id
489
+ insertChannel(raw, {
490
+ id: "ch-blocker",
491
+ contactId: "c2",
492
+ type: "slack",
493
+ address: "U12345",
494
+ externalUserId: null,
495
+ status: "unverified",
496
+ updatedAt: 1000,
497
+ });
498
+
499
+ migrateContactChannelsUniqueExtUser(db);
500
+
501
+ const channels = getAllChannels(raw);
502
+ // Blocker removed; normalizer keeps its address (normalization deferred)
503
+ expect(channels).toHaveLength(1);
504
+ expect(channels[0]!.id).toBe("ch-normalizer");
505
+ expect(channels[0]!.address).toBe("old-handle");
506
+ });
507
+
508
+ test("cross-column collision: case-insensitive match removes lowercased blocker", () => {
509
+ const db = createTestDb();
510
+ const raw = getSqliteFrom(db);
511
+ bootstrap(db);
512
+
513
+ insertContact(raw, "c1");
514
+ insertContact(raw, "c2");
515
+
516
+ // Row A: has external_user_id with original Slack casing
517
+ insertChannel(raw, {
518
+ id: "ch-normalizer",
519
+ contactId: "c1",
520
+ type: "slack",
521
+ address: "old-handle",
522
+ externalUserId: "U12345ABC",
523
+ status: "active",
524
+ updatedAt: 2000,
525
+ });
526
+
527
+ // Row B: occupies a lowercased variant of the target address (from old write paths)
528
+ insertChannel(raw, {
529
+ id: "ch-blocker",
530
+ contactId: "c2",
531
+ type: "slack",
532
+ address: "u12345abc",
533
+ externalUserId: null,
534
+ status: "unverified",
535
+ updatedAt: 1000,
536
+ });
537
+
538
+ migrateContactChannelsUniqueExtUser(db);
539
+
540
+ const channels = getAllChannels(raw);
541
+ // Blocker removed despite case difference — COLLATE NOCASE catches it
542
+ expect(channels).toHaveLength(1);
543
+ expect(channels[0]!.id).toBe("ch-normalizer");
544
+ expect(channels[0]!.address).toBe("old-handle");
545
+ });
546
+
547
+ test("idempotent — safe to run twice", () => {
548
+ const db = createTestDb();
549
+ const raw = getSqliteFrom(db);
550
+ bootstrap(db);
551
+
552
+ insertContact(raw, "c1");
553
+ insertChannel(raw, {
554
+ id: "ch1",
555
+ contactId: "c1",
556
+ type: "slack",
557
+ address: "U999",
558
+ externalUserId: "U999",
559
+ status: "active",
560
+ updatedAt: 1000,
561
+ });
562
+
563
+ migrateContactChannelsUniqueExtUser(db);
564
+ migrateContactChannelsUniqueExtUser(db);
565
+
566
+ const channels = getAllChannels(raw);
567
+ expect(channels).toHaveLength(1);
568
+ // Original casing preserved
569
+ expect(channels[0]!.address).toBe("U999");
570
+ });
571
+ });
@@ -272,6 +272,16 @@ export { migrateAddMemoryV3EverInjected } from "./277-add-memory-v3-ever-injecte
272
272
  export { migrateToolInvocationsTelemetryColumns } from "./278-tool-invocations-telemetry-columns.js";
273
273
  export { createSkillLoadedEventsTable } from "./279-create-skill-loaded-events.js";
274
274
  export { migrateConversationsSurfacedAt } from "./280-conversations-surfaced-at.js";
275
+ export { migrateMemoryRetrospectiveRememberedLog } from "./281-memory-retrospective-remembered-log.js";
276
+ export { migrateScheduleInferenceProfile } from "./282-schedule-inference-profile.js";
277
+ export { migrateMemoryV3SelectionsMessageIdAndSections } from "./283-memory-v3-selections-message-id-and-sections.js";
278
+ export { migrateWorkflowRuns } from "./284-workflow-runs.js";
279
+ export { migrateScheduleWorkflowMode } from "./285-schedule-workflow-mode.js";
280
+ export { migrateWorkflowRunTrust } from "./286-workflow-run-trust.js";
281
+ export { migrateConversationOriginChannelIndex } from "./287-conversation-origin-channel-index.js";
282
+ export { migrateBackfillOriginChannelFromBindings } from "./288-backfill-origin-channel-from-bindings.js";
283
+ export { migrateContactChannelsUniqueExtUser } from "./289-contact-channels-unique-ext-user.js";
284
+ export { migrateScheduleCapabilities } from "./290-schedule-capabilities.js";
275
285
  export {
276
286
  MIGRATION_REGISTRY,
277
287
  type MigrationRegistryEntry,
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Seeds preloaded apps into the workspace apps directory at daemon startup.
3
+ *
4
+ * A preloaded app ships as plain source files under
5
+ * `src/config/preloaded-apps/<dirName>/` and is copied into
6
+ * `data/apps/<dirName>/` with a fixed, well-known app id, then compiled.
7
+ * The assistant later populates it by overwriting designated content files
8
+ * (e.g. the personal page's `src/profile-data.ts`) and calling app_refresh —
9
+ * it never authors the layout itself.
10
+ *
11
+ * Seeding is idempotent and deliberately conservative: once the definition
12
+ * JSON exists the app is considered user-owned (its content may have been
13
+ * populated) and the template is never re-copied. A missing `dist/` triggers
14
+ * a recompile only.
15
+ *
16
+ * Seeding is gated on the activation-flow experiment: only assistants in the
17
+ * personal-page treatment arm get the app. The gate is evaluated once per
18
+ * daemon startup — an assistant moved into the arm later picks it up on its
19
+ * next restart, and an already-seeded app is never removed when the
20
+ * assistant leaves the arm (it is user-owned by then).
21
+ */
22
+
23
+ import { cpSync, existsSync, writeFileSync } from "node:fs";
24
+ import { join } from "node:path";
25
+
26
+ import { compileApp } from "../bundler/app-compiler.js";
27
+ import {
28
+ getAssistantFeatureFlagValue,
29
+ initFeatureFlagOverrides,
30
+ } from "../config/assistant-feature-flags.js";
31
+ import type { AssistantConfig } from "../config/schema.js";
32
+ import { resolveBundledDir } from "../util/bundled-asset.js";
33
+ import { getLogger } from "../util/logger.js";
34
+ import { getAppsDir } from "./app-store.js";
35
+
36
+ const log = getLogger("preloaded-apps");
37
+
38
+ /** Well-known id (and dirName) of the personal landing page app. */
39
+ export const PERSONAL_PAGE_APP_ID = "personal-page";
40
+
41
+ /** Multivariate activation-flow experiment flag (scope "both"). */
42
+ export const ACTIVATION_FLOW_FLAG = "experiment-activation-flow-2026-06-03";
43
+
44
+ /** Arm of the activation-flow experiment that gets the preseeded page. */
45
+ export const PERSONAL_PAGE_ARM = "personal-page";
46
+
47
+ export async function seedPreloadedApps(
48
+ config: AssistantConfig,
49
+ ): Promise<void> {
50
+ // The lifecycle hook fires this early in startup, possibly before the
51
+ // gateway override fetch has completed. Await it here (no-op when already
52
+ // cached; bounded retries otherwise) so the arm read below sees the
53
+ // platform-assigned value rather than the registry default.
54
+ await initFeatureFlagOverrides();
55
+
56
+ const arm = getAssistantFeatureFlagValue(ACTIVATION_FLOW_FLAG, config);
57
+ if (arm !== PERSONAL_PAGE_ARM) return;
58
+
59
+ await seedPersonalPageApp();
60
+ }
61
+
62
+ async function seedPersonalPageApp(): Promise<void> {
63
+ const appsDir = getAppsDir();
64
+ const definitionPath = join(appsDir, `${PERSONAL_PAGE_APP_ID}.json`);
65
+ const appDir = join(appsDir, PERSONAL_PAGE_APP_ID);
66
+ const alreadySeeded = existsSync(definitionPath);
67
+
68
+ if (alreadySeeded && existsSync(join(appDir, "dist", "index.html"))) {
69
+ return;
70
+ }
71
+
72
+ if (!alreadySeeded) {
73
+ const templateDir = resolveBundledDir(
74
+ import.meta.dirname ?? __dirname,
75
+ "../config/preloaded-apps/personal-page",
76
+ "preloaded-apps/personal-page",
77
+ );
78
+ if (!existsSync(join(templateDir, "src"))) {
79
+ log.warn(
80
+ { templateDir },
81
+ "Personal page template not found — skipping preloaded app seeding",
82
+ );
83
+ return;
84
+ }
85
+
86
+ cpSync(join(templateDir, "src"), join(appDir, "src"), { recursive: true });
87
+ // Multifile (formatVersion 2) apps keep an empty root index.html; the
88
+ // real entrypoint is src/index.html compiled into dist/. Matches the
89
+ // on-disk shape produced by createApp().
90
+ writeFileSync(join(appDir, "index.html"), "", "utf-8");
91
+
92
+ const now = Date.now();
93
+ const definition = {
94
+ id: PERSONAL_PAGE_APP_ID,
95
+ name: "Your Page",
96
+ description:
97
+ "A personal landing page the assistant fills in with what it learns about you.",
98
+ icon: "✨",
99
+ schemaJson: "{}",
100
+ createdAt: now,
101
+ updatedAt: now,
102
+ formatVersion: 2,
103
+ dirName: PERSONAL_PAGE_APP_ID,
104
+ };
105
+ writeFileSync(definitionPath, JSON.stringify(definition, null, 2), "utf-8");
106
+ log.info({ appDir }, "Seeded personal page app");
107
+ }
108
+
109
+ const result = await compileApp(appDir);
110
+ if (!result.ok) {
111
+ log.warn(
112
+ { errors: result.errors },
113
+ "Personal page app failed to compile after seeding",
114
+ );
115
+ }
116
+ }