@vellumai/assistant 0.8.3 → 0.8.4

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 (342) hide show
  1. package/docker-entrypoint.sh +0 -1
  2. package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
  3. package/openapi.yaml +610 -16
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-loop-exit-reason.test.ts +4 -5
  6. package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
  7. package/src/__tests__/agent-loop.test.ts +88 -3
  8. package/src/__tests__/anthropic-provider.test.ts +272 -0
  9. package/src/__tests__/approval-cascade.test.ts +1 -1
  10. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  11. package/src/__tests__/channel-delivery-store.test.ts +193 -0
  12. package/src/__tests__/channel-reply-delivery.test.ts +284 -5
  13. package/src/__tests__/channel-retry-sweep.test.ts +274 -1
  14. package/src/__tests__/compaction-events.test.ts +1 -1
  15. package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
  16. package/src/__tests__/config-watcher.test.ts +1 -1
  17. package/src/__tests__/context-token-estimator.test.ts +91 -1
  18. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  19. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +25 -7
  22. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
  23. package/src/__tests__/conversation-clean-command.test.ts +137 -0
  24. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  25. package/src/__tests__/conversation-fork-crud.test.ts +161 -0
  26. package/src/__tests__/conversation-lifecycle.test.ts +1 -1
  27. package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
  28. package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
  29. package/src/__tests__/conversation-pairing.test.ts +2 -2
  30. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  31. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  32. package/src/__tests__/conversation-queue.test.ts +1 -1
  33. package/src/__tests__/conversation-runtime-assembly.test.ts +264 -81
  34. package/src/__tests__/conversation-seed-composer.test.ts +66 -4
  35. package/src/__tests__/conversation-slash-commands.test.ts +36 -8
  36. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  37. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  38. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  39. package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
  40. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  41. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  42. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  43. package/src/__tests__/credential-security-invariants.test.ts +6 -0
  44. package/src/__tests__/cu-unified-flow.test.ts +10 -1
  45. package/src/__tests__/dm-backfill.test.ts +64 -0
  46. package/src/__tests__/dm-persistence.test.ts +33 -0
  47. package/src/__tests__/document-find-replace.test.ts +501 -0
  48. package/src/__tests__/first-greeting.test.ts +23 -2
  49. package/src/__tests__/headless-browser-navigate.test.ts +172 -0
  50. package/src/__tests__/host-bash-proxy.test.ts +6 -0
  51. package/src/__tests__/host-browser-proxy.test.ts +10 -0
  52. package/src/__tests__/host-cu-proxy.test.ts +8 -1
  53. package/src/__tests__/host-file-proxy.test.ts +8 -1
  54. package/src/__tests__/host-transfer-proxy.test.ts +8 -1
  55. package/src/__tests__/identity-routes.test.ts +57 -0
  56. package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
  57. package/src/__tests__/injector-chain.test.ts +2 -0
  58. package/src/__tests__/injector-document-comments.test.ts +378 -0
  59. package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
  60. package/src/__tests__/list-messages-attachments.test.ts +21 -17
  61. package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
  62. package/src/__tests__/list-messages-page-latest.test.ts +130 -14
  63. package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
  64. package/src/__tests__/llm-context-normalization.test.ts +0 -2
  65. package/src/__tests__/llm-resolver.test.ts +85 -1
  66. package/src/__tests__/log-export-routes.test.ts +99 -2
  67. package/src/__tests__/message-queue-steer.test.ts +114 -0
  68. package/src/__tests__/openai-provider.test.ts +105 -0
  69. package/src/__tests__/openai-responses-provider.test.ts +4 -4
  70. package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
  71. package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
  72. package/src/__tests__/platform.test.ts +0 -3
  73. package/src/__tests__/plugin-source-watcher.test.ts +302 -0
  74. package/src/__tests__/process-message-background-slack.test.ts +1 -51
  75. package/src/__tests__/process-message-display-content.test.ts +21 -16
  76. package/src/__tests__/server-history-render.test.ts +83 -4
  77. package/src/__tests__/steer-tool-repair.test.ts +249 -0
  78. package/src/__tests__/system-prompt.test.ts +51 -28
  79. package/src/__tests__/terminal-tools.test.ts +11 -1
  80. package/src/__tests__/thinking-block-replay.test.ts +113 -0
  81. package/src/__tests__/thread-backfill.test.ts +370 -22
  82. package/src/__tests__/tool-executor.test.ts +90 -1
  83. package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
  84. package/src/__tests__/twilio-routes.test.ts +1 -1
  85. package/src/__tests__/web-fetch.test.ts +2 -2
  86. package/src/__tests__/workspace-git-service.test.ts +88 -5
  87. package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
  88. package/src/agent/attachments.ts +1 -0
  89. package/src/agent/loop.ts +57 -20
  90. package/src/background-wake/next-wake.test.ts +289 -0
  91. package/src/background-wake/next-wake.ts +172 -0
  92. package/src/browser/operations.ts +15 -0
  93. package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
  94. package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
  95. package/src/cli/commands/conversations.ts +128 -1
  96. package/src/cli/commands/inference-providers.ts +147 -1
  97. package/src/cli/commands/memory-v2.ts +308 -0
  98. package/src/cli/commands/notifications.ts +24 -2
  99. package/src/cli/utils/conversation-id.ts +17 -5
  100. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  101. package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
  102. package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
  103. package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
  104. package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
  105. package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
  106. package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
  107. package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
  108. package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
  109. package/src/config/bundled-skills/schedule/SKILL.md +8 -0
  110. package/src/config/bundled-tool-registry.ts +22 -12
  111. package/src/config/call-site-defaults.ts +19 -0
  112. package/src/config/feature-flag-registry.json +99 -3
  113. package/src/config/llm-resolver.ts +16 -2
  114. package/src/config/schemas/__tests__/memory-v2.test.ts +4 -0
  115. package/src/config/schemas/call-site-catalog.ts +21 -0
  116. package/src/config/schemas/llm.ts +3 -0
  117. package/src/config/schemas/memory-v2.ts +48 -1
  118. package/src/context/compactor.ts +8 -1
  119. package/src/context/token-estimator.ts +47 -4
  120. package/src/context/window-manager.ts +25 -0
  121. package/src/credential-health/credential-health-service.ts +34 -19
  122. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
  123. package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
  124. package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
  125. package/src/daemon/conversation-agent-loop-handlers.ts +153 -23
  126. package/src/daemon/conversation-agent-loop.ts +223 -54
  127. package/src/daemon/conversation-lifecycle.ts +142 -116
  128. package/src/daemon/conversation-messaging.ts +3 -0
  129. package/src/daemon/conversation-process.ts +273 -0
  130. package/src/daemon/conversation-queue-manager.ts +14 -0
  131. package/src/daemon/conversation-runtime-assembly.ts +135 -75
  132. package/src/daemon/conversation-slash.ts +37 -5
  133. package/src/daemon/conversation-surfaces.ts +45 -2
  134. package/src/daemon/conversation-tool-setup.ts +7 -0
  135. package/src/daemon/conversation.ts +42 -5
  136. package/src/daemon/first-greeting.ts +10 -0
  137. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
  138. package/src/daemon/handlers/config-a2a.ts +160 -0
  139. package/src/daemon/handlers/config-model.test.ts +1 -0
  140. package/src/daemon/handlers/conversations.ts +79 -0
  141. package/src/daemon/handlers/shared.ts +92 -29
  142. package/src/daemon/host-bash-proxy.ts +1 -1
  143. package/src/daemon/host-cu-proxy.ts +1 -1
  144. package/src/daemon/host-file-proxy.ts +1 -1
  145. package/src/daemon/host-transfer-proxy.ts +1 -1
  146. package/src/daemon/lifecycle.ts +18 -4
  147. package/src/daemon/message-protocol.ts +4 -0
  148. package/src/daemon/message-types/conversations.ts +8 -0
  149. package/src/daemon/message-types/document-comments.ts +50 -0
  150. package/src/daemon/message-types/messages.ts +68 -1
  151. package/src/daemon/message-types/surfaces.ts +3 -1
  152. package/src/daemon/message-types/web-activity.ts +57 -0
  153. package/src/daemon/plugin-source-watcher.ts +135 -3
  154. package/src/daemon/process-message.ts +69 -12
  155. package/src/daemon/query-complexity-router.ts +75 -0
  156. package/src/daemon/trust-context.ts +6 -0
  157. package/src/documents/document-comments-store.test.ts +338 -0
  158. package/src/documents/document-comments-store.ts +237 -0
  159. package/src/documents/document-store.ts +202 -0
  160. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  161. package/src/heartbeat/heartbeat-service.ts +1 -0
  162. package/src/home/__tests__/suggested-prompts.test.ts +33 -2
  163. package/src/home/feed-types.ts +6 -1
  164. package/src/home/home-content-refresh.ts +52 -0
  165. package/src/home/home-greeting-cache.ts +69 -0
  166. package/src/home/home-greeting.ts +94 -0
  167. package/src/home/suggested-prompts.ts +177 -9
  168. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
  169. package/src/memory/__tests__/memory-retrospective-job.test.ts +320 -6
  170. package/src/memory/conversation-crud.ts +133 -43
  171. package/src/memory/db-init.ts +16 -0
  172. package/src/memory/delivery-crud.ts +41 -0
  173. package/src/memory/delivery-status.ts +141 -15
  174. package/src/memory/external-conversation-store.ts +32 -1
  175. package/src/memory/jobs-worker.ts +21 -1
  176. package/src/memory/memory-retrospective-constants.ts +28 -0
  177. package/src/memory/memory-retrospective-enqueue.ts +3 -2
  178. package/src/memory/memory-retrospective-job.ts +408 -18
  179. package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
  180. package/src/memory/memory-v2-activation-log-store.ts +26 -8
  181. package/src/memory/migrations/100-core-tables.ts +1 -0
  182. package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
  183. package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
  184. package/src/memory/migrations/253-document-comments.ts +47 -0
  185. package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
  186. package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
  187. package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
  188. package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
  189. package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
  190. package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
  191. package/src/memory/migrations/index.ts +17 -0
  192. package/src/memory/migrations/registry.ts +25 -0
  193. package/src/memory/onboarding-events-store.ts +7 -0
  194. package/src/memory/schema/calls.ts +1 -0
  195. package/src/memory/schema/conversations.ts +3 -0
  196. package/src/memory/schema/infrastructure.ts +1 -0
  197. package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
  198. package/src/memory/v2/__tests__/injection.test.ts +31 -14
  199. package/src/memory/v2/__tests__/page-index.test.ts +365 -1
  200. package/src/memory/v2/__tests__/router.test.ts +489 -1
  201. package/src/memory/v2/consolidation-job.ts +14 -0
  202. package/src/memory/v2/injection-events.ts +101 -0
  203. package/src/memory/v2/injection.ts +21 -10
  204. package/src/memory/v2/page-index.ts +209 -7
  205. package/src/memory/v2/page-store.ts +18 -0
  206. package/src/memory/v2/router.ts +209 -55
  207. package/src/messaging/providers/index.ts +7 -1
  208. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
  209. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
  210. package/src/messaging/providers/slack/adapter.ts +178 -25
  211. package/src/messaging/providers/slack/api.test.ts +54 -0
  212. package/src/messaging/providers/slack/api.ts +119 -3
  213. package/src/messaging/providers/slack/client.ts +12 -0
  214. package/src/messaging/providers/slack/deep-link.ts +20 -1
  215. package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
  216. package/src/messaging/providers/slack/message-metadata.ts +156 -0
  217. package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
  218. package/src/messaging/providers/slack/render-transcript.ts +176 -49
  219. package/src/messaging/providers/slack/send.test.ts +77 -0
  220. package/src/messaging/providers/slack/send.ts +8 -2
  221. package/src/messaging/providers/slack/types.ts +14 -0
  222. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +4 -1
  223. package/src/notifications/__tests__/home-feed-side-effect.test.ts +116 -54
  224. package/src/notifications/conversation-seed-composer.ts +14 -2
  225. package/src/notifications/deferred-emit.ts +135 -0
  226. package/src/notifications/emit-signal.ts +9 -1
  227. package/src/notifications/home-feed-side-effect.ts +60 -30
  228. package/src/oauth/connect-orchestrator.ts +3 -0
  229. package/src/oauth/credential-token-resolver.ts +2 -0
  230. package/src/oauth/manual-token-connection.ts +19 -0
  231. package/src/oauth/oauth-store.ts +12 -0
  232. package/src/oauth/seed-providers.ts +22 -0
  233. package/src/permissions/prompter.ts +5 -2
  234. package/src/permissions/secret-prompter.ts +4 -1
  235. package/src/plugins/defaults/injectors.ts +82 -9
  236. package/src/prompts/__tests__/system-prompt.test.ts +46 -2
  237. package/src/prompts/normalize-onboarding.ts +40 -0
  238. package/src/prompts/sections.ts +32 -14
  239. package/src/prompts/system-prompt.ts +105 -68
  240. package/src/prompts/template-detection.ts +37 -0
  241. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
  242. package/src/prompts/templates/BOOTSTRAP.md +8 -0
  243. package/src/prompts/templates/VOICE.md +3 -0
  244. package/src/prompts/templates/system-sections.ts +53 -3
  245. package/src/providers/anthropic/client.ts +132 -5
  246. package/src/providers/fireworks/client.ts +20 -2
  247. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
  248. package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
  249. package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
  250. package/src/providers/inference/adapter-factory.ts +15 -1
  251. package/src/providers/inference/auth.ts +3 -3
  252. package/src/providers/inference/codex-token-refresh.ts +128 -0
  253. package/src/providers/inference/resolve-auth.ts +49 -6
  254. package/src/providers/model-catalog.ts +48 -1
  255. package/src/providers/openai/chat-completions-provider.ts +57 -20
  256. package/src/providers/openai/responses-provider.ts +9 -3
  257. package/src/providers/openrouter/client.ts +5 -1
  258. package/src/providers/types.ts +25 -0
  259. package/src/runtime/__tests__/agent-wake.test.ts +214 -0
  260. package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
  261. package/src/runtime/agent-wake.ts +151 -56
  262. package/src/runtime/auth/route-policy.ts +7 -3
  263. package/src/runtime/background-job-runner.ts +26 -0
  264. package/src/runtime/channel-reply-delivery.ts +182 -47
  265. package/src/runtime/channel-retry-sweep.ts +141 -16
  266. package/src/runtime/http-types.ts +7 -4
  267. package/src/runtime/pending-interactions.ts +51 -8
  268. package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
  269. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +55 -1
  270. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
  271. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
  272. package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
  273. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
  274. package/src/runtime/routes/approval-routes.ts +4 -1
  275. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
  276. package/src/runtime/routes/content-source-routes.ts +78 -0
  277. package/src/runtime/routes/conversation-cli-routes.ts +146 -1
  278. package/src/runtime/routes/conversation-query-routes.ts +60 -1
  279. package/src/runtime/routes/conversation-routes.ts +281 -76
  280. package/src/runtime/routes/document-comments-routes.ts +287 -0
  281. package/src/runtime/routes/documents-routes.ts +33 -0
  282. package/src/runtime/routes/home-feed-routes.ts +6 -3
  283. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  284. package/src/runtime/routes/host-browser-routes.ts +8 -1
  285. package/src/runtime/routes/identity-routes.ts +21 -0
  286. package/src/runtime/routes/inbound-message-handler.ts +288 -58
  287. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
  288. package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
  289. package/src/runtime/routes/index.ts +12 -4
  290. package/src/runtime/routes/inference-provider-connection-routes.ts +63 -7
  291. package/src/runtime/routes/integrations/a2a.ts +60 -1
  292. package/src/runtime/routes/log-export-routes.ts +39 -0
  293. package/src/runtime/routes/memory-v2-routes.ts +217 -0
  294. package/src/runtime/routes/notification-routes.ts +19 -2
  295. package/src/runtime/routes/question-routes.ts +4 -1
  296. package/src/runtime/routes/sanity-routes.ts +159 -0
  297. package/src/runtime/routes/slack-channel-routes.ts +187 -0
  298. package/src/runtime/services/conversation-serializer.ts +30 -4
  299. package/src/schedule/integration-status.ts +3 -1
  300. package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
  301. package/src/security/oauth2-device-code.ts +307 -0
  302. package/src/security/oauth2.ts +26 -9
  303. package/src/security/secure-keys.ts +5 -0
  304. package/src/skills/catalog-install.ts +6 -2
  305. package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
  306. package/src/tools/browser/browser-execution.ts +93 -0
  307. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
  308. package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
  309. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
  310. package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
  311. package/src/tools/browser/cdp-client/factory.ts +87 -3
  312. package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
  313. package/src/tools/browser/cdp-client/types.ts +36 -0
  314. package/src/tools/browser/pinned-tabs.ts +90 -0
  315. package/src/tools/document/document-comment-tool.test.ts +379 -0
  316. package/src/tools/document/document-comment-tool.ts +156 -0
  317. package/src/tools/document/document-tool.ts +128 -2
  318. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
  319. package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
  320. package/src/tools/network/domain-normalize.ts +17 -0
  321. package/src/tools/network/web-fetch.ts +213 -64
  322. package/src/tools/network/web-search.ts +191 -66
  323. package/src/tools/terminal/safe-env.ts +3 -2
  324. package/src/tools/tool-approval-handler.ts +19 -12
  325. package/src/tools/types.ts +4 -0
  326. package/src/tools/ui-surface/definitions.ts +3 -1
  327. package/src/types/onboarding-context.ts +4 -0
  328. package/src/util/__tests__/favicon.test.ts +84 -0
  329. package/src/util/favicon.ts +40 -0
  330. package/src/util/platform.ts +0 -5
  331. package/src/workspace/git-service.ts +75 -4
  332. package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
  333. package/src/workspace/migrations/registry.ts +2 -0
  334. package/src/config/bundled-skills/document/SKILL.md +0 -54
  335. package/src/config/bundled-skills/document/TOOLS.json +0 -106
  336. package/src/daemon/seed-files.ts +0 -18
  337. package/src/runtime/routes/interface-routes.ts +0 -43
  338. /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
  339. /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
  340. /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
  341. /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
  342. /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
@@ -33,11 +33,52 @@ let priorRetroMessages: Array<{ role: string; content: string }> = [];
33
33
 
34
34
  let mockWakeResult: { invoked: boolean; reason?: string } = { invoked: true };
35
35
  let mockWakeThrows: Error | null = null;
36
- let wakeCalls: Array<{ conversationId: string; hint: string }> = [];
36
+ let wakeCalls: Array<{
37
+ conversationId: string;
38
+ hint: string;
39
+ opts: Record<string, unknown>;
40
+ }> = [];
37
41
  let bootstrappedConversationId = "bg-conv-new";
38
42
  let bootstrapCalls: Array<{ forkParentConversationId?: string }> = [];
39
43
  let deletedConversationIds: string[] = [];
40
44
 
45
+ // Fork-path mocks. Flag off by default so legacy-path tests stay untouched.
46
+ let forkFlagEnabled = false;
47
+ let forkedConversationId = "fork-conv-1";
48
+ let forkCalls: Array<{
49
+ conversationId: string;
50
+ throughMessageId?: string;
51
+ source: string;
52
+ title: string;
53
+ conversationType?: string;
54
+ groupId?: string;
55
+ }> = [];
56
+ let addMessageCalls: Array<{
57
+ conversationId: string;
58
+ role: string;
59
+ content: string;
60
+ metadata: unknown;
61
+ }> = [];
62
+
63
+ // Per-conversation overrides for getConversation. Lets fork-path tests stage
64
+ // a fork-kind prior retrospective row alongside the default legacy stub.
65
+ type ConversationStub = {
66
+ source: string;
67
+ forkParentMessageId: string | null;
68
+ title?: string;
69
+ };
70
+ let conversationOverrides: Record<string, ConversationStub> = {};
71
+
72
+ // Per-conversation overrides for getMessages so fork-path tests can return
73
+ // fork-shaped message rows (with metadata stamps + createdAt boundaries).
74
+ type StubMessage = {
75
+ role: string;
76
+ content: string;
77
+ createdAt: number;
78
+ metadata: string | null;
79
+ };
80
+ let messagesByConversationId: Record<string, StubMessage[]> = {};
81
+
41
82
  mock.module("../memory-retrospective-state.js", () => ({
42
83
  getRetrospectiveState: (_id: string) => mockState,
43
84
  upsertRetrospectiveState: (args: {
@@ -55,16 +96,61 @@ mock.module("../memory-retrospective-state.js", () => ({
55
96
  mock.module("../conversation-crud.js", () => ({
56
97
  getMessagesAfter: (_id: string, _afterId: string | null) => newMessages,
57
98
  getMessages: (id: string) => {
99
+ if (messagesByConversationId[id]) return messagesByConversationId[id];
58
100
  if (id === priorRetroId) return priorRetroMessages;
59
101
  return [];
60
102
  },
61
103
  findMostRecentRetrospectiveFor: (_id: string) =>
62
104
  priorRetroId ? { id: priorRetroId } : null,
105
+ // The fork path calls `getConversation(sourceConversationId)` to read the
106
+ // source's title for the fork title. `collectPriorRetrospectiveRemembers`
107
+ // also calls it with the prior retro id to discriminate legacy vs fork
108
+ // sources — for that id return a legacy-shaped row by default so existing
109
+ // tests exercise the unchanged extract-everything code path.
110
+ // `conversationOverrides` lets per-test setup stage fork-kind priors.
111
+ getConversation: (id: string) => {
112
+ if (conversationOverrides[id]) return conversationOverrides[id];
113
+ if (id === priorRetroId) {
114
+ return {
115
+ source: "memory-retrospective",
116
+ forkParentMessageId: null,
117
+ };
118
+ }
119
+ return {
120
+ source: "user",
121
+ forkParentMessageId: null,
122
+ title: "Source conversation",
123
+ };
124
+ },
125
+ forkConversation: (params: {
126
+ conversationId: string;
127
+ throughMessageId?: string;
128
+ source: string;
129
+ title: string;
130
+ conversationType?: string;
131
+ groupId?: string;
132
+ }) => {
133
+ forkCalls.push(params);
134
+ return { id: forkedConversationId };
135
+ },
136
+ addMessage: async (
137
+ conversationId: string,
138
+ role: string,
139
+ content: string,
140
+ metadata: unknown,
141
+ ) => {
142
+ addMessageCalls.push({ conversationId, role, content, metadata });
143
+ },
63
144
  deleteConversation: (id: string) => {
64
145
  deletedConversationIds.push(id);
65
146
  },
66
147
  }));
67
148
 
149
+ mock.module("../../config/assistant-feature-flags.js", () => ({
150
+ isAssistantFeatureFlagEnabled: (flag: string) =>
151
+ flag === "memory-retrospective-fork" && forkFlagEnabled,
152
+ }));
153
+
68
154
  let transcriptFormatterCalls: Array<{
69
155
  messageIds: string[];
70
156
  timeZone?: string;
@@ -117,11 +203,14 @@ mock.module("../../daemon/trust-context.js", () => ({
117
203
  }));
118
204
 
119
205
  mock.module("../../runtime/agent-wake.js", () => ({
120
- wakeAgentForOpportunity: async (opts: {
121
- conversationId: string;
122
- hint: string;
123
- }) => {
124
- wakeCalls.push({ conversationId: opts.conversationId, hint: opts.hint });
206
+ wakeAgentForOpportunity: async (
207
+ opts: { conversationId: string; hint: string } & Record<string, unknown>,
208
+ ) => {
209
+ wakeCalls.push({
210
+ conversationId: opts.conversationId,
211
+ hint: opts.hint,
212
+ opts,
213
+ });
125
214
  if (mockWakeThrows) throw mockWakeThrows;
126
215
  return mockWakeResult;
127
216
  },
@@ -200,6 +289,12 @@ describe("memoryRetrospectiveJob", () => {
200
289
  transcriptFormatterCalls = [];
201
290
  mockAssistantName = "Bob";
202
291
  mockUserName = "Alice";
292
+ forkFlagEnabled = false;
293
+ forkedConversationId = "fork-conv-1";
294
+ forkCalls = [];
295
+ addMessageCalls = [];
296
+ conversationOverrides = {};
297
+ messagesByConversationId = {};
203
298
  });
204
299
 
205
300
  test("first-run happy path: no state row, no prior retrospective, both pointer fields set on success", async () => {
@@ -408,4 +503,223 @@ describe("memoryRetrospectiveJob", () => {
408
503
  const hint = wakeCalls[0]!.hint;
409
504
  expect(hint).toContain("<\u200B/already_remembered>");
410
505
  });
506
+
507
+ test("fork path: persisted instruction is stamped with hidden: true so the UI list serializer drops it", async () => {
508
+ forkFlagEnabled = true;
509
+ await memoryRetrospectiveJob(makeJob(), stubConfig);
510
+
511
+ expect(addMessageCalls).toHaveLength(1);
512
+ expect(addMessageCalls[0]!.conversationId).toBe("fork-conv-1");
513
+ expect(addMessageCalls[0]!.role).toBe("user");
514
+ expect(addMessageCalls[0]!.metadata).toEqual({
515
+ kind: "memory_retrospective_instruction",
516
+ hidden: true,
517
+ });
518
+ });
519
+
520
+ test("fork path: forked retrospective is bucketed as background under the retrospective group", async () => {
521
+ forkFlagEnabled = true;
522
+ const outcome = await memoryRetrospectiveJob(makeJob(), stubConfig);
523
+
524
+ expect(outcome.kind).toBe("invoked");
525
+ expect(forkCalls).toHaveLength(1);
526
+ expect(forkCalls[0]!.conversationType).toBe("background");
527
+ expect(forkCalls[0]!.groupId).toBe("system:background");
528
+ });
529
+
530
+ test("fork path: wake opts include suppressWakeSurface so clients don't render an empty wake card on top of the '(Retrospective)' fork", async () => {
531
+ forkFlagEnabled = true;
532
+ await memoryRetrospectiveJob(makeJob(), stubConfig);
533
+
534
+ expect(forkCalls).toHaveLength(1);
535
+ expect(wakeCalls).toHaveLength(1);
536
+ expect(wakeCalls[0]!.conversationId).toBe("fork-conv-1");
537
+ const opts = wakeCalls[0]!.opts;
538
+ expect(opts.suppressWakeSurface).toBe(true);
539
+ // Sanity: the other fork-specific opts the handler relies on are still set.
540
+ expect(opts.skipHintInjection).toBe(true);
541
+ expect(opts.suppressAutoCompaction).toBe(true);
542
+ expect(opts.hintRole).toBe("user");
543
+ });
544
+
545
+ test("fork path: fork is pinned to the computed cutoffMessageId so late-arriving messages don't sneak into this run", async () => {
546
+ // Without `throughMessageId`, the fork snapshots the latest source
547
+ // message at fork time. If a new user/assistant turn lands between the
548
+ // slice read and the fork, this run would process the late turn while
549
+ // state advances only to `cutoffMessageId`, causing the next
550
+ // retrospective to reprocess it.
551
+ forkFlagEnabled = true;
552
+ await memoryRetrospectiveJob(makeJob(), stubConfig);
553
+
554
+ expect(forkCalls).toHaveLength(1);
555
+ expect(forkCalls[0]!.throughMessageId).toBe("m3");
556
+ });
557
+
558
+ test("fork path: prior fork-kind retrospective with nested-fork ancestry still surfaces its post-fork remembers in <already_remembered>", async () => {
559
+ // The source conversation was itself a fork. Its assistant messages
560
+ // therefore carry `forkSourceMessageId` values pointing at the
561
+ // ANCESTOR's message ids — not at the new fork's `forkParentMessageId`.
562
+ // The boundary detector must locate the boundary by scanning for the
563
+ // last metadata stamp regardless of value, not by equality against
564
+ // `forkParentMessageId` (which would miss every copied row and lose
565
+ // dedup context).
566
+ forkFlagEnabled = true;
567
+ priorRetroId = "prior-fork-retro-1";
568
+
569
+ // The fork's `forkParentMessageId` is the source conv's tip ("m-src-2"),
570
+ // but the cloned messages preserve ancestor stamps ("m-ancestor-*").
571
+ conversationOverrides[priorRetroId] = {
572
+ source: "memory-retrospective-fork",
573
+ forkParentMessageId: "m-src-2",
574
+ };
575
+ messagesByConversationId[priorRetroId] = [
576
+ // Copied prefix — note metadata stamps point at the ANCESTOR, not
577
+ // `forkParentMessageId`. The old detector would return null here.
578
+ {
579
+ role: "user",
580
+ content: JSON.stringify([{ type: "text", text: "hi" }]),
581
+ createdAt: 1000,
582
+ metadata: JSON.stringify({ forkSourceMessageId: "m-ancestor-1" }),
583
+ },
584
+ {
585
+ role: "assistant",
586
+ // An inline `remember` from the source conv (should NOT leak into
587
+ // dedup baseline — it's part of the copied prefix, not the post-fork
588
+ // retrospective tail).
589
+ content: JSON.stringify([
590
+ {
591
+ type: "tool_use",
592
+ name: "remember",
593
+ input: { content: "source-inline save — must be excluded" },
594
+ },
595
+ ]),
596
+ createdAt: 2000,
597
+ metadata: JSON.stringify({ forkSourceMessageId: "m-ancestor-2" }),
598
+ },
599
+ // Post-fork instruction (no forkSourceMessageId) + the wake's tail
600
+ // assistant turn with the retrospective's own remember call.
601
+ {
602
+ role: "user",
603
+ content: JSON.stringify([
604
+ { type: "text", text: "Retrospective instruction" },
605
+ ]),
606
+ createdAt: 3000,
607
+ metadata: JSON.stringify({
608
+ kind: "memory_retrospective_instruction",
609
+ hidden: true,
610
+ }),
611
+ },
612
+ {
613
+ role: "assistant",
614
+ content: JSON.stringify([
615
+ {
616
+ type: "tool_use",
617
+ name: "remember",
618
+ input: { content: "retrospective save — must be included" },
619
+ },
620
+ ]),
621
+ createdAt: 4000,
622
+ metadata: null,
623
+ },
624
+ ];
625
+
626
+ await memoryRetrospectiveJob(makeJob(), stubConfig);
627
+
628
+ // The fork path persists the prompt as a user-role message, not via the
629
+ // wake's hint. Pull the rendered text block out of the persisted JSON.
630
+ expect(addMessageCalls).toHaveLength(1);
631
+ const blocks = JSON.parse(addMessageCalls[0]!.content) as Array<{
632
+ type: string;
633
+ text: string;
634
+ }>;
635
+ const instructionText = blocks[0]!.text;
636
+ expect(instructionText).toContain(
637
+ "- retrospective save — must be included",
638
+ );
639
+ expect(instructionText).not.toContain("source-inline save");
640
+ // Sanity: the "first retrospective" sentinel should not appear — we
641
+ // located dedup context.
642
+ expect(instructionText).not.toContain(
643
+ "(none — this is your first retrospective over this conversation)",
644
+ );
645
+ });
646
+
647
+ test("fork path: prior fork-kind retrospective with no copied messages degrades to empty dedup", async () => {
648
+ // Corrupted/empty fork-kind prior: no message carries
649
+ // `forkSourceMessageId`. The detector should return null and the
650
+ // handler should treat dedup as empty rather than dumping everything
651
+ // (which would leak any pre-fork content into the baseline).
652
+ forkFlagEnabled = true;
653
+ priorRetroId = "prior-fork-retro-2";
654
+
655
+ conversationOverrides[priorRetroId] = {
656
+ source: "memory-retrospective-fork",
657
+ forkParentMessageId: "m-src-2",
658
+ };
659
+ messagesByConversationId[priorRetroId] = [
660
+ {
661
+ role: "assistant",
662
+ content: JSON.stringify([
663
+ {
664
+ type: "tool_use",
665
+ name: "remember",
666
+ input: { content: "would-be-leaked save" },
667
+ },
668
+ ]),
669
+ createdAt: 1000,
670
+ metadata: null,
671
+ },
672
+ ];
673
+
674
+ await memoryRetrospectiveJob(makeJob(), stubConfig);
675
+
676
+ expect(addMessageCalls).toHaveLength(1);
677
+ const blocks = JSON.parse(addMessageCalls[0]!.content) as Array<{
678
+ type: string;
679
+ text: string;
680
+ }>;
681
+ const instructionText = blocks[0]!.text;
682
+ expect(instructionText).not.toContain("- would-be-leaked save");
683
+ expect(instructionText).toContain(
684
+ "(none — this is your first retrospective over this conversation)",
685
+ );
686
+ });
687
+
688
+ test("fork path: prompt anchors review window at first turn_context current_time and disambiguates first-pass vs incremental", async () => {
689
+ forkFlagEnabled = true;
690
+ // Stage a user turn whose content carries a turn_context current_time
691
+ // block — the handler should anchor the prompt at that timestamp.
692
+ newMessages = [
693
+ {
694
+ id: "m1",
695
+ createdAt: Date.parse("2026-05-11T10:00:00Z"),
696
+ role: "user",
697
+ content: JSON.stringify([
698
+ {
699
+ type: "text",
700
+ text: "<turn_context>\ncurrent_time: 2026-05-11T10:00:00-07:00\n</turn_context>\n\nhi",
701
+ },
702
+ ]),
703
+ },
704
+ // Wake's response — no turn_context, not used as anchor.
705
+ {
706
+ id: "m2",
707
+ createdAt: Date.parse("2026-05-11T10:05:00Z"),
708
+ role: "assistant",
709
+ content: JSON.stringify([{ type: "text", text: "hello" }]),
710
+ },
711
+ ] as Array<{ id: string; createdAt: number } & Record<string, unknown>>;
712
+
713
+ // Incremental run — `lastProcessedMessageId` already set.
714
+ mockState = {
715
+ conversationId: "src-conv-1",
716
+ lastProcessedMessageId: "prev-msg",
717
+ lastRunAt: Date.now() - 60 * 60 * 1000,
718
+ };
719
+ await memoryRetrospectiveJob(makeJob(), stubConfig);
720
+
721
+ expect(addMessageCalls).toHaveLength(1);
722
+ expect(forkCalls).toHaveLength(1);
723
+ expect(forkCalls[0]!.throughMessageId).toBe("m2");
724
+ });
411
725
  });
@@ -50,7 +50,7 @@ import { ensureGroupMigration } from "./conversation-group-migration.js";
50
50
  import { getDb, getSqliteFrom } from "./db-connection.js";
51
51
  import { forkGraphMemoryState } from "./graph/graph-memory-state-store.js";
52
52
  import { indexMessageNow } from "./indexer.js";
53
- import { MEMORY_RETROSPECTIVE_SOURCE } from "./memory-retrospective-constants.js";
53
+ import { MEMORY_RETROSPECTIVE_SOURCES } from "./memory-retrospective-constants.js";
54
54
  import { forkRetrospectiveState } from "./memory-retrospective-state.js";
55
55
  import { rawExec, rawGet, rawRun } from "./raw-query.js";
56
56
  import {
@@ -190,6 +190,7 @@ export interface ConversationRow {
190
190
  contextSummary: string | null;
191
191
  contextCompactedMessageCount: number;
192
192
  contextCompactedAt: number | null;
193
+ cleanedAt: number | null;
193
194
  slackContextCompactionWatermarkTs: string | null;
194
195
  slackContextCompactionWatermarkAt: number | null;
195
196
  conversationType: string;
@@ -206,6 +207,7 @@ export interface ConversationRow {
206
207
  inferenceProfile: string | null;
207
208
  inferenceProfileSessionId: string | null;
208
209
  inferenceProfileExpiresAt: number | null;
210
+ lastNotifiedInferenceProfile: string | null;
209
211
  }
210
212
 
211
213
  export const parseConversation = createRowMapper<
@@ -222,6 +224,7 @@ export const parseConversation = createRowMapper<
222
224
  contextSummary: "contextSummary",
223
225
  contextCompactedMessageCount: "contextCompactedMessageCount",
224
226
  contextCompactedAt: "contextCompactedAt",
227
+ cleanedAt: "cleanedAt",
225
228
  slackContextCompactionWatermarkTs: "slackContextCompactionWatermarkTs",
226
229
  slackContextCompactionWatermarkAt: "slackContextCompactionWatermarkAt",
227
230
  conversationType: "conversationType",
@@ -238,6 +241,7 @@ export const parseConversation = createRowMapper<
238
241
  inferenceProfile: "inferenceProfile",
239
242
  inferenceProfileSessionId: "inferenceProfileSessionId",
240
243
  inferenceProfileExpiresAt: "inferenceProfileExpiresAt",
244
+ lastNotifiedInferenceProfile: "lastNotifiedInferenceProfile",
241
245
  });
242
246
 
243
247
  export interface MessageRow {
@@ -482,7 +486,7 @@ export function findMostRecentRetrospectiveFor(
482
486
  .from(conversations)
483
487
  .where(
484
488
  and(
485
- eq(conversations.source, MEMORY_RETROSPECTIVE_SOURCE),
489
+ inArray(conversations.source, MEMORY_RETROSPECTIVE_SOURCES),
486
490
  eq(conversations.forkParentConversationId, currentId),
487
491
  ),
488
492
  )
@@ -535,6 +539,32 @@ function getConversationGroupId(conversationId: string): string | null {
535
539
  export function forkConversation(params: {
536
540
  conversationId: string;
537
541
  throughMessageId?: string;
542
+ /**
543
+ * Override the fork's `source` column. Defaults to the standard
544
+ * `createConversation` default (`"user"`). Used by fork-based memory
545
+ * retrospectives to mark the fork as a retrospective artifact distinct
546
+ * from a user-initiated fork, so dedup and cleanup queries can scope
547
+ * correctly.
548
+ */
549
+ source?: string;
550
+ /**
551
+ * Optional title for the fork. Defaults to `<parent title> (Fork)`.
552
+ */
553
+ title?: string;
554
+ /**
555
+ * Override the fork's `conversationType` column. Defaults to `"standard"`.
556
+ * Used by fork-based memory retrospectives to bucket the fork as a
557
+ * `"background"` conversation so it doesn't surface in the user's
558
+ * conversation list.
559
+ */
560
+ conversationType?: ConversationCreateType;
561
+ /**
562
+ * Override the fork's `groupId`. Defaults to the parent conversation's
563
+ * group (or `"system:all"` when the parent has none). Used by fork-based
564
+ * memory retrospectives to route the fork into a dedicated background
565
+ * group.
566
+ */
567
+ groupId?: string;
538
568
  }): ConversationRow {
539
569
  const { conversationId, throughMessageId } = params;
540
570
  const db = getDb();
@@ -576,8 +606,19 @@ export function forkConversation(params: {
576
606
  copyBoundaryIndex >= 0
577
607
  ? sourceMessages.slice(0, copyBoundaryIndex + 1)
578
608
  : ([] as MessageRow[]);
609
+
610
+ // Inherit /clean state only when the fork boundary is at-or-after the
611
+ // clean event. Pre-clean forks branch from history that pre-dates the
612
+ // clean, so the marker would be a no-op and is misleading to copy.
613
+ const sourceCleanedAt = sourceConversation.cleanedAt ?? null;
614
+ const boundaryMessageCreatedAt = messagesToCopy.at(-1)?.createdAt ?? null;
615
+ const inheritsCleanedAt =
616
+ sourceCleanedAt != null &&
617
+ boundaryMessageCreatedAt != null &&
618
+ boundaryMessageCreatedAt >= sourceCleanedAt;
579
619
  const forkParentMessageId = messagesToCopy.at(-1)?.id ?? null;
580
- const forkTitle = `${sourceConversation.title ?? "Untitled"} (Fork)`;
620
+ const forkTitle =
621
+ params.title ?? `${sourceConversation.title ?? "Untitled"} (Fork)`;
581
622
 
582
623
  // Collect disk-sync work to run after the transaction commits.
583
624
  const diskSyncQueue: Array<{
@@ -597,8 +638,9 @@ export function forkConversation(params: {
597
638
  const forkedConversation = db.transaction(() => {
598
639
  const fc = createConversation({
599
640
  title: forkTitle,
600
- conversationType: "standard",
601
- groupId: parentGroupId ?? "system:all",
641
+ conversationType: params.conversationType ?? "standard",
642
+ groupId: params.groupId ?? parentGroupId ?? "system:all",
643
+ ...(params.source != null ? { source: params.source } : {}),
602
644
  });
603
645
 
604
646
  db.update(conversations)
@@ -620,6 +662,7 @@ export function forkConversation(params: {
620
662
  slackContextCompactionWatermarkAt: preserveSourceCompactionState
621
663
  ? sourceConversation.slackContextCompactionWatermarkAt
622
664
  : null,
665
+ cleanedAt: inheritsCleanedAt ? sourceCleanedAt : null,
623
666
  inferenceProfile: sourceConversation.inferenceProfile,
624
667
  })
625
668
  .where(eq(conversations.id, fc.id))
@@ -1229,10 +1272,14 @@ interface PaginatedMessagesResult {
1229
1272
  hasMore: boolean;
1230
1273
  }
1231
1274
 
1275
+ const PAGINATION_CHUNK_MIN = 50;
1276
+ const PAGINATION_SCAN_CAP = 10_000;
1277
+
1232
1278
  export function getMessagesPaginated(
1233
1279
  conversationId: string,
1234
1280
  limit: number | undefined,
1235
1281
  beforeTimestamp?: number,
1282
+ filter?: (row: MessageRow) => boolean,
1236
1283
  ): PaginatedMessagesResult {
1237
1284
  const db = getDb();
1238
1285
 
@@ -1248,51 +1295,69 @@ export function getMessagesPaginated(
1248
1295
  .orderBy(asc(messages.createdAt))
1249
1296
  .all()
1250
1297
  .map(parseMessage);
1251
- return { messages: rows, hasMore: false };
1298
+ return {
1299
+ messages: filter ? rows.filter(filter) : rows,
1300
+ hasMore: false,
1301
+ };
1252
1302
  }
1253
1303
 
1254
- const conditions = [eq(messages.conversationId, conversationId)];
1255
- if (beforeTimestamp !== undefined) {
1256
- conditions.push(lt(messages.createdAt, beforeTimestamp));
1257
- }
1304
+ // Walk pages newest→oldest, applying `filter` in TS (metadata parsing is
1305
+ // JSON, not a structured column). Keep fetching until we have `limit + 1`
1306
+ // visible rows or the DB is exhausted, so `hasMore` and the cursor reflect
1307
+ // the visible page rather than the unfiltered row count. Without this loop,
1308
+ // a fully-hidden page returns `{ messages: [], hasMore: true }` with no
1309
+ // cursor, which stalls the web client's older-page fetch.
1310
+ let cursorCreatedAt = beforeTimestamp;
1311
+ let cursorMessageId: string | undefined;
1312
+ const visible: MessageRow[] = [];
1313
+ const chunkSize = Math.max(limit + 1, PAGINATION_CHUNK_MIN);
1314
+ // Bound the work a single request can do when `filter` rejects nearly every
1315
+ // row — otherwise a pathological filter against a huge conversation would
1316
+ // tie up a connection for thousands of roundtrips.
1317
+ let rowsScanned = 0;
1318
+
1319
+ while (visible.length < limit + 1 && rowsScanned < PAGINATION_SCAN_CAP) {
1320
+ const cursorPredicate =
1321
+ cursorCreatedAt === undefined
1322
+ ? undefined
1323
+ : cursorMessageId === undefined
1324
+ ? lt(messages.createdAt, cursorCreatedAt)
1325
+ : or(
1326
+ lt(messages.createdAt, cursorCreatedAt),
1327
+ and(
1328
+ eq(messages.createdAt, cursorCreatedAt),
1329
+ lt(messages.id, cursorMessageId),
1330
+ ),
1331
+ );
1258
1332
 
1259
- const rows = db
1260
- .select()
1261
- .from(messages)
1262
- .where(and(...conditions))
1263
- .orderBy(desc(messages.createdAt))
1264
- .limit(limit + 1)
1265
- .all()
1266
- .map(parseMessage);
1333
+ const chunk = db
1334
+ .select()
1335
+ .from(messages)
1336
+ .where(and(eq(messages.conversationId, conversationId), cursorPredicate))
1337
+ .orderBy(desc(messages.createdAt), desc(messages.id))
1338
+ .limit(chunkSize)
1339
+ .all()
1340
+ .map(parseMessage);
1267
1341
 
1268
- const hasMore = rows.length > limit;
1269
- if (hasMore) {
1270
- rows.splice(limit);
1342
+ if (chunk.length === 0) break;
1343
+ rowsScanned += chunk.length;
1344
+
1345
+ for (const row of chunk) {
1346
+ if (!filter || filter(row)) visible.push(row);
1347
+ if (visible.length >= limit + 1) break;
1348
+ }
1349
+
1350
+ if (chunk.length < chunkSize) break;
1351
+ const lastRow = chunk[chunk.length - 1];
1352
+ cursorCreatedAt = lastRow.createdAt;
1353
+ cursorMessageId = lastRow.id;
1271
1354
  }
1272
- rows.reverse();
1273
1355
 
1274
- return { messages: rows, hasMore };
1275
- }
1356
+ const hasMore = visible.length > limit;
1357
+ if (hasMore) visible.splice(limit);
1358
+ visible.reverse();
1276
1359
 
1277
- export function getLastAssistantTimestampBefore(
1278
- conversationId: string,
1279
- beforeTimestamp: number,
1280
- ): number {
1281
- const db = getDb();
1282
- const row = db
1283
- .select({ createdAt: messages.createdAt })
1284
- .from(messages)
1285
- .where(
1286
- and(
1287
- eq(messages.conversationId, conversationId),
1288
- eq(messages.role, "assistant"),
1289
- lt(messages.createdAt, beforeTimestamp),
1290
- ),
1291
- )
1292
- .orderBy(desc(messages.createdAt))
1293
- .limit(1)
1294
- .get();
1295
- return row?.createdAt ?? 0;
1360
+ return { messages: visible, hasMore };
1296
1361
  }
1297
1362
 
1298
1363
  export function getLastUserTimestampBefore(
@@ -1386,6 +1451,20 @@ export function updateConversationContextWindow(
1386
1451
  .run();
1387
1452
  }
1388
1453
 
1454
+ export function setConversationCleanedAt(
1455
+ id: string,
1456
+ cleanedAt: number | null,
1457
+ ): void {
1458
+ const db = getDb();
1459
+ db.update(conversations)
1460
+ .set({
1461
+ cleanedAt,
1462
+ updatedAt: Date.now(),
1463
+ })
1464
+ .where(eq(conversations.id, id))
1465
+ .run();
1466
+ }
1467
+
1389
1468
  export function updateConversationSlackContextWatermark(
1390
1469
  id: string,
1391
1470
  watermarkTs: string,
@@ -1609,6 +1688,17 @@ export function getConversationOverrideProfile(
1609
1688
  return getConversationOverrideProfileFromRow(getConversation(conversationId));
1610
1689
  }
1611
1690
 
1691
+ export function setLastNotifiedInferenceProfile(
1692
+ conversationId: string,
1693
+ profileKey: string | null,
1694
+ ): void {
1695
+ rawRun(
1696
+ "UPDATE conversations SET last_notified_inference_profile = ? WHERE id = ?",
1697
+ profileKey,
1698
+ conversationId,
1699
+ );
1700
+ }
1701
+
1612
1702
  /**
1613
1703
  * Delete all conversations, messages, and related data (tool invocations,
1614
1704
  * memory segments, etc.) from the daemon database.