@vellumai/assistant 0.5.1 → 0.5.3

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 (405) hide show
  1. package/ARCHITECTURE.md +163 -54
  2. package/docs/architecture/integrations.md +62 -67
  3. package/docs/credential-execution-service.md +3 -3
  4. package/docs/skills.md +100 -0
  5. package/package.json +1 -1
  6. package/src/__tests__/agent-loop.test.ts +111 -0
  7. package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
  8. package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
  9. package/src/__tests__/app-dir-path-guard.test.ts +78 -0
  10. package/src/__tests__/app-executors.test.ts +1 -291
  11. package/src/__tests__/app-git-history.test.ts +4 -4
  12. package/src/__tests__/app-routes-csp.test.ts +1 -0
  13. package/src/__tests__/app-store-dir-names.test.ts +426 -0
  14. package/src/__tests__/attachments-store.test.ts +169 -21
  15. package/src/__tests__/attachments.test.ts +115 -1
  16. package/src/__tests__/btw-routes.test.ts +1 -0
  17. package/src/__tests__/canonical-guardian-store.test.ts +38 -0
  18. package/src/__tests__/channel-reply-delivery.test.ts +55 -0
  19. package/src/__tests__/checker.test.ts +54 -0
  20. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  21. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  22. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  23. package/src/__tests__/config-schema-cmd.test.ts +68 -21
  24. package/src/__tests__/config-schema.test.ts +1 -1
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +156 -5
  26. package/src/__tests__/conversation-agent-loop.test.ts +297 -2
  27. package/src/__tests__/conversation-attachments.test.ts +17 -19
  28. package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
  29. package/src/__tests__/conversation-disk-view.test.ts +810 -0
  30. package/src/__tests__/conversation-error.test.ts +1 -1
  31. package/src/__tests__/conversation-fork-crud.test.ts +551 -0
  32. package/src/__tests__/conversation-fork-route.test.ts +386 -0
  33. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  34. package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
  35. package/src/__tests__/conversation-media-retry.test.ts +8 -2
  36. package/src/__tests__/conversation-memory-dirty-tail.test.ts +150 -0
  37. package/src/__tests__/conversation-provider-retry-repair.test.ts +7 -0
  38. package/src/__tests__/conversation-queue.test.ts +36 -1
  39. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  40. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  41. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  42. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  43. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  44. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  45. package/src/__tests__/conversation-store.test.ts +24 -21
  46. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  47. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  48. package/src/__tests__/conversation-title-service.test.ts +137 -0
  49. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  50. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  51. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  52. package/src/__tests__/conversation-wipe.test.ts +226 -0
  53. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  54. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  55. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  56. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  57. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  58. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  59. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  60. package/src/__tests__/db-memory-archive-migration.test.ts +372 -0
  61. package/src/__tests__/db-memory-brief-state-migration.test.ts +213 -0
  62. package/src/__tests__/db-memory-reducer-checkpoints.test.ts +273 -0
  63. package/src/__tests__/diagnostics-export.test.ts +70 -1
  64. package/src/__tests__/first-greeting.test.ts +80 -0
  65. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  66. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  67. package/src/__tests__/history-repair.test.ts +32 -10
  68. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  69. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  70. package/src/__tests__/inline-command-runner.test.ts +311 -0
  71. package/src/__tests__/inline-skill-authoring-guard.test.ts +220 -0
  72. package/src/__tests__/inline-skill-load-permissions.test.ts +435 -0
  73. package/src/__tests__/list-messages-attachments.test.ts +96 -0
  74. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  75. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  76. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  77. package/src/__tests__/media-generate-image.test.ts +47 -94
  78. package/src/__tests__/memory-brief-open-loops.test.ts +530 -0
  79. package/src/__tests__/memory-brief-time.test.ts +285 -0
  80. package/src/__tests__/memory-brief-wrapper.test.ts +311 -0
  81. package/src/__tests__/memory-chunk-archive.test.ts +400 -0
  82. package/src/__tests__/memory-chunk-dual-write.test.ts +453 -0
  83. package/src/__tests__/memory-episode-archive.test.ts +370 -0
  84. package/src/__tests__/memory-episode-dual-write.test.ts +626 -0
  85. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  86. package/src/__tests__/memory-observation-archive.test.ts +375 -0
  87. package/src/__tests__/memory-observation-dual-write.test.ts +318 -0
  88. package/src/__tests__/memory-recall-quality.test.ts +7 -7
  89. package/src/__tests__/memory-reducer-store.test.ts +728 -0
  90. package/src/__tests__/memory-reducer-types.test.ts +699 -0
  91. package/src/__tests__/memory-reducer.test.ts +698 -0
  92. package/src/__tests__/memory-regressions.test.ts +6 -4
  93. package/src/__tests__/memory-simplified-config.test.ts +281 -0
  94. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  95. package/src/__tests__/migration-export-http.test.ts +3 -1
  96. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  97. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  98. package/src/__tests__/mime-builder.test.ts +3 -2
  99. package/src/__tests__/non-member-access-request.test.ts +12 -1
  100. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  101. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  102. package/src/__tests__/oauth-store.test.ts +115 -0
  103. package/src/__tests__/parse-identity-fields.test.ts +129 -0
  104. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  105. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  106. package/src/__tests__/recording-handler.test.ts +17 -0
  107. package/src/__tests__/registry.test.ts +3 -8
  108. package/src/__tests__/relay-server.test.ts +1 -1
  109. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  110. package/src/__tests__/schema-transforms.test.ts +165 -5
  111. package/src/__tests__/server-history-render.test.ts +2 -2
  112. package/src/__tests__/skill-load-inline-command.test.ts +598 -0
  113. package/src/__tests__/skill-load-inline-includes.test.ts +644 -0
  114. package/src/__tests__/skills-inline-command-expansions.test.ts +301 -0
  115. package/src/__tests__/skills-transitive-hash.test.ts +333 -0
  116. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  117. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  118. package/src/__tests__/starter-task-flow.test.ts +1 -0
  119. package/src/__tests__/suggestion-routes.test.ts +443 -0
  120. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  121. package/src/__tests__/swarm-recursion.test.ts +1 -0
  122. package/src/__tests__/swarm-tool.test.ts +1 -0
  123. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  124. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  125. package/src/__tests__/top-level-renderer.test.ts +22 -0
  126. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  127. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +320 -0
  128. package/src/__tests__/web-fetch.test.ts +6 -2
  129. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  130. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  131. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  132. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  133. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  134. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  135. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  136. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  137. package/src/agent/attachments.ts +27 -1
  138. package/src/agent/loop.ts +29 -1
  139. package/src/avatar/traits-png-sync.ts +80 -25
  140. package/src/bundler/app-bundler.ts +4 -4
  141. package/src/calls/call-domain.ts +1 -0
  142. package/src/calls/voice-session-bridge.ts +1 -0
  143. package/src/cli/commands/auth.ts +92 -0
  144. package/src/cli/commands/avatar.ts +7 -6
  145. package/src/cli/commands/config.ts +2 -0
  146. package/src/cli/commands/oauth/providers.ts +29 -0
  147. package/src/cli/program.ts +12 -0
  148. package/src/cli.ts +15 -48
  149. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  150. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  151. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  152. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  153. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  154. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  155. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  156. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  157. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  158. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  159. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  160. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  161. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  162. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  163. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  164. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  165. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  166. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  167. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  168. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  169. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  170. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  171. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  172. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  173. package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
  174. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  175. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  176. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  177. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  178. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  179. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  180. package/src/config/bundled-tool-registry.ts +2 -14
  181. package/src/config/feature-flag-registry.json +24 -0
  182. package/src/config/loader.ts +65 -0
  183. package/src/config/raw-config-utils.ts +58 -0
  184. package/src/config/schema-utils.ts +28 -7
  185. package/src/config/schema.ts +20 -0
  186. package/src/config/schemas/elevenlabs.ts +18 -0
  187. package/src/config/schemas/memory-lifecycle.ts +4 -2
  188. package/src/config/schemas/memory-simplified.ts +101 -0
  189. package/src/config/schemas/memory-storage.ts +1 -1
  190. package/src/config/schemas/memory.ts +4 -0
  191. package/src/config/schemas/services.ts +8 -6
  192. package/src/config/skills.ts +50 -4
  193. package/src/contacts/contact-store.ts +13 -6
  194. package/src/contacts/contacts-write.ts +0 -1
  195. package/src/context/window-manager.ts +13 -2
  196. package/src/daemon/conversation-agent-loop-handlers.ts +54 -8
  197. package/src/daemon/conversation-agent-loop.ts +127 -20
  198. package/src/daemon/conversation-attachments.ts +18 -36
  199. package/src/daemon/conversation-error.ts +2 -1
  200. package/src/daemon/conversation-history.ts +18 -4
  201. package/src/daemon/conversation-lifecycle.ts +50 -16
  202. package/src/daemon/conversation-messaging.ts +70 -26
  203. package/src/daemon/conversation-process.ts +58 -34
  204. package/src/daemon/conversation-runtime-assembly.ts +22 -38
  205. package/src/daemon/conversation-slash.ts +121 -256
  206. package/src/daemon/conversation-surfaces.ts +170 -24
  207. package/src/daemon/conversation-tool-setup.ts +0 -6
  208. package/src/daemon/conversation-workspace.ts +21 -1
  209. package/src/daemon/conversation.ts +69 -30
  210. package/src/daemon/first-greeting.ts +35 -0
  211. package/src/daemon/handlers/config-embeddings.ts +156 -0
  212. package/src/daemon/handlers/config-model.ts +62 -26
  213. package/src/daemon/handlers/conversations.ts +0 -23
  214. package/src/daemon/handlers/identity.ts +12 -1
  215. package/src/daemon/handlers/recording.ts +26 -21
  216. package/src/daemon/host-cu-proxy.ts +2 -2
  217. package/src/daemon/lifecycle.ts +115 -65
  218. package/src/daemon/message-protocol.ts +3 -0
  219. package/src/daemon/message-types/conversations.ts +18 -0
  220. package/src/daemon/message-types/messages.ts +1 -0
  221. package/src/daemon/message-types/shared.ts +2 -0
  222. package/src/daemon/message-types/surfaces.ts +2 -0
  223. package/src/daemon/message-types/upgrades.ts +23 -0
  224. package/src/daemon/server.ts +83 -12
  225. package/src/daemon/shutdown-handlers.ts +8 -5
  226. package/src/daemon/startup-error.ts +9 -0
  227. package/src/daemon/tool-side-effects.ts +11 -28
  228. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  229. package/src/followups/followup-store.ts +47 -1
  230. package/src/instrument.ts +0 -4
  231. package/src/media/app-icon-generator.ts +2 -2
  232. package/src/memory/app-git-service.ts +28 -16
  233. package/src/memory/app-store.ts +230 -41
  234. package/src/memory/archive-store.ts +400 -0
  235. package/src/memory/attachments-store.ts +558 -130
  236. package/src/memory/brief-formatting.ts +33 -0
  237. package/src/memory/brief-open-loops.ts +266 -0
  238. package/src/memory/brief-time.ts +161 -0
  239. package/src/memory/brief.ts +75 -0
  240. package/src/memory/conversation-attention-store.ts +70 -0
  241. package/src/memory/conversation-crud.ts +591 -8
  242. package/src/memory/conversation-directories.ts +125 -0
  243. package/src/memory/conversation-disk-view.ts +390 -0
  244. package/src/memory/conversation-key-store.ts +17 -5
  245. package/src/memory/conversation-queries.ts +5 -1
  246. package/src/memory/conversation-title-service.ts +21 -49
  247. package/src/memory/db-init.ts +40 -0
  248. package/src/memory/embedding-backend.ts +42 -53
  249. package/src/memory/embedding-gemini.test.ts +4 -4
  250. package/src/memory/embedding-local.ts +1 -3
  251. package/src/memory/embedding-ollama.ts +1 -3
  252. package/src/memory/embedding-openai.ts +1 -3
  253. package/src/memory/indexer.ts +114 -21
  254. package/src/memory/items-extractor.ts +42 -13
  255. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  256. package/src/memory/job-handlers/embedding.test.ts +2 -4
  257. package/src/memory/job-handlers/embedding.ts +83 -0
  258. package/src/memory/job-utils.ts +1 -1
  259. package/src/memory/jobs-store.ts +6 -0
  260. package/src/memory/jobs-worker.ts +12 -0
  261. package/src/memory/llm-request-log-store.ts +100 -1
  262. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  263. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  264. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  265. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  266. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  267. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  268. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  269. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  270. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  271. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  272. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  273. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  274. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  275. package/src/memory/migrations/185-memory-brief-state.ts +52 -0
  276. package/src/memory/migrations/186-memory-archive.ts +109 -0
  277. package/src/memory/migrations/187-memory-reducer-checkpoints.ts +19 -0
  278. package/src/memory/migrations/index.ts +10 -0
  279. package/src/memory/migrations/registry.ts +13 -0
  280. package/src/memory/qdrant-client.ts +23 -4
  281. package/src/memory/reducer-store.ts +271 -0
  282. package/src/memory/reducer-types.ts +99 -0
  283. package/src/memory/reducer.ts +453 -0
  284. package/src/memory/retriever.test.ts +601 -2
  285. package/src/memory/retriever.ts +85 -9
  286. package/src/memory/schema/conversations.ts +9 -0
  287. package/src/memory/schema/index.ts +2 -0
  288. package/src/memory/schema/infrastructure.ts +13 -7
  289. package/src/memory/schema/memory-archive.ts +121 -0
  290. package/src/memory/schema/memory-brief.ts +55 -0
  291. package/src/memory/schema/oauth.ts +6 -0
  292. package/src/memory/search/semantic.ts +17 -4
  293. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  294. package/src/notifications/copy-composer.ts +26 -0
  295. package/src/notifications/decision-engine.ts +14 -1
  296. package/src/notifications/emit-signal.ts +1 -1
  297. package/src/notifications/signal.ts +36 -0
  298. package/src/oauth/byo-connection.test.ts +1 -45
  299. package/src/oauth/byo-connection.ts +2 -8
  300. package/src/oauth/connect-orchestrator.ts +15 -11
  301. package/src/oauth/connection-resolver.test.ts +191 -0
  302. package/src/oauth/connection-resolver.ts +66 -38
  303. package/src/oauth/connection.ts +0 -1
  304. package/src/oauth/oauth-store.ts +99 -47
  305. package/src/oauth/platform-connection.test.ts +0 -1
  306. package/src/oauth/platform-connection.ts +11 -3
  307. package/src/oauth/seed-providers.ts +78 -3
  308. package/src/oauth/token-persistence.ts +16 -10
  309. package/src/permissions/checker.ts +160 -14
  310. package/src/permissions/defaults.ts +14 -0
  311. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  312. package/src/providers/anthropic/client.ts +8 -1
  313. package/src/providers/failover.ts +4 -1
  314. package/src/providers/gemini/client.ts +50 -0
  315. package/src/providers/model-catalog.ts +92 -0
  316. package/src/providers/model-intents.ts +29 -20
  317. package/src/providers/openai/client.ts +49 -0
  318. package/src/providers/types.ts +2 -0
  319. package/src/runtime/access-request-helper.ts +16 -7
  320. package/src/runtime/auth/credential-service.ts +3 -1
  321. package/src/runtime/auth/route-policy.ts +14 -1
  322. package/src/runtime/btw-sidechain.ts +101 -0
  323. package/src/runtime/channel-reply-delivery.ts +17 -1
  324. package/src/runtime/http-router.ts +3 -1
  325. package/src/runtime/http-server.ts +196 -141
  326. package/src/runtime/http-types.ts +1 -0
  327. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  328. package/src/runtime/routes/access-request-decision.ts +41 -0
  329. package/src/runtime/routes/app-management-routes.ts +6 -3
  330. package/src/runtime/routes/app-routes.ts +7 -3
  331. package/src/runtime/routes/approval-routes.ts +1 -0
  332. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  333. package/src/runtime/routes/attachment-routes.ts +45 -15
  334. package/src/runtime/routes/btw-routes.ts +21 -61
  335. package/src/runtime/routes/conversation-management-routes.ts +74 -0
  336. package/src/runtime/routes/conversation-query-routes.ts +187 -10
  337. package/src/runtime/routes/conversation-routes.ts +269 -28
  338. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  339. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  340. package/src/runtime/routes/identity-routes.ts +2 -35
  341. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  342. package/src/runtime/routes/llm-context-normalization.ts +1212 -0
  343. package/src/runtime/routes/log-export-routes.ts +3 -0
  344. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  345. package/src/runtime/routes/memory-item-routes.ts +94 -5
  346. package/src/runtime/routes/migration-routes.ts +4 -1
  347. package/src/runtime/routes/oauth-apps.ts +291 -0
  348. package/src/runtime/routes/secret-routes.ts +30 -1
  349. package/src/runtime/routes/settings-routes.ts +14 -0
  350. package/src/runtime/routes/surface-action-routes.ts +68 -1
  351. package/src/runtime/routes/trace-event-routes.ts +4 -1
  352. package/src/schedule/schedule-store.ts +30 -21
  353. package/src/security/secure-keys.ts +21 -0
  354. package/src/signals/bash.ts +1 -1
  355. package/src/skills/inline-command-expansions.ts +204 -0
  356. package/src/skills/inline-command-render.ts +127 -0
  357. package/src/skills/inline-command-runner.ts +242 -0
  358. package/src/skills/transitive-version-hash.ts +88 -0
  359. package/src/swarm/backend-claude-code.ts +3 -6
  360. package/src/tasks/task-store.ts +43 -1
  361. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  362. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  363. package/src/tools/AGENTS.md +6 -10
  364. package/src/tools/apps/executors.ts +17 -232
  365. package/src/tools/claude-code/claude-code.ts +2 -3
  366. package/src/tools/credentials/vault.ts +7 -12
  367. package/src/tools/host-filesystem/read.ts +13 -10
  368. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  369. package/src/tools/permission-checker.ts +8 -1
  370. package/src/tools/schedule/list.ts +2 -7
  371. package/src/tools/schema-transforms.ts +5 -0
  372. package/src/tools/shared/filesystem/format-diff.ts +2 -7
  373. package/src/tools/skills/execute.ts +1 -1
  374. package/src/tools/skills/load.ts +140 -6
  375. package/src/tools/tool-manifest.ts +0 -6
  376. package/src/tools/ui-surface/definitions.ts +2 -2
  377. package/src/util/device-id.ts +28 -5
  378. package/src/util/platform.ts +24 -0
  379. package/src/util/pricing.ts +1 -0
  380. package/src/util/retry.ts +1 -3
  381. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  382. package/src/workspace/migrations/006-services-config.ts +5 -0
  383. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  384. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  385. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  386. package/src/workspace/migrations/{002-backfill-installation-id.ts → 011-backfill-installation-id.ts} +24 -13
  387. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  388. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  389. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  390. package/src/workspace/migrations/registry.ts +11 -1
  391. package/src/workspace/top-level-renderer.ts +12 -0
  392. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  393. package/src/__tests__/asset-search-tool.test.ts +0 -536
  394. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  395. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  396. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  397. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  398. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  399. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  400. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  401. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  402. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  403. package/src/daemon/media-visibility-policy.ts +0 -59
  404. package/src/tools/assets/materialize.ts +0 -248
  405. package/src/tools/assets/search.ts +0 -400
@@ -0,0 +1,46 @@
1
+ import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";
2
+ import { withCrashRecovery } from "./validate-migration-state.js";
3
+
4
+ /**
5
+ * Rename checkpoint keys from the old `thread_starters:` prefix to
6
+ * `conversation_starters:` so that the renamed code paths in
7
+ * `conversation-starters-cadence.ts` and `conversation-starters.ts`
8
+ * find existing generation state and avoid unnecessary re-generation.
9
+ *
10
+ * This was originally appended to migration 174, but that migration
11
+ * had already shipped with its own checkpoint key
12
+ * (`migration_rename_thread_starters_table_v1`), so `withCrashRecovery`
13
+ * would skip the entire body for users who had already run it. Moving
14
+ * the checkpoint-key rewrite into its own migration with an independent
15
+ * checkpoint key ensures it runs for all users.
16
+ *
17
+ * The rename is collision-safe: if a database already has both old
18
+ * `thread_starters:*` keys and new `conversation_starters:*` keys
19
+ * (written by updated code after the table rename), we drop the stale
20
+ * old rows first to avoid UNIQUE constraint violations on the primary key.
21
+ */
22
+ export function migrateRenameThreadStartersCheckpoints(
23
+ database: DrizzleDb,
24
+ ): void {
25
+ withCrashRecovery(
26
+ database,
27
+ "migration_rename_thread_starters_checkpoints_v1",
28
+ () => {
29
+ const raw = getSqliteFrom(database);
30
+
31
+ // 1. Delete old thread_starters: keys where a corresponding
32
+ // conversation_starters: key already exists (the newer key
33
+ // written by updated code takes precedence).
34
+ raw.exec(/*sql*/ `DELETE FROM memory_checkpoints
35
+ WHERE key LIKE 'thread_starters:%'
36
+ AND replace(key, 'thread_starters:', 'conversation_starters:') IN (
37
+ SELECT key FROM memory_checkpoints WHERE key LIKE 'conversation_starters:%'
38
+ )`);
39
+
40
+ // 2. Rename remaining old keys that have no collision.
41
+ raw.exec(
42
+ /*sql*/ `UPDATE memory_checkpoints SET key = replace(key, 'thread_starters:', 'conversation_starters:') WHERE key LIKE 'thread_starters:%'`,
43
+ );
44
+ },
45
+ );
46
+ }
@@ -0,0 +1,20 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { getSqliteFrom } from "../db-connection.js";
3
+
4
+ export function migrateOAuthProvidersDisplayMetadata(database: DrizzleDb): void {
5
+ const raw = getSqliteFrom(database);
6
+ const columns = [
7
+ "display_name TEXT",
8
+ "description TEXT",
9
+ "dashboard_url TEXT",
10
+ "client_id_placeholder TEXT",
11
+ "requires_client_secret INTEGER NOT NULL DEFAULT 1",
12
+ ];
13
+ for (const col of columns) {
14
+ try {
15
+ raw.exec(`ALTER TABLE oauth_providers ADD COLUMN ${col}`);
16
+ } catch {
17
+ // Column already exists — nothing to do.
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,22 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { getSqliteFrom } from "../db-connection.js";
3
+
4
+ export function migrateConversationForkLineage(database: DrizzleDb): void {
5
+ const raw = getSqliteFrom(database);
6
+ const columns = [
7
+ "fork_parent_conversation_id TEXT",
8
+ "fork_parent_message_id TEXT",
9
+ ];
10
+
11
+ for (const column of columns) {
12
+ try {
13
+ raw.exec(`ALTER TABLE conversations ADD COLUMN ${column}`);
14
+ } catch {
15
+ // Column already exists — nothing to do.
16
+ }
17
+ }
18
+
19
+ raw.exec(
20
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_conversations_fork_parent_conversation_id ON conversations(fork_parent_conversation_id)`,
21
+ );
22
+ }
@@ -0,0 +1,12 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { getSqliteFrom } from "../db-connection.js";
3
+
4
+ export function migrateLlmRequestLogProvider(database: DrizzleDb): void {
5
+ const raw = getSqliteFrom(database);
6
+
7
+ try {
8
+ raw.exec(/*sql*/ `ALTER TABLE llm_request_logs ADD COLUMN provider TEXT`);
9
+ } catch {
10
+ // Column already exists — nothing to do.
11
+ }
12
+ }
@@ -0,0 +1,52 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { getSqliteFrom } from "../db-connection.js";
3
+
4
+ /**
5
+ * Create the memory brief state tables: time_contexts and open_loops.
6
+ *
7
+ * Both tables use CREATE TABLE IF NOT EXISTS and CREATE INDEX IF NOT EXISTS,
8
+ * making this migration inherently idempotent — safe to re-run on every startup
9
+ * without a checkpoint guard.
10
+ */
11
+ export function migrateMemoryBriefState(database: DrizzleDb): void {
12
+ const raw = getSqliteFrom(database);
13
+
14
+ // -- time_contexts: bounded temporal windows for the brief --
15
+ raw.exec(/*sql*/ `
16
+ CREATE TABLE IF NOT EXISTS time_contexts (
17
+ id TEXT PRIMARY KEY,
18
+ scope_id TEXT NOT NULL,
19
+ summary TEXT NOT NULL,
20
+ source TEXT NOT NULL,
21
+ active_from INTEGER NOT NULL,
22
+ active_until INTEGER NOT NULL,
23
+ created_at INTEGER NOT NULL,
24
+ updated_at INTEGER NOT NULL
25
+ )
26
+ `);
27
+
28
+ raw.exec(/*sql*/ `
29
+ CREATE INDEX IF NOT EXISTS idx_time_contexts_scope_active_until
30
+ ON time_contexts (scope_id, active_until)
31
+ `);
32
+
33
+ // -- open_loops: unresolved items the brief should surface --
34
+ raw.exec(/*sql*/ `
35
+ CREATE TABLE IF NOT EXISTS open_loops (
36
+ id TEXT PRIMARY KEY,
37
+ scope_id TEXT NOT NULL,
38
+ summary TEXT NOT NULL,
39
+ status TEXT NOT NULL DEFAULT 'open',
40
+ source TEXT NOT NULL,
41
+ due_at INTEGER,
42
+ surfaced_at INTEGER,
43
+ created_at INTEGER NOT NULL,
44
+ updated_at INTEGER NOT NULL
45
+ )
46
+ `);
47
+
48
+ raw.exec(/*sql*/ `
49
+ CREATE INDEX IF NOT EXISTS idx_open_loops_scope_status_due
50
+ ON open_loops (scope_id, status, due_at)
51
+ `);
52
+ }
@@ -0,0 +1,109 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { getSqliteFrom } from "../db-connection.js";
3
+
4
+ /**
5
+ * Create the memory archive tables (memory_observations, memory_chunks,
6
+ * memory_episodes) with prefetch indexes on scopeId, conversationId, and
7
+ * createdAt.
8
+ *
9
+ * All statements use IF NOT EXISTS / IF NOT EXISTS guards so the migration
10
+ * is safe to re-run on every startup.
11
+ */
12
+ export function migrateMemoryArchiveTables(database: DrizzleDb): void {
13
+ const raw = getSqliteFrom(database);
14
+
15
+ // -- memory_observations --------------------------------------------------
16
+ raw.exec(/*sql*/ `
17
+ CREATE TABLE IF NOT EXISTS memory_observations (
18
+ id TEXT PRIMARY KEY,
19
+ scope_id TEXT NOT NULL DEFAULT 'default',
20
+ conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
21
+ message_id TEXT REFERENCES messages(id) ON DELETE SET NULL,
22
+ role TEXT NOT NULL,
23
+ content TEXT NOT NULL,
24
+ modality TEXT NOT NULL DEFAULT 'text',
25
+ source TEXT,
26
+ created_at INTEGER NOT NULL
27
+ )
28
+ `);
29
+
30
+ raw.exec(/*sql*/ `
31
+ CREATE INDEX IF NOT EXISTS idx_memory_observations_scope_id
32
+ ON memory_observations (scope_id)
33
+ `);
34
+
35
+ raw.exec(/*sql*/ `
36
+ CREATE INDEX IF NOT EXISTS idx_memory_observations_conversation_id
37
+ ON memory_observations (conversation_id)
38
+ `);
39
+
40
+ raw.exec(/*sql*/ `
41
+ CREATE INDEX IF NOT EXISTS idx_memory_observations_created_at
42
+ ON memory_observations (created_at)
43
+ `);
44
+
45
+ // -- memory_chunks --------------------------------------------------------
46
+ raw.exec(/*sql*/ `
47
+ CREATE TABLE IF NOT EXISTS memory_chunks (
48
+ id TEXT PRIMARY KEY,
49
+ scope_id TEXT NOT NULL DEFAULT 'default',
50
+ observation_id TEXT NOT NULL REFERENCES memory_observations(id) ON DELETE CASCADE,
51
+ content TEXT NOT NULL,
52
+ token_estimate INTEGER NOT NULL,
53
+ content_hash TEXT NOT NULL,
54
+ created_at INTEGER NOT NULL
55
+ )
56
+ `);
57
+
58
+ raw.exec(/*sql*/ `
59
+ CREATE INDEX IF NOT EXISTS idx_memory_chunks_scope_id
60
+ ON memory_chunks (scope_id)
61
+ `);
62
+
63
+ raw.exec(/*sql*/ `
64
+ CREATE INDEX IF NOT EXISTS idx_memory_chunks_observation_id
65
+ ON memory_chunks (observation_id)
66
+ `);
67
+
68
+ raw.exec(/*sql*/ `
69
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_chunks_content_hash
70
+ ON memory_chunks (scope_id, content_hash)
71
+ `);
72
+
73
+ raw.exec(/*sql*/ `
74
+ CREATE INDEX IF NOT EXISTS idx_memory_chunks_created_at
75
+ ON memory_chunks (created_at)
76
+ `);
77
+
78
+ // -- memory_episodes ------------------------------------------------------
79
+ raw.exec(/*sql*/ `
80
+ CREATE TABLE IF NOT EXISTS memory_episodes (
81
+ id TEXT PRIMARY KEY,
82
+ scope_id TEXT NOT NULL DEFAULT 'default',
83
+ conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
84
+ title TEXT NOT NULL,
85
+ summary TEXT NOT NULL,
86
+ token_estimate INTEGER NOT NULL,
87
+ source TEXT,
88
+ start_at INTEGER NOT NULL,
89
+ end_at INTEGER NOT NULL,
90
+ created_at INTEGER NOT NULL,
91
+ updated_at INTEGER NOT NULL
92
+ )
93
+ `);
94
+
95
+ raw.exec(/*sql*/ `
96
+ CREATE INDEX IF NOT EXISTS idx_memory_episodes_scope_id
97
+ ON memory_episodes (scope_id)
98
+ `);
99
+
100
+ raw.exec(/*sql*/ `
101
+ CREATE INDEX IF NOT EXISTS idx_memory_episodes_conversation_id
102
+ ON memory_episodes (conversation_id)
103
+ `);
104
+
105
+ raw.exec(/*sql*/ `
106
+ CREATE INDEX IF NOT EXISTS idx_memory_episodes_created_at
107
+ ON memory_episodes (created_at)
108
+ `);
109
+ }
@@ -0,0 +1,19 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { getSqliteFrom } from "../db-connection.js";
3
+
4
+ export function migrateMemoryReducerCheckpoints(database: DrizzleDb): void {
5
+ const raw = getSqliteFrom(database);
6
+ const columns = [
7
+ "memory_reduced_through_message_id TEXT",
8
+ "memory_dirty_tail_since_message_id TEXT",
9
+ "memory_last_reduced_at INTEGER",
10
+ ];
11
+
12
+ for (const column of columns) {
13
+ try {
14
+ raw.exec(`ALTER TABLE conversations ADD COLUMN ${column}`);
15
+ } catch {
16
+ // Column already exists — nothing to do.
17
+ }
18
+ }
19
+ }
@@ -119,6 +119,16 @@ export { migrateRenameThreadStartersTable } from "./174-rename-thread-starters-t
119
119
  export { createLifecycleEventsTable } from "./175-create-lifecycle-events.js";
120
120
  export { migrateDropCapabilityCardState } from "./176-drop-capability-card-state.js";
121
121
  export { migrateCreateTraceEventsTable } from "./177-create-trace-events-table.js";
122
+ export { migrateOAuthProvidersManagedServiceConfigKey } from "./178-oauth-providers-managed-service-config-key.js";
123
+ export { migrateLlmRequestLogMessageId } from "./179-llm-request-log-message-id.js";
124
+ export { migrateBackfillInlineAttachmentsToDisk } from "./180-backfill-inline-attachments-to-disk.js";
125
+ export { migrateRenameThreadStartersCheckpoints } from "./181-rename-thread-starters-checkpoints.js";
126
+ export { migrateOAuthProvidersDisplayMetadata } from "./182-oauth-providers-display-metadata.js";
127
+ export { migrateConversationForkLineage } from "./183-add-conversation-fork-lineage.js";
128
+ export { migrateLlmRequestLogProvider } from "./184-llm-request-log-provider.js";
129
+ export { migrateMemoryBriefState } from "./185-memory-brief-state.js";
130
+ export { migrateMemoryArchiveTables } from "./186-memory-archive.js";
131
+ export { migrateMemoryReducerCheckpoints } from "./187-memory-reducer-checkpoints.js";
122
132
  export {
123
133
  MIGRATION_REGISTRY,
124
134
  type MigrationRegistryEntry,
@@ -228,6 +228,19 @@ export const MIGRATION_REGISTRY: MigrationRegistryEntry[] = [
228
228
  description:
229
229
  "Remove deleted capability-card rows, jobs, checkpoints, and category state",
230
230
  },
231
+ {
232
+ key: "migration_backfill_inline_attachments_v1",
233
+ version: 34,
234
+ description:
235
+ "Backfill existing inline base64 attachments to on-disk storage and clear dataBase64",
236
+ },
237
+ {
238
+ key: "migration_rename_thread_starters_checkpoints_v1",
239
+ version: 35,
240
+ dependsOn: ["migration_rename_thread_starters_table_v1"],
241
+ description:
242
+ "Rename checkpoint keys from thread_starters: to conversation_starters: prefix so renamed code paths find existing generation state",
243
+ },
231
244
  ];
232
245
 
233
246
  export interface MigrationValidationResult {
@@ -20,7 +20,7 @@ export interface QdrantClientConfig {
20
20
  }
21
21
 
22
22
  export interface QdrantPointPayload {
23
- target_type: "segment" | "item" | "summary" | "media";
23
+ target_type: "segment" | "item" | "summary" | "observation" | "chunk" | "episode" | "media";
24
24
  target_id: string;
25
25
  text: string;
26
26
  kind?: string;
@@ -230,7 +230,7 @@ export class VellumQdrantClient {
230
230
  }
231
231
 
232
232
  async upsert(
233
- targetType: "segment" | "item" | "summary" | "media",
233
+ targetType: "segment" | "item" | "summary" | "observation" | "chunk" | "episode" | "media",
234
234
  targetId: string,
235
235
  vector: number[],
236
236
  payload: Omit<QdrantPointPayload, "target_type" | "target_id">,
@@ -324,8 +324,11 @@ export class VellumQdrantClient {
324
324
  async searchWithFilter(
325
325
  vector: number[],
326
326
  limit: number,
327
- targetTypes: Array<"segment" | "item" | "summary" | "media">,
327
+ targetTypes: Array<
328
+ "segment" | "item" | "summary" | "media" | "chunk" | "episode"
329
+ >,
328
330
  excludeMessageIds?: string[],
331
+ scopeIds?: string[],
329
332
  ): Promise<QdrantSearchResult[]> {
330
333
  const mustConditions: Array<Record<string, unknown>> = [
331
334
  {
@@ -346,12 +349,24 @@ export class VellumQdrantClient {
346
349
  },
347
350
  {
348
351
  key: "target_type",
349
- match: { any: ["segment", "summary", "media"] },
352
+ match: { any: ["segment", "summary", "media", "chunk"] },
350
353
  },
351
354
  ],
352
355
  });
353
356
  }
354
357
 
358
+ // Scope filtering: accept points whose memory_scope_id matches one of the
359
+ // allowed scopes, OR points that lack the field entirely (legacy data).
360
+ // Post-query DB filtering remains as defense-in-depth for legacy points.
361
+ if (scopeIds && scopeIds.length > 0) {
362
+ mustConditions.push({
363
+ should: [
364
+ { key: "memory_scope_id", match: { any: scopeIds } },
365
+ { is_empty: { key: "memory_scope_id" } },
366
+ ],
367
+ });
368
+ }
369
+
355
370
  const mustNotConditions: Array<Record<string, unknown>> = [
356
371
  { key: "_meta", match: { value: true } },
357
372
  ];
@@ -561,6 +576,10 @@ export class VellumQdrantClient {
561
576
  field_name: "modality",
562
577
  field_schema: "keyword",
563
578
  }),
579
+ this.client.createPayloadIndex(this.collection, {
580
+ field_name: "memory_scope_id",
581
+ field_schema: "keyword",
582
+ }),
564
583
  ]);
565
584
  }
566
585
 
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Reducer store — transactional application of reducer results to brief-state
3
+ * tables (time_contexts, open_loops) and conversation reducer checkpoints.
4
+ *
5
+ * The `applyReducerResult` helper is the single entry point for persisting
6
+ * reducer output. It runs all upserts, resolves, and checkpoint advances
7
+ * inside a single SQLite transaction so the DB is never left in a
8
+ * partially-applied state.
9
+ *
10
+ * Archive writes are intentionally out of scope — they have their own
11
+ * lifecycle and can be tested independently.
12
+ */
13
+
14
+ import { and, eq, gt } from "drizzle-orm";
15
+ import { v4 as uuid } from "uuid";
16
+
17
+ import { getLogger } from "../util/logger.js";
18
+ import { getDb } from "./db.js";
19
+ import type { ReducerResult } from "./reducer-types.js";
20
+ import { conversations, messages, openLoops, timeContexts } from "./schema.js";
21
+
22
+ const log = getLogger("reducer-store");
23
+
24
+ // ── Read helpers ─────────────────────────────────────────────────────
25
+
26
+ /**
27
+ * Return all active (non-expired) time contexts for a memory scope.
28
+ * "Active" means `activeUntil` is in the future relative to `now`.
29
+ */
30
+ export function getActiveTimeContexts(
31
+ scopeId: string,
32
+ now: number = Date.now(),
33
+ ): Array<{
34
+ id: string;
35
+ summary: string;
36
+ activeFrom: number;
37
+ activeUntil: number;
38
+ }> {
39
+ const db = getDb();
40
+ return db
41
+ .select({
42
+ id: timeContexts.id,
43
+ summary: timeContexts.summary,
44
+ activeFrom: timeContexts.activeFrom,
45
+ activeUntil: timeContexts.activeUntil,
46
+ })
47
+ .from(timeContexts)
48
+ .where(
49
+ and(eq(timeContexts.scopeId, scopeId), gt(timeContexts.activeUntil, now)),
50
+ )
51
+ .all();
52
+ }
53
+
54
+ /**
55
+ * Return all open loops for a memory scope.
56
+ */
57
+ export function getActiveOpenLoops(
58
+ scopeId: string,
59
+ ): Array<{ id: string; summary: string; status: string }> {
60
+ const db = getDb();
61
+ return db
62
+ .select({
63
+ id: openLoops.id,
64
+ summary: openLoops.summary,
65
+ status: openLoops.status,
66
+ })
67
+ .from(openLoops)
68
+ .where(and(eq(openLoops.scopeId, scopeId), eq(openLoops.status, "open")))
69
+ .all();
70
+ }
71
+
72
+ // ── Brief-compiler helper ────────────────────────────────────────────
73
+
74
+ /**
75
+ * Update the `surfaced_at` timestamp on a single open loop.
76
+ *
77
+ * Called by the brief compiler after resurfacing a low-salience loop
78
+ * so it is not immediately resurfaced again on the next turn.
79
+ */
80
+ export function updateLastSurfacedAt(loopId: string, surfacedAt: number): void {
81
+ const db = getDb();
82
+ db.update(openLoops)
83
+ .set({ surfacedAt, updatedAt: surfacedAt })
84
+ .where(eq(openLoops.id, loopId))
85
+ .run();
86
+ }
87
+
88
+ // ── Transactional apply ──────────────────────────────────────────────
89
+
90
+ export interface ApplyReducerResultParams {
91
+ /** The validated reducer result to persist. */
92
+ result: ReducerResult;
93
+ /** Conversation that was reduced. */
94
+ conversationId: string;
95
+ /** Memory scope for new rows (e.g. assistant instance ID). */
96
+ scopeId: string;
97
+ /** ID of the last message that was included in this reducer run. */
98
+ reducedThroughMessageId: string;
99
+ /** Current timestamp in epoch ms (injectable for testing). */
100
+ now?: number;
101
+ }
102
+
103
+ /**
104
+ * Atomically apply a reducer result to the database.
105
+ *
106
+ * Within a single transaction this function:
107
+ * 1. Upserts time_contexts (create / update / resolve)
108
+ * 2. Upserts open_loops (create / update / resolve)
109
+ * 3. Advances the conversation's reducer checkpoint columns
110
+ * 4. Clears `memoryDirtyTailSinceMessageId` when the conversation is
111
+ * fully caught up (no messages exist after `reducedThroughMessageId`)
112
+ *
113
+ * Archive candidates in the result are intentionally ignored — they are
114
+ * handled by a separate pipeline.
115
+ *
116
+ * The function is idempotent: applying the same result twice leaves the
117
+ * database in the same state. Create operations use deterministic IDs
118
+ * derived from the reducer output position so re-application produces
119
+ * the same rows.
120
+ */
121
+ export function applyReducerResult(params: ApplyReducerResultParams): void {
122
+ const {
123
+ result,
124
+ conversationId,
125
+ scopeId,
126
+ reducedThroughMessageId,
127
+ now = Date.now(),
128
+ } = params;
129
+
130
+ const db = getDb();
131
+
132
+ db.transaction((tx) => {
133
+ // ── 1. Time contexts ───────────────────────────────────────────
134
+ for (let i = 0; i < result.timeContexts.length; i++) {
135
+ const op = result.timeContexts[i];
136
+
137
+ if (op.action === "create") {
138
+ const id = uuid();
139
+ tx.insert(timeContexts)
140
+ .values({
141
+ id,
142
+ scopeId,
143
+ summary: op.summary,
144
+ source: op.source,
145
+ activeFrom: op.activeFrom,
146
+ activeUntil: op.activeUntil,
147
+ createdAt: now,
148
+ updatedAt: now,
149
+ })
150
+ .run();
151
+ } else if (op.action === "update") {
152
+ const setFields: Record<string, unknown> = { updatedAt: now };
153
+ if (op.summary !== undefined) setFields.summary = op.summary;
154
+ if (op.activeFrom !== undefined) setFields.activeFrom = op.activeFrom;
155
+ if (op.activeUntil !== undefined)
156
+ setFields.activeUntil = op.activeUntil;
157
+
158
+ tx.update(timeContexts)
159
+ .set(setFields)
160
+ .where(eq(timeContexts.id, op.id))
161
+ .run();
162
+ } else {
163
+ // resolve — delete the row (resolved time contexts are no longer relevant)
164
+ tx.delete(timeContexts).where(eq(timeContexts.id, op.id)).run();
165
+ }
166
+ }
167
+
168
+ // ── 2. Open loops ──────────────────────────────────────────────
169
+ for (let i = 0; i < result.openLoops.length; i++) {
170
+ const op = result.openLoops[i];
171
+
172
+ if (op.action === "create") {
173
+ const id = uuid();
174
+ tx.insert(openLoops)
175
+ .values({
176
+ id,
177
+ scopeId,
178
+ summary: op.summary,
179
+ source: op.source,
180
+ status: "open",
181
+ dueAt: op.dueAt ?? null,
182
+ createdAt: now,
183
+ updatedAt: now,
184
+ })
185
+ .run();
186
+ } else if (op.action === "update") {
187
+ const setFields: Record<string, unknown> = { updatedAt: now };
188
+ if (op.summary !== undefined) setFields.summary = op.summary;
189
+ if (op.dueAt !== undefined) setFields.dueAt = op.dueAt;
190
+
191
+ tx.update(openLoops)
192
+ .set(setFields)
193
+ .where(eq(openLoops.id, op.id))
194
+ .run();
195
+ } else {
196
+ // resolve — mark status (resolved | expired)
197
+ tx.update(openLoops)
198
+ .set({ status: op.status, updatedAt: now })
199
+ .where(eq(openLoops.id, op.id))
200
+ .run();
201
+ }
202
+ }
203
+
204
+ // ── 3. Advance reducer checkpoint ──────────────────────────────
205
+ //
206
+ // Check whether the conversation is fully caught up: no messages
207
+ // exist after the one we just reduced through. If caught up, clear
208
+ // the dirty tail marker so the reducer knows there's nothing left
209
+ // to process.
210
+ const laterMessage = tx
211
+ .select({ id: messages.id })
212
+ .from(messages)
213
+ .where(
214
+ and(
215
+ eq(messages.conversationId, conversationId),
216
+ gt(
217
+ messages.createdAt,
218
+ getMessageCreatedAt(tx, reducedThroughMessageId),
219
+ ),
220
+ ),
221
+ )
222
+ .limit(1)
223
+ .get();
224
+
225
+ const isCaughtUp = !laterMessage;
226
+
227
+ const checkpointUpdate: Record<string, unknown> = {
228
+ memoryReducedThroughMessageId: reducedThroughMessageId,
229
+ memoryLastReducedAt: now,
230
+ };
231
+
232
+ if (isCaughtUp) {
233
+ checkpointUpdate.memoryDirtyTailSinceMessageId = null;
234
+ }
235
+
236
+ tx.update(conversations)
237
+ .set(checkpointUpdate)
238
+ .where(eq(conversations.id, conversationId))
239
+ .run();
240
+
241
+ log.debug(
242
+ {
243
+ conversationId,
244
+ reducedThroughMessageId,
245
+ timeContextOps: result.timeContexts.length,
246
+ openLoopOps: result.openLoops.length,
247
+ isCaughtUp,
248
+ },
249
+ "Applied reducer result",
250
+ );
251
+ });
252
+ }
253
+
254
+ // ── Internal helpers ─────────────────────────────────────────────────
255
+
256
+ /**
257
+ * Get the createdAt timestamp for a message by ID.
258
+ * Returns 0 if the message doesn't exist (which means the gt() comparison
259
+ * will match all messages — safe fallback that prevents clearing dirty tail).
260
+ */
261
+ function getMessageCreatedAt(
262
+ tx: Parameters<Parameters<ReturnType<typeof getDb>["transaction"]>[0]>[0],
263
+ messageId: string,
264
+ ): number {
265
+ const row = tx
266
+ .select({ createdAt: messages.createdAt })
267
+ .from(messages)
268
+ .where(eq(messages.id, messageId))
269
+ .get();
270
+ return row?.createdAt ?? 0;
271
+ }