@vellumai/assistant 0.6.5 → 0.6.6

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 (443) hide show
  1. package/AGENTS.md +9 -1
  2. package/ARCHITECTURE.md +15 -17
  3. package/Dockerfile +6 -4
  4. package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
  5. package/docs/architecture/integrations.md +32 -39
  6. package/docs/architecture/memory.md +25 -30
  7. package/docs/architecture/security.md +7 -6
  8. package/docs/browser-use-architecture-phase2.md +63 -20
  9. package/docs/plugins.md +761 -0
  10. package/examples/plugins/echo/README.md +132 -0
  11. package/examples/plugins/echo/package.json +17 -0
  12. package/examples/plugins/echo/register.ts +187 -0
  13. package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
  14. package/openapi.yaml +212 -68
  15. package/package.json +1 -1
  16. package/src/__tests__/app-compiler.test.ts +57 -0
  17. package/src/__tests__/approval-cascade.test.ts +7 -2
  18. package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
  19. package/src/__tests__/avatar-generator.test.ts +4 -2
  20. package/src/__tests__/bundled-asset.test.ts +6 -6
  21. package/src/__tests__/catalog-cache.test.ts +69 -0
  22. package/src/__tests__/checker.test.ts +459 -171
  23. package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
  24. package/src/__tests__/compaction-events.test.ts +501 -0
  25. package/src/__tests__/compaction-pipeline.test.ts +210 -0
  26. package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
  27. package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
  28. package/src/__tests__/config-model-image-provider.test.ts +110 -0
  29. package/src/__tests__/config-schema.test.ts +22 -9
  30. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
  31. package/src/__tests__/contacts-tools.test.ts +26 -0
  32. package/src/__tests__/context-overflow-policy.test.ts +7 -7
  33. package/src/__tests__/context-window-manager.test.ts +355 -4
  34. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  35. package/src/__tests__/conversation-agent-loop-overflow.test.ts +26 -30
  36. package/src/__tests__/conversation-agent-loop.test.ts +30 -141
  37. package/src/__tests__/conversation-confirmation-signals.test.ts +6 -1
  38. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  39. package/src/__tests__/conversation-init.benchmark.test.ts +2 -16
  40. package/src/__tests__/conversation-pairing.test.ts +174 -10
  41. package/src/__tests__/conversation-pre-run-repair.test.ts +4 -1
  42. package/src/__tests__/conversation-process-callsite.test.ts +3 -0
  43. package/src/__tests__/conversation-provider-retry-repair.test.ts +16 -7
  44. package/src/__tests__/conversation-queue.test.ts +29 -14
  45. package/src/__tests__/conversation-routes-disk-view.test.ts +7 -6
  46. package/src/__tests__/conversation-runtime-assembly.test.ts +155 -110
  47. package/src/__tests__/conversation-runtime-workspace.test.ts +23 -38
  48. package/src/__tests__/conversation-seed-composer.test.ts +2 -2
  49. package/src/__tests__/conversation-slash-queue.test.ts +7 -2
  50. package/src/__tests__/conversation-slash-unknown.test.ts +25 -2
  51. package/src/__tests__/conversation-speed-override.test.ts +6 -1
  52. package/src/__tests__/conversation-title-service.test.ts +116 -0
  53. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
  54. package/src/__tests__/conversation-usage.test.ts +1 -1
  55. package/src/__tests__/conversation-workspace-cache-state.test.ts +4 -1
  56. package/src/__tests__/conversation-workspace-injection.test.ts +3 -0
  57. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +4 -1
  58. package/src/__tests__/credential-health-service.test.ts +78 -9
  59. package/src/__tests__/credential-security-invariants.test.ts +2 -2
  60. package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
  61. package/src/__tests__/empty-response-pipeline.test.ts +305 -0
  62. package/src/__tests__/extension-id-sync-guard.test.ts +3 -3
  63. package/src/__tests__/first-greeting.test.ts +247 -5
  64. package/src/__tests__/headless-browser-mode.test.ts +57 -0
  65. package/src/__tests__/history-repair-pipeline.test.ts +399 -0
  66. package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
  67. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
  68. package/src/__tests__/host-proxy-interface.test.ts +36 -2
  69. package/src/__tests__/image-credentials.test.ts +137 -0
  70. package/src/__tests__/image-service-dispatcher.test.ts +186 -0
  71. package/src/__tests__/injector-chain.test.ts +526 -0
  72. package/src/__tests__/intent-routing.test.ts +0 -26
  73. package/src/__tests__/llm-call-pipeline.test.ts +285 -0
  74. package/src/__tests__/llm-schema.test.ts +1 -1
  75. package/src/__tests__/media-generate-image.test.ts +119 -13
  76. package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
  77. package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
  78. package/src/__tests__/migration-import-from-url.test.ts +5 -68
  79. package/src/__tests__/model-intents.test.ts +4 -2
  80. package/src/__tests__/notification-broadcaster.test.ts +3 -3
  81. package/src/__tests__/notification-decision-strategy.test.ts +0 -11
  82. package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
  83. package/src/__tests__/oauth-apps-routes.test.ts +1 -1
  84. package/src/__tests__/oauth-cli.test.ts +14 -12
  85. package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
  86. package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
  87. package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
  88. package/src/__tests__/oauth-providers-routes.test.ts +3 -2
  89. package/src/__tests__/oauth-store.test.ts +41 -76
  90. package/src/__tests__/onboarding-template-contract.test.ts +16 -64
  91. package/src/__tests__/openai-image-service.test.ts +368 -0
  92. package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
  93. package/src/__tests__/permission-checker-host-gate.test.ts +0 -24
  94. package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
  95. package/src/__tests__/persistence-pipeline.test.ts +377 -0
  96. package/src/__tests__/pipeline-runner.test.ts +565 -0
  97. package/src/__tests__/platform.test.ts +5 -2
  98. package/src/__tests__/plugin-bootstrap.test.ts +483 -0
  99. package/src/__tests__/plugin-registry.test.ts +273 -0
  100. package/src/__tests__/plugin-route-contribution.test.ts +288 -0
  101. package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
  102. package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
  103. package/src/__tests__/plugin-types.test.ts +320 -0
  104. package/src/__tests__/pricing.test.ts +44 -12
  105. package/src/__tests__/proxy-approval-callback.test.ts +69 -8
  106. package/src/__tests__/reaction-persistence.test.ts +1 -0
  107. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
  108. package/src/__tests__/registry.test.ts +0 -2
  109. package/src/__tests__/schedule-routes.test.ts +131 -1
  110. package/src/__tests__/scheduler-recurrence.test.ts +14 -70
  111. package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
  112. package/src/__tests__/secret-detection-handler.test.ts +0 -10
  113. package/src/__tests__/shell-identity.test.ts +0 -134
  114. package/src/__tests__/suggestion-routes.test.ts +103 -4
  115. package/src/__tests__/task-memory-cleanup.test.ts +1 -0
  116. package/src/__tests__/task-scheduler.test.ts +3 -15
  117. package/src/__tests__/test-preload.ts +11 -0
  118. package/src/__tests__/title-generate-pipeline.test.ts +224 -0
  119. package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
  120. package/src/__tests__/tool-error-pipeline.test.ts +244 -0
  121. package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
  122. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -6
  123. package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
  124. package/src/__tests__/tool-executor.test.ts +141 -0
  125. package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
  126. package/src/__tests__/tool-result-truncation.test.ts +0 -110
  127. package/src/__tests__/user-plugin-loader.test.ts +191 -0
  128. package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
  129. package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
  130. package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
  131. package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
  132. package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
  133. package/src/__tests__/workspace-policy.test.ts +21 -3
  134. package/src/agent/loop.ts +340 -102
  135. package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
  136. package/src/approvals/guardian-request-resolvers.ts +80 -0
  137. package/src/backup/__tests__/backup-worker.test.ts +2 -13
  138. package/src/backup/backup-worker.ts +3 -15
  139. package/src/bundler/app-compiler.ts +84 -1
  140. package/src/calls/call-state.ts +2 -2
  141. package/src/channels/__tests__/types.test.ts +3 -3
  142. package/src/channels/types.ts +6 -4
  143. package/src/cli/__tests__/notifications.test.ts +87 -211
  144. package/src/cli/commands/__tests__/backup.test.ts +1 -1
  145. package/src/cli/commands/__tests__/image-generation.test.ts +255 -35
  146. package/src/cli/commands/__tests__/inference-send.test.ts +12 -0
  147. package/src/cli/commands/__tests__/tts-synthesize.test.ts +12 -0
  148. package/src/cli/commands/backup.ts +2 -2
  149. package/src/cli/commands/clients.ts +138 -0
  150. package/src/cli/commands/completions.ts +2 -9
  151. package/src/cli/commands/conversations.ts +55 -7
  152. package/src/cli/commands/image-generation.ts +33 -34
  153. package/src/cli/commands/notifications.ts +68 -103
  154. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
  155. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
  156. package/src/cli/commands/oauth/connect.ts +2 -2
  157. package/src/cli/commands/oauth/providers.ts +176 -8
  158. package/src/cli/commands/oauth/status.ts +46 -36
  159. package/src/cli/commands/skills.ts +3 -4
  160. package/src/cli/program.ts +25 -29
  161. package/src/config/__tests__/backup-schema.test.ts +7 -2
  162. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  163. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
  164. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
  165. package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
  166. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
  167. package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
  168. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
  169. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
  170. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  171. package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
  172. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +12 -0
  173. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +58 -0
  174. package/src/config/bundled-skills/schedule/SKILL.md +8 -3
  175. package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
  176. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
  177. package/src/config/bundled-tool-registry.ts +0 -15
  178. package/src/config/feature-flag-registry.json +17 -1
  179. package/src/config/schema.ts +19 -0
  180. package/src/config/schemas/backup.ts +1 -1
  181. package/src/config/schemas/conversations.ts +16 -0
  182. package/src/config/schemas/llm.ts +2 -3
  183. package/src/config/schemas/security.ts +6 -6
  184. package/src/config/schemas/tts.ts +11 -0
  185. package/src/config/skill-state.ts +6 -2
  186. package/src/config/skills.ts +94 -5
  187. package/src/context/__tests__/compact-prompt.test.ts +27 -9
  188. package/src/context/prompts/compact.md +26 -12
  189. package/src/context/tool-result-truncation.ts +3 -63
  190. package/src/context/window-manager.ts +190 -16
  191. package/src/credential-health/credential-health-service.ts +19 -6
  192. package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
  193. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
  194. package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
  195. package/src/daemon/config-watcher.ts +0 -2
  196. package/src/daemon/context-overflow-policy.ts +4 -13
  197. package/src/daemon/conversation-agent-loop-handlers.ts +83 -22
  198. package/src/daemon/conversation-agent-loop.ts +984 -683
  199. package/src/daemon/conversation-history.ts +10 -19
  200. package/src/daemon/conversation-lifecycle.ts +37 -19
  201. package/src/daemon/conversation-notifiers.ts +2 -110
  202. package/src/daemon/conversation-process.ts +14 -7
  203. package/src/daemon/conversation-runtime-assembly.ts +532 -411
  204. package/src/daemon/conversation-tool-setup.ts +41 -4
  205. package/src/daemon/conversation.ts +80 -35
  206. package/src/daemon/external-plugins-bootstrap.ts +478 -0
  207. package/src/daemon/first-greeting.ts +191 -14
  208. package/src/daemon/handlers/config-model.ts +11 -0
  209. package/src/daemon/handlers/skills.ts +5 -1
  210. package/src/daemon/lifecycle.ts +33 -68
  211. package/src/daemon/message-types/computer-use.ts +2 -34
  212. package/src/daemon/message-types/conversations.ts +49 -0
  213. package/src/daemon/message-types/messages.ts +12 -0
  214. package/src/daemon/server.ts +5 -3
  215. package/src/daemon/shutdown-handlers.ts +2 -12
  216. package/src/daemon/tool-side-effects.ts +14 -56
  217. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
  218. package/src/heartbeat/heartbeat-service.ts +24 -1
  219. package/src/home/__tests__/feed-population-integration.test.ts +312 -0
  220. package/src/home/emit-feed-event.ts +7 -0
  221. package/src/home/feed-types.ts +41 -2
  222. package/src/home/rewrite-command-preview.ts +66 -0
  223. package/src/ipc/__tests__/socket-path.test.ts +11 -50
  224. package/src/ipc/cli-client.ts +1 -1
  225. package/src/ipc/cli-server.ts +3 -3
  226. package/src/ipc/gateway-client.ts +4 -1
  227. package/src/ipc/routes/browser-context.ts +2 -0
  228. package/src/ipc/routes/browser.ts +1 -0
  229. package/src/ipc/routes/get-contact.ts +16 -0
  230. package/src/ipc/routes/index.ts +14 -0
  231. package/src/ipc/routes/list-clients.ts +31 -0
  232. package/src/ipc/routes/merge-contacts.ts +17 -0
  233. package/src/ipc/routes/notification.ts +133 -0
  234. package/src/ipc/routes/rename-conversation.ts +59 -0
  235. package/src/ipc/routes/search-contacts.ts +19 -0
  236. package/src/ipc/routes/upsert-contact.ts +25 -0
  237. package/src/ipc/socket-path.ts +14 -38
  238. package/src/media/app-icon-generator.ts +23 -46
  239. package/src/media/avatar-router.ts +26 -41
  240. package/src/media/gemini-image-service.ts +8 -41
  241. package/src/media/image-credentials.ts +73 -0
  242. package/src/media/image-service.ts +85 -0
  243. package/src/media/openai-image-service.ts +131 -0
  244. package/src/media/types.ts +46 -0
  245. package/src/memory/conversation-crud.ts +48 -18
  246. package/src/memory/conversation-queries.ts +57 -4
  247. package/src/memory/conversation-title-service.ts +25 -0
  248. package/src/memory/db-init.ts +8 -0
  249. package/src/memory/embedding-gemini.test.ts +41 -2
  250. package/src/memory/embedding-gemini.ts +6 -1
  251. package/src/memory/graph/bootstrap.test.ts +282 -0
  252. package/src/memory/graph/bootstrap.ts +8 -5
  253. package/src/memory/graph/extraction.ts +10 -2
  254. package/src/memory/graph/graph-search.test.ts +1 -0
  255. package/src/memory/graph/inspect.ts +2 -2
  256. package/src/memory/graph/retriever.ts +10 -3
  257. package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
  258. package/src/memory/migrations/149-oauth-tables.ts +1 -0
  259. package/src/memory/migrations/223-schedule-script-column.ts +11 -0
  260. package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
  261. package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
  262. package/src/memory/migrations/index.ts +4 -0
  263. package/src/memory/pkb/pkb-index.test.ts +1 -0
  264. package/src/memory/pkb/pkb-reconcile.test.ts +1 -0
  265. package/src/memory/pkb/pkb-search.test.ts +65 -4
  266. package/src/memory/pkb/pkb-search.ts +40 -18
  267. package/src/memory/qdrant-client.test.ts +60 -0
  268. package/src/memory/qdrant-client.ts +25 -0
  269. package/src/memory/schema/infrastructure.ts +1 -0
  270. package/src/memory/schema/oauth.ts +4 -1
  271. package/src/messaging/providers/slack/render-transcript.test.ts +77 -29
  272. package/src/messaging/providers/slack/render-transcript.ts +58 -0
  273. package/src/notifications/conversation-pairing.ts +78 -19
  274. package/src/notifications/copy-composer.ts +0 -5
  275. package/src/notifications/emit-signal.ts +1 -1
  276. package/src/notifications/signal.ts +1 -2
  277. package/src/oauth/AGENTS.md +1 -1
  278. package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
  279. package/src/oauth/connect-orchestrator.ts +8 -34
  280. package/src/oauth/connect-types.ts +6 -10
  281. package/src/oauth/manual-token-connection.ts +23 -0
  282. package/src/oauth/oauth-store.ts +30 -14
  283. package/src/oauth/provider-serializer.ts +6 -1
  284. package/src/oauth/seed-providers.ts +56 -108
  285. package/src/outbound-proxy/http-forwarder.ts +9 -0
  286. package/src/permissions/approval-policy.test.ts +293 -18
  287. package/src/permissions/approval-policy.ts +110 -58
  288. package/src/permissions/arg-parser.test.ts +161 -0
  289. package/src/permissions/arg-parser.ts +141 -0
  290. package/src/permissions/bash-risk-classifier.test.ts +414 -2
  291. package/src/permissions/bash-risk-classifier.ts +303 -60
  292. package/src/permissions/checker.ts +157 -29
  293. package/src/permissions/command-registry.test.ts +239 -0
  294. package/src/permissions/command-registry.ts +234 -54
  295. package/src/permissions/defaults.ts +5 -4
  296. package/src/permissions/gateway-threshold-reader.ts +196 -0
  297. package/src/permissions/prompter.ts +4 -0
  298. package/src/permissions/risk-types.ts +61 -4
  299. package/src/permissions/schedule-risk-classifier.test.ts +129 -0
  300. package/src/permissions/schedule-risk-classifier.ts +85 -0
  301. package/src/permissions/shell-identity.ts +2 -42
  302. package/src/permissions/types.ts +2 -0
  303. package/src/permissions/workspace-policy.ts +8 -3
  304. package/src/plugins/defaults/circuit-breaker.ts +146 -0
  305. package/src/plugins/defaults/compaction.ts +145 -0
  306. package/src/plugins/defaults/empty-response.ts +126 -0
  307. package/src/plugins/defaults/history-repair.ts +85 -0
  308. package/src/plugins/defaults/index.ts +116 -0
  309. package/src/plugins/defaults/injectors.ts +491 -0
  310. package/src/plugins/defaults/llm-call.ts +82 -0
  311. package/src/plugins/defaults/memory-retrieval.ts +226 -0
  312. package/src/plugins/defaults/overflow-reduce.ts +181 -0
  313. package/src/plugins/defaults/persistence.ts +129 -0
  314. package/src/plugins/defaults/title-generate.ts +95 -0
  315. package/src/plugins/defaults/token-estimate.ts +104 -0
  316. package/src/plugins/defaults/tool-error.ts +126 -0
  317. package/src/plugins/defaults/tool-execute.ts +89 -0
  318. package/src/plugins/defaults/tool-result-truncate.ts +88 -0
  319. package/src/plugins/pipeline.ts +316 -0
  320. package/src/plugins/plugin-skill-contributions.ts +292 -0
  321. package/src/plugins/registry.ts +241 -0
  322. package/src/plugins/types.ts +1134 -0
  323. package/src/plugins/user-loader.ts +177 -0
  324. package/src/prompts/templates/BOOTSTRAP.md +27 -77
  325. package/src/providers/model-catalog.ts +52 -29
  326. package/src/providers/model-intents.ts +1 -1
  327. package/src/providers/openrouter/client.ts +5 -1
  328. package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
  329. package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
  330. package/src/providers/speech-to-text/xai-realtime.test.ts +72 -4
  331. package/src/providers/speech-to-text/xai-realtime.ts +39 -14
  332. package/src/runtime/AGENTS.md +25 -16
  333. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
  334. package/src/runtime/__tests__/client-registry.test.ts +293 -0
  335. package/src/runtime/client-registry.ts +261 -0
  336. package/src/runtime/http-server.ts +77 -8
  337. package/src/runtime/http-types.ts +0 -2
  338. package/src/runtime/migrations/vbundle-builder.ts +1 -22
  339. package/src/runtime/routes/approval-prompt-ts-tracker.ts +51 -31
  340. package/src/runtime/routes/approval-routes.ts +17 -0
  341. package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
  342. package/src/runtime/routes/conversation-routes.ts +223 -116
  343. package/src/runtime/routes/inbound-message-handler.ts +88 -13
  344. package/src/runtime/routes/memory-item-routes.test.ts +1 -0
  345. package/src/runtime/routes/migration-routes.ts +0 -3
  346. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
  347. package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
  348. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
  349. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
  350. package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
  351. package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
  352. package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
  353. package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
  354. package/src/runtime/routes/playground/deps.ts +56 -0
  355. package/src/runtime/routes/playground/force-compact.ts +73 -0
  356. package/src/runtime/routes/playground/guard.ts +37 -0
  357. package/src/runtime/routes/playground/index.ts +28 -0
  358. package/src/runtime/routes/playground/inject-failures.ts +159 -0
  359. package/src/runtime/routes/playground/reset-circuit.ts +115 -0
  360. package/src/runtime/routes/playground/seed-conversation.ts +139 -0
  361. package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
  362. package/src/runtime/routes/playground/state.ts +78 -0
  363. package/src/runtime/routes/schedule-routes.ts +89 -8
  364. package/src/runtime/skill-route-registry.ts +75 -15
  365. package/src/schedule/run-script.ts +68 -0
  366. package/src/schedule/schedule-store.ts +7 -1
  367. package/src/schedule/scheduler.ts +48 -8
  368. package/src/skills/catalog-cache.ts +12 -5
  369. package/src/tools/browser/__tests__/browser-status.test.ts +189 -0
  370. package/src/tools/browser/browser-execution.ts +88 -19
  371. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
  372. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
  373. package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
  374. package/src/tools/browser/cdp-client/factory.ts +15 -4
  375. package/src/tools/executor.ts +126 -74
  376. package/src/tools/network/script-proxy/session-manager.ts +37 -1
  377. package/src/tools/permission-checker.ts +98 -49
  378. package/src/tools/policy-context.ts +4 -0
  379. package/src/tools/registry.ts +140 -3
  380. package/src/tools/schedule/create.ts +23 -8
  381. package/src/tools/schedule/update.ts +3 -1
  382. package/src/tools/secret-detection-handler.ts +0 -51
  383. package/src/tools/system/avatar-generator.ts +6 -2
  384. package/src/tools/types.ts +28 -2
  385. package/src/util/platform.ts +7 -2
  386. package/src/util/pricing.ts +26 -3
  387. package/src/workspace/migrations/006-services-config.ts +2 -4
  388. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
  389. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +3 -4
  390. package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
  391. package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
  392. package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
  393. package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
  394. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
  395. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
  396. package/src/workspace/migrations/registry.ts +12 -0
  397. package/tsconfig.json +1 -1
  398. package/hook-templates/debug-prompt-logger/hook.json +0 -7
  399. package/hook-templates/debug-prompt-logger/run.sh +0 -66
  400. package/src/__tests__/compaction-circuit-breaker.test.ts +0 -336
  401. package/src/__tests__/context-overflow-approval.test.ts +0 -156
  402. package/src/__tests__/hooks-blocking.test.ts +0 -178
  403. package/src/__tests__/hooks-cli.test.ts +0 -182
  404. package/src/__tests__/hooks-config.test.ts +0 -108
  405. package/src/__tests__/hooks-discovery.test.ts +0 -211
  406. package/src/__tests__/hooks-integration.test.ts +0 -196
  407. package/src/__tests__/hooks-manager.test.ts +0 -226
  408. package/src/__tests__/hooks-runner.test.ts +0 -175
  409. package/src/__tests__/hooks-settings.test.ts +0 -160
  410. package/src/__tests__/hooks-templates.test.ts +0 -169
  411. package/src/__tests__/hooks-ts-runner.test.ts +0 -170
  412. package/src/__tests__/hooks-watch.test.ts +0 -112
  413. package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
  414. package/src/__tests__/oauth-scope-policy.test.ts +0 -180
  415. package/src/__tests__/send-notification-tool.test.ts +0 -83
  416. package/src/cli/commands/shotgun.ts +0 -266
  417. package/src/config/bundled-skills/conversations/SKILL.md +0 -20
  418. package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
  419. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -88
  420. package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
  421. package/src/config/bundled-skills/notifications/SKILL.md +0 -40
  422. package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
  423. package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
  424. package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
  425. package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
  426. package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
  427. package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
  428. package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
  429. package/src/daemon/context-overflow-approval.ts +0 -52
  430. package/src/daemon/watch-handler.ts +0 -399
  431. package/src/hooks/cli.ts +0 -253
  432. package/src/hooks/config.ts +0 -100
  433. package/src/hooks/discovery.ts +0 -135
  434. package/src/hooks/manager.ts +0 -179
  435. package/src/hooks/runner.ts +0 -117
  436. package/src/hooks/templates.ts +0 -77
  437. package/src/hooks/types.ts +0 -75
  438. package/src/oauth/scope-policy.ts +0 -89
  439. package/src/runtime/gateway-internal-client.ts +0 -94
  440. package/src/runtime/routes/watch-routes.ts +0 -156
  441. package/src/signals/shotgun.ts +0 -203
  442. package/src/tools/watch/screen-watch.ts +0 -144
  443. package/src/tools/watch/watch-state.ts +0 -142
@@ -107,6 +107,9 @@ export const messageMetadataSchema = z
107
107
  memoryInjectedBlock: z.string().optional(),
108
108
  turnContextBlock: z.string().optional(),
109
109
  pkbSystemReminderBlock: z.string().optional(),
110
+ workspaceBlock: z.string().optional(),
111
+ nowScratchpadBlock: z.string().optional(),
112
+ pkbContextBlock: z.string().optional(),
110
113
  })
111
114
  .passthrough();
112
115
 
@@ -1048,20 +1051,26 @@ export function getMessages(conversationId: string): MessageRow[] {
1048
1051
  }
1049
1052
 
1050
1053
  /**
1051
- * Count messages whose metadata JSON contains a `slackMeta` envelope, capped
1052
- * at `limit`. Pushes the cap into SQL (`LIKE` + `LIMIT`) so warm Slack DM
1053
- * conversations don't require a full-table scan + JSON parse on every
1054
- * inbound message to confirm the cold-start threshold has been cleared.
1055
- * Returns the number of matching rows up to `limit`; callers compare against
1056
- * the cold-start threshold to decide whether to backfill.
1054
+ * Return raw `metadata` strings for messages whose metadata contains the
1055
+ * literal substring `"slackMeta"`, capped at `limit` and skipping the first
1056
+ * `offset` matches. Pushes `LIKE` + `LIMIT`/`OFFSET` into SQL so warm Slack
1057
+ * DM conversations don't require a full-table scan on the webhook critical
1058
+ * path. The substring match is an indexable prefilter only callers must
1059
+ * parse and validate each returned string against the Slack metadata schema,
1060
+ * because a malformed row (partial write, legacy format, unrelated key
1061
+ * accidentally containing the literal) can still slip through the substring
1062
+ * match. Callers that need a fixed number of *valid* rows should iterate
1063
+ * with increasing offsets until the target is reached (capped at a
1064
+ * reasonable maximum to bound scan cost).
1057
1065
  */
1058
- export function countMessagesWithSlackMeta(
1066
+ export function selectSlackMetaCandidateMetadata(
1059
1067
  conversationId: string,
1060
1068
  limit: number,
1061
- ): number {
1069
+ offset = 0,
1070
+ ): string[] {
1062
1071
  const db = getDb();
1063
1072
  const rows = db
1064
- .select({ one: sql`1` })
1073
+ .select({ metadata: messages.metadata })
1065
1074
  .from(messages)
1066
1075
  .where(
1067
1076
  and(
@@ -1069,9 +1078,17 @@ export function countMessagesWithSlackMeta(
1069
1078
  like(messages.metadata, '%"slackMeta"%'),
1070
1079
  ),
1071
1080
  )
1081
+ .orderBy(asc(messages.createdAt))
1072
1082
  .limit(limit)
1083
+ .offset(offset)
1073
1084
  .all();
1074
- return rows.length;
1085
+ const out: string[] = [];
1086
+ for (const r of rows) {
1087
+ if (typeof r.metadata === "string" && r.metadata.length > 0) {
1088
+ out.push(r.metadata);
1089
+ }
1090
+ }
1091
+ return out;
1075
1092
  }
1076
1093
 
1077
1094
  /**
@@ -1503,22 +1520,35 @@ export function updateMessageMetadata(
1503
1520
  }
1504
1521
 
1505
1522
  /**
1506
- * Bulk-remove the `pkbSystemReminderBlock` field from every user-message
1507
- * metadata row in a conversation. Called from compaction-strip sites so
1508
- * post-restart rehydration stays consistent with the in-memory state
1509
- * produced by `stripInjectionsForCompaction` (which removes
1510
- * `<system_reminder>` from live messages but cannot touch the DB).
1523
+ * Bulk-remove the metadata fields that back the blocks stripped by
1524
+ * `stripInjectionsForCompaction` currently `pkbSystemReminderBlock`
1525
+ * (`<system_reminder>`), `nowScratchpadBlock` (`<NOW.md …>`), and
1526
+ * `pkbContextBlock` (`<knowledge_base>`). Called from compaction-strip
1527
+ * sites so post-restart rehydration stays consistent with the in-memory
1528
+ * state produced by `stripInjectionsForCompaction` (which removes those
1529
+ * tags from live messages but cannot touch the DB). Fields backing
1530
+ * blocks that are intentionally NOT stripped (`turnContextBlock`,
1531
+ * `workspaceBlock`, `memoryInjectedBlock`) are preserved.
1511
1532
  */
1512
- export function clearPkbSystemReminderMetadataForConversation(
1533
+ export function clearStrippedInjectionMetadataForConversation(
1513
1534
  conversationId: string,
1514
1535
  ): void {
1515
1536
  rawRun(
1516
1537
  `UPDATE messages
1517
- SET metadata = json_remove(metadata, '$.pkbSystemReminderBlock')
1538
+ SET metadata = json_remove(
1539
+ metadata,
1540
+ '$.pkbSystemReminderBlock',
1541
+ '$.nowScratchpadBlock',
1542
+ '$.pkbContextBlock'
1543
+ )
1518
1544
  WHERE conversation_id = ?
1519
1545
  AND role = 'user'
1520
1546
  AND metadata IS NOT NULL
1521
- AND json_extract(metadata, '$.pkbSystemReminderBlock') IS NOT NULL`,
1547
+ AND (
1548
+ json_extract(metadata, '$.pkbSystemReminderBlock') IS NOT NULL
1549
+ OR json_extract(metadata, '$.nowScratchpadBlock') IS NOT NULL
1550
+ OR json_extract(metadata, '$.pkbContextBlock') IS NOT NULL
1551
+ )`,
1522
1552
  conversationId,
1523
1553
  );
1524
1554
  }
@@ -88,6 +88,59 @@ export function listPinnedConversations(): ConversationRow[] {
88
88
  return query.all().map(parseConversation);
89
89
  }
90
90
 
91
+ /**
92
+ * Row shape returned by {@link listConversationsByTitlePrefix}.
93
+ *
94
+ * Kept deliberately narrow (no full `ConversationRow`) since the only caller
95
+ * today is the playground's seeded-conversation listing endpoint, which only
96
+ * needs display metadata plus a message count to show in a list.
97
+ */
98
+ export interface ConversationTitlePrefixRow {
99
+ id: string;
100
+ title: string;
101
+ messageCount: number;
102
+ createdAt: number;
103
+ }
104
+
105
+ /**
106
+ * List non-archived conversations whose `title` begins with `prefix`.
107
+ *
108
+ * Uses raw SQL with a subquery for `messageCount` so a single round-trip
109
+ * returns everything the caller needs. The `LIKE ? || '%'` pattern does a
110
+ * prefix match; SQLite optimizes this with an index when one exists on
111
+ * `title`, otherwise it degrades to a table scan — acceptable for the
112
+ * playground-seeded set, which is small by construction.
113
+ *
114
+ * Escaping is unnecessary here because the prefix is a build-time constant
115
+ * (`PLAYGROUND_TITLE_PREFIX`) rather than user input. If callers ever pass
116
+ * dynamic prefixes, switch to `ESCAPE '\\'` and pre-escape `%` / `_` / `\`.
117
+ */
118
+ export function listConversationsByTitlePrefix(
119
+ prefix: string,
120
+ ): ConversationTitlePrefixRow[] {
121
+ interface Row {
122
+ id: string;
123
+ title: string;
124
+ message_count: number;
125
+ created_at: number;
126
+ }
127
+ const rows = rawAll<Row>(
128
+ `SELECT c.id, c.title,
129
+ (SELECT COUNT(*) FROM messages WHERE conversation_id = c.id) AS message_count,
130
+ c.created_at
131
+ FROM conversations c
132
+ WHERE c.title LIKE ? || '%' AND c.archived_at IS NULL
133
+ ORDER BY c.created_at DESC`,
134
+ prefix,
135
+ );
136
+ return rows.map((r) => ({
137
+ id: r.id,
138
+ title: r.title,
139
+ messageCount: r.message_count,
140
+ createdAt: r.created_at,
141
+ }));
142
+ }
143
+
91
144
  export function countConversations(backgroundOnly = false): number {
92
145
  const db = getDb();
93
146
  const where = backgroundOnly
@@ -257,10 +310,10 @@ export function searchConversations(
257
310
  .from(conversations)
258
311
  .where(
259
312
  and(
260
- sql`${conversations.conversationType} NOT IN ('background', 'private', 'scheduled')`,
261
- sql`${conversations.title} LIKE ${titlePattern} ESCAPE '\\'`,
262
- sql`${conversations.archivedAt} IS NULL`,
263
- ),
313
+ sql`${conversations.conversationType} NOT IN ('background', 'private', 'scheduled')`,
314
+ sql`${conversations.title} LIKE ${titlePattern} ESCAPE '\\'`,
315
+ sql`${conversations.archivedAt} IS NULL`,
316
+ ),
264
317
  )
265
318
  .all();
266
319
  for (const row of titleMatchConvs) ftsConvIds.add(row.id);
@@ -234,6 +234,12 @@ export async function regenerateConversationTitle(
234
234
  }
235
235
 
236
236
  const prompt = buildRegenerationPrompt(recentMessages);
237
+ // Skip the LLM call if no messages yielded extractable text — the prompt
238
+ // would be just the "Recent messages:" header, and the model tends to
239
+ // fabricate a meta-title about the emptiness rather than decline.
240
+ if (!/\n(?:User|Assistant): /.test(prompt)) {
241
+ return { title: conversation.title ?? UNTITLED_FALLBACK, updated: false };
242
+ }
237
243
  const result = await runBtwSidechain({
238
244
  content: prompt,
239
245
  provider,
@@ -296,6 +302,7 @@ function buildTitleSystemPrompt(): string {
296
302
  "- Do NOT echo back what the user asked you to do",
297
303
  "- Do NOT respond to the conversation content",
298
304
  "- Do NOT assess feasibility or comment on capabilities",
305
+ "- If input is sparse or references external context, extract a topic from the words that ARE present (e.g. 'so about that t-shirt...' → 'T-Shirt Discussion'). Never describe the absence, emptiness, or insufficiency of context — titles like 'Missing Context', 'Unclear Request', 'No Topic' are forbidden",
299
306
  ].join("\n");
300
307
  }
301
308
 
@@ -329,9 +336,27 @@ function buildTitlePrompt(
329
336
  return parts.join("\n");
330
337
  }
331
338
 
339
+ const META_FAILURE_TITLES = new Set([
340
+ "missing context",
341
+ "no context",
342
+ "insufficient context",
343
+ "unclear context",
344
+ "empty context",
345
+ "no topic",
346
+ "unclear topic",
347
+ "unclear request",
348
+ "unclear message",
349
+ "empty conversation",
350
+ "empty message",
351
+ "no content",
352
+ ]);
353
+
332
354
  function normalizeTitle(raw: string): string {
333
355
  let title = raw.trim().replace(/^["']|["']$/g, "");
334
356
  title = stripMarkdown(title);
357
+ if (META_FAILURE_TITLES.has(title.toLowerCase())) {
358
+ return "";
359
+ }
335
360
  return title;
336
361
  }
337
362
 
@@ -17,6 +17,7 @@ import {
17
17
  addCoreColumns,
18
18
  createActorRefreshTokenRecordsTable,
19
19
  createActorTokenRecordsTable,
20
+ createApprovalPromptTsTrackerTable,
20
21
  createAssistantInboxTables,
21
22
  createCallSessionsTables,
22
23
  createCanonicalGuardianTables,
@@ -110,11 +111,13 @@ import {
110
111
  migrateNormalizeUserFileByPrincipal,
111
112
  migrateNotificationDeliveryThreadDecision,
112
113
  migrateOAuthAppsClientSecretPath,
114
+ migrateOAuthProvidersAvailableScopes,
113
115
  migrateOAuthProvidersBehaviorColumns,
114
116
  migrateOAuthProvidersDisplayMetadata,
115
117
  migrateOAuthProvidersFeatureFlag,
116
118
  migrateOAuthProvidersLogoUrl,
117
119
  migrateOAuthProvidersManagedServiceConfigKey,
120
+ migrateOAuthProvidersManagedServiceIsPaid,
118
121
  migrateOAuthProvidersPingConfig,
119
122
  migrateOAuthProvidersPingUrl,
120
123
  migrateOAuthProvidersRefreshUrl,
@@ -143,6 +146,7 @@ import {
143
146
  migrateScheduleOneShotRouting,
144
147
  migrateScheduleQuietFlag,
145
148
  migrateScheduleReuseConversation,
149
+ migrateScheduleScriptColumn,
146
150
  migrateSchemaIndexesAndColumns,
147
151
  migrateScrubCorruptedImageAttachments,
148
152
  migrateStripIntegrationPrefixFromProviderKeys,
@@ -267,6 +271,7 @@ export function initializeDb(): void {
267
271
  migrateInviteCodeHashColumn,
268
272
  createActorTokenRecordsTable,
269
273
  createActorRefreshTokenRecordsTable,
274
+ createApprovalPromptTsTrackerTable,
270
275
  migrateGuardianPrincipalIdColumns,
271
276
  migrateBackfillGuardianPrincipalId,
272
277
  migrateGuardianPrincipalIdNotNull,
@@ -360,6 +365,7 @@ export function initializeDb(): void {
360
365
  migrateConversationsLastMessageAt,
361
366
  migrateStripThinkingFromConsolidated,
362
367
  migrateScheduleReuseConversation,
368
+ migrateScheduleScriptColumn,
363
369
  migrateMemoryRecallLogsQueryContext,
364
370
  migrateLlmRequestLogsCreatedAtIndex,
365
371
  migrateOAuthProvidersScopeSeparator,
@@ -372,6 +378,8 @@ export function initializeDb(): void {
372
378
  migrateNormalizeUserFileByPrincipal,
373
379
  migrateConversationsArchivedAt,
374
380
  migrateStripPlaceholderSentinelsFromMessages,
381
+ migrateOAuthProvidersManagedServiceIsPaid,
382
+ migrateOAuthProvidersAvailableScopes,
375
383
  ];
376
384
 
377
385
  // Run each migration step, catching and logging individual failures so one
@@ -35,7 +35,7 @@ describe("GeminiEmbeddingBackend", () => {
35
35
  expect(url).toContain("key=test-key");
36
36
 
37
37
  const body = JSON.parse(init.body as string);
38
- expect(body.model).toBe("models/test-model");
38
+ expect(body.model).toBeUndefined();
39
39
  expect(body.content).toEqual({ parts: [{ text: "hello world" }] });
40
40
  });
41
41
 
@@ -181,7 +181,7 @@ describe("GeminiEmbeddingBackend", () => {
181
181
  const body = JSON.parse(init.body as string);
182
182
  expect(body.taskType).toBeUndefined();
183
183
  expect(body.outputDimensionality).toBeUndefined();
184
- expect(Object.keys(body)).toEqual(["model", "content"]);
184
+ expect(Object.keys(body)).toEqual(["content"]);
185
185
  });
186
186
  });
187
187
 
@@ -276,6 +276,45 @@ describe("GeminiEmbeddingBackend", () => {
276
276
  // Should have Bearer auth header
277
277
  const headers = init.headers as Record<string, string>;
278
278
  expect(headers["Authorization"]).toBe("Bearer ast-managed-key");
279
+ // Managed path must NOT include `model` in the body — Gemini models it
280
+ // as a protobuf oneof populated from the URL path (internally `_model`)
281
+ // and rejects the duplicate with "oneof field '_model' is already set".
282
+ // See the comment in embedSingle() for the full invariant.
283
+ const body = JSON.parse(init.body as string);
284
+ expect(body.model).toBeUndefined();
285
+ expect(body._model).toBeUndefined();
286
+ });
287
+
288
+ test("never sets `model` or `_model` in the request body (oneof invariant)", async () => {
289
+ // Regression for JARVIS-587: every embed_segment job was failing with
290
+ // `Invalid value (oneof), oneof field '_model' is already set. Cannot
291
+ // set 'model'`. Ensure neither field is ever present on the wire,
292
+ // regardless of transport.
293
+ const managedBackend = new GeminiEmbeddingBackend(
294
+ "ast-managed-key",
295
+ "gemini-embedding-2-preview",
296
+ {
297
+ managedBaseUrl:
298
+ "https://platform.example.com/v1/runtime-proxy/gemini",
299
+ taskType: "RETRIEVAL_DOCUMENT",
300
+ dimensions: 3072,
301
+ },
302
+ );
303
+ await managedBackend.embed(["hello"]);
304
+
305
+ const directBackend = new GeminiEmbeddingBackend(
306
+ "direct-key",
307
+ "test-model",
308
+ );
309
+ await directBackend.embed(["hello"]);
310
+
311
+ expect(mockFetch).toHaveBeenCalledTimes(2);
312
+ for (const call of mockFetch.mock.calls) {
313
+ const [, init] = call as [string, RequestInit];
314
+ const body = JSON.parse(init.body as string);
315
+ expect(body.model).toBeUndefined();
316
+ expect(body._model).toBeUndefined();
317
+ }
279
318
  });
280
319
 
281
320
  test("uses direct Google API URL when managedBaseUrl is not set", async () => {
@@ -58,9 +58,14 @@ export class GeminiEmbeddingBackend implements EmbeddingBackend {
58
58
  const parts = this.buildParts(normalized);
59
59
 
60
60
  const body: Record<string, unknown> = {
61
- model: `models/${this.model}`,
62
61
  content: { parts },
63
62
  };
63
+ // Do NOT set `model` in the body. Gemini's embedContent API models `model`
64
+ // as a protobuf oneof populated from the URL path (internally `_model`),
65
+ // so adding it to the body triggers a 400: "oneof field '_model' is
66
+ // already set. Cannot set 'model'". This holds for both the direct API
67
+ // and the managed proxy, which forwards the body unchanged; the platform
68
+ // billing layer parses the model from the URL path instead.
64
69
  if (this.taskType) body.taskType = this.taskType;
65
70
  if (this.dimensions) body.outputDimensionality = this.dimensions;
66
71
 
@@ -0,0 +1,282 @@
1
+ import { beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ import { setMemoryCheckpoint } from "../checkpoints.js";
4
+ import {
5
+ initializeDb,
6
+ rawAll,
7
+ rawGet,
8
+ rawRun,
9
+ resetTestTables,
10
+ } from "../db.js";
11
+ import { migrateToolCreatedItems } from "./bootstrap.js";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // The checkpoint key used by migrateToolCreatedItems (not exported, so we
15
+ // inline the literal value).
16
+ // ---------------------------------------------------------------------------
17
+ const MIGRATE_ITEMS_CHECKPOINT = "graph_bootstrap:migrated_tool_items";
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Setup
21
+ // ---------------------------------------------------------------------------
22
+
23
+ beforeAll(() => {
24
+ initializeDb();
25
+ });
26
+
27
+ beforeEach(() => {
28
+ // Clear graph nodes and checkpoints between tests so each test starts clean.
29
+ resetTestTables("memory_graph_nodes", "memory_checkpoints", "memory_jobs");
30
+ });
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // migrateToolCreatedItems
34
+ // ---------------------------------------------------------------------------
35
+
36
+ describe("migrateToolCreatedItems", () => {
37
+ test("migrates legacy memory_items to graph nodes", () => {
38
+ // The memory_items table has been dropped by migration 203, so we need to
39
+ // recreate it for this test. We create a minimal version with just the
40
+ // columns the migration reads.
41
+ rawRun(
42
+ `CREATE TABLE IF NOT EXISTS memory_items (
43
+ id TEXT PRIMARY KEY,
44
+ kind TEXT NOT NULL,
45
+ subject TEXT NOT NULL,
46
+ statement TEXT NOT NULL,
47
+ status TEXT NOT NULL,
48
+ confidence REAL NOT NULL,
49
+ importance REAL,
50
+ scope_id TEXT NOT NULL DEFAULT 'default',
51
+ first_seen_at INTEGER NOT NULL,
52
+ fingerprint TEXT NOT NULL DEFAULT ''
53
+ )`,
54
+ );
55
+
56
+ try {
57
+ // Insert a legacy playbook item
58
+ rawRun(
59
+ `INSERT INTO memory_items (id, kind, subject, statement, status, confidence, importance, scope_id, first_seen_at)
60
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
61
+ "legacy-item-1",
62
+ "playbook",
63
+ "Test Playbook",
64
+ "Do the thing correctly",
65
+ "active",
66
+ 0.9,
67
+ 0.8,
68
+ "default",
69
+ 1700000000000,
70
+ );
71
+
72
+ // Clear the checkpoint so the migration re-runs
73
+ rawRun(
74
+ "DELETE FROM memory_checkpoints WHERE key = ?",
75
+ MIGRATE_ITEMS_CHECKPOINT,
76
+ );
77
+
78
+ // Run migration
79
+ migrateToolCreatedItems();
80
+
81
+ // Assert a corresponding graph node was created
82
+ const node = rawGet<{
83
+ id: string;
84
+ content: string;
85
+ type: string;
86
+ source_conversations: string;
87
+ image_refs: string | null;
88
+ }>(
89
+ `SELECT id, content, type, source_conversations, image_refs
90
+ FROM memory_graph_nodes
91
+ WHERE source_conversations LIKE ?`,
92
+ "%playbook:legacy-item-1%",
93
+ );
94
+
95
+ expect(node).not.toBeNull();
96
+ expect(node!.content).toBe("Test Playbook\nDo the thing correctly");
97
+ expect(node!.type).toBe("semantic");
98
+ expect(JSON.parse(node!.source_conversations)).toContain(
99
+ "playbook:legacy-item-1",
100
+ );
101
+ expect(node!.image_refs).toBeNull();
102
+ } finally {
103
+ // Clean up the recreated table so it does not interfere with other tests
104
+ rawRun("DROP TABLE IF EXISTS memory_items");
105
+ }
106
+ });
107
+
108
+ test("succeeds when image_refs column does not exist", () => {
109
+ // This is the regression test for the v0.6.0 bug: when upgrading from
110
+ // v0.5.x, migrateToolCreatedItems ran before the image_refs column was
111
+ // added by migration 205, causing a crash.
112
+
113
+ // Recreate memory_items for legacy data
114
+ rawRun(
115
+ `CREATE TABLE IF NOT EXISTS memory_items (
116
+ id TEXT PRIMARY KEY,
117
+ kind TEXT NOT NULL,
118
+ subject TEXT NOT NULL,
119
+ statement TEXT NOT NULL,
120
+ status TEXT NOT NULL,
121
+ confidence REAL NOT NULL,
122
+ importance REAL,
123
+ scope_id TEXT NOT NULL DEFAULT 'default',
124
+ first_seen_at INTEGER NOT NULL,
125
+ fingerprint TEXT NOT NULL DEFAULT ''
126
+ )`,
127
+ );
128
+
129
+ try {
130
+ // Rebuild memory_graph_nodes WITHOUT the image_refs column to simulate
131
+ // the pre-205 schema. SQLite doesn't support DROP COLUMN on all
132
+ // versions, so we use the standard CREATE-new/INSERT-SELECT/DROP-old/
133
+ // RENAME pattern.
134
+ rawRun(`CREATE TABLE memory_graph_nodes_backup AS
135
+ SELECT
136
+ id, content, type, created, last_accessed, last_consolidated,
137
+ event_date, emotional_charge, fidelity, confidence, significance,
138
+ stability, reinforcement_count, last_reinforced,
139
+ source_conversations, source_type, narrative_role, part_of_story,
140
+ scope_id
141
+ FROM memory_graph_nodes`);
142
+ rawRun("DROP TABLE memory_graph_nodes");
143
+ rawRun(
144
+ `CREATE TABLE memory_graph_nodes (
145
+ id TEXT PRIMARY KEY,
146
+ content TEXT NOT NULL,
147
+ type TEXT NOT NULL,
148
+ created INTEGER NOT NULL,
149
+ last_accessed INTEGER NOT NULL,
150
+ last_consolidated INTEGER NOT NULL,
151
+ event_date INTEGER,
152
+ emotional_charge TEXT NOT NULL,
153
+ fidelity TEXT NOT NULL DEFAULT 'vivid',
154
+ confidence REAL NOT NULL,
155
+ significance REAL NOT NULL,
156
+ stability REAL NOT NULL DEFAULT 14,
157
+ reinforcement_count INTEGER NOT NULL DEFAULT 0,
158
+ last_reinforced INTEGER NOT NULL,
159
+ source_conversations TEXT NOT NULL DEFAULT '[]',
160
+ source_type TEXT NOT NULL DEFAULT 'inferred',
161
+ narrative_role TEXT,
162
+ part_of_story TEXT,
163
+ scope_id TEXT NOT NULL DEFAULT 'default'
164
+ )`,
165
+ );
166
+ rawRun(
167
+ `INSERT INTO memory_graph_nodes
168
+ SELECT * FROM memory_graph_nodes_backup`,
169
+ );
170
+ rawRun("DROP TABLE memory_graph_nodes_backup");
171
+
172
+ // Clear checkpoint
173
+ rawRun(
174
+ "DELETE FROM memory_checkpoints WHERE key = ?",
175
+ MIGRATE_ITEMS_CHECKPOINT,
176
+ );
177
+
178
+ // Insert a legacy item
179
+ rawRun(
180
+ `INSERT INTO memory_items (id, kind, subject, statement, status, confidence, importance, scope_id, first_seen_at)
181
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
182
+ "legacy-item-2",
183
+ "style",
184
+ "Formal tone",
185
+ "Always use formal language",
186
+ "active",
187
+ 0.85,
188
+ 0.7,
189
+ "default",
190
+ 1700000000000,
191
+ );
192
+
193
+ // This should NOT throw — the migration should succeed without image_refs
194
+ expect(() => migrateToolCreatedItems()).not.toThrow();
195
+
196
+ // Verify the row was migrated
197
+ const node = rawGet<{ id: string; content: string }>(
198
+ `SELECT id, content FROM memory_graph_nodes
199
+ WHERE source_conversations LIKE ?`,
200
+ "%style:legacy-item-2%",
201
+ );
202
+ expect(node).not.toBeNull();
203
+ expect(node!.content).toBe("Formal tone\nAlways use formal language");
204
+ } finally {
205
+ rawRun("DROP TABLE IF EXISTS memory_items");
206
+ // Restore the full schema for subsequent tests by re-adding image_refs
207
+ try {
208
+ rawRun("ALTER TABLE memory_graph_nodes ADD COLUMN image_refs TEXT");
209
+ } catch {
210
+ // Column may already exist
211
+ }
212
+ }
213
+ });
214
+
215
+ test("skips migration when checkpoint is already set", () => {
216
+ // Recreate memory_items for legacy data
217
+ rawRun(
218
+ `CREATE TABLE IF NOT EXISTS memory_items (
219
+ id TEXT PRIMARY KEY,
220
+ kind TEXT NOT NULL,
221
+ subject TEXT NOT NULL,
222
+ statement TEXT NOT NULL,
223
+ status TEXT NOT NULL,
224
+ confidence REAL NOT NULL,
225
+ importance REAL,
226
+ scope_id TEXT NOT NULL DEFAULT 'default',
227
+ first_seen_at INTEGER NOT NULL,
228
+ fingerprint TEXT NOT NULL DEFAULT ''
229
+ )`,
230
+ );
231
+
232
+ try {
233
+ // Set the checkpoint BEFORE inserting data — the migration should skip
234
+ setMemoryCheckpoint(MIGRATE_ITEMS_CHECKPOINT, "done");
235
+
236
+ rawRun(
237
+ `INSERT INTO memory_items (id, kind, subject, statement, status, confidence, importance, scope_id, first_seen_at)
238
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
239
+ "legacy-item-3",
240
+ "relationship",
241
+ "Colleague",
242
+ "Works with user on project X",
243
+ "active",
244
+ 0.75,
245
+ 0.6,
246
+ "default",
247
+ 1700000000000,
248
+ );
249
+
250
+ // Run migration — should be a no-op because checkpoint is set
251
+ migrateToolCreatedItems();
252
+
253
+ // Assert no rows were inserted into memory_graph_nodes
254
+ const rows = rawAll<{ id: string }>("SELECT id FROM memory_graph_nodes");
255
+ expect(rows).toHaveLength(0);
256
+ } finally {
257
+ rawRun("DROP TABLE IF EXISTS memory_items");
258
+ }
259
+ });
260
+
261
+ test("handles missing memory_items table gracefully", () => {
262
+ // Ensure memory_items table does not exist
263
+ rawRun("DROP TABLE IF EXISTS memory_items");
264
+
265
+ // Clear the checkpoint so migration attempts to run
266
+ rawRun(
267
+ "DELETE FROM memory_checkpoints WHERE key = ?",
268
+ MIGRATE_ITEMS_CHECKPOINT,
269
+ );
270
+
271
+ // Should not throw even though the table doesn't exist
272
+ expect(() => migrateToolCreatedItems()).not.toThrow();
273
+
274
+ // The checkpoint should be set to "done" (migration handled the missing table)
275
+ const checkpoint = rawGet<{ value: string }>(
276
+ "SELECT value FROM memory_checkpoints WHERE key = ?",
277
+ MIGRATE_ITEMS_CHECKPOINT,
278
+ );
279
+ expect(checkpoint).not.toBeNull();
280
+ expect(checkpoint!.value).toBe("done");
281
+ });
282
+ });