@vellumai/assistant 0.11.1 → 0.11.2-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (318) hide show
  1. package/Dockerfile +1 -3
  2. package/README.md +1 -1
  3. package/eslint.config.mjs +28 -8
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  12. package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  13. package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
  14. package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
  15. package/openapi.yaml +292 -5
  16. package/package.json +1 -1
  17. package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
  18. package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
  19. package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
  20. package/src/__tests__/compaction-events.test.ts +139 -2
  21. package/src/__tests__/config-loader-backfill.test.ts +35 -3
  22. package/src/__tests__/config-schema.test.ts +1 -0
  23. package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
  24. package/src/__tests__/conversation-agent-loop.test.ts +180 -7
  25. package/src/__tests__/conversation-error.test.ts +211 -4
  26. package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
  27. package/src/__tests__/conversation-queue.test.ts +126 -0
  28. package/src/__tests__/conversation-retry-route.test.ts +46 -7
  29. package/src/__tests__/conversation-slash.test.ts +4 -2
  30. package/src/__tests__/conversation-summarize-route.test.ts +7 -1
  31. package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
  32. package/src/__tests__/document-create-dedupe.test.ts +2 -1
  33. package/src/__tests__/document-find-replace.test.ts +2 -1
  34. package/src/__tests__/document-tool-security.test.ts +2 -1
  35. package/src/__tests__/document-update-default-surface.test.ts +2 -1
  36. package/src/__tests__/document-workspace-file.test.ts +467 -0
  37. package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
  38. package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
  39. package/src/__tests__/http-user-message-parity.test.ts +66 -0
  40. package/src/__tests__/list-messages-attachments.test.ts +87 -0
  41. package/src/__tests__/list-messages-provider-error.test.ts +143 -0
  42. package/src/__tests__/list-messages-system-card.test.ts +100 -0
  43. package/src/__tests__/llm-resolver.test.ts +19 -9
  44. package/src/__tests__/managed-profile-guard.test.ts +23 -0
  45. package/src/__tests__/notification-platform-adapter.test.ts +130 -2
  46. package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
  47. package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
  48. package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
  49. package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
  50. package/src/__tests__/provider-error-scenarios.test.ts +140 -0
  51. package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
  52. package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
  53. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
  54. package/src/__tests__/skills.test.ts +27 -0
  55. package/src/__tests__/slack-channels-routes.test.ts +0 -2
  56. package/src/__tests__/slack-share-routes.test.ts +0 -3
  57. package/src/__tests__/slack-users-routes.test.ts +0 -2
  58. package/src/__tests__/subagent-tools.test.ts +11 -0
  59. package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
  60. package/src/__tests__/tool-result-spool.test.ts +5 -4
  61. package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
  62. package/src/__tests__/turn-events-store.test.ts +26 -0
  63. package/src/__tests__/ui-visual-surface.test.ts +695 -0
  64. package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
  65. package/src/__tests__/visible-app-context.test.ts +189 -0
  66. package/src/__tests__/workspace-git-service.test.ts +173 -33
  67. package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
  68. package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
  69. package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
  70. package/src/agent/loop.ts +36 -7
  71. package/src/api/events/context-window-usage.ts +31 -0
  72. package/src/api/events/notification-intent.ts +8 -0
  73. package/src/api/events/ui-surface-pending.ts +35 -0
  74. package/src/api/index.ts +14 -0
  75. package/src/api/responses/conversation-message.ts +25 -4
  76. package/src/api/surfaces.ts +90 -1
  77. package/src/approvals/guardian-card-withdrawal.ts +66 -31
  78. package/src/approvals/guardian-decision-primitive.ts +4 -0
  79. package/src/calls/__tests__/call-setup-router.test.ts +156 -26
  80. package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
  81. package/src/calls/call-setup-router.ts +80 -36
  82. package/src/calls/voice-session-bridge.ts +173 -26
  83. package/src/cli/commands/inference.help.ts +3 -3
  84. package/src/cli/commands/notifications.help.ts +3 -2
  85. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
  86. package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
  87. package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
  88. package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
  89. package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
  90. package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
  91. package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
  92. package/src/cli/commands/platform/index.help.ts +92 -0
  93. package/src/cli/commands/platform/index.ts +7 -0
  94. package/src/cli/commands/platform/invoices.ts +132 -0
  95. package/src/cli/commands/usage.help.ts +1 -1
  96. package/src/cli/lib/list-installed-plugins.ts +2 -1
  97. package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
  98. package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
  99. package/src/config/assistant-feature-flags.ts +7 -2
  100. package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
  101. package/src/config/bundled-skills/subagent/SKILL.md +3 -1
  102. package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
  103. package/src/config/bundled-skills/visualize/SKILL.md +163 -0
  104. package/src/config/call-site-defaults.ts +3 -2
  105. package/src/config/default-profile-catalog.ts +75 -70
  106. package/src/config/default-profile-names.ts +13 -28
  107. package/src/config/env-registry.ts +1 -0
  108. package/src/config/feature-flag-registry.json +16 -0
  109. package/src/config/llm-resolver.ts +21 -5
  110. package/src/config/loader.ts +18 -11
  111. package/src/config/schemas/llm.ts +1 -9
  112. package/src/config/schemas/memory-retrospective.ts +9 -0
  113. package/src/config/schemas/monitoring.ts +28 -2
  114. package/src/config/schemas/workspace-git.ts +15 -0
  115. package/src/config/seed-inference-profiles.ts +19 -5
  116. package/src/context/post-turn-tool-result-truncation.ts +2 -2
  117. package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
  118. package/src/conversations/message-consolidation.ts +17 -15
  119. package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
  120. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
  121. package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
  122. package/src/daemon/conversation-agent-loop.ts +107 -22
  123. package/src/daemon/conversation-error.ts +169 -48
  124. package/src/daemon/conversation-messaging.ts +87 -4
  125. package/src/daemon/conversation-process.ts +57 -45
  126. package/src/daemon/conversation-runtime-assembly.ts +57 -0
  127. package/src/daemon/conversation-store.ts +36 -1
  128. package/src/daemon/conversation-surfaces.ts +68 -6
  129. package/src/daemon/conversation-turn-finalize.ts +76 -18
  130. package/src/daemon/conversation.ts +122 -27
  131. package/src/daemon/lifecycle.ts +9 -0
  132. package/src/daemon/message-types/conversations.ts +8 -0
  133. package/src/daemon/message-types/surfaces.ts +3 -0
  134. package/src/documents/document-store.ts +247 -10
  135. package/src/home/__tests__/feed-types.test.ts +43 -8
  136. package/src/home/__tests__/feed-writer.test.ts +59 -0
  137. package/src/home/feed-types.ts +34 -7
  138. package/src/home/feed-writer.ts +15 -6
  139. package/src/live-voice/__tests__/activity-label.test.ts +95 -0
  140. package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
  141. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
  142. package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
  143. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
  144. package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
  145. package/src/live-voice/activity-label.ts +169 -0
  146. package/src/live-voice/live-activity-reporter.ts +35 -6
  147. package/src/live-voice/live-voice-session.ts +282 -41
  148. package/src/live-voice/protocol.ts +35 -0
  149. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
  150. package/src/messaging/providers/slack/adapter.ts +86 -19
  151. package/src/messaging/providers/slack/api.test.ts +85 -1
  152. package/src/messaging/providers/slack/api.ts +121 -288
  153. package/src/messaging/providers/slack/client.ts +20 -251
  154. package/src/messaging/providers/slack/send.test.ts +4 -9
  155. package/src/messaging/providers/slack/send.ts +1 -1
  156. package/src/messaging/providers/slack/types.ts +5 -0
  157. package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
  158. package/src/messaging/providers/slack/web-api-transport.ts +367 -0
  159. package/src/messaging/providers/slack/withdraw.ts +6 -14
  160. package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
  161. package/src/messaging/providers/telegram-bot/send.ts +23 -2
  162. package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
  163. package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
  164. package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
  165. package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
  166. package/src/monitoring/file-descriptors.ts +262 -0
  167. package/src/monitoring/process-memory.ts +4 -11
  168. package/src/monitoring/resource-sampler.ts +5 -0
  169. package/src/monitoring/worker.ts +12 -0
  170. package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
  171. package/src/notifications/__tests__/broadcaster.test.ts +347 -7
  172. package/src/notifications/__tests__/copy-composer.test.ts +53 -3
  173. package/src/notifications/__tests__/decision-engine.test.ts +349 -29
  174. package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
  175. package/src/notifications/__tests__/edit-notification.test.ts +330 -0
  176. package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
  177. package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
  178. package/src/notifications/adapters/macos.ts +3 -0
  179. package/src/notifications/adapters/platform.ts +90 -14
  180. package/src/notifications/adapters/telegram.ts +6 -4
  181. package/src/notifications/assistant-reply-producer.ts +219 -0
  182. package/src/notifications/broadcaster.ts +523 -317
  183. package/src/notifications/copy-composer.ts +27 -6
  184. package/src/notifications/decision-engine.ts +154 -77
  185. package/src/notifications/deterministic-checks.ts +6 -7
  186. package/src/notifications/edit-notification.ts +8 -4
  187. package/src/notifications/emit-signal.ts +49 -16
  188. package/src/notifications/guardian-delivery-recorder.ts +8 -2
  189. package/src/notifications/home-feed-side-effect.ts +43 -6
  190. package/src/notifications/notification-utils.ts +39 -4
  191. package/src/notifications/signal.ts +10 -0
  192. package/src/notifications/types.ts +17 -0
  193. package/src/persistence/bookmark-crud.ts +18 -9
  194. package/src/persistence/conversation-attention-store.ts +26 -0
  195. package/src/persistence/conversation-crud.ts +79 -32
  196. package/src/persistence/conversation-queries.ts +30 -15
  197. package/src/persistence/conversation-title-service.ts +5 -170
  198. package/src/persistence/conversation-types.ts +192 -1
  199. package/src/persistence/db-init.ts +14 -1
  200. package/src/persistence/db-maintenance.ts +5 -4
  201. package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
  202. package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
  203. package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
  204. package/src/persistence/embeddings/embedding-local.ts +2 -1
  205. package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
  206. package/src/persistence/embeddings/plugin-index.ts +84 -12
  207. package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
  208. package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
  209. package/src/persistence/planner-statistics.ts +14 -0
  210. package/src/persistence/schema/documents.ts +26 -11
  211. package/src/persistence/steps.ts +2 -0
  212. package/src/platform/client.test.ts +172 -2
  213. package/src/platform/client.ts +178 -55
  214. package/src/plugin-api/conversation-turn.ts +13 -0
  215. package/src/plugin-api/model-profiles.test.ts +6 -2
  216. package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
  217. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
  218. package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
  219. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
  220. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
  221. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
  224. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
  225. package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
  226. package/src/plugins/defaults/memory/host-utils.ts +5 -0
  227. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
  228. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
  229. package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
  230. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
  231. package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
  232. package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
  233. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
  234. package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
  235. package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
  236. package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
  237. package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
  238. package/src/plugins/defaults/memory/v2/injection.ts +48 -3
  239. package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
  240. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
  241. package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
  242. package/src/plugins/defaults/turn-context/injectors.ts +1 -0
  243. package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
  244. package/src/plugins/types.ts +17 -0
  245. package/src/prompts/templates/system-sections.ts +2 -2
  246. package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
  247. package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
  248. package/src/providers/call-site-routing.ts +77 -14
  249. package/src/providers/connection-resolution.ts +42 -2
  250. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
  251. package/src/providers/inference/adapter-factory.ts +70 -12
  252. package/src/providers/model-catalog.ts +0 -11
  253. package/src/providers/model-intents.ts +11 -0
  254. package/src/providers/registry.ts +8 -0
  255. package/src/providers/retry.ts +85 -17
  256. package/src/providers/types.ts +8 -1
  257. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  258. package/src/runtime/agent-wake.ts +132 -9
  259. package/src/runtime/channel-approval-types.ts +25 -0
  260. package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
  261. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
  262. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
  263. package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
  264. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
  265. package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
  266. package/src/runtime/routes/consolidation-routes.ts +1 -1
  267. package/src/runtime/routes/conversation-management-routes.ts +18 -6
  268. package/src/runtime/routes/conversation-query-routes.ts +14 -23
  269. package/src/runtime/routes/conversation-routes.ts +113 -8
  270. package/src/runtime/routes/credential-routes.ts +1 -5
  271. package/src/runtime/routes/documents-routes.ts +207 -12
  272. package/src/runtime/routes/identity-routes.ts +3 -93
  273. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
  274. package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
  275. package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
  276. package/src/runtime/routes/notification-routes.ts +4 -1
  277. package/src/runtime/routes/platform-routes.ts +238 -3
  278. package/src/runtime/routes/playground/guard.ts +1 -2
  279. package/src/runtime/routes/slack-channel-routes.ts +2 -4
  280. package/src/runtime/routes/workspace-commit-routes.ts +49 -2
  281. package/src/runtime/routes/workspace-routes.ts +11 -12
  282. package/src/runtime/routes/workspace-utils.ts +49 -2
  283. package/src/runtime/services/conversation-serializer.ts +2 -4
  284. package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
  285. package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
  286. package/src/subagent/__tests__/consult-context.test.ts +84 -0
  287. package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
  288. package/src/subagent/consult-context.ts +410 -0
  289. package/src/subagent/consult-prompt.ts +38 -5
  290. package/src/subagent/manager.ts +9 -4
  291. package/src/subagent/types.ts +8 -0
  292. package/src/telemetry/telemetry-event-sources.ts +7 -0
  293. package/src/telemetry/telemetry-wire-source.json +1 -1
  294. package/src/telemetry/telemetry-wire.generated.ts +1 -0
  295. package/src/telemetry/turn-events-store.ts +30 -1
  296. package/src/telemetry/types.ts +24 -0
  297. package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
  298. package/src/tools/browser/pinned-tabs.ts +3 -1
  299. package/src/tools/subagent/spawn.ts +23 -1
  300. package/src/tools/terminal/safe-env.ts +1 -0
  301. package/src/tools/ui-surface/definitions.ts +39 -1
  302. package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
  303. package/src/tools/ui-surface/visual-validation.ts +787 -0
  304. package/src/tools/workflows/run-workflow.test.ts +1 -0
  305. package/src/util/__tests__/short-title.test.ts +229 -0
  306. package/src/util/__tests__/worker-compute.test.ts +65 -0
  307. package/src/util/cgroup-cpu.ts +93 -0
  308. package/src/util/errors.ts +27 -0
  309. package/src/util/process-tree.ts +19 -0
  310. package/src/util/short-title.ts +189 -0
  311. package/src/util/worker-compute.ts +83 -0
  312. package/src/workspace/byok-default-profile-ensure.ts +25 -15
  313. package/src/workspace/git-service.ts +79 -30
  314. package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
  315. package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
  316. package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
  317. package/src/workspace/migrations/registry.ts +6 -0
  318. package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
@@ -0,0 +1,199 @@
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ createConversation,
5
+ type MessageRow,
6
+ } from "../../../../persistence/conversation-crud.js";
7
+ import { getDb } from "../../../../persistence/db-connection.js";
8
+ import { initializeDb } from "../../../../persistence/db-init.js";
9
+ import { messages } from "../../../../persistence/schema/index.js";
10
+ import {
11
+ hasQualifyingUserMessageAfter,
12
+ messagesHaveUserActivity,
13
+ } from "../memory-retrospective-accounting.js";
14
+
15
+ await initializeDb();
16
+
17
+ const CONV = "conv-accounting";
18
+
19
+ const TEXT = JSON.stringify([{ type: "text", text: "hello" }]);
20
+ const TOOL_RESULT_ONLY = JSON.stringify([
21
+ { type: "tool_result", tool_use_id: "t1", content: "ok" },
22
+ ]);
23
+ const MIXED = JSON.stringify([
24
+ { type: "tool_result", tool_use_id: "t1", content: "ok" },
25
+ { type: "text", text: "note" },
26
+ ]);
27
+
28
+ let seq = 0;
29
+ function insertRaw(opts: {
30
+ role: string;
31
+ content: string;
32
+ createdAt: number;
33
+ id?: string;
34
+ }): string {
35
+ const id = opts.id ?? `msg-${String(++seq).padStart(4, "0")}`;
36
+ getDb()
37
+ .insert(messages)
38
+ .values({
39
+ id,
40
+ conversationId: CONV,
41
+ role: opts.role,
42
+ content: opts.content,
43
+ createdAt: opts.createdAt,
44
+ metadata: null,
45
+ })
46
+ .run();
47
+ return id;
48
+ }
49
+
50
+ describe("hasQualifyingUserMessageAfter", () => {
51
+ beforeEach(() => {
52
+ const db = getDb();
53
+ db.run(`DELETE FROM messages`);
54
+ db.run(`DELETE FROM conversations`);
55
+ createConversation({ id: CONV });
56
+ });
57
+
58
+ test("assistant-only tail does not qualify", () => {
59
+ insertRaw({ role: "assistant", content: TEXT, createdAt: 1_000 });
60
+ insertRaw({ role: "assistant", content: TEXT, createdAt: 2_000 });
61
+
62
+ expect(hasQualifyingUserMessageAfter(CONV, null)).toBe(false);
63
+ });
64
+
65
+ test("tool_result-only user rows do not qualify", () => {
66
+ insertRaw({ role: "assistant", content: TEXT, createdAt: 1_000 });
67
+ insertRaw({ role: "user", content: TOOL_RESULT_ONLY, createdAt: 2_000 });
68
+
69
+ expect(hasQualifyingUserMessageAfter(CONV, null)).toBe(false);
70
+ });
71
+
72
+ test("a user text row qualifies", () => {
73
+ insertRaw({ role: "user", content: TEXT, createdAt: 1_000 });
74
+
75
+ expect(hasQualifyingUserMessageAfter(CONV, null)).toBe(true);
76
+ });
77
+
78
+ test("a mixed tool_result + text user row qualifies", () => {
79
+ insertRaw({ role: "user", content: MIXED, createdAt: 1_000 });
80
+
81
+ expect(hasQualifyingUserMessageAfter(CONV, null)).toBe(true);
82
+ });
83
+
84
+ test("non-array user content qualifies (fails toward running)", () => {
85
+ // File-backed rows persist a `{ ref }` object; legacy rows persist a
86
+ // bare string. Neither shape can prove the row is a tool-result carrier.
87
+ insertRaw({
88
+ role: "user",
89
+ content: JSON.stringify({ ref: "deltas/abc" }),
90
+ createdAt: 1_000,
91
+ });
92
+ expect(hasQualifyingUserMessageAfter(CONV, null)).toBe(true);
93
+
94
+ getDb().run(`DELETE FROM messages`);
95
+ insertRaw({ role: "user", content: "plain legacy text", createdAt: 1_000 });
96
+ expect(hasQualifyingUserMessageAfter(CONV, null)).toBe(true);
97
+ });
98
+
99
+ test("an empty content array does not qualify", () => {
100
+ insertRaw({ role: "user", content: "[]", createdAt: 1_000 });
101
+
102
+ expect(hasQualifyingUserMessageAfter(CONV, null)).toBe(false);
103
+ });
104
+
105
+ test("only rows after the cursor count", () => {
106
+ const cursor = insertRaw({ role: "user", content: TEXT, createdAt: 1_000 });
107
+ insertRaw({ role: "assistant", content: TEXT, createdAt: 2_000 });
108
+
109
+ expect(hasQualifyingUserMessageAfter(CONV, cursor)).toBe(false);
110
+
111
+ insertRaw({ role: "user", content: TEXT, createdAt: 3_000 });
112
+ expect(hasQualifyingUserMessageAfter(CONV, cursor)).toBe(true);
113
+ });
114
+
115
+ test("the empty-string sentinel scans the whole conversation", () => {
116
+ insertRaw({ role: "user", content: TEXT, createdAt: 1_000 });
117
+
118
+ expect(hasQualifyingUserMessageAfter(CONV, "")).toBe(true);
119
+ });
120
+
121
+ test("a vanished cursor reference means no new work", () => {
122
+ insertRaw({ role: "user", content: TEXT, createdAt: 1_000 });
123
+
124
+ expect(hasQualifyingUserMessageAfter(CONV, "msg-gone")).toBe(false);
125
+ });
126
+
127
+ test("rows sharing the cursor's timestamp are split by the id tie-break", () => {
128
+ insertRaw({
129
+ role: "user",
130
+ content: TEXT,
131
+ createdAt: 1_000,
132
+ id: "msg-before",
133
+ });
134
+ insertRaw({
135
+ role: "user",
136
+ content: TOOL_RESULT_ONLY,
137
+ createdAt: 1_000,
138
+ id: "msg-cursor",
139
+ });
140
+ expect(hasQualifyingUserMessageAfter(CONV, "msg-cursor")).toBe(false);
141
+
142
+ insertRaw({
143
+ role: "user",
144
+ content: TEXT,
145
+ createdAt: 1_000,
146
+ id: "msg-later",
147
+ });
148
+ expect(hasQualifyingUserMessageAfter(CONV, "msg-cursor")).toBe(true);
149
+ });
150
+ });
151
+
152
+ describe("messagesHaveUserActivity", () => {
153
+ function row(
154
+ role: string,
155
+ content: Array<Record<string, unknown>>,
156
+ ): Pick<MessageRow, "role" | "content"> {
157
+ return { role, content } as unknown as Pick<MessageRow, "role" | "content">;
158
+ }
159
+
160
+ test("a user text row counts as user activity", () => {
161
+ expect(
162
+ messagesHaveUserActivity([
163
+ row("assistant", [{ type: "text", text: "recap" }]),
164
+ row("user", [{ type: "text", text: "hey" }]),
165
+ ]),
166
+ ).toBe(true);
167
+ });
168
+
169
+ test("tool_result-only user rows do not count", () => {
170
+ expect(
171
+ messagesHaveUserActivity([
172
+ row("assistant", [{ type: "text", text: "recap" }]),
173
+ row("user", [
174
+ { type: "tool_result", tool_use_id: "t1", content: "ok" },
175
+ ]),
176
+ ]),
177
+ ).toBe(false);
178
+ });
179
+
180
+ test("a mixed tool_result + text user row counts", () => {
181
+ expect(
182
+ messagesHaveUserActivity([
183
+ row("user", [
184
+ { type: "tool_result", tool_use_id: "t1", content: "ok" },
185
+ { type: "text", text: "note" },
186
+ ]),
187
+ ]),
188
+ ).toBe(true);
189
+ });
190
+
191
+ test("assistant rows never count, and an empty slice has no activity", () => {
192
+ expect(
193
+ messagesHaveUserActivity([
194
+ row("assistant", [{ type: "text", text: "hello" }]),
195
+ ]),
196
+ ).toBe(false);
197
+ expect(messagesHaveUserActivity([])).toBe(false);
198
+ });
199
+ });
@@ -11,6 +11,19 @@ const upsertCalls: Array<{
11
11
  payload: { conversationId: string };
12
12
  runAfter: number;
13
13
  }> = [];
14
+ let cfgRequireUserActivity = true;
15
+ let mockStateRow: {
16
+ conversationId: string;
17
+ lastProcessedMessageId: string;
18
+ lastRunAt: number;
19
+ rememberedLog: string[];
20
+ } | null = null;
21
+ let tailQualifies = true;
22
+ let gateProbeThrows = false;
23
+ let gateProbeCalls: Array<{
24
+ conversationId: string;
25
+ afterMessageId: string | null;
26
+ }> = [];
14
27
 
15
28
  mock.module("../../../../persistence/conversation-crud.js", () => ({
16
29
  getConversationSource: (_id: string) => sourceTag,
@@ -30,6 +43,29 @@ mock.module("../../../../persistence/jobs-store.js", () => ({
30
43
  },
31
44
  }));
32
45
 
46
+ mock.module("../../../../config/loader.js", () => ({
47
+ getConfig: () => ({
48
+ memory: { retrospective: { requireUserActivity: cfgRequireUserActivity } },
49
+ }),
50
+ }));
51
+
52
+ mock.module("../memory-retrospective-state.js", () => ({
53
+ getRetrospectiveState: (_id: string) => mockStateRow,
54
+ }));
55
+
56
+ mock.module("../memory-retrospective-accounting.js", () => ({
57
+ hasQualifyingUserMessageAfter: (
58
+ conversationId: string,
59
+ afterMessageId: string | null,
60
+ ) => {
61
+ gateProbeCalls.push({ conversationId, afterMessageId });
62
+ if (gateProbeThrows) {
63
+ throw new Error("messages table unavailable");
64
+ }
65
+ return tailQualifies;
66
+ },
67
+ }));
68
+
33
69
  import {
34
70
  enqueueMemoryRetrospectiveIfEnabled,
35
71
  enqueueMemoryRetrospectiveOnCompaction,
@@ -42,16 +78,22 @@ describe("enqueueMemoryRetrospectiveIfEnabled", () => {
42
78
  convType = "standard";
43
79
  convSource = "user";
44
80
  upsertCalls.length = 0;
81
+ cfgRequireUserActivity = true;
82
+ mockStateRow = null;
83
+ tailQualifies = true;
84
+ gateProbeThrows = false;
85
+ gateProbeCalls = [];
45
86
  });
46
87
 
47
88
  test("standard source — interval trigger enqueues with runAfter ≈ now", () => {
48
89
  const before = Date.now();
49
- enqueueMemoryRetrospectiveIfEnabled({
90
+ const result = enqueueMemoryRetrospectiveIfEnabled({
50
91
  conversationId: "c1",
51
92
  trigger: "interval",
52
93
  });
53
94
  const after = Date.now();
54
95
 
96
+ expect(result).toBe(true);
55
97
  expect(upsertCalls).toHaveLength(1);
56
98
  const call = upsertCalls[0]!;
57
99
  expect(call.payload).toEqual({ conversationId: "c1" });
@@ -59,6 +101,58 @@ describe("enqueueMemoryRetrospectiveIfEnabled", () => {
59
101
  expect(call.runAfter).toBeLessThanOrEqual(after);
60
102
  });
61
103
 
104
+ test("assistant-only tail — user-activity gate skips the enqueue", () => {
105
+ tailQualifies = false;
106
+ const result = enqueueMemoryRetrospectiveIfEnabled({
107
+ conversationId: "c1",
108
+ trigger: "interval",
109
+ });
110
+
111
+ expect(result).toBe(false);
112
+ expect(upsertCalls).toHaveLength(0);
113
+ });
114
+
115
+ test("requireUserActivity=false — assistant-only tail still enqueues", () => {
116
+ cfgRequireUserActivity = false;
117
+ tailQualifies = false;
118
+ const result = enqueueMemoryRetrospectiveIfEnabled({
119
+ conversationId: "c1",
120
+ trigger: "interval",
121
+ });
122
+
123
+ expect(result).toBe(true);
124
+ expect(upsertCalls).toHaveLength(1);
125
+ expect(gateProbeCalls).toHaveLength(0);
126
+ });
127
+
128
+ test("gate probes the tail from the state row's cursor", () => {
129
+ mockStateRow = {
130
+ conversationId: "c1",
131
+ lastProcessedMessageId: "m9",
132
+ lastRunAt: 1,
133
+ rememberedLog: [],
134
+ };
135
+ enqueueMemoryRetrospectiveIfEnabled({
136
+ conversationId: "c1",
137
+ trigger: "sweep",
138
+ });
139
+
140
+ expect(gateProbeCalls).toEqual([
141
+ { conversationId: "c1", afterMessageId: "m9" },
142
+ ]);
143
+ });
144
+
145
+ test("an unevaluable gate fails open — the enqueue proceeds", () => {
146
+ gateProbeThrows = true;
147
+ const result = enqueueMemoryRetrospectiveIfEnabled({
148
+ conversationId: "c1",
149
+ trigger: "interval",
150
+ });
151
+
152
+ expect(result).toBe(true);
153
+ expect(upsertCalls).toHaveLength(1);
154
+ });
155
+
62
156
  test("compaction trigger applies the small debounce", () => {
63
157
  const before = Date.now();
64
158
  enqueueMemoryRetrospectiveIfEnabled({
@@ -135,6 +229,9 @@ describe("enqueueMemoryRetrospectiveOnCompaction", () => {
135
229
  beforeEach(() => {
136
230
  sourceTag = null;
137
231
  upsertCalls.length = 0;
232
+ cfgRequireUserActivity = true;
233
+ tailQualifies = true;
234
+ gateProbeThrows = false;
138
235
  });
139
236
 
140
237
  test("untrusted trust class — no enqueue", () => {
@@ -28,7 +28,7 @@ let newMessages: Array<{
28
28
  id: string;
29
29
  createdAt: number;
30
30
  role?: string;
31
- content?: string;
31
+ content?: string | Array<Record<string, unknown>>;
32
32
  metadata?: string | null;
33
33
  }> = [];
34
34
 
@@ -327,6 +327,7 @@ mock.module("../../../../config/memory-v3-gate.js", () => ({
327
327
  import type { MemoryJob } from "../../../../persistence/jobs-store.js";
328
328
  import {
329
329
  memoryRetrospectiveJob,
330
+ runForkBasedRetrospective,
330
331
  SOURCE_PROCESSING_REQUEUE_DELAY_MS,
331
332
  } from "../memory-retrospective-job.js";
332
333
 
@@ -337,6 +338,7 @@ function makeConfig(
337
338
  keepSupersededRuns?: boolean;
338
339
  matchConversationProfile?: boolean;
339
340
  promptPath?: string;
341
+ requireUserActivity?: boolean;
340
342
  } = {},
341
343
  ): Parameters<typeof memoryRetrospectiveJob>[1] {
342
344
  return {
@@ -346,6 +348,10 @@ function makeConfig(
346
348
  keepSupersededRuns: overrides.keepSupersededRuns ?? false,
347
349
  matchConversationProfile: overrides.matchConversationProfile ?? false,
348
350
  promptPath: overrides.promptPath ?? null,
351
+ // Neutral in the stub (the shipped schema default is true): the
352
+ // default `newMessages` rows carry no roles, so the gate's own tests
353
+ // opt in explicitly with role-shaped slices.
354
+ requireUserActivity: overrides.requireUserActivity ?? false,
349
355
  sweepIntervalMs: 8 * 60 * 60 * 1000,
350
356
  sweepLookbackMs: 7 * 24 * 60 * 60 * 1000,
351
357
  },
@@ -503,6 +509,104 @@ describe("memoryRetrospectiveJob", () => {
503
509
  ]);
504
510
  });
505
511
 
512
+ test("assistant-only slice with the user-activity gate on: completed as a no-op, both pointers untouched", async () => {
513
+ newMessages = [
514
+ {
515
+ id: "m1",
516
+ createdAt: Date.parse("2026-05-11T10:00:00Z"),
517
+ role: "assistant",
518
+ content: [{ type: "text", text: "proactive recap" }],
519
+ },
520
+ {
521
+ id: "m2",
522
+ createdAt: Date.parse("2026-05-11T10:05:00Z"),
523
+ role: "user",
524
+ content: [{ type: "tool_result", tool_use_id: "t1", content: "ok" }],
525
+ },
526
+ ];
527
+
528
+ const outcome = await memoryRetrospectiveJob(
529
+ makeJob(),
530
+ makeConfig({ requireUserActivity: true }),
531
+ );
532
+
533
+ expect(outcome.kind).toBe("no_user_activity");
534
+ expect(stateUpserts).toHaveLength(0);
535
+ expect(lastRunAtBumps).toHaveLength(0);
536
+ expect(wakeCalls).toHaveLength(0);
537
+ expect(forkCalls).toHaveLength(0);
538
+ expect(
539
+ watchdogEvents.filter((e) => e.checkName === "memory_retrospective_run"),
540
+ ).toEqual([
541
+ {
542
+ checkName: "memory_retrospective_run",
543
+ value: 1,
544
+ detail: { outcome: "no_user_activity" },
545
+ },
546
+ ]);
547
+ });
548
+
549
+ test("a user text row in the slice passes the user-activity gate", async () => {
550
+ newMessages = [
551
+ {
552
+ id: "m1",
553
+ createdAt: Date.parse("2026-05-11T10:00:00Z"),
554
+ role: "assistant",
555
+ content: [{ type: "text", text: "proactive recap" }],
556
+ },
557
+ {
558
+ id: "m2",
559
+ createdAt: Date.parse("2026-05-11T10:05:00Z"),
560
+ role: "user",
561
+ content: [{ type: "text", text: "hey" }],
562
+ },
563
+ ];
564
+
565
+ const outcome = await memoryRetrospectiveJob(
566
+ makeJob(),
567
+ makeConfig({ requireUserActivity: true }),
568
+ );
569
+
570
+ expect(outcome.kind).toBe("invoked");
571
+ expect(forkCalls).toHaveLength(1);
572
+ });
573
+
574
+ test("requireUserActivity=false: an assistant-only slice still runs", async () => {
575
+ newMessages = [
576
+ {
577
+ id: "m1",
578
+ createdAt: Date.parse("2026-05-11T10:00:00Z"),
579
+ role: "assistant",
580
+ content: [{ type: "text", text: "proactive recap" }],
581
+ },
582
+ ];
583
+
584
+ const outcome = await memoryRetrospectiveJob(
585
+ makeJob(),
586
+ makeConfig({ requireUserActivity: false }),
587
+ );
588
+
589
+ expect(outcome.kind).toBe("invoked");
590
+ });
591
+
592
+ test("manual runs bypass the user-activity gate: no opts means an assistant-only slice runs", async () => {
593
+ newMessages = [
594
+ {
595
+ id: "m1",
596
+ createdAt: Date.parse("2026-05-11T10:00:00Z"),
597
+ role: "assistant",
598
+ content: [{ type: "text", text: "proactive recap" }],
599
+ },
600
+ ];
601
+
602
+ const outcome = await runForkBasedRetrospective(
603
+ "src-conv-1",
604
+ makeConfig({ requireUserActivity: true }),
605
+ );
606
+
607
+ expect(outcome.kind).toBe("invoked");
608
+ });
609
+
506
610
  test("a slice whose only row is the retrospective's own skill card is no new work", async () => {
507
611
  // The card is inserted AFTER the cursor the run just persisted, so it is
508
612
  // always "after" lastProcessedMessageId — but it must never constitute
@@ -2,14 +2,18 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  // Record enqueues instead of writing job rows — the sweep's scan/gate decision
4
4
  // is the unit under test, not the jobs store's coalescing. The enqueue's own
5
- // recursion/low-yield guards are covered by memory-retrospective-enqueue tests.
5
+ // recursion/low-yield/user-activity guards are covered by
6
+ // memory-retrospective-enqueue tests; `enqueueDeclines` scripts a declined
7
+ // conversation so the cap accounting can be asserted.
6
8
  let enqueueCalls: Array<{ conversationId: string; trigger: string }> = [];
9
+ let enqueueDeclines = new Set<string>();
7
10
  mock.module("../memory-retrospective-enqueue.js", () => ({
8
11
  enqueueMemoryRetrospectiveIfEnabled: (args: {
9
12
  conversationId: string;
10
13
  trigger: string;
11
14
  }) => {
12
15
  enqueueCalls.push(args);
16
+ return !enqueueDeclines.has(args.conversationId);
13
17
  },
14
18
  }));
15
19
 
@@ -18,6 +22,7 @@ import { eq } from "drizzle-orm";
18
22
  import type { AssistantConfig } from "../../../../config/types.js";
19
23
  import { AUTO_ANALYSIS_SOURCE } from "../../../../persistence/auto-analysis-constants.js";
20
24
  import { createConversation } from "../../../../persistence/conversation-crud.js";
25
+ import { MEMORY_V2_CONSOLIDATION_SOURCE } from "../../../../persistence/conversation-types.js";
21
26
  import {
22
27
  getDb,
23
28
  getMemorySqlite,
@@ -38,7 +43,6 @@ import {
38
43
  runRetrospectiveSweep,
39
44
  SWEEP_MAX_ENQUEUES_PER_PASS,
40
45
  } from "../memory-retrospective-sweep.js";
41
- import { MEMORY_V2_CONSOLIDATION_SOURCE } from "../substrate/constants.js";
42
46
 
43
47
  await initializeDb();
44
48
 
@@ -104,6 +108,7 @@ describe("runRetrospectiveSweep", () => {
104
108
  beforeEach(() => {
105
109
  resetTables();
106
110
  enqueueCalls = [];
111
+ enqueueDeclines = new Set();
107
112
  });
108
113
 
109
114
  test("never-run conversation with unprocessed messages is swept", async () => {
@@ -315,6 +320,23 @@ describe("runRetrospectiveSweep", () => {
315
320
  expect(result).toEqual({ scanned: 1, enqueued: 1 });
316
321
  });
317
322
 
323
+ test("an enqueue the funnel declines is not counted and does not consume the cap", async () => {
324
+ const declined = createConversation({ id: "conv-a" });
325
+ insertMessage(declined.id, { createdAt: 1_000 });
326
+ const accepted = createConversation({ id: "conv-b" });
327
+ insertMessage(accepted.id, { createdAt: 1_000 });
328
+ enqueueDeclines.add(declined.id);
329
+
330
+ const result = await runRetrospectiveSweep(makeConfig());
331
+
332
+ // Both conversations reach the funnel; only the accepted one counts.
333
+ expect(enqueueCalls.map((c) => c.conversationId)).toEqual([
334
+ declined.id,
335
+ accepted.id,
336
+ ]);
337
+ expect(result).toEqual({ scanned: 2, enqueued: 1 });
338
+ });
339
+
318
340
  test("enqueues clamp at the per-pass cap and defer the remainder", async () => {
319
341
  for (let i = 0; i < SWEEP_MAX_ENQUEUES_PER_PASS + 5; i++) {
320
342
  const conv = createConversation({