@vellumai/assistant 0.5.16 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (592) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +69 -16
  3. package/Dockerfile +2 -5
  4. package/bun.lock +6 -2
  5. package/docker-entrypoint.sh +32 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/knip.json +2 -1
  9. package/openapi.yaml +1198 -83
  10. package/package.json +5 -1
  11. package/src/__tests__/actor-token-service.test.ts +68 -0
  12. package/src/__tests__/agent-loop.test.ts +0 -32
  13. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -2
  14. package/src/__tests__/anthropic-provider.test.ts +217 -98
  15. package/src/__tests__/app-compiler.test.ts +120 -0
  16. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  17. package/src/__tests__/app-executors.test.ts +47 -1
  18. package/src/__tests__/app-source-watcher.test.ts +159 -0
  19. package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
  20. package/src/__tests__/call-conversation-messages.test.ts +2 -6
  21. package/src/__tests__/call-domain.test.ts +2 -6
  22. package/src/__tests__/call-pointer-messages.test.ts +2 -14
  23. package/src/__tests__/call-recovery.test.ts +2 -6
  24. package/src/__tests__/call-routes-http.test.ts +2 -6
  25. package/src/__tests__/call-store.test.ts +2 -6
  26. package/src/__tests__/cancel-resolves-conversation-key.test.ts +2 -6
  27. package/src/__tests__/canonical-guardian-store.test.ts +2 -6
  28. package/src/__tests__/channel-delivery-store.test.ts +2 -6
  29. package/src/__tests__/channel-retry-sweep.test.ts +2 -6
  30. package/src/__tests__/checker.test.ts +63 -9
  31. package/src/__tests__/clawhub.test.ts +54 -24
  32. package/src/__tests__/cli-command-risk-guard.test.ts +14 -0
  33. package/src/__tests__/config-schema.test.ts +6 -1
  34. package/src/__tests__/config-set-platform-guard.test.ts +302 -0
  35. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -6
  36. package/src/__tests__/contacts-tools.test.ts +31 -0
  37. package/src/__tests__/context-overflow-reducer.test.ts +86 -0
  38. package/src/__tests__/context-token-estimator.test.ts +175 -10
  39. package/src/__tests__/conversation-agent-loop-overflow.test.ts +13 -6
  40. package/src/__tests__/conversation-agent-loop.test.ts +13 -51
  41. package/src/__tests__/conversation-attachments.test.ts +2 -6
  42. package/src/__tests__/conversation-attention-store.test.ts +2 -6
  43. package/src/__tests__/conversation-clear-safety.test.ts +2 -6
  44. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +4 -10
  45. package/src/__tests__/conversation-disk-view-integration.test.ts +2 -6
  46. package/src/__tests__/conversation-disk-view.test.ts +2 -6
  47. package/src/__tests__/conversation-error.test.ts +33 -2
  48. package/src/__tests__/conversation-fork-crud.test.ts +2 -6
  49. package/src/__tests__/conversation-history-web-search.test.ts +6 -1
  50. package/src/__tests__/conversation-load-history-repair.test.ts +5 -1
  51. package/src/__tests__/conversation-media-retry.test.ts +91 -0
  52. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  53. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  54. package/src/__tests__/conversation-starter-routes.test.ts +20 -11
  55. package/src/__tests__/conversation-store.test.ts +2 -6
  56. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  57. package/src/__tests__/conversation-usage.test.ts +2 -6
  58. package/src/__tests__/conversation-wipe.test.ts +13 -414
  59. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  60. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  61. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  62. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  63. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -3
  64. package/src/__tests__/credential-execution-shell-lockdown.test.ts +2 -2
  65. package/src/__tests__/credential-security-e2e.test.ts +2 -0
  66. package/src/__tests__/date-context.test.ts +76 -210
  67. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  68. package/src/__tests__/file-list-tool.test.ts +219 -0
  69. package/src/__tests__/first-greeting.test.ts +1 -1
  70. package/src/__tests__/followup-tools.test.ts +2 -6
  71. package/src/__tests__/graph-extraction-event-date.test.ts +186 -0
  72. package/src/__tests__/guardian-action-conversation-turn.test.ts +2 -6
  73. package/src/__tests__/guardian-action-followup-executor.test.ts +2 -6
  74. package/src/__tests__/guardian-action-followup-store.test.ts +2 -6
  75. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +2 -6
  76. package/src/__tests__/guardian-action-late-reply.test.ts +2 -6
  77. package/src/__tests__/guardian-action-store.test.ts +2 -6
  78. package/src/__tests__/guardian-binding-drift-heal.test.ts +2 -6
  79. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +8 -8
  80. package/src/__tests__/guardian-dispatch.test.ts +2 -6
  81. package/src/__tests__/guardian-grant-minting.test.ts +2 -14
  82. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +2 -6
  83. package/src/__tests__/guardian-routing-invariants.test.ts +192 -6
  84. package/src/__tests__/guardian-routing-state.test.ts +2 -6
  85. package/src/__tests__/guardian-verification-voice-binding.test.ts +2 -6
  86. package/src/__tests__/heartbeat-service.test.ts +180 -3
  87. package/src/__tests__/identity-routes.test.ts +328 -0
  88. package/src/__tests__/inbound-invite-redemption.test.ts +2 -6
  89. package/src/__tests__/injection-block.test.ts +178 -0
  90. package/src/__tests__/install-meta.test.ts +506 -0
  91. package/src/__tests__/install-skill-routing.test.ts +293 -0
  92. package/src/__tests__/invite-redemption-service.test.ts +2 -6
  93. package/src/__tests__/invite-routes-http.test.ts +2 -6
  94. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +17 -28
  95. package/src/__tests__/list-messages-attachments.test.ts +2 -6
  96. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  97. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  98. package/src/__tests__/llm-context-route-provider.test.ts +103 -6
  99. package/src/__tests__/llm-request-log-turn-query.test.ts +164 -6
  100. package/src/__tests__/llm-usage-store.test.ts +2 -6
  101. package/src/__tests__/log-export-workspace.test.ts +74 -111
  102. package/src/__tests__/managed-store.test.ts +38 -11
  103. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  104. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  105. package/src/__tests__/memory-jobs-worker-backoff.test.ts +2 -8
  106. package/src/__tests__/memory-recall-log-store.test.ts +134 -6
  107. package/src/__tests__/memory-upsert-concurrency.test.ts +4 -112
  108. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  109. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  110. package/src/__tests__/mock-fetch.ts +87 -0
  111. package/src/__tests__/non-member-access-request.test.ts +2 -6
  112. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  113. package/src/__tests__/notification-guardian-path.test.ts +2 -6
  114. package/src/__tests__/oauth-cli.test.ts +364 -2
  115. package/src/__tests__/oauth2-gateway-transport.test.ts +18 -3
  116. package/src/__tests__/onboarding-template-contract.test.ts +62 -14
  117. package/src/__tests__/outlook-attachments.test.ts +301 -0
  118. package/src/__tests__/outlook-automation-tools.test.ts +425 -0
  119. package/src/__tests__/outlook-categories.test.ts +212 -0
  120. package/src/__tests__/outlook-client-automation.test.ts +246 -0
  121. package/src/__tests__/outlook-compose-tools.test.ts +325 -0
  122. package/src/__tests__/outlook-declutter-tools.test.ts +585 -0
  123. package/src/__tests__/outlook-email-watcher.test.ts +322 -0
  124. package/src/__tests__/outlook-follow-up.test.ts +196 -0
  125. package/src/__tests__/outlook-messaging-provider.test.ts +498 -3
  126. package/src/__tests__/outlook-trash.test.ts +77 -0
  127. package/src/__tests__/outlook-unsubscribe.test.ts +250 -0
  128. package/src/__tests__/parser.test.ts +32 -0
  129. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  130. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  131. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  132. package/src/__tests__/permission-mode-store.test.ts +277 -0
  133. package/src/__tests__/permission-mode.test.ts +101 -0
  134. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  135. package/src/__tests__/platform-callback-registration.test.ts +4 -4
  136. package/src/__tests__/playbook-execution.test.ts +76 -80
  137. package/src/__tests__/playbook-tools.test.ts +5 -7
  138. package/src/__tests__/profiler-routes.test.ts +502 -0
  139. package/src/__tests__/profiler-run-store.test.ts +441 -0
  140. package/src/__tests__/provider-error-scenarios.test.ts +21 -0
  141. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  142. package/src/__tests__/rebuild-index-graph-nodes.test.ts +273 -0
  143. package/src/__tests__/registry.test.ts +3 -3
  144. package/src/__tests__/require-fresh-approval.test.ts +64 -2
  145. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -6
  146. package/src/__tests__/runtime-events-sse.test.ts +2 -6
  147. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  148. package/src/__tests__/schedule-store.test.ts +2 -6
  149. package/src/__tests__/schedule-tools.test.ts +2 -6
  150. package/src/__tests__/scheduler-recurrence.test.ts +1 -5
  151. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  152. package/src/__tests__/scoped-approval-grants.test.ts +2 -6
  153. package/src/__tests__/scoped-grant-security-matrix.test.ts +2 -6
  154. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  155. package/src/__tests__/search-skills-unified.test.ts +422 -0
  156. package/src/__tests__/secret-onetime-send.test.ts +2 -0
  157. package/src/__tests__/send-endpoint-busy.test.ts +44 -9
  158. package/src/__tests__/sequence-store.test.ts +2 -6
  159. package/src/__tests__/server-history-render.test.ts +2 -6
  160. package/src/__tests__/set-permission-mode.test.ts +274 -0
  161. package/src/__tests__/skill-feature-flags-integration.test.ts +38 -31
  162. package/src/__tests__/skill-feature-flags.test.ts +6 -6
  163. package/src/__tests__/skill-load-feature-flag.test.ts +23 -11
  164. package/src/__tests__/skill-memory.test.ts +2 -741
  165. package/src/__tests__/skills-uninstall.test.ts +2 -2
  166. package/src/__tests__/skills.test.ts +1 -1
  167. package/src/__tests__/slack-inbound-verification.test.ts +2 -6
  168. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  169. package/src/__tests__/subagent-detail.test.ts +84 -0
  170. package/src/__tests__/subagent-disposal.test.ts +308 -0
  171. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  172. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  173. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  174. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  175. package/src/__tests__/subagent-tools.test.ts +464 -4
  176. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  177. package/src/__tests__/task-compiler.test.ts +2 -6
  178. package/src/__tests__/task-management-tools.test.ts +2 -6
  179. package/src/__tests__/task-memory-cleanup.test.ts +185 -241
  180. package/src/__tests__/task-runner.test.ts +2 -6
  181. package/src/__tests__/task-scheduler.test.ts +2 -6
  182. package/src/__tests__/terminal-tools.test.ts +17 -27
  183. package/src/__tests__/test-preload.ts +7 -0
  184. package/src/__tests__/tool-approval-handler.test.ts +2 -6
  185. package/src/__tests__/tool-executor.test.ts +4 -26
  186. package/src/__tests__/tool-grant-request-escalation.test.ts +2 -6
  187. package/src/__tests__/tool-side-effects-slack-dm.test.ts +277 -0
  188. package/src/__tests__/top-level-renderer.test.ts +10 -13
  189. package/src/__tests__/trust-store.test.ts +1 -1
  190. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -6
  191. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +118 -8
  192. package/src/__tests__/trusted-contact-multichannel.test.ts +2 -6
  193. package/src/__tests__/trusted-contact-verification.test.ts +2 -6
  194. package/src/__tests__/turn-boundary-resolution.test.ts +2 -6
  195. package/src/__tests__/usage-cache-backfill-migration.test.ts +1 -6
  196. package/src/__tests__/usage-routes.test.ts +2 -6
  197. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  198. package/src/__tests__/voice-invite-redemption.test.ts +2 -6
  199. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -6
  200. package/src/__tests__/voice-session-bridge.test.ts +2 -6
  201. package/src/__tests__/volume-security-guard.test.ts +2 -0
  202. package/src/__tests__/workspace-lifecycle.test.ts +29 -1
  203. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +2 -6
  204. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +2 -6
  205. package/src/__tests__/workspace-migration-026-backfill-install-meta.test.ts +558 -0
  206. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  207. package/src/__tests__/workspace-policy.test.ts +1 -1
  208. package/src/agent/attachments.ts +7 -2
  209. package/src/agent/image-optimize.ts +165 -0
  210. package/src/agent/loop.ts +7 -15
  211. package/src/approvals/guardian-request-resolvers.ts +24 -0
  212. package/src/avatar/traits-png-sync.ts +3 -3
  213. package/src/bundler/app-compiler.ts +179 -2
  214. package/src/bundler/package-resolver.ts +3 -5
  215. package/src/cli/__tests__/notifications.test.ts +1 -2
  216. package/src/cli/__tests__/run-assistant-command.ts +29 -0
  217. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  218. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  219. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  220. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  221. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  222. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  223. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  224. package/src/cli/commands/avatar.ts +3 -3
  225. package/src/cli/commands/config.ts +26 -13
  226. package/src/cli/commands/conversations.ts +1 -8
  227. package/src/cli/commands/doctor.ts +2 -2
  228. package/src/cli/commands/email.ts +584 -835
  229. package/src/cli/commands/memory.ts +37 -84
  230. package/src/cli/commands/notifications.ts +7 -2
  231. package/src/cli/commands/oauth/__tests__/connect.test.ts +2 -2
  232. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +2 -2
  233. package/src/cli/commands/oauth/__tests__/mode.test.ts +8 -1
  234. package/src/cli/commands/oauth/__tests__/status.test.ts +2 -2
  235. package/src/cli/commands/oauth/connect.ts +25 -11
  236. package/src/cli/commands/oauth/mode.ts +7 -0
  237. package/src/cli/commands/oauth/shared.ts +39 -3
  238. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  239. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  240. package/src/cli/commands/platform/__tests__/status.test.ts +5 -5
  241. package/src/cli/commands/platform/index.ts +16 -16
  242. package/src/cli/commands/routes.ts +396 -0
  243. package/src/cli/commands/skills.ts +218 -36
  244. package/src/cli/commands/trust.ts +2 -2
  245. package/src/cli/lib/daemon-credential-client.ts +2 -3
  246. package/src/cli/program.ts +2 -0
  247. package/src/cli.ts +1 -120
  248. package/src/config/bundled-skills/acp/TOOLS.json +1 -1
  249. package/src/config/bundled-skills/app-builder/SKILL.md +4 -1
  250. package/src/config/bundled-skills/contacts/SKILL.md +0 -1
  251. package/src/config/bundled-skills/contacts/TOOLS.json +0 -8
  252. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +0 -4
  253. package/src/config/bundled-skills/gmail/SKILL.md +4 -12
  254. package/src/config/bundled-skills/google-calendar/SKILL.md +1 -9
  255. package/src/config/bundled-skills/messaging/SKILL.md +17 -18
  256. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +40 -33
  257. package/src/config/bundled-skills/outlook/SKILL.md +189 -0
  258. package/src/config/bundled-skills/outlook/TOOLS.json +530 -0
  259. package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +85 -0
  260. package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +77 -0
  261. package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +84 -0
  262. package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +94 -0
  263. package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +49 -0
  264. package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +237 -0
  265. package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +161 -0
  266. package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +32 -0
  267. package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +272 -0
  268. package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +29 -0
  269. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +129 -0
  270. package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +87 -0
  271. package/src/config/bundled-skills/outlook/tools/shared.ts +20 -0
  272. package/src/config/bundled-skills/outlook-calendar/SKILL.md +51 -0
  273. package/src/config/bundled-skills/outlook-calendar/TOOLS.json +221 -0
  274. package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +252 -0
  275. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +53 -0
  276. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +74 -0
  277. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +18 -0
  278. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +46 -0
  279. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +36 -0
  280. package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +17 -0
  281. package/src/config/bundled-skills/outlook-calendar/types.ts +120 -0
  282. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +47 -40
  283. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +16 -29
  284. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +16 -18
  285. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +39 -47
  286. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  287. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  288. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  289. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  290. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  291. package/src/config/bundled-skills/slack/SKILL.md +3 -7
  292. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  293. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  294. package/src/config/bundled-tool-registry.ts +56 -4
  295. package/src/config/env-registry.ts +78 -8
  296. package/src/config/feature-flag-registry.json +38 -125
  297. package/src/config/schema.ts +8 -0
  298. package/src/config/schemas/filing.ts +51 -0
  299. package/src/config/schemas/heartbeat.ts +15 -12
  300. package/src/config/schemas/memory-lifecycle.ts +12 -0
  301. package/src/config/schemas/platform.ts +8 -0
  302. package/src/config/schemas/security.ts +14 -0
  303. package/src/config/schemas/timeouts.ts +1 -1
  304. package/src/config/skills.ts +18 -7
  305. package/src/context/token-estimator.ts +25 -18
  306. package/src/context/window-manager.ts +6 -2
  307. package/src/credential-execution/process-manager.ts +3 -1
  308. package/src/daemon/app-source-watcher.ts +93 -0
  309. package/src/daemon/config-watcher.ts +79 -1
  310. package/src/daemon/context-overflow-reducer.ts +46 -2
  311. package/src/daemon/conversation-agent-loop-handlers.ts +143 -82
  312. package/src/daemon/conversation-agent-loop.ts +236 -108
  313. package/src/daemon/conversation-error.ts +31 -8
  314. package/src/daemon/conversation-history.ts +4 -19
  315. package/src/daemon/conversation-lifecycle.ts +36 -9
  316. package/src/daemon/conversation-media-retry.ts +85 -7
  317. package/src/daemon/conversation-notifiers.ts +4 -1
  318. package/src/daemon/conversation-process.ts +13 -7
  319. package/src/daemon/conversation-runtime-assembly.ts +305 -306
  320. package/src/daemon/conversation-tool-setup.ts +44 -14
  321. package/src/daemon/conversation-workspace.ts +1 -2
  322. package/src/daemon/conversation.ts +59 -2
  323. package/src/daemon/daemon-control.ts +8 -2
  324. package/src/daemon/date-context.ts +26 -53
  325. package/src/daemon/first-greeting.ts +1 -1
  326. package/src/daemon/handlers/conversations.ts +4 -7
  327. package/src/daemon/handlers/shared.test.ts +143 -0
  328. package/src/daemon/handlers/shared.ts +85 -17
  329. package/src/daemon/handlers/skills.ts +416 -209
  330. package/src/daemon/lifecycle.ts +212 -131
  331. package/src/daemon/main.ts +5 -1
  332. package/src/daemon/message-types/conversations.ts +29 -7
  333. package/src/daemon/message-types/messages.ts +12 -2
  334. package/src/daemon/message-types/schedules.ts +1 -0
  335. package/src/daemon/message-types/settings.ts +6 -0
  336. package/src/daemon/message-types/skills.ts +97 -36
  337. package/src/daemon/profiler-run-store.ts +557 -0
  338. package/src/daemon/providers-setup.ts +5 -0
  339. package/src/daemon/server.ts +100 -11
  340. package/src/daemon/shutdown-handlers.ts +5 -0
  341. package/src/daemon/tool-side-effects.ts +50 -8
  342. package/src/export/transcript-formatter.ts +148 -0
  343. package/src/filing/filing-service.ts +228 -0
  344. package/src/heartbeat/heartbeat-service.ts +97 -7
  345. package/src/hooks/cli.ts +2 -2
  346. package/src/hooks/runner.ts +15 -38
  347. package/src/inbound/platform-callback-registration.ts +14 -14
  348. package/src/mcp/client.ts +6 -0
  349. package/src/mcp/mcp-oauth-provider.ts +149 -27
  350. package/src/memory/admin.ts +42 -75
  351. package/src/memory/app-store.ts +69 -0
  352. package/src/memory/conversation-bootstrap.ts +3 -1
  353. package/src/memory/conversation-crud.ts +211 -288
  354. package/src/memory/conversation-group-migration.ts +157 -0
  355. package/src/memory/conversation-queries.ts +61 -13
  356. package/src/memory/conversation-title-service.ts +1 -0
  357. package/src/memory/db-init.ts +194 -361
  358. package/src/memory/embed.ts +73 -0
  359. package/src/memory/embedding-backend.ts +8 -14
  360. package/src/memory/embedding-runtime-manager.ts +12 -114
  361. package/src/memory/fingerprint.ts +2 -2
  362. package/src/memory/graph/bootstrap.ts +521 -0
  363. package/src/memory/graph/capability-seed.ts +449 -0
  364. package/src/memory/graph/consolidation.ts +725 -0
  365. package/src/memory/graph/conversation-graph-memory.ts +659 -0
  366. package/src/memory/graph/decay.test.ts +208 -0
  367. package/src/memory/graph/decay.ts +195 -0
  368. package/src/memory/graph/extraction-job.ts +74 -0
  369. package/src/memory/graph/extraction.test.ts +936 -0
  370. package/src/memory/graph/extraction.ts +1297 -0
  371. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  372. package/src/memory/graph/graph-search.ts +280 -0
  373. package/src/memory/graph/image-ref-utils.ts +29 -0
  374. package/src/memory/graph/injection.test.ts +513 -0
  375. package/src/memory/graph/injection.ts +469 -0
  376. package/src/memory/graph/inspect.ts +543 -0
  377. package/src/memory/graph/narrative.ts +267 -0
  378. package/src/memory/graph/pattern-scan.ts +269 -0
  379. package/src/memory/graph/retriever.ts +1111 -0
  380. package/src/memory/graph/scoring.test.ts +548 -0
  381. package/src/memory/graph/scoring.ts +232 -0
  382. package/src/memory/graph/serendipity.ts +65 -0
  383. package/src/memory/graph/store.test.ts +1098 -0
  384. package/src/memory/graph/store.ts +838 -0
  385. package/src/memory/graph/tool-handlers.ts +301 -0
  386. package/src/memory/graph/tools.ts +97 -0
  387. package/src/memory/graph/triggers.test.ts +487 -0
  388. package/src/memory/graph/triggers.ts +223 -0
  389. package/src/memory/graph/types.ts +295 -0
  390. package/src/memory/group-crud.ts +191 -0
  391. package/src/memory/indexer.ts +37 -19
  392. package/src/memory/job-handlers/cleanup.ts +32 -42
  393. package/src/memory/job-handlers/conversation-starters.ts +91 -53
  394. package/src/memory/job-handlers/embedding.ts +5 -31
  395. package/src/memory/job-handlers/index-maintenance.ts +23 -11
  396. package/src/memory/job-handlers/summarization.ts +32 -17
  397. package/src/memory/job-utils.ts +1 -1
  398. package/src/memory/jobs-store.ts +21 -31
  399. package/src/memory/jobs-worker.ts +180 -129
  400. package/src/memory/llm-request-log-store.ts +96 -12
  401. package/src/memory/memory-recall-log-store.ts +49 -5
  402. package/src/memory/message-content.ts +1 -0
  403. package/src/memory/migrations/202-memory-graph-tables.ts +130 -0
  404. package/src/memory/migrations/203-drop-memory-items-tables.ts +55 -0
  405. package/src/memory/migrations/204-rename-memory-graph-type-values.ts +46 -0
  406. package/src/memory/migrations/205-memory-graph-image-refs.ts +11 -0
  407. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  408. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  409. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  410. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  411. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  412. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  413. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  414. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  415. package/src/memory/migrations/index.ts +12 -0
  416. package/src/memory/migrations/registry.ts +16 -0
  417. package/src/memory/qdrant-client.ts +44 -17
  418. package/src/memory/schema/conversations.ts +14 -0
  419. package/src/memory/schema/index.ts +1 -0
  420. package/src/memory/schema/infrastructure.ts +8 -1
  421. package/src/memory/schema/memory-core.ts +0 -51
  422. package/src/memory/schema/memory-graph.ts +154 -0
  423. package/src/memory/search/semantic.ts +47 -91
  424. package/src/memory/task-memory-cleanup.ts +58 -61
  425. package/src/messaging/providers/outlook/adapter.ts +8 -1
  426. package/src/messaging/providers/outlook/client.ts +299 -0
  427. package/src/messaging/providers/outlook/types.ts +118 -0
  428. package/src/notifications/adapters/macos.ts +1 -0
  429. package/src/notifications/copy-composer.ts +95 -0
  430. package/src/notifications/decision-engine.ts +35 -0
  431. package/src/notifications/signal.ts +16 -0
  432. package/src/oauth/seed-providers.ts +2 -1
  433. package/src/permissions/checker.ts +36 -4
  434. package/src/permissions/defaults.ts +4 -4
  435. package/src/permissions/permission-mode-store.ts +180 -0
  436. package/src/permissions/permission-mode.ts +31 -0
  437. package/src/permissions/workspace-policy.ts +10 -1
  438. package/src/playbooks/playbook-compiler.ts +19 -18
  439. package/src/playbooks/types.ts +4 -3
  440. package/src/prompts/system-prompt.ts +62 -36
  441. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  442. package/src/prompts/templates/BOOTSTRAP.md +70 -165
  443. package/src/prompts/templates/HEARTBEAT.md +3 -1
  444. package/src/prompts/templates/SOUL.md +25 -4
  445. package/src/prompts/templates/UPDATES.md +8 -0
  446. package/src/providers/anthropic/client.ts +136 -220
  447. package/src/providers/gemini/client.ts +1 -1
  448. package/src/providers/openai/client.ts +1 -1
  449. package/src/providers/registry.ts +1 -1
  450. package/src/providers/retry.ts +19 -3
  451. package/src/runtime/actor-trust-resolver.ts +5 -1
  452. package/src/runtime/auth/route-policy.ts +30 -0
  453. package/src/runtime/guardian-reply-router.ts +5 -1
  454. package/src/runtime/http-server.ts +55 -5
  455. package/src/runtime/http-types.ts +12 -1
  456. package/src/runtime/middleware/auth.ts +20 -0
  457. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  458. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  459. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  460. package/src/runtime/routes/app-management-routes.ts +1 -11
  461. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  462. package/src/runtime/routes/archive-utils.ts +29 -0
  463. package/src/runtime/routes/attachment-routes.test.ts +106 -0
  464. package/src/runtime/routes/attachment-routes.ts +106 -16
  465. package/src/runtime/routes/avatar-routes.ts +2 -9
  466. package/src/runtime/routes/brain-graph-routes.ts +21 -22
  467. package/src/runtime/routes/btw-routes.ts +22 -1
  468. package/src/runtime/routes/conversation-analysis-routes.ts +173 -0
  469. package/src/runtime/routes/conversation-management-routes.ts +3 -14
  470. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  471. package/src/runtime/routes/conversation-routes.ts +264 -44
  472. package/src/runtime/routes/conversation-starter-routes.ts +2 -2
  473. package/src/runtime/routes/debug-routes.ts +1 -1
  474. package/src/runtime/routes/global-search-routes.ts +21 -19
  475. package/src/runtime/routes/group-routes.ts +207 -0
  476. package/src/runtime/routes/guardian-action-routes.ts +21 -10
  477. package/src/runtime/routes/guardian-bootstrap-routes.ts +23 -19
  478. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  479. package/src/runtime/routes/identity-routes.ts +53 -18
  480. package/src/runtime/routes/inbound-message-handler.ts +19 -0
  481. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +292 -0
  482. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +207 -0
  483. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  484. package/src/runtime/routes/log-export-routes.ts +23 -275
  485. package/src/runtime/routes/memory-item-routes.test.ts +170 -247
  486. package/src/runtime/routes/memory-item-routes.ts +341 -388
  487. package/src/runtime/routes/migration-routes.ts +18 -7
  488. package/src/runtime/routes/profiler-routes.ts +350 -0
  489. package/src/runtime/routes/schedule-routes.ts +28 -11
  490. package/src/runtime/routes/settings-routes.ts +95 -8
  491. package/src/runtime/routes/skills-routes.ts +103 -37
  492. package/src/runtime/routes/subagents-routes.ts +28 -7
  493. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  494. package/src/runtime/routes/user-routes.ts +41 -0
  495. package/src/runtime/routes/work-items-routes.test.ts +2 -6
  496. package/src/runtime/routes/workspace-routes.ts +0 -1
  497. package/src/schedule/schedule-store.ts +30 -0
  498. package/src/schedule/scheduler.ts +52 -18
  499. package/src/security/oauth2.ts +1 -1
  500. package/src/security/secure-keys.ts +4 -8
  501. package/src/shared/provider-env-vars.ts +19 -0
  502. package/src/skills/catalog-cache.ts +5 -0
  503. package/src/skills/catalog-install.ts +25 -16
  504. package/src/skills/clawhub.ts +134 -154
  505. package/src/skills/install-meta.ts +208 -0
  506. package/src/skills/managed-store.ts +29 -18
  507. package/src/skills/skill-memory.ts +12 -229
  508. package/src/skills/skillssh-registry.ts +19 -17
  509. package/src/subagent/index.ts +13 -3
  510. package/src/subagent/manager.ts +308 -29
  511. package/src/subagent/types.ts +68 -0
  512. package/src/tasks/task-runner.ts +7 -5
  513. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -5
  514. package/src/tools/apps/executors.ts +29 -4
  515. package/src/tools/browser/runtime-check.ts +3 -1
  516. package/src/tools/filesystem/list.ts +93 -0
  517. package/src/tools/memory/register.ts +63 -46
  518. package/src/tools/permission-checker.ts +85 -1
  519. package/src/tools/registry.ts +4 -0
  520. package/src/tools/schedule/create.ts +3 -0
  521. package/src/tools/schedule/list.ts +1 -0
  522. package/src/tools/schedule/update.ts +6 -0
  523. package/src/tools/shared/filesystem/errors.ts +5 -0
  524. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  525. package/src/tools/shared/filesystem/image-read.ts +22 -85
  526. package/src/tools/shared/filesystem/types.ts +17 -0
  527. package/src/tools/shared/shell-output.ts +31 -2
  528. package/src/tools/subagent/abort.ts +12 -2
  529. package/src/tools/subagent/message.ts +9 -2
  530. package/src/tools/subagent/notify-parent.ts +79 -0
  531. package/src/tools/subagent/read.ts +29 -8
  532. package/src/tools/subagent/resolve.ts +21 -0
  533. package/src/tools/subagent/spawn.ts +2 -0
  534. package/src/tools/subagent/status.ts +11 -1
  535. package/src/tools/system/avatar-generator.ts +3 -3
  536. package/src/tools/system/register.ts +23 -0
  537. package/src/tools/system/set-permission-mode.ts +103 -0
  538. package/src/tools/terminal/parser.ts +30 -5
  539. package/src/tools/terminal/safe-env.ts +17 -1
  540. package/src/tools/tool-manifest.ts +9 -3
  541. package/src/tools/types.ts +2 -0
  542. package/src/util/browser.ts +25 -10
  543. package/src/util/bun-runtime.ts +172 -0
  544. package/src/util/logger.ts +1 -1
  545. package/src/util/platform.ts +50 -17
  546. package/src/watcher/providers/outlook-calendar.ts +343 -0
  547. package/src/watcher/providers/outlook.ts +198 -0
  548. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  549. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  550. package/src/workspace/migrations/025-remove-oauth-app-setup-skills.ts +76 -0
  551. package/src/workspace/migrations/026-backfill-install-meta.ts +325 -0
  552. package/src/workspace/migrations/027-remove-orphaned-optimized-images-cache.ts +42 -0
  553. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  554. package/src/workspace/migrations/029-seed-pkb.ts +84 -0
  555. package/src/workspace/migrations/registry.ts +10 -0
  556. package/src/workspace/top-level-renderer.ts +5 -9
  557. package/src/__tests__/cli-memory.test.ts +0 -372
  558. package/src/__tests__/clipboard.test.ts +0 -88
  559. package/src/__tests__/context-memory-e2e.test.ts +0 -415
  560. package/src/__tests__/journal-context.test.ts +0 -268
  561. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -297
  562. package/src/__tests__/memory-lifecycle-e2e.test.ts +0 -459
  563. package/src/__tests__/memory-query-builder.test.ts +0 -59
  564. package/src/__tests__/memory-recall-quality.test.ts +0 -1046
  565. package/src/__tests__/memory-regressions.experimental.test.ts +0 -629
  566. package/src/__tests__/memory-regressions.test.ts +0 -3696
  567. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -295
  568. package/src/cli/cli-memory.ts +0 -176
  569. package/src/daemon/conversation-memory.ts +0 -207
  570. package/src/memory/conversation-starters-cadence.ts +0 -74
  571. package/src/memory/items-extractor.ts +0 -860
  572. package/src/memory/job-handlers/batch-extraction.ts +0 -753
  573. package/src/memory/job-handlers/extraction.ts +0 -40
  574. package/src/memory/job-handlers/journal-carry-forward.test.ts +0 -355
  575. package/src/memory/job-handlers/journal-carry-forward.ts +0 -255
  576. package/src/memory/journal-memory.ts +0 -224
  577. package/src/memory/query-builder.ts +0 -47
  578. package/src/memory/query-expansion.ts +0 -83
  579. package/src/memory/retriever.test.ts +0 -1592
  580. package/src/memory/retriever.ts +0 -1331
  581. package/src/memory/search/formatting.test.ts +0 -140
  582. package/src/memory/search/formatting.ts +0 -262
  583. package/src/memory/search/mmr.ts +0 -139
  584. package/src/memory/search/ranking.ts +0 -15
  585. package/src/memory/search/staleness.ts +0 -40
  586. package/src/memory/search/tier-classifier.ts +0 -18
  587. package/src/memory/search/types.ts +0 -121
  588. package/src/prompts/journal-context.ts +0 -154
  589. package/src/tools/memory/definitions.ts +0 -69
  590. package/src/tools/memory/handlers.test.ts +0 -562
  591. package/src/tools/memory/handlers.ts +0 -434
  592. package/src/util/clipboard.ts +0 -34
@@ -0,0 +1,368 @@
1
+ import {
2
+ afterAll,
3
+ beforeAll,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ mock,
8
+ test,
9
+ } from "bun:test";
10
+
11
+ mock.module("../util/logger.js", () => ({
12
+ getLogger: () =>
13
+ new Proxy({} as Record<string, unknown>, {
14
+ get: () => () => {},
15
+ }),
16
+ truncateForLog: (value: string) => value,
17
+ }));
18
+
19
+ import { deleteConversation } from "../memory/conversation-crud.js";
20
+ import { getDb, initializeDb } from "../memory/db.js";
21
+ import { createSchedule, getScheduleRuns } from "../schedule/schedule-store.js";
22
+ import { startScheduler } from "../schedule/scheduler.js";
23
+
24
+ initializeDb();
25
+
26
+ /** Access the underlying bun:sqlite Database for raw parameterized queries. */
27
+ function getRawDb(): import("bun:sqlite").Database {
28
+ return (getDb() as unknown as { $client: import("bun:sqlite").Database })
29
+ .$client;
30
+ }
31
+
32
+ /** Force a schedule to be due by setting next_run_at in the past. */
33
+ function forceScheduleDue(scheduleId: string): void {
34
+ getRawDb().run("UPDATE cron_jobs SET next_run_at = ? WHERE id = ?", [
35
+ Date.now() - 1000,
36
+ scheduleId,
37
+ ]);
38
+ }
39
+
40
+ // Build an RRULE expression anchored at the given start date, recurring every minute.
41
+ function buildEveryMinuteRrule(dtstart: Date = new Date()): string {
42
+ const pad = (n: number) => String(n).padStart(2, "0");
43
+ const ds = `${dtstart.getUTCFullYear()}${pad(dtstart.getUTCMonth() + 1)}${pad(
44
+ dtstart.getUTCDate(),
45
+ )}T${pad(dtstart.getUTCHours())}${pad(dtstart.getUTCMinutes())}${pad(
46
+ dtstart.getUTCSeconds(),
47
+ )}Z`;
48
+ return `DTSTART:${ds}\nRRULE:FREQ=MINUTELY;INTERVAL=1`;
49
+ }
50
+
51
+ // Replace setTimeout with a zero-delay version so the 500ms scheduler
52
+ // wait calls fire instantly instead of waiting real time.
53
+ let origSetTimeout: typeof globalThis.setTimeout;
54
+
55
+ describe("scheduler conversation reuse", () => {
56
+ beforeAll(() => {
57
+ origSetTimeout = globalThis.setTimeout;
58
+ globalThis.setTimeout = ((
59
+ fn: TimerHandler,
60
+ _ms?: number,
61
+ ...args: unknown[]
62
+ ) => {
63
+ return origSetTimeout(fn, 200, ...args);
64
+ }) as typeof setTimeout;
65
+ });
66
+
67
+ afterAll(() => {
68
+ globalThis.setTimeout = origSetTimeout;
69
+ });
70
+
71
+ beforeEach(() => {
72
+ const db = getDb();
73
+ db.run("DELETE FROM cron_runs");
74
+ db.run("DELETE FROM cron_jobs");
75
+ db.run("DELETE FROM task_runs");
76
+ db.run("DELETE FROM tasks");
77
+ db.run("DELETE FROM messages");
78
+ db.run("DELETE FROM conversations");
79
+ });
80
+
81
+ test("recurring schedule with reuseConversation=true reuses conversation across runs", async () => {
82
+ /**
83
+ * When a recurring schedule has reuseConversation enabled, the second run
84
+ * should reuse the conversation created by the first run.
85
+ */
86
+
87
+ // GIVEN a recurring schedule with reuseConversation enabled
88
+ const rruleExpr = buildEveryMinuteRrule();
89
+ const schedule = createSchedule({
90
+ name: "Reuse Test",
91
+ cronExpression: rruleExpr,
92
+ message: "Reuse conversation message",
93
+ syntax: "rrule",
94
+ expression: rruleExpr,
95
+ reuseConversation: true,
96
+ });
97
+
98
+ // WHEN the schedule fires for the first time
99
+ forceScheduleDue(schedule.id);
100
+
101
+ const processedMessages: { conversationId: string; message: string }[] = [];
102
+ const processMessage = async (conversationId: string, message: string) => {
103
+ processedMessages.push({ conversationId, message });
104
+ };
105
+
106
+ const scheduler1 = startScheduler(
107
+ processMessage,
108
+ () => {},
109
+ () => {},
110
+ );
111
+ await new Promise((resolve) => setTimeout(resolve, 500));
112
+ scheduler1.stop();
113
+
114
+ // THEN a conversation is created and recorded
115
+ expect(processedMessages).toHaveLength(1);
116
+ const firstConversationId = processedMessages[0].conversationId;
117
+ expect(firstConversationId).toBeTruthy();
118
+
119
+ // AND a successful run is recorded
120
+ const runs1 = getScheduleRuns(schedule.id);
121
+ expect(runs1.length).toBe(1);
122
+ expect(runs1[0].status).toBe("ok");
123
+ expect(runs1[0].conversationId).toBe(firstConversationId);
124
+
125
+ // WHEN the schedule fires for the second time
126
+ forceScheduleDue(schedule.id);
127
+ processedMessages.length = 0;
128
+
129
+ const scheduler2 = startScheduler(
130
+ processMessage,
131
+ () => {},
132
+ () => {},
133
+ );
134
+ await new Promise((resolve) => setTimeout(resolve, 500));
135
+ scheduler2.stop();
136
+
137
+ // THEN the same conversation is reused
138
+ expect(processedMessages).toHaveLength(1);
139
+ expect(processedMessages[0].conversationId).toBe(firstConversationId);
140
+
141
+ // AND the run references the reused conversation
142
+ const runs2 = getScheduleRuns(schedule.id);
143
+ expect(runs2.length).toBe(2);
144
+ expect(runs2[0].conversationId).toBe(firstConversationId);
145
+ });
146
+
147
+ test("recurring schedule with reuseConversation=false creates new conversation each run", async () => {
148
+ /**
149
+ * Default behavior: each run creates a brand-new conversation.
150
+ */
151
+
152
+ // GIVEN a recurring schedule with reuseConversation disabled (default)
153
+ const rruleExpr = buildEveryMinuteRrule();
154
+ const schedule = createSchedule({
155
+ name: "No Reuse Test",
156
+ cronExpression: rruleExpr,
157
+ message: "New conv each run",
158
+ syntax: "rrule",
159
+ expression: rruleExpr,
160
+ // reuseConversation defaults to false
161
+ });
162
+
163
+ // WHEN the schedule fires for the first time
164
+ forceScheduleDue(schedule.id);
165
+
166
+ const processedMessages: { conversationId: string; message: string }[] = [];
167
+ const processMessage = async (conversationId: string, message: string) => {
168
+ processedMessages.push({ conversationId, message });
169
+ };
170
+
171
+ const scheduler1 = startScheduler(
172
+ processMessage,
173
+ () => {},
174
+ () => {},
175
+ );
176
+ await new Promise((resolve) => setTimeout(resolve, 500));
177
+ scheduler1.stop();
178
+
179
+ expect(processedMessages).toHaveLength(1);
180
+ const firstConversationId = processedMessages[0].conversationId;
181
+
182
+ // WHEN the schedule fires for the second time
183
+ forceScheduleDue(schedule.id);
184
+ processedMessages.length = 0;
185
+
186
+ const scheduler2 = startScheduler(
187
+ processMessage,
188
+ () => {},
189
+ () => {},
190
+ );
191
+ await new Promise((resolve) => setTimeout(resolve, 500));
192
+ scheduler2.stop();
193
+
194
+ // THEN a different conversation is created
195
+ expect(processedMessages).toHaveLength(1);
196
+ expect(processedMessages[0].conversationId).not.toBe(firstConversationId);
197
+ });
198
+
199
+ test("reuseConversation creates a new conversation when prior one is deleted", async () => {
200
+ /**
201
+ * If the conversation from the last successful run has been deleted,
202
+ * a fresh conversation should be bootstrapped.
203
+ */
204
+
205
+ // GIVEN a recurring schedule with reuseConversation enabled that has already run once
206
+ const rruleExpr = buildEveryMinuteRrule();
207
+ const schedule = createSchedule({
208
+ name: "Deleted Conv Test",
209
+ cronExpression: rruleExpr,
210
+ message: "Handle deleted conv",
211
+ syntax: "rrule",
212
+ expression: rruleExpr,
213
+ reuseConversation: true,
214
+ });
215
+
216
+ forceScheduleDue(schedule.id);
217
+
218
+ const processedMessages: { conversationId: string; message: string }[] = [];
219
+ const processMessage = async (conversationId: string, message: string) => {
220
+ processedMessages.push({ conversationId, message });
221
+ };
222
+
223
+ const scheduler1 = startScheduler(
224
+ processMessage,
225
+ () => {},
226
+ () => {},
227
+ );
228
+ await new Promise((resolve) => setTimeout(resolve, 500));
229
+ scheduler1.stop();
230
+
231
+ expect(processedMessages).toHaveLength(1);
232
+ const firstConversationId = processedMessages[0].conversationId;
233
+
234
+ // AND the conversation is deleted
235
+ deleteConversation(firstConversationId);
236
+
237
+ // WHEN the schedule fires again
238
+ forceScheduleDue(schedule.id);
239
+ processedMessages.length = 0;
240
+
241
+ const scheduler2 = startScheduler(
242
+ processMessage,
243
+ () => {},
244
+ () => {},
245
+ );
246
+ await new Promise((resolve) => setTimeout(resolve, 500));
247
+ scheduler2.stop();
248
+
249
+ // THEN a new conversation is created (not the deleted one)
250
+ expect(processedMessages).toHaveLength(1);
251
+ expect(processedMessages[0].conversationId).not.toBe(firstConversationId);
252
+ });
253
+
254
+ test("one-shot schedule ignores reuseConversation flag", async () => {
255
+ /**
256
+ * One-shot schedules always create a new conversation regardless of the
257
+ * reuseConversation flag since they only fire once.
258
+ */
259
+
260
+ // GIVEN a one-shot schedule with reuseConversation enabled
261
+ const schedule = createSchedule({
262
+ name: "One-shot Reuse Ignored",
263
+ message: "One-shot with reuse flag",
264
+ mode: "execute",
265
+ nextRunAt: Date.now() - 1000,
266
+ reuseConversation: true,
267
+ // No expression = one-shot
268
+ });
269
+
270
+ // WHEN the schedule fires
271
+ const processedMessages: { conversationId: string; message: string }[] = [];
272
+ const processMessage = async (conversationId: string, message: string) => {
273
+ processedMessages.push({ conversationId, message });
274
+ };
275
+
276
+ const scheduler = startScheduler(
277
+ processMessage,
278
+ () => {},
279
+ () => {},
280
+ );
281
+ await new Promise((resolve) => setTimeout(resolve, 500));
282
+ scheduler.stop();
283
+
284
+ // THEN the message is processed with a new conversation
285
+ expect(processedMessages).toHaveLength(1);
286
+ expect(processedMessages[0].conversationId).toBeTruthy();
287
+
288
+ // AND the schedule is marked as fired
289
+ const runs = getScheduleRuns(schedule.id);
290
+ expect(runs.length).toBeGreaterThanOrEqual(1);
291
+ expect(runs[0].status).toBe("ok");
292
+ });
293
+
294
+ test("reuseConversation uses the conversation from the most recent successful run", async () => {
295
+ /**
296
+ * When multiple runs exist, reuseConversation should pick the conversation
297
+ * from the most recent successful run (not a failed one).
298
+ */
299
+
300
+ // GIVEN a recurring schedule with reuseConversation enabled
301
+ const rruleExpr = buildEveryMinuteRrule();
302
+ const schedule = createSchedule({
303
+ name: "Most Recent Success Test",
304
+ cronExpression: rruleExpr,
305
+ message: "Pick latest success",
306
+ syntax: "rrule",
307
+ expression: rruleExpr,
308
+ reuseConversation: true,
309
+ });
310
+
311
+ // AND a first successful run
312
+ forceScheduleDue(schedule.id);
313
+
314
+ let shouldFail = false;
315
+ const processedMessages: { conversationId: string; message: string }[] = [];
316
+ const processMessage = async (conversationId: string, message: string) => {
317
+ processedMessages.push({ conversationId, message });
318
+ if (shouldFail) throw new Error("Simulated failure");
319
+ };
320
+
321
+ const scheduler1 = startScheduler(
322
+ processMessage,
323
+ () => {},
324
+ () => {},
325
+ );
326
+ await new Promise((resolve) => setTimeout(resolve, 500));
327
+ scheduler1.stop();
328
+
329
+ expect(processedMessages).toHaveLength(1);
330
+ const successConversationId = processedMessages[0].conversationId;
331
+
332
+ // AND a second run that fails
333
+ forceScheduleDue(schedule.id);
334
+ processedMessages.length = 0;
335
+ shouldFail = true;
336
+
337
+ const scheduler2 = startScheduler(
338
+ processMessage,
339
+ () => {},
340
+ () => {},
341
+ );
342
+ await new Promise((resolve) => setTimeout(resolve, 500));
343
+ scheduler2.stop();
344
+
345
+ // The failed run created a different conversation (since it failed
346
+ // before the run could reuse — actually it does reuse the same one
347
+ // because the lookup happens before the error). Let's verify the next
348
+ // successful run still uses the original successful conversation.
349
+
350
+ // AND a third run that succeeds
351
+ forceScheduleDue(schedule.id);
352
+ processedMessages.length = 0;
353
+ shouldFail = false;
354
+
355
+ const scheduler3 = startScheduler(
356
+ processMessage,
357
+ () => {},
358
+ () => {},
359
+ );
360
+ await new Promise((resolve) => setTimeout(resolve, 500));
361
+ scheduler3.stop();
362
+
363
+ // THEN the third run reuses the conversation from the first successful run
364
+ // (the lookup queries for status="ok", so it picks the first run's conversation)
365
+ expect(processedMessages).toHaveLength(1);
366
+ expect(processedMessages[0].conversationId).toBe(successConversationId);
367
+ });
368
+ });
@@ -1,4 +1,4 @@
1
- import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  mock.module("../util/logger.js", () => ({
4
4
  getLogger: () =>
@@ -8,7 +8,7 @@ mock.module("../util/logger.js", () => ({
8
8
  truncateForLog: (value: string) => value,
9
9
  }));
10
10
 
11
- import { getDb, initializeDb, resetDb } from "../memory/db.js";
11
+ import { getDb, initializeDb } from "../memory/db.js";
12
12
  import { scopedApprovalGrants } from "../memory/schema.js";
13
13
  import {
14
14
  _internal,
@@ -34,10 +34,6 @@ function clearTables(): void {
34
34
  db.delete(scopedApprovalGrants).run();
35
35
  }
36
36
 
37
- afterAll(() => {
38
- resetDb();
39
- });
40
-
41
37
  // ---------------------------------------------------------------------------
42
38
  // Helper to build grant params with sensible defaults
43
39
  // ---------------------------------------------------------------------------
@@ -23,7 +23,7 @@
23
23
  * 11. Guardian identity mismatch cannot mint grant — guardian-grant-minting.test.ts
24
24
  */
25
25
 
26
- import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
26
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
27
27
 
28
28
  mock.module("../util/logger.js", () => ({
29
29
  getLogger: () =>
@@ -33,7 +33,7 @@ mock.module("../util/logger.js", () => ({
33
33
  truncateForLog: (value: string) => value,
34
34
  }));
35
35
 
36
- import { getDb, initializeDb, resetDb } from "../memory/db.js";
36
+ import { getDb, initializeDb } from "../memory/db.js";
37
37
  import { scopedApprovalGrants } from "../memory/schema.js";
38
38
  import {
39
39
  _internal,
@@ -51,10 +51,6 @@ function clearTables(): void {
51
51
  db.delete(scopedApprovalGrants).run();
52
52
  }
53
53
 
54
- afterAll(() => {
55
- resetDb();
56
- });
57
-
58
54
  // ---------------------------------------------------------------------------
59
55
  // Helper to build grant params with sensible defaults
60
56
  // ---------------------------------------------------------------------------
@@ -0,0 +1,278 @@
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 { migrateScrubCorruptedImageAttachments } from "../memory/migrations/206-scrub-corrupted-image-attachments.js";
7
+ import * as schema from "../memory/schema.js";
8
+
9
+ function createTestDb() {
10
+ const sqlite = new Database(":memory:");
11
+ sqlite.exec("PRAGMA journal_mode=WAL");
12
+ sqlite.exec("PRAGMA foreign_keys = OFF");
13
+ return drizzle(sqlite, { schema });
14
+ }
15
+
16
+ type TestDb = ReturnType<typeof createTestDb>;
17
+
18
+ function getRawSqlite(db: TestDb): Database {
19
+ return (db as unknown as { $client: Database }).$client;
20
+ }
21
+
22
+ function createRequiredTables(raw: Database) {
23
+ raw.exec(/*sql*/ `
24
+ CREATE TABLE memory_checkpoints (
25
+ key TEXT PRIMARY KEY,
26
+ value TEXT NOT NULL,
27
+ updated_at INTEGER NOT NULL
28
+ )
29
+ `);
30
+
31
+ raw.exec(/*sql*/ `
32
+ CREATE TABLE attachments (
33
+ id TEXT PRIMARY KEY,
34
+ original_filename TEXT NOT NULL,
35
+ mime_type TEXT NOT NULL,
36
+ size_bytes INTEGER NOT NULL,
37
+ kind TEXT NOT NULL,
38
+ data_base64 TEXT NOT NULL DEFAULT '',
39
+ content_hash TEXT,
40
+ thumbnail_base64 TEXT,
41
+ file_path TEXT,
42
+ created_at INTEGER NOT NULL
43
+ )
44
+ `);
45
+
46
+ raw.exec(/*sql*/ `
47
+ CREATE TABLE message_attachments (
48
+ id TEXT PRIMARY KEY,
49
+ message_id TEXT NOT NULL,
50
+ attachment_id TEXT NOT NULL,
51
+ position INTEGER NOT NULL DEFAULT 0,
52
+ created_at INTEGER NOT NULL
53
+ )
54
+ `);
55
+ }
56
+
57
+ // A minimal valid PNG header (8 bytes)
58
+ const VALID_PNG_BASE64 = Buffer.from(
59
+ Uint8Array.from([137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 0]),
60
+ ).toString("base64");
61
+
62
+ // HTML error page encoded as base64 (simulating Slack CDN auth failure)
63
+ const HTML_ERROR_BASE64 = Buffer.from(
64
+ "<!DOCTYPE html><html><head><title>Sign in</title></head><body>Please sign in</body></html>",
65
+ ).toString("base64");
66
+
67
+ // HTML with leading whitespace/BOM
68
+ const HTML_WITH_BOM_BASE64 = Buffer.from(
69
+ "\uFEFF <!DOCTYPE html><html><body>Error</body></html>",
70
+ ).toString("base64");
71
+
72
+ const HTML_UPPERCASE_BASE64 = Buffer.from(
73
+ "<HTML><BODY>Error page</BODY></HTML>",
74
+ ).toString("base64");
75
+
76
+ describe("migrateScrubCorruptedImageAttachments", () => {
77
+ test("removes corrupted image attachment with HTML data_base64", () => {
78
+ const db = createTestDb();
79
+ const raw = getRawSqlite(db);
80
+ const now = Date.now();
81
+
82
+ createRequiredTables(raw);
83
+
84
+ // Insert corrupted attachment (HTML stored as image/png)
85
+ raw.exec(/*sql*/ `
86
+ INSERT INTO attachments (id, original_filename, mime_type, size_bytes, kind, data_base64, created_at)
87
+ VALUES ('corrupt-1', 'image.png', 'image/png', 100, 'image', '${HTML_ERROR_BASE64}', ${now})
88
+ `);
89
+
90
+ // Insert message_attachments link
91
+ raw.exec(/*sql*/ `
92
+ INSERT INTO message_attachments (id, message_id, attachment_id, position, created_at)
93
+ VALUES ('ma-1', 'msg-1', 'corrupt-1', 0, ${now})
94
+ `);
95
+
96
+ migrateScrubCorruptedImageAttachments(db);
97
+
98
+ const attachmentCount = raw
99
+ .query(`SELECT COUNT(*) AS count FROM attachments`)
100
+ .get() as { count: number };
101
+ expect(attachmentCount.count).toBe(0);
102
+
103
+ const linkCount = raw
104
+ .query(`SELECT COUNT(*) AS count FROM message_attachments`)
105
+ .get() as { count: number };
106
+ expect(linkCount.count).toBe(0);
107
+ });
108
+
109
+ test("does NOT remove valid PNG attachment", () => {
110
+ const db = createTestDb();
111
+ const raw = getRawSqlite(db);
112
+ const now = Date.now();
113
+
114
+ createRequiredTables(raw);
115
+
116
+ // Insert valid PNG attachment
117
+ raw.exec(/*sql*/ `
118
+ INSERT INTO attachments (id, original_filename, mime_type, size_bytes, kind, data_base64, created_at)
119
+ VALUES ('valid-1', 'photo.png', 'image/png', 200, 'image', '${VALID_PNG_BASE64}', ${now})
120
+ `);
121
+
122
+ raw.exec(/*sql*/ `
123
+ INSERT INTO message_attachments (id, message_id, attachment_id, position, created_at)
124
+ VALUES ('ma-valid', 'msg-2', 'valid-1', 0, ${now})
125
+ `);
126
+
127
+ migrateScrubCorruptedImageAttachments(db);
128
+
129
+ const attachmentCount = raw
130
+ .query(`SELECT COUNT(*) AS count FROM attachments`)
131
+ .get() as { count: number };
132
+ expect(attachmentCount.count).toBe(1);
133
+
134
+ const linkCount = raw
135
+ .query(`SELECT COUNT(*) AS count FROM message_attachments`)
136
+ .get() as { count: number };
137
+ expect(linkCount.count).toBe(1);
138
+ });
139
+
140
+ test("removes corrupted and preserves valid attachments together", () => {
141
+ const db = createTestDb();
142
+ const raw = getRawSqlite(db);
143
+ const now = Date.now();
144
+
145
+ createRequiredTables(raw);
146
+
147
+ // Corrupted attachment
148
+ raw.exec(/*sql*/ `
149
+ INSERT INTO attachments (id, original_filename, mime_type, size_bytes, kind, data_base64, created_at)
150
+ VALUES ('corrupt-1', 'slack-img.png', 'image/png', 100, 'image', '${HTML_ERROR_BASE64}', ${now})
151
+ `);
152
+ raw.exec(/*sql*/ `
153
+ INSERT INTO message_attachments (id, message_id, attachment_id, position, created_at)
154
+ VALUES ('ma-corrupt', 'msg-1', 'corrupt-1', 0, ${now})
155
+ `);
156
+
157
+ // Valid attachment
158
+ raw.exec(/*sql*/ `
159
+ INSERT INTO attachments (id, original_filename, mime_type, size_bytes, kind, data_base64, created_at)
160
+ VALUES ('valid-1', 'photo.png', 'image/png', 200, 'image', '${VALID_PNG_BASE64}', ${now})
161
+ `);
162
+ raw.exec(/*sql*/ `
163
+ INSERT INTO message_attachments (id, message_id, attachment_id, position, created_at)
164
+ VALUES ('ma-valid', 'msg-2', 'valid-1', 0, ${now})
165
+ `);
166
+
167
+ migrateScrubCorruptedImageAttachments(db);
168
+
169
+ const remaining = raw.query(`SELECT id FROM attachments`).all() as Array<{
170
+ id: string;
171
+ }>;
172
+ expect(remaining).toEqual([{ id: "valid-1" }]);
173
+
174
+ const links = raw
175
+ .query(`SELECT attachment_id FROM message_attachments`)
176
+ .all() as Array<{ attachment_id: string }>;
177
+ expect(links).toEqual([{ attachment_id: "valid-1" }]);
178
+ });
179
+
180
+ test("detects HTML with leading BOM and whitespace", () => {
181
+ const db = createTestDb();
182
+ const raw = getRawSqlite(db);
183
+ const now = Date.now();
184
+
185
+ createRequiredTables(raw);
186
+
187
+ raw.exec(/*sql*/ `
188
+ INSERT INTO attachments (id, original_filename, mime_type, size_bytes, kind, data_base64, created_at)
189
+ VALUES ('bom-1', 'img.jpg', 'image/jpeg', 50, 'image', '${HTML_WITH_BOM_BASE64}', ${now})
190
+ `);
191
+
192
+ migrateScrubCorruptedImageAttachments(db);
193
+
194
+ const count = raw
195
+ .query(`SELECT COUNT(*) AS count FROM attachments`)
196
+ .get() as { count: number };
197
+ expect(count.count).toBe(0);
198
+ });
199
+
200
+ test("detects uppercase <HTML> tag", () => {
201
+ const db = createTestDb();
202
+ const raw = getRawSqlite(db);
203
+ const now = Date.now();
204
+
205
+ createRequiredTables(raw);
206
+
207
+ raw.exec(/*sql*/ `
208
+ INSERT INTO attachments (id, original_filename, mime_type, size_bytes, kind, data_base64, created_at)
209
+ VALUES ('upper-1', 'img.gif', 'image/gif', 50, 'image', '${HTML_UPPERCASE_BASE64}', ${now})
210
+ `);
211
+
212
+ migrateScrubCorruptedImageAttachments(db);
213
+
214
+ const count = raw
215
+ .query(`SELECT COUNT(*) AS count FROM attachments`)
216
+ .get() as { count: number };
217
+ expect(count.count).toBe(0);
218
+ });
219
+
220
+ test("is idempotent — running twice does not error", () => {
221
+ const db = createTestDb();
222
+ const raw = getRawSqlite(db);
223
+ const now = Date.now();
224
+
225
+ createRequiredTables(raw);
226
+
227
+ raw.exec(/*sql*/ `
228
+ INSERT INTO attachments (id, original_filename, mime_type, size_bytes, kind, data_base64, created_at)
229
+ VALUES ('corrupt-1', 'image.png', 'image/png', 100, 'image', '${HTML_ERROR_BASE64}', ${now})
230
+ `);
231
+ raw.exec(/*sql*/ `
232
+ INSERT INTO message_attachments (id, message_id, attachment_id, position, created_at)
233
+ VALUES ('ma-1', 'msg-1', 'corrupt-1', 0, ${now})
234
+ `);
235
+
236
+ migrateScrubCorruptedImageAttachments(db);
237
+ migrateScrubCorruptedImageAttachments(db);
238
+
239
+ const attachmentCount = raw
240
+ .query(`SELECT COUNT(*) AS count FROM attachments`)
241
+ .get() as { count: number };
242
+ expect(attachmentCount.count).toBe(0);
243
+
244
+ const linkCount = raw
245
+ .query(`SELECT COUNT(*) AS count FROM message_attachments`)
246
+ .get() as { count: number };
247
+ expect(linkCount.count).toBe(0);
248
+
249
+ // The checkpoint should be set to '1' (completed)
250
+ const checkpoint = raw
251
+ .query(
252
+ `SELECT value FROM memory_checkpoints WHERE key = 'migration_scrub_corrupted_image_attachments_v1'`,
253
+ )
254
+ .get() as { value: string } | null;
255
+ expect(checkpoint?.value).toBe("1");
256
+ });
257
+
258
+ test("skips non-image MIME types", () => {
259
+ const db = createTestDb();
260
+ const raw = getRawSqlite(db);
261
+ const now = Date.now();
262
+
263
+ createRequiredTables(raw);
264
+
265
+ // HTML content with text/html MIME type — should NOT be touched
266
+ raw.exec(/*sql*/ `
267
+ INSERT INTO attachments (id, original_filename, mime_type, size_bytes, kind, data_base64, created_at)
268
+ VALUES ('html-1', 'page.html', 'text/html', 100, 'document', '${HTML_ERROR_BASE64}', ${now})
269
+ `);
270
+
271
+ migrateScrubCorruptedImageAttachments(db);
272
+
273
+ const count = raw
274
+ .query(`SELECT COUNT(*) AS count FROM attachments`)
275
+ .get() as { count: number };
276
+ expect(count.count).toBe(1);
277
+ });
278
+ });