@vellumai/assistant 0.8.12 → 0.9.0-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 (442) hide show
  1. package/AGENTS.md +0 -14
  2. package/ARCHITECTURE.md +45 -45
  3. package/README.md +1 -1
  4. package/bun.lock +200 -154
  5. package/docs/architecture/integrations.md +3 -3
  6. package/docs/architecture/memory.md +2 -2
  7. package/docs/architecture/security.md +10 -10
  8. package/docs/runbook-trusted-contacts.md +12 -12
  9. package/docs/skills.md +6 -6
  10. package/docs/workflows-testing.md +221 -0
  11. package/docs/workflows.md +510 -0
  12. package/examples/plugins/echo/README.md +5 -5
  13. package/knip.json +2 -0
  14. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +105 -0
  15. package/node_modules/@vellumai/gateway-client/src/index.ts +12 -0
  16. package/openapi.yaml +7197 -5708
  17. package/package.json +8 -4
  18. package/scripts/generate-openapi.ts +66 -114
  19. package/src/__tests__/access-request-seed-content-blocks.test.ts +213 -0
  20. package/src/__tests__/adaptive-thinking-repair.test.ts +32 -3
  21. package/src/__tests__/agent-loop-output-hooks.test.ts +183 -0
  22. package/src/__tests__/agent-loop-regrowth-guard.test.ts +506 -0
  23. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  24. package/src/__tests__/agent-wake-override-profile.test.ts +77 -0
  25. package/src/__tests__/app-compiler.test.ts +7 -1
  26. package/src/__tests__/app-dir-path-guard.test.ts +27 -3
  27. package/src/__tests__/app-executors.test.ts +43 -0
  28. package/src/__tests__/approval-cascade.test.ts +0 -5
  29. package/src/__tests__/approval-routes-http.test.ts +91 -0
  30. package/src/__tests__/assistant-stream-state.test.ts +107 -0
  31. package/src/__tests__/browser-fill-credential.test.ts +3 -3
  32. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -1
  33. package/src/__tests__/compaction-events.test.ts +63 -7
  34. package/src/__tests__/compaction-trail-store.test.ts +74 -1
  35. package/src/__tests__/compaction.benchmark.test.ts +63 -41
  36. package/src/__tests__/compactor-low-watermark-cut.test.ts +349 -0
  37. package/src/__tests__/context-window-manager-compact-retry.test.ts +64 -0
  38. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -5
  39. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -5
  40. package/src/__tests__/conversation-history-web-search.test.ts +7 -0
  41. package/src/__tests__/conversation-process-callsite.test.ts +0 -5
  42. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -5
  43. package/src/__tests__/conversation-queue.test.ts +0 -5
  44. package/src/__tests__/conversation-slash-queue.test.ts +0 -5
  45. package/src/__tests__/conversation-slash-unknown.test.ts +0 -5
  46. package/src/__tests__/conversation-speed-override.test.ts +0 -5
  47. package/src/__tests__/conversation-surfaces-data-persist.test.ts +97 -0
  48. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  49. package/src/__tests__/conversation-usage.test.ts +2 -0
  50. package/src/__tests__/conversation-workspace-injection.test.ts +0 -5
  51. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -5
  52. package/src/__tests__/credential-broker-browser-fill.test.ts +2 -2
  53. package/src/__tests__/credential-broker-server-use.test.ts +2 -2
  54. package/src/__tests__/credential-broker.test.ts +1 -1
  55. package/src/__tests__/credential-prompt-route.test.ts +417 -0
  56. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  57. package/src/__tests__/credential-vault.test.ts +37 -0
  58. package/src/__tests__/db-schedule-syntax-migration.test.ts +24 -0
  59. package/src/__tests__/dynamic-page-surface.test.ts +219 -0
  60. package/src/__tests__/empty-state-greeting-cache.test.ts +94 -0
  61. package/src/__tests__/gateway-flag-listener.test.ts +24 -7
  62. package/src/__tests__/guardian-action-sweep.test.ts +56 -219
  63. package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
  64. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  65. package/src/__tests__/list-messages-hidden-metadata.test.ts +99 -0
  66. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +87 -1
  67. package/src/__tests__/llm-resolver.test.ts +115 -0
  68. package/src/__tests__/managed-profile-guard.test.ts +6 -5
  69. package/src/__tests__/max-tokens-continue-hook.test.ts +184 -0
  70. package/src/__tests__/media-generate-image.test.ts +20 -9
  71. package/src/__tests__/mock-gateway-ipc.ts +23 -0
  72. package/src/__tests__/model-intents.test.ts +1 -1
  73. package/src/__tests__/normalize-onboarding.test.ts +26 -0
  74. package/src/__tests__/notification-decision-strategy.test.ts +4 -2
  75. package/src/__tests__/notification-telegram-adapter.test.ts +21 -3
  76. package/src/__tests__/pending-interactions-resolved-event.test.ts +62 -0
  77. package/src/__tests__/post-turn-tool-result-truncation.test.ts +72 -18
  78. package/src/__tests__/require-fresh-approval.test.ts +425 -1
  79. package/src/__tests__/resolve-app-id.test.ts +56 -0
  80. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -0
  81. package/src/__tests__/schedule-routes-workflow-validation.test.ts +408 -0
  82. package/src/__tests__/schedule-routes.test.ts +257 -4
  83. package/src/__tests__/schedule-store.test.ts +60 -0
  84. package/src/__tests__/schedule-tools.test.ts +247 -2
  85. package/src/__tests__/skill-execute-input.test.ts +85 -0
  86. package/src/__tests__/skill-secret-handling-guard.test.ts +21 -20
  87. package/src/__tests__/skills.test.ts +3 -3
  88. package/src/__tests__/slack-app-setup-skill-regression.test.ts +1 -1
  89. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  90. package/src/__tests__/subagent-tool-gate-mode.test.ts +547 -0
  91. package/src/__tests__/system-prompt.test.ts +1 -1
  92. package/src/__tests__/task-progress-nudge-hook.test.ts +372 -0
  93. package/src/__tests__/task-scheduler.test.ts +299 -0
  94. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +209 -0
  95. package/src/__tests__/tool-result-spool.test.ts +3 -1
  96. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +181 -0
  97. package/src/__tests__/workspace-migration-103-upgrade-quality-profile-to-opus-4-8.test.ts +174 -0
  98. package/src/agent/compaction-circuit.ts +11 -0
  99. package/src/agent/loop.ts +181 -12
  100. package/src/api/constants/call-sites.ts +12 -0
  101. package/src/api/events/assistant-thinking-delta.ts +10 -0
  102. package/src/api/events/usage-update.ts +7 -0
  103. package/src/api/index.ts +4 -1
  104. package/src/api/responses/memory-v3-selection-log.ts +18 -11
  105. package/src/approvals/approval-primitive.ts +2 -2
  106. package/src/background-wake/background-wake-routes.test.ts +5 -2
  107. package/src/bundler/compiler-tools.ts +1 -1
  108. package/src/bundler/package-resolver.ts +0 -1
  109. package/src/calls/call-domain.ts +1 -1
  110. package/src/calls/guardian-action-sweep.ts +16 -93
  111. package/src/cli/AGENTS.md +4 -0
  112. package/src/cli/commands/__tests__/schedules.test.ts +430 -1
  113. package/src/cli/commands/credentials.ts +28 -24
  114. package/src/cli/commands/image-generation.ts +23 -9
  115. package/src/cli/commands/notifications.ts +1 -1
  116. package/src/cli/commands/plugins.ts +89 -46
  117. package/src/cli/commands/schedules.ts +384 -11
  118. package/src/cli/lib/__tests__/inspect-plugin.test.ts +69 -5
  119. package/src/cli/lib/__tests__/install-from-github.test.ts +15 -0
  120. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +81 -4
  121. package/src/cli/lib/inspect-plugin.ts +62 -1
  122. package/src/cli/lib/install-from-github.ts +52 -5
  123. package/src/cli/lib/upgrade-plugin.ts +18 -0
  124. package/src/config/__tests__/workflows-schema.test.ts +60 -0
  125. package/src/config/bundled-skills/acp/SKILL.md +2 -2
  126. package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
  127. package/src/config/bundled-skills/app-builder/tools/app-create.ts +6 -1
  128. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +7 -1
  129. package/src/config/bundled-skills/app-builder/tools/app-refresh.ts +7 -1
  130. package/src/config/bundled-skills/app-builder/tools/app-update.ts +10 -1
  131. package/src/config/bundled-skills/image-studio/SKILL.md +66 -19
  132. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -6
  133. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +22 -3
  134. package/src/config/bundled-skills/personal-page/SKILL.md +57 -0
  135. package/src/config/bundled-skills/personal-page/TOOLS.json +27 -0
  136. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +17 -0
  137. package/src/config/bundled-skills/schedule/SKILL.md +7 -2
  138. package/src/config/bundled-skills/schedule/TOOLS.json +48 -4
  139. package/src/config/bundled-skills/workflows/SKILL.md +214 -0
  140. package/src/config/bundled-skills/workflows/TOOLS.json +84 -0
  141. package/src/config/bundled-skills/workflows/tools/manage-workflows.ts +12 -0
  142. package/src/config/bundled-skills/workflows/tools/run-workflow.ts +12 -0
  143. package/src/config/bundled-tool-registry.ts +14 -2
  144. package/src/config/call-site-defaults.ts +5 -0
  145. package/src/config/feature-flag-registry.json +12 -4
  146. package/src/config/llm-context-resolution.ts +8 -0
  147. package/src/config/llm-resolver.ts +30 -0
  148. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +22 -0
  149. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +16 -0
  150. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +77 -0
  151. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +57 -0
  152. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +28 -0
  153. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +234 -0
  154. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +48 -0
  155. package/src/config/preloaded-apps/personal-page/src/components/media.ts +16 -0
  156. package/src/config/preloaded-apps/personal-page/src/index.html +20 -0
  157. package/src/config/preloaded-apps/personal-page/src/main.tsx +7 -0
  158. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +82 -0
  159. package/src/config/preloaded-apps/personal-page/src/styles.css +759 -0
  160. package/src/config/schema.ts +2 -0
  161. package/src/config/schemas/call-site-catalog.ts +7 -0
  162. package/src/config/schemas/heartbeat.ts +4 -1
  163. package/src/config/schemas/llm.ts +33 -26
  164. package/src/config/schemas/memory-retrospective.ts +19 -0
  165. package/src/config/schemas/platform.ts +8 -0
  166. package/src/config/schemas/services.ts +5 -2
  167. package/src/config/schemas/workflows.ts +42 -0
  168. package/src/config/skills.ts +3 -3
  169. package/src/context/compactor.ts +273 -39
  170. package/src/context/post-turn-tool-result-truncation.ts +23 -6
  171. package/src/context/tool-result-spool.ts +12 -17
  172. package/src/credential-execution/executable-discovery.ts +1 -1
  173. package/src/credential-execution/process-manager.ts +37 -3
  174. package/src/credential-execution/prompted-credential.ts +205 -0
  175. package/src/daemon/conversation-agent-loop-handlers.ts +14 -0
  176. package/src/daemon/conversation-process.ts +11 -2
  177. package/src/daemon/conversation-surfaces.ts +167 -3
  178. package/src/daemon/conversation-tool-setup.ts +103 -26
  179. package/src/daemon/conversation-usage.ts +2 -0
  180. package/src/daemon/conversation.ts +115 -11
  181. package/src/daemon/handlers/shared.ts +26 -14
  182. package/src/daemon/host-cu-proxy.ts +15 -12
  183. package/src/daemon/host-file-proxy.ts +15 -12
  184. package/src/daemon/host-transfer-proxy.ts +30 -24
  185. package/src/daemon/lifecycle.ts +40 -3
  186. package/src/daemon/message-protocol.ts +3 -0
  187. package/src/daemon/message-types/messages.ts +2 -10
  188. package/src/daemon/message-types/workflows.ts +49 -0
  189. package/src/daemon/parse-actual-tokens-from-error.test.ts +62 -1
  190. package/src/daemon/parse-actual-tokens-from-error.ts +43 -4
  191. package/src/daemon/process-message.ts +6 -0
  192. package/src/daemon/tool-setup-types.ts +57 -0
  193. package/src/daemon/wake-conversation-ops.ts +18 -0
  194. package/src/heartbeat/heartbeat-run-store.ts +8 -2
  195. package/src/home/feed-types.ts +1 -1
  196. package/src/ipc/gateway-flag-listener.ts +28 -6
  197. package/src/mcp/mcp-auth-state.ts +8 -20
  198. package/src/media/__tests__/image-models.test.ts +57 -0
  199. package/src/media/image-models.ts +66 -0
  200. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +38 -0
  201. package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +12 -2
  202. package/src/memory/__tests__/memory-retrospective-job.test.ts +911 -34
  203. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +227 -5
  204. package/src/memory/__tests__/memory-retrospective-state.test.ts +195 -0
  205. package/src/memory/__tests__/preloaded-apps.test.ts +85 -0
  206. package/src/memory/auto-analysis-enqueue.ts +14 -1
  207. package/src/memory/compaction-log-store-clickhouse.ts +6 -4
  208. package/src/memory/conversation-crud.ts +9 -2
  209. package/src/memory/conversation-disk-view.ts +1 -1
  210. package/src/memory/conversation-queries.ts +22 -7
  211. package/src/memory/db-init.ts +20 -0
  212. package/src/memory/db-maintenance.ts +16 -0
  213. package/src/memory/embedding-runtime-manager.ts +1 -1
  214. package/src/memory/llm-request-log-source-clickhouse.ts +112 -14
  215. package/src/memory/llm-request-log-source-local.ts +19 -1
  216. package/src/memory/llm-request-log-source.ts +35 -6
  217. package/src/memory/llm-request-log-store.ts +90 -2
  218. package/src/memory/memory-retrospective-constants.ts +9 -0
  219. package/src/memory/memory-retrospective-enqueue.ts +3 -6
  220. package/src/memory/memory-retrospective-fork-boundary.ts +94 -0
  221. package/src/memory/memory-retrospective-job.ts +500 -208
  222. package/src/memory/memory-retrospective-startup-cleanup.ts +97 -19
  223. package/src/memory/memory-retrospective-state.ts +85 -2
  224. package/src/memory/migrations/281-memory-retrospective-remembered-log.ts +40 -0
  225. package/src/memory/migrations/282-schedule-inference-profile.test.ts +77 -0
  226. package/src/memory/migrations/282-schedule-inference-profile.ts +26 -0
  227. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.test.ts +102 -0
  228. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.ts +53 -0
  229. package/src/memory/migrations/284-workflow-runs.ts +51 -0
  230. package/src/memory/migrations/285-schedule-workflow-mode.ts +26 -0
  231. package/src/memory/migrations/286-workflow-run-trust.ts +27 -0
  232. package/src/memory/migrations/287-conversation-origin-channel-index.ts +15 -0
  233. package/src/memory/migrations/288-backfill-origin-channel-from-bindings.ts +43 -0
  234. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +115 -0
  235. package/src/memory/migrations/290-schedule-capabilities.test.ts +77 -0
  236. package/src/memory/migrations/290-schedule-capabilities.ts +25 -0
  237. package/src/memory/migrations/__tests__/281-memory-retrospective-remembered-log.test.ts +96 -0
  238. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +571 -0
  239. package/src/memory/migrations/index.ts +10 -0
  240. package/src/memory/preloaded-apps.ts +116 -0
  241. package/src/memory/schema/infrastructure.ts +4 -0
  242. package/src/memory/schema/memory-core.ts +4 -0
  243. package/src/memory/v2/__tests__/concept-page-frontmatter-schema.test.ts +45 -0
  244. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +11 -7
  245. package/src/memory/v2/__tests__/page-store.test.ts +13 -2
  246. package/src/memory/v2/__tests__/qdrant.test.ts +24 -0
  247. package/src/memory/v2/frontmatter-sweep.ts +7 -6
  248. package/src/memory/v2/page-store.ts +4 -3
  249. package/src/memory/v2/qdrant.ts +42 -3
  250. package/src/memory/v2/types.ts +16 -10
  251. package/src/messaging/draft-store.ts +1 -1
  252. package/src/notifications/access-request-copy.ts +200 -113
  253. package/src/notifications/adapters/slack.ts +250 -111
  254. package/src/notifications/adapters/telegram.ts +7 -44
  255. package/src/notifications/approval-card-builder.ts +93 -0
  256. package/src/notifications/broadcaster.ts +74 -0
  257. package/src/notifications/conversation-pairing.ts +8 -6
  258. package/src/notifications/copy-composer.ts +32 -26
  259. package/src/notifications/decision-engine.ts +59 -7
  260. package/src/notifications/guardian-question-mode.ts +145 -155
  261. package/src/notifications/home-feed-side-effect.ts +28 -11
  262. package/src/notifications/notification-utils.ts +66 -0
  263. package/src/notifications/signal.ts +6 -0
  264. package/src/notifications/tool-approval-copy.ts +142 -0
  265. package/src/notifications/types.ts +19 -0
  266. package/src/permissions/threshold.ts +11 -0
  267. package/src/plugin-api/types.ts +16 -4
  268. package/src/plugins/defaults/compaction/window-manager.ts +44 -0
  269. package/src/plugins/defaults/index.ts +46 -0
  270. package/src/plugins/defaults/max-tokens-continue/continue-state-store.ts +53 -0
  271. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +80 -0
  272. package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +20 -0
  273. package/src/plugins/defaults/max-tokens-continue/package.json +14 -0
  274. package/src/plugins/defaults/memory-retrieval/hooks/__tests__/user-prompt-submit.test.ts +37 -0
  275. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +29 -1
  276. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +8 -3
  277. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +4 -2
  278. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +28 -18
  279. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +67 -0
  280. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +122 -22
  281. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +2 -0
  282. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +63 -1
  283. package/src/plugins/defaults/memory-v3-shadow/injector.ts +61 -18
  284. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +1 -1
  285. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +39 -10
  286. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +34 -1
  287. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +112 -47
  288. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +78 -15
  289. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +206 -0
  290. package/src/plugins/defaults/task-progress-nudge/package.json +15 -0
  291. package/src/prompts/__tests__/system-prompt.test.ts +100 -1
  292. package/src/prompts/__tests__/task-progress-hint-section.test.ts +5 -7
  293. package/src/prompts/normalize-onboarding.ts +2 -0
  294. package/src/prompts/persona-resolver.ts +3 -0
  295. package/src/prompts/system-prompt.ts +51 -2
  296. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +3 -1
  297. package/src/prompts/templates/system-sections.ts +8 -3
  298. package/src/providers/call-site-routing.ts +6 -3
  299. package/src/providers/fireworks/client.ts +3 -0
  300. package/src/providers/inference/auth.ts +52 -46
  301. package/src/providers/model-intents.ts +2 -2
  302. package/src/providers/openai/__tests__/coerce-object-args.test.ts +105 -0
  303. package/src/providers/openai/chat-completions-provider.ts +47 -9
  304. package/src/providers/openai/coerce-object-args.ts +104 -0
  305. package/src/providers/retry.ts +8 -5
  306. package/src/providers/types.ts +10 -0
  307. package/src/runtime/__tests__/agent-wake.test.ts +629 -7
  308. package/src/runtime/access-request-helper.ts +16 -9
  309. package/src/runtime/agent-wake.ts +302 -51
  310. package/src/runtime/assistant-stream-state.ts +141 -8
  311. package/src/runtime/background-job-runner.ts +9 -0
  312. package/src/runtime/channel-approval-types.ts +1 -0
  313. package/src/runtime/channel-invite-transports/telegram.ts +6 -5
  314. package/src/runtime/channel-invite-transports/voice.ts +2 -2
  315. package/src/runtime/channel-invite-types.ts +4 -2
  316. package/src/runtime/channel-retry-sweep.ts +19 -41
  317. package/src/runtime/finalize-event-delivery.ts +72 -0
  318. package/src/runtime/guardian-action-message-composer.ts +0 -54
  319. package/src/runtime/http-server.ts +6 -14
  320. package/src/runtime/http-types.ts +0 -1
  321. package/src/runtime/message-composer-types.ts +0 -9
  322. package/src/runtime/middleware/__tests__/rate-limiter.test.ts +63 -0
  323. package/src/runtime/middleware/auth.ts +27 -3
  324. package/src/runtime/middleware/rate-limiter.ts +28 -1
  325. package/src/runtime/migrations/vbundle-builder.ts +6 -5
  326. package/src/runtime/pending-interactions.ts +20 -1
  327. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +232 -173
  328. package/src/runtime/routes/__tests__/plugins-routes.test.ts +18 -0
  329. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +436 -0
  330. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +11 -0
  331. package/src/runtime/routes/approval-routes.ts +35 -8
  332. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -1
  333. package/src/runtime/routes/btw-routes.ts +37 -1
  334. package/src/runtime/routes/channel-delivery-routes.ts +11 -7
  335. package/src/runtime/routes/channel-route-definitions.ts +3 -0
  336. package/src/runtime/routes/channel-route-shared.ts +3 -1
  337. package/src/runtime/routes/consolidation-routes.ts +17 -13
  338. package/src/runtime/routes/conversation-compaction-routes.ts +159 -119
  339. package/src/runtime/routes/conversation-list-routes.ts +41 -4
  340. package/src/runtime/routes/conversation-query-routes.ts +196 -11
  341. package/src/runtime/routes/conversation-routes.ts +15 -1
  342. package/src/runtime/routes/credential-prompt-routes.ts +39 -17
  343. package/src/runtime/routes/empty-state-greeting-cache.ts +65 -0
  344. package/src/runtime/routes/heartbeat-routes.ts +18 -13
  345. package/src/runtime/routes/image-generation-routes.ts +20 -2
  346. package/src/runtime/routes/inbound-message-handler.ts +13 -12
  347. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +32 -31
  348. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +7 -5
  349. package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -21
  350. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +5 -5
  351. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +6 -15
  352. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +1 -1
  353. package/src/runtime/routes/index.ts +6 -0
  354. package/src/runtime/routes/log-export/AGENTS.md +1 -1
  355. package/src/runtime/routes/log-export/workspace-allowlist.ts +1 -1
  356. package/src/runtime/routes/migration-routes.ts +5 -9
  357. package/src/runtime/routes/plugins-routes.ts +26 -0
  358. package/src/runtime/routes/ps-routes.ts +10 -8
  359. package/src/runtime/routes/retrospective-routes.ts +235 -0
  360. package/src/runtime/routes/runs-pagination.ts +75 -0
  361. package/src/runtime/routes/schedule-routes.ts +367 -59
  362. package/src/runtime/routes/sounds-config-routes.ts +239 -0
  363. package/src/runtime/routes/surface-action-routes.ts +84 -4
  364. package/src/runtime/routes/workflow-routes.test.ts +372 -0
  365. package/src/runtime/routes/workflow-routes.ts +363 -0
  366. package/src/runtime/routes/workspace-routes.test.ts +61 -1
  367. package/src/runtime/routes/workspace-routes.ts +26 -1
  368. package/src/runtime/services/__tests__/analyze-conversation.test.ts +38 -0
  369. package/src/runtime/services/analyze-conversation.ts +26 -13
  370. package/src/schedule/inference-profile.ts +28 -0
  371. package/src/schedule/schedule-store.ts +152 -4
  372. package/src/schedule/scheduler-types.ts +6 -0
  373. package/src/schedule/scheduler.ts +96 -0
  374. package/src/security/secret-allowlist.ts +1 -1
  375. package/src/skills/path-classifier.ts +1 -1
  376. package/src/tools/apps/executors.ts +23 -0
  377. package/src/tools/apps/resolve-app-id.ts +42 -0
  378. package/src/tools/browser/browser-execution.ts +9 -11
  379. package/src/tools/credentials/broker.ts +4 -4
  380. package/src/tools/credentials/vault.ts +26 -137
  381. package/src/tools/executor.ts +69 -0
  382. package/src/tools/flag-gated-tools.test.ts +76 -0
  383. package/src/tools/permission-checker.ts +8 -1
  384. package/src/tools/registry.ts +51 -0
  385. package/src/tools/schedule/create.ts +77 -1
  386. package/src/tools/schedule/list.ts +1 -0
  387. package/src/tools/schedule/update.ts +73 -1
  388. package/src/tools/skills/execute.ts +56 -0
  389. package/src/tools/terminal/shell.ts +1 -1
  390. package/src/tools/ui-surface/definitions.ts +91 -2
  391. package/src/tools/workflows/manage-workflows.ts +183 -0
  392. package/src/tools/workflows/run-workflow.test.ts +442 -0
  393. package/src/tools/workflows/run-workflow.ts +88 -0
  394. package/src/types/onboarding-context.ts +2 -0
  395. package/src/usage/attribution.ts +24 -0
  396. package/src/util/canonicalize-identity.ts +12 -3
  397. package/src/util/platform.ts +17 -17
  398. package/src/watcher/__tests__/engine.test.ts +24 -0
  399. package/src/watcher/__tests__/telemetry.test.ts +135 -0
  400. package/src/watcher/engine.ts +7 -0
  401. package/src/watcher/telemetry.ts +74 -0
  402. package/src/workflows/capabilities.test.ts +365 -0
  403. package/src/workflows/capabilities.ts +359 -0
  404. package/src/workflows/deterministic-stringify.ts +27 -0
  405. package/src/workflows/engine-integration.test.ts +656 -0
  406. package/src/workflows/engine.test.ts +1144 -0
  407. package/src/workflows/engine.ts +1078 -0
  408. package/src/workflows/fanout-load.test.ts +168 -0
  409. package/src/workflows/journal-store.test.ts +369 -0
  410. package/src/workflows/journal-store.ts +470 -0
  411. package/src/workflows/leaf-runner.test.ts +704 -0
  412. package/src/workflows/leaf-runner.ts +589 -0
  413. package/src/workflows/library.test.ts +134 -0
  414. package/src/workflows/library.ts +124 -0
  415. package/src/workflows/run-manager.test.ts +711 -0
  416. package/src/workflows/run-manager.ts +593 -0
  417. package/src/workflows/sandbox-escape.test.ts +339 -0
  418. package/src/workflows/sandbox.test.ts +251 -0
  419. package/src/workflows/sandbox.ts +447 -0
  420. package/src/workspace/adaptive-thinking-repair.ts +33 -11
  421. package/src/workspace/migrations/021-move-signals-to-workspace.ts +1 -1
  422. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +1 -1
  423. package/src/workspace/migrations/026-backfill-install-meta.ts +1 -1
  424. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +2 -1
  425. package/src/workspace/migrations/031-drop-user-md.ts +1 -4
  426. package/src/workspace/migrations/048-remove-workspace-hooks.ts +1 -1
  427. package/src/workspace/migrations/056-release-notes-inference-profile-reordering.ts +5 -2
  428. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +1 -1
  429. package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +8 -2
  430. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +41 -14
  431. package/src/workspace/migrations/102-preserve-heartbeat-enabled-for-existing-workspaces.ts +69 -0
  432. package/src/workspace/migrations/103-upgrade-quality-profile-to-opus-4-8.ts +83 -0
  433. package/src/workspace/migrations/104-recheck-adaptive-thinking-model-implied-anthropic.ts +133 -0
  434. package/src/workspace/migrations/registry.ts +6 -0
  435. package/src/workspace/migrations/runner.ts +1 -1
  436. package/tsconfig.json +1 -1
  437. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -200
  438. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -579
  439. package/src/__tests__/guardian-action-store.test.ts +0 -106
  440. package/src/daemon/guardian-action-generators.ts +0 -71
  441. package/src/memory/guardian-action-store.ts +0 -484
  442. package/src/runtime/guardian-action-grant-minter.ts +0 -150
@@ -23,11 +23,23 @@ type JobRow = {
23
23
  status: string;
24
24
  payload: string;
25
25
  };
26
+ type MessageRow = {
27
+ role: string;
28
+ createdAt: number;
29
+ metadata: string | null;
30
+ };
26
31
 
27
32
  let mockConversations: ConvRow[] = [];
28
33
  let mockJobs: JobRow[] = [];
34
+ let mockMessages: Record<string, MessageRow[]> = {};
29
35
  let deletedIds: string[] = [];
30
36
 
37
+ const RETRO_SOURCES = ["memory-retrospective", "memory-retrospective-fork"];
38
+
39
+ function isRetroSource(source: string | null): boolean {
40
+ return source !== null && RETRO_SOURCES.includes(source);
41
+ }
42
+
31
43
  mock.module("../db-connection.js", () => ({
32
44
  getDb: () => ({
33
45
  select: (cols?: Record<string, unknown>) => ({
@@ -60,18 +72,20 @@ mock.module("../db-connection.js", () => ({
60
72
  return { conversationId: convId };
61
73
  });
62
74
  }
63
- // The "all retros" query (used to compute most-recent-per-source
64
- // preservation) requests id + forkParentConversationId + createdAt
65
- // with only the source + isNotNull(forkParent) predicate.
75
+ // The "all retros" query (used to compute the preserved
76
+ // dedup-baseline per source) requests id + source +
77
+ // forkParentConversationId + createdAt with only the source +
78
+ // isNotNull(forkParent) predicate.
66
79
  if (
67
80
  colKeys.includes("forkParentConversationId") &&
68
81
  colKeys.includes("createdAt")
69
82
  ) {
70
83
  return mockConversations
71
- .filter((c) => c.source === "memory-retrospective")
84
+ .filter((c) => isRetroSource(c.source))
72
85
  .filter((c) => c.fork_parent_conversation_id !== null)
73
86
  .map((c) => ({
74
87
  id: c.id,
88
+ source: c.source,
75
89
  forkParentConversationId: c.fork_parent_conversation_id,
76
90
  createdAt: c.created_at,
77
91
  }));
@@ -81,7 +95,7 @@ mock.module("../db-connection.js", () => ({
81
95
  // encoded on the background conversation row) against the set
82
96
  // of source IDs extracted from active jobs.
83
97
  return mockConversations
84
- .filter((c) => c.source === "memory-retrospective")
98
+ .filter((c) => isRetroSource(c.source))
85
99
  .filter(
86
100
  (c) =>
87
101
  c.last_message_at !== null &&
@@ -102,12 +116,22 @@ mock.module("../db-connection.js", () => ({
102
116
 
103
117
  let activeJobSourceConvIds = new Set<string>();
104
118
  let injectedNowMinusOrphanAgeMs = 0;
119
+ let mockKeepSupersededRuns = false;
120
+
121
+ mock.module("../../config/loader.js", () => ({
122
+ getConfig: () => ({
123
+ memory: {
124
+ retrospective: { keepSupersededRuns: mockKeepSupersededRuns },
125
+ },
126
+ }),
127
+ }));
105
128
 
106
129
  mock.module("../conversation-crud.js", () => ({
107
130
  deleteConversation: (id: string) => {
108
131
  deletedIds.push(id);
109
132
  mockConversations = mockConversations.filter((c) => c.id !== id);
110
133
  },
134
+ getMessages: (conversationId: string) => mockMessages[conversationId] ?? [],
111
135
  reserveMessage: mock(async () => ({ id: "msg-reserve" })),
112
136
  }));
113
137
 
@@ -139,14 +163,17 @@ describe("sweepOrphanMemoryRetrospectiveConversations", () => {
139
163
  beforeEach(() => {
140
164
  mockConversations = [];
141
165
  mockJobs = [];
166
+ mockMessages = {};
142
167
  deletedIds = [];
143
168
  activeJobSourceConvIds = new Set();
144
169
  injectedNowMinusOrphanAgeMs = 0;
170
+ mockKeepSupersededRuns = false;
145
171
  });
146
172
 
147
173
  afterEach(() => {
148
174
  mockConversations = [];
149
175
  mockJobs = [];
176
+ mockMessages = {};
150
177
  });
151
178
 
152
179
  test("sweeps an old memory-retrospective orphan that has been superseded by a newer retro for the same source", () => {
@@ -316,4 +343,199 @@ describe("sweepOrphanMemoryRetrospectiveConversations", () => {
316
343
  expect(result.swept).toBe(0);
317
344
  expect(deletedIds).toEqual([]);
318
345
  });
346
+
347
+ test("keepSupersededRuns=true skips the sweep entirely — sweepable orphans are retained", () => {
348
+ mockKeepSupersededRuns = true;
349
+ const now = Date.now();
350
+ injectedNowMinusOrphanAgeMs = now - ORPHAN_AGE_MS;
351
+ // Same shape as the first test's sweepable orphan — with the flag off
352
+ // this would be swept (older retro superseded by a newer one for the
353
+ // same source).
354
+ mockConversations = [
355
+ {
356
+ id: "old-orphan",
357
+ source: "memory-retrospective",
358
+ last_message_at: now - 2 * ORPHAN_AGE_MS,
359
+ fork_parent_conversation_id: "source-A",
360
+ created_at: now - 3 * ORPHAN_AGE_MS,
361
+ },
362
+ {
363
+ id: "newer-retro",
364
+ source: "memory-retrospective",
365
+ last_message_at: now - 90 * 60 * 1000,
366
+ fork_parent_conversation_id: "source-A",
367
+ created_at: now - 2 * ORPHAN_AGE_MS,
368
+ },
369
+ ];
370
+ rebuildActiveJobSet();
371
+
372
+ const result = sweepOrphanMemoryRetrospectiveConversations(now);
373
+
374
+ expect(result.swept).toBe(0);
375
+ expect(deletedIds).toEqual([]);
376
+ });
377
+
378
+ // -------------------------------------------------------------------------
379
+ // Dedup-baseline selection: prefer rows that actually produced output.
380
+ // -------------------------------------------------------------------------
381
+
382
+ /**
383
+ * Build a fork-kind retrospective's message rows: a copied source prefix
384
+ * (stamped with `forkSourceMessageId`, ending in an assistant turn AT the
385
+ * boundary — exercising the strictly-greater check), the post-fork
386
+ * user-role instruction, and optionally the retrospective's own assistant
387
+ * output after the boundary.
388
+ */
389
+ function forkKindMessages(opts: {
390
+ boundaryAt: number;
391
+ withOutput: boolean;
392
+ }): MessageRow[] {
393
+ const rows: MessageRow[] = [
394
+ {
395
+ role: "user",
396
+ createdAt: opts.boundaryAt - 1000,
397
+ metadata: JSON.stringify({ forkSourceMessageId: "src-msg-1" }),
398
+ },
399
+ {
400
+ role: "assistant",
401
+ createdAt: opts.boundaryAt,
402
+ metadata: JSON.stringify({ forkSourceMessageId: "src-msg-2" }),
403
+ },
404
+ {
405
+ role: "user",
406
+ createdAt: opts.boundaryAt + 500,
407
+ metadata: JSON.stringify({ kind: "memory_retrospective_instruction" }),
408
+ },
409
+ ];
410
+ if (opts.withOutput) {
411
+ rows.push({
412
+ role: "assistant",
413
+ createdAt: opts.boundaryAt + 1000,
414
+ metadata: null,
415
+ });
416
+ }
417
+ return rows;
418
+ }
419
+
420
+ test("sweeps an orphan MOST-RECENT fork-kind row (no post-fork output) and preserves an older row WITH output as the dedup baseline", () => {
421
+ const now = Date.now();
422
+ injectedNowMinusOrphanAgeMs = now - ORPHAN_AGE_MS;
423
+ mockConversations = [
424
+ {
425
+ id: "fork-with-output",
426
+ source: "memory-retrospective-fork",
427
+ last_message_at: now - 3 * ORPHAN_AGE_MS,
428
+ fork_parent_conversation_id: "source-A",
429
+ created_at: now - 3 * ORPHAN_AGE_MS,
430
+ },
431
+ // Most recent for source-A, but a crash orphan: its only post-fork
432
+ // message is the instruction — no assistant output after the boundary.
433
+ // The copied prefix DOES contain an assistant turn (at the boundary),
434
+ // which must not count.
435
+ {
436
+ id: "fork-orphan",
437
+ source: "memory-retrospective-fork",
438
+ last_message_at: now - 2 * ORPHAN_AGE_MS,
439
+ fork_parent_conversation_id: "source-A",
440
+ created_at: now - 2 * ORPHAN_AGE_MS,
441
+ },
442
+ ];
443
+ mockMessages = {
444
+ "fork-with-output": forkKindMessages({
445
+ boundaryAt: now - 3 * ORPHAN_AGE_MS,
446
+ withOutput: true,
447
+ }),
448
+ "fork-orphan": forkKindMessages({
449
+ boundaryAt: now - 2 * ORPHAN_AGE_MS,
450
+ withOutput: false,
451
+ }),
452
+ };
453
+ rebuildActiveJobSet();
454
+
455
+ const result = sweepOrphanMemoryRetrospectiveConversations(now);
456
+
457
+ expect(result.swept).toBe(1);
458
+ expect(deletedIds).toEqual(["fork-orphan"]);
459
+ });
460
+
461
+ test("falls back to preserving the plain most-recent row when EVERY row is an orphan", () => {
462
+ const now = Date.now();
463
+ injectedNowMinusOrphanAgeMs = now - ORPHAN_AGE_MS;
464
+ mockConversations = [
465
+ {
466
+ id: "older-orphan",
467
+ source: "memory-retrospective-fork",
468
+ last_message_at: now - 3 * ORPHAN_AGE_MS,
469
+ fork_parent_conversation_id: "source-A",
470
+ created_at: now - 3 * ORPHAN_AGE_MS,
471
+ },
472
+ {
473
+ id: "newest-orphan",
474
+ source: "memory-retrospective-fork",
475
+ last_message_at: now - 2 * ORPHAN_AGE_MS,
476
+ fork_parent_conversation_id: "source-A",
477
+ created_at: now - 2 * ORPHAN_AGE_MS,
478
+ },
479
+ ];
480
+ mockMessages = {
481
+ "older-orphan": forkKindMessages({
482
+ boundaryAt: now - 3 * ORPHAN_AGE_MS,
483
+ withOutput: false,
484
+ }),
485
+ "newest-orphan": forkKindMessages({
486
+ boundaryAt: now - 2 * ORPHAN_AGE_MS,
487
+ withOutput: false,
488
+ }),
489
+ };
490
+ rebuildActiveJobSet();
491
+
492
+ const result = sweepOrphanMemoryRetrospectiveConversations(now);
493
+
494
+ // Current behavior preserved: the most-recent row survives even though
495
+ // it has no output; the older orphan is swept.
496
+ expect(result.swept).toBe(1);
497
+ expect(deletedIds).toEqual(["older-orphan"]);
498
+ });
499
+
500
+ test("legacy-kind selection: ANY assistant message counts as output (no fork boundary required)", () => {
501
+ const now = Date.now();
502
+ injectedNowMinusOrphanAgeMs = now - ORPHAN_AGE_MS;
503
+ mockConversations = [
504
+ // Older legacy retro that produced output — preserved.
505
+ {
506
+ id: "legacy-with-output",
507
+ source: "memory-retrospective",
508
+ last_message_at: now - 3 * ORPHAN_AGE_MS,
509
+ fork_parent_conversation_id: "source-A",
510
+ created_at: now - 3 * ORPHAN_AGE_MS,
511
+ },
512
+ // Most-recent legacy retro with no assistant message — a wake that
513
+ // never produced output. Swept.
514
+ {
515
+ id: "legacy-orphan",
516
+ source: "memory-retrospective",
517
+ last_message_at: now - 2 * ORPHAN_AGE_MS,
518
+ fork_parent_conversation_id: "source-A",
519
+ created_at: now - 2 * ORPHAN_AGE_MS,
520
+ },
521
+ ];
522
+ mockMessages = {
523
+ "legacy-with-output": [
524
+ // Legacy rows carry no forkSourceMessageId stamps — any assistant
525
+ // message qualifies.
526
+ {
527
+ role: "assistant",
528
+ createdAt: now - 3 * ORPHAN_AGE_MS,
529
+ metadata: null,
530
+ },
531
+ ],
532
+ "legacy-orphan": [],
533
+ };
534
+ rebuildActiveJobSet();
535
+
536
+ const result = sweepOrphanMemoryRetrospectiveConversations(now);
537
+
538
+ expect(result.swept).toBe(1);
539
+ expect(deletedIds).toEqual(["legacy-orphan"]);
540
+ });
319
541
  });
@@ -0,0 +1,195 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import { eq } from "drizzle-orm";
4
+
5
+ import { makeMockLogger } from "../../__tests__/helpers/mock-logger.js";
6
+
7
+ mock.module("../../util/logger.js", () => ({
8
+ getLogger: () => makeMockLogger(),
9
+ }));
10
+
11
+ mock.module("../../config/loader.js", () => ({
12
+ getConfig: () => ({
13
+ ui: {},
14
+ model: "test",
15
+ provider: "test",
16
+ memory: { enabled: false },
17
+ rateLimit: { maxRequestsPerMinute: 0 },
18
+ secretDetection: { enabled: false },
19
+ }),
20
+ }));
21
+
22
+ import { createConversation } from "../conversation-crud.js";
23
+ import { getDb } from "../db-connection.js";
24
+ import { initializeDb } from "../db-init.js";
25
+ import {
26
+ appendToRememberedLog,
27
+ bumpRetrospectiveLastRunAt,
28
+ forkRetrospectiveState,
29
+ getRetrospectiveState,
30
+ REMEMBERED_LOG_MAX_BYTES,
31
+ REMEMBERED_LOG_MAX_ENTRIES,
32
+ upsertRetrospectiveState,
33
+ } from "../memory-retrospective-state.js";
34
+ import { memoryRetrospectiveState } from "../schema.js";
35
+
36
+ initializeDb();
37
+
38
+ function resetTables(): void {
39
+ const db = getDb();
40
+ db.delete(memoryRetrospectiveState).run();
41
+ db.run("DELETE FROM messages");
42
+ db.run("DELETE FROM conversations");
43
+ }
44
+
45
+ describe("appendToRememberedLog", () => {
46
+ test("appends new entries after existing ones, order preserved", () => {
47
+ expect(appendToRememberedLog(["a", "b"], ["c"])).toEqual(["a", "b", "c"]);
48
+ expect(appendToRememberedLog([], ["x"])).toEqual(["x"]);
49
+ expect(appendToRememberedLog(["x"], [])).toEqual(["x"]);
50
+ });
51
+
52
+ test("entry cap keeps the most recent entries", () => {
53
+ const existing = Array.from({ length: 99 }, (_, i) => `old-${i}`);
54
+ const result = appendToRememberedLog(existing, ["new-1", "new-2", "new-3"]);
55
+
56
+ expect(result).toHaveLength(REMEMBERED_LOG_MAX_ENTRIES);
57
+ // Oldest entries dropped first.
58
+ expect(result[0]).toBe("old-2");
59
+ expect(result.at(-1)).toBe("new-3");
60
+ });
61
+
62
+ test("byte cap binds before the entry cap when entries are large", () => {
63
+ // 20 entries of ~1 KB each — well under the entry cap but over 8 KB.
64
+ const entries = Array.from(
65
+ { length: 20 },
66
+ (_, i) => `${i}-${"x".repeat(1024)}`,
67
+ );
68
+ const result = appendToRememberedLog([], entries);
69
+
70
+ expect(result.length).toBeLessThan(entries.length);
71
+ expect(result.length).toBeGreaterThan(0);
72
+ expect(
73
+ Buffer.byteLength(JSON.stringify(result), "utf8"),
74
+ ).toBeLessThanOrEqual(REMEMBERED_LOG_MAX_BYTES);
75
+ // Most recent entries survive.
76
+ expect(result.at(-1)).toBe(entries.at(-1));
77
+ });
78
+
79
+ test("a single entry larger than the byte cap yields an empty log", () => {
80
+ const huge = "y".repeat(REMEMBERED_LOG_MAX_BYTES + 1);
81
+ expect(appendToRememberedLog([], [huge])).toEqual([]);
82
+ });
83
+ });
84
+
85
+ describe("memory-retrospective-state remembered log persistence", () => {
86
+ beforeEach(() => {
87
+ resetTables();
88
+ });
89
+
90
+ test("upsert persists the log and getRetrospectiveState parses it back", () => {
91
+ const conv = createConversation("Log thread");
92
+ upsertRetrospectiveState({
93
+ conversationId: conv.id,
94
+ lastProcessedMessageId: "m1",
95
+ lastRunAt: 1000,
96
+ rememberedLog: ["save one", "save two"],
97
+ });
98
+
99
+ const state = getRetrospectiveState(conv.id);
100
+ expect(state?.rememberedLog).toEqual(["save one", "save two"]);
101
+ expect(state?.lastProcessedMessageId).toBe("m1");
102
+ });
103
+
104
+ test("upsert without rememberedLog preserves the stored log", () => {
105
+ const conv = createConversation("Preserve thread");
106
+ upsertRetrospectiveState({
107
+ conversationId: conv.id,
108
+ lastProcessedMessageId: "m1",
109
+ lastRunAt: 1000,
110
+ rememberedLog: ["keep me"],
111
+ });
112
+ upsertRetrospectiveState({
113
+ conversationId: conv.id,
114
+ lastProcessedMessageId: "m2",
115
+ lastRunAt: 2000,
116
+ });
117
+
118
+ const state = getRetrospectiveState(conv.id);
119
+ expect(state?.lastProcessedMessageId).toBe("m2");
120
+ expect(state?.rememberedLog).toEqual(["keep me"]);
121
+ });
122
+
123
+ test("missing/NULL column value parses as an empty log", () => {
124
+ const conv = createConversation("Null-log thread");
125
+ upsertRetrospectiveState({
126
+ conversationId: conv.id,
127
+ lastProcessedMessageId: "m1",
128
+ lastRunAt: 1000,
129
+ });
130
+
131
+ expect(getRetrospectiveState(conv.id)?.rememberedLog).toEqual([]);
132
+ });
133
+
134
+ test("malformed stored JSON degrades to an empty log instead of throwing", () => {
135
+ const conv = createConversation("Corrupt thread");
136
+ upsertRetrospectiveState({
137
+ conversationId: conv.id,
138
+ lastProcessedMessageId: "m1",
139
+ lastRunAt: 1000,
140
+ rememberedLog: ["x"],
141
+ });
142
+ getDb()
143
+ .update(memoryRetrospectiveState)
144
+ .set({ rememberedLog: "not-json{{" })
145
+ .where(eq(memoryRetrospectiveState.conversationId, conv.id))
146
+ .run();
147
+
148
+ expect(getRetrospectiveState(conv.id)?.rememberedLog).toEqual([]);
149
+ });
150
+
151
+ test("bumpRetrospectiveLastRunAt seeds the empty-string sentinel with an empty log and preserves an existing log", () => {
152
+ // Failure-only row: "" sentinel, empty log.
153
+ const fresh = createConversation("Failure-only thread");
154
+ bumpRetrospectiveLastRunAt(fresh.id, 1000);
155
+ const freshState = getRetrospectiveState(fresh.id);
156
+ expect(freshState?.lastProcessedMessageId).toBe("");
157
+ expect(freshState?.rememberedLog).toEqual([]);
158
+
159
+ // Existing row with a log: a failure bump must not clobber it.
160
+ const seasoned = createConversation("Seasoned thread");
161
+ upsertRetrospectiveState({
162
+ conversationId: seasoned.id,
163
+ lastProcessedMessageId: "m1",
164
+ lastRunAt: 1000,
165
+ rememberedLog: ["survives failures"],
166
+ });
167
+ bumpRetrospectiveLastRunAt(seasoned.id, 2000);
168
+ const seasonedState = getRetrospectiveState(seasoned.id);
169
+ expect(seasonedState?.lastRunAt).toBe(2000);
170
+ expect(seasonedState?.rememberedLog).toEqual(["survives failures"]);
171
+ });
172
+
173
+ test("forkRetrospectiveState copies the log verbatim to the forked child", () => {
174
+ const source = createConversation("Fork source");
175
+ const child = createConversation("Fork child");
176
+ upsertRetrospectiveState({
177
+ conversationId: source.id,
178
+ lastProcessedMessageId: "",
179
+ lastRunAt: 1000,
180
+ rememberedLog: ["parent baseline"],
181
+ });
182
+
183
+ forkRetrospectiveState({
184
+ database: getDb(),
185
+ sourceConversationId: source.id,
186
+ forkedConversationId: child.id,
187
+ forkedMessageIds: new Map(),
188
+ lastCopiedSourceMessageId: null,
189
+ });
190
+
191
+ expect(getRetrospectiveState(child.id)?.rememberedLog).toEqual([
192
+ "parent baseline",
193
+ ]);
194
+ });
195
+ });
@@ -0,0 +1,85 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
10
+
11
+ import { setOverridesForTesting } from "../../__tests__/feature-flag-test-helpers.js";
12
+ import type { AssistantConfig } from "../../config/schema.js";
13
+ import { getAppsDir } from "../app-store.js";
14
+ import {
15
+ ACTIVATION_FLOW_FLAG,
16
+ PERSONAL_PAGE_APP_ID,
17
+ PERSONAL_PAGE_ARM,
18
+ seedPreloadedApps,
19
+ } from "../preloaded-apps.js";
20
+
21
+ // The flag resolver reads the override cache and registry; the config
22
+ // argument is part of the signature but not consulted for resolution.
23
+ const config = {} as AssistantConfig;
24
+
25
+ function definitionPath(): string {
26
+ return join(getAppsDir(), `${PERSONAL_PAGE_APP_ID}.json`);
27
+ }
28
+
29
+ function appDir(): string {
30
+ return join(getAppsDir(), PERSONAL_PAGE_APP_ID);
31
+ }
32
+
33
+ beforeEach(() => {
34
+ setOverridesForTesting({});
35
+ });
36
+
37
+ afterEach(() => {
38
+ setOverridesForTesting({});
39
+ rmSync(definitionPath(), { force: true });
40
+ rmSync(appDir(), { recursive: true, force: true });
41
+ });
42
+
43
+ describe("seedPreloadedApps gating", () => {
44
+ test("does not seed on the control arm", async () => {
45
+ setOverridesForTesting({ [ACTIVATION_FLOW_FLAG]: "control" });
46
+ await seedPreloadedApps(config);
47
+ expect(existsSync(definitionPath())).toBe(false);
48
+ expect(existsSync(appDir())).toBe(false);
49
+ });
50
+
51
+ test("does not seed on other treatment arms", async () => {
52
+ setOverridesForTesting({ [ACTIVATION_FLOW_FLAG]: "variant-a" });
53
+ await seedPreloadedApps(config);
54
+ expect(existsSync(definitionPath())).toBe(false);
55
+ });
56
+
57
+ test("does not seed when the flag resolves to the registry default", async () => {
58
+ // No override → declared default "control".
59
+ await seedPreloadedApps(config);
60
+ expect(existsSync(definitionPath())).toBe(false);
61
+ });
62
+
63
+ test("leaves an already-seeded, compiled app untouched", async () => {
64
+ setOverridesForTesting({ [ACTIVATION_FLOW_FLAG]: PERSONAL_PAGE_ARM });
65
+
66
+ const sentinelDefinition = JSON.stringify({
67
+ id: PERSONAL_PAGE_APP_ID,
68
+ userEdited: true,
69
+ });
70
+ mkdirSync(join(appDir(), "dist"), { recursive: true });
71
+ writeFileSync(definitionPath(), sentinelDefinition, "utf-8");
72
+ writeFileSync(
73
+ join(appDir(), "dist", "index.html"),
74
+ "<!-- compiled -->",
75
+ "utf-8",
76
+ );
77
+
78
+ await seedPreloadedApps(config);
79
+
80
+ // Seeding must not re-copy the template or rewrite the definition once
81
+ // the app exists — its content is considered user-owned.
82
+ expect(readFileSync(definitionPath(), "utf-8")).toBe(sentinelDefinition);
83
+ expect(existsSync(join(appDir(), "src"))).toBe(false);
84
+ });
85
+ });
@@ -7,6 +7,7 @@ import {
7
7
  import { getLogger } from "../util/logger.js";
8
8
  import { isAutoAnalysisConversation } from "./auto-analysis-guard.js";
9
9
  import { isMemoryEnabled, upsertAutoAnalysisJob } from "./jobs-store.js";
10
+ import { isMemoryRetrospectiveConversation } from "./memory-retrospective-enqueue.js";
10
11
 
11
12
  const log = getLogger("auto-analysis-enqueue");
12
13
 
@@ -31,7 +32,11 @@ export type AutoAnalysisTrigger = "batch" | "idle" | "lifecycle" | "compaction";
31
32
  * conversation. Skips silently when:
32
33
  * - the `auto-analyze` feature flag is disabled, OR
33
34
  * - the source conversation is itself an auto-analysis conversation
34
- * (recursion guard — we never analyze our own analysis output).
35
+ * (recursion guard — we never analyze our own analysis output), OR
36
+ * - the source conversation is a memory-retrospective conversation.
37
+ * Fork-kind retrospectives carry a full copy of the source
38
+ * conversation's history, so auto-analyzing one would re-process the
39
+ * entire source conversation and double-write memory.
35
40
  *
36
41
  * Immediate triggers (`"batch"`, `"compaction"`) and debounced triggers
37
42
  * (`"idle"`, `"lifecycle"`) are written to separate rows keyed by a
@@ -73,6 +78,14 @@ export function enqueueAutoAnalysisIfEnabled(args: {
73
78
  return;
74
79
  }
75
80
 
81
+ if (isMemoryRetrospectiveConversation(conversationId)) {
82
+ log.debug(
83
+ { conversationId, trigger },
84
+ "Skipping auto-analysis enqueue: source is a memory-retrospective conversation",
85
+ );
86
+ return;
87
+ }
88
+
76
89
  const idleTimeoutMs = config.analysis?.idleTimeoutMs ?? 600_000;
77
90
  const runImmediately = trigger === "batch" || trigger === "compaction";
78
91
  const triggerGroup: "immediate" | "debounced" = runImmediately
@@ -213,10 +213,12 @@ export class ClickHouseCompactionLogStore {
213
213
 
214
214
  /**
215
215
  * Read all compaction attempts whose `started_at` falls strictly inside
216
- * the given window, pairing start/end rows by `compaction_id`. Mirrors
217
- * the strict `>` / `<` predicate contract of
218
- * `getCompactionLogsBetween` on the llm-request-log sources, so the
219
- * route can apply the same ±1ms boundary shifts to both.
216
+ * the open window `(afterStartedAt, beforeStartedAt)`, pairing start/end
217
+ * rows by `compaction_id`. Mirrors the strict `>` / `<` predicate
218
+ * contract of `getCompactionLogsBetween` on the llm-request-log sources
219
+ * so the compaction route can scope both stores to the same call window
220
+ * (floor = previous real call's `createdAt`, ceiling = selected call's
221
+ * `createdAt`).
220
222
  */
221
223
  async getEventsBetween(
222
224
  conversationId: string,
@@ -731,6 +731,13 @@ export function findAnalysisConversationFor(
731
731
  * Once the fork accumulates its own retros, those are found at the first
732
732
  * iteration and we never walk up.
733
733
  *
734
+ * The returned `forkParentConversationId` is the conversation the
735
+ * retrospective row is rooted at — `parentConversationId` itself when it has
736
+ * its own retros, or an ancestor when the chain walk found the row higher
737
+ * up. Callers that mutate the prior (e.g. GC of superseded runs) must check
738
+ * it against the source conversation: an ancestor's row is that ancestor's
739
+ * dedup baseline, not the caller's to delete.
740
+ *
734
741
  * Returns `null` when no prior retrospective exists anywhere in the fork
735
742
  * chain (true first-run case).
736
743
  *
@@ -741,7 +748,7 @@ const MAX_FORK_CHAIN_DEPTH = 16;
741
748
 
742
749
  export function findMostRecentRetrospectiveFor(
743
750
  parentConversationId: string,
744
- ): { id: string } | null {
751
+ ): { id: string; forkParentConversationId: string } | null {
745
752
  const db = getDb();
746
753
  let currentId: string | null = parentConversationId;
747
754
  for (let depth = 0; depth < MAX_FORK_CHAIN_DEPTH && currentId; depth++) {
@@ -757,7 +764,7 @@ export function findMostRecentRetrospectiveFor(
757
764
  .orderBy(desc(conversations.createdAt))
758
765
  .limit(1)
759
766
  .get();
760
- if (row) return { id: row.id };
767
+ if (row) return { id: row.id, forkParentConversationId: currentId };
761
768
 
762
769
  const parent = db
763
770
  .select({
@@ -2,7 +2,7 @@
2
2
  * Write-through disk view for conversations.
3
3
  *
4
4
  * Projects conversation metadata, messages, and attachments to a browsable
5
- * filesystem layout under ~/.vellum/workspace/conversations/. This enables
5
+ * filesystem layout under $VELLUM_WORKSPACE_DIR/conversations/. This enables
6
6
  * the assistant to search/read/manipulate conversation data using standard
7
7
  * file tools.
8
8
  *