@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
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Tests for the compaction-trail route + projection.
2
+ * Tests for the compaction route + projection.
3
3
  *
4
4
  * The handler is exercised against a fake `LlmRequestLogSource` so we
5
5
  * can pin (a) the BadRequestError / NotFoundError branches that don't
6
- * involve any logs, and (b) the happy path that returns a projected
7
- * trail with the expected shape. The projection function is unit-tested
8
- * directly so its branches stay covered even if the handler ever swaps
9
- * to a different log source.
6
+ * involve any logs, and (b) the happy path that returns the projected
7
+ * compaction(s) with the expected shape. The projection functions are
8
+ * unit-tested directly so their branches stay covered even if the
9
+ * handler ever swaps to a different log source.
10
10
  */
11
11
 
12
12
  import { beforeEach, describe, expect, mock, test } from "bun:test";
@@ -35,13 +35,17 @@ mock.module("../../../config/loader.js", () => ({
35
35
  // ---------------------------------------------------------------------
36
36
 
37
37
  import type { CompactionLogEvent } from "../../../memory/compaction-log-store-clickhouse.js";
38
- import type { LogRow } from "../../../memory/llm-request-log-store.js";
38
+ import type {
39
+ CompactionAgentLogRow,
40
+ LogMetaRow,
41
+ } from "../../../memory/llm-request-log-store.js";
39
42
 
40
43
  interface FakeSourceState {
41
44
  conversation: { id: string } | null;
42
- selectedCall: LogRow | null;
43
- turnBounds: { startTime: number; endTime: number } | null;
44
- compactionLogs: LogRow[];
45
+ selectedCall: LogMetaRow | null;
46
+ /** `createdAt` of the previous real call, or null = no earlier call. */
47
+ previousNonCompactionCallCreatedAt: number | null;
48
+ compactionLogs: CompactionAgentLogRow[];
45
49
  /** null = compactionLogs destination not configured (legacy-only). */
46
50
  compactionStoreEvents: CompactionLogEvent[] | null;
47
51
  compactionStoreError: Error | null;
@@ -50,7 +54,7 @@ interface FakeSourceState {
50
54
  const state: FakeSourceState = {
51
55
  conversation: null,
52
56
  selectedCall: null,
53
- turnBounds: null,
57
+ previousNonCompactionCallCreatedAt: null,
54
58
  compactionLogs: [],
55
59
  compactionStoreEvents: null,
56
60
  compactionStoreError: null,
@@ -59,10 +63,10 @@ const state: FakeSourceState = {
59
63
  // Records the inputs the handler passed to its collaborators so tests
60
64
  // can pin the windowing plumbing without relying on the projection.
61
65
  const sourceCalls = {
62
- getRequestLogByIdArgs: [] as string[],
63
- getTurnTimeBoundsArgs: [] as Array<{
66
+ getRequestLogMetaByIdArgs: [] as string[],
67
+ getPreviousNonCompactionCallCreatedAtArgs: [] as Array<{
64
68
  conversationId: string;
65
- messageCreatedAt: number;
69
+ beforeCreatedAt: number;
66
70
  }>,
67
71
  getCompactionLogsBetweenArgs: [] as Array<{
68
72
  conversationId: string;
@@ -102,24 +106,28 @@ mock.module("../../../memory/conversation-crud.js", () => ({
102
106
  state.conversation && state.conversation.id === id
103
107
  ? state.conversation
104
108
  : null,
105
- getTurnTimeBounds: (conversationId: string, messageCreatedAt: number) => {
106
- sourceCalls.getTurnTimeBoundsArgs.push({
107
- conversationId,
108
- messageCreatedAt,
109
- });
110
- return state.turnBounds;
111
- },
112
109
  reserveMessage: mock(async () => ({ id: "msg-reserve" })),
113
110
  }));
114
111
 
115
112
  mock.module("../../../memory/llm-request-log-source.js", () => ({
116
113
  getLlmRequestLogSource: async () => ({
117
- getRequestLogById: async (id: string) => {
118
- sourceCalls.getRequestLogByIdArgs.push(id);
114
+ getRequestLogById: async () => null,
115
+ getRequestLogMetaById: async (id: string) => {
116
+ sourceCalls.getRequestLogMetaByIdArgs.push(id);
119
117
  return state.selectedCall;
120
118
  },
121
119
  getRequestLogsByMessageId: async () => [],
122
120
  getRequestLogsByConversationId: async () => [],
121
+ getPreviousNonCompactionCallCreatedAt: async (
122
+ conversationId: string,
123
+ beforeCreatedAt: number,
124
+ ) => {
125
+ sourceCalls.getPreviousNonCompactionCallCreatedAtArgs.push({
126
+ conversationId,
127
+ beforeCreatedAt,
128
+ });
129
+ return state.previousNonCompactionCallCreatedAt;
130
+ },
123
131
  getCompactionLogsBetween: async (
124
132
  conversationId: string,
125
133
  afterCreatedAt: number | null,
@@ -150,14 +158,12 @@ const handler = route.handler as (
150
158
  args: Record<string, unknown>,
151
159
  ) => Promise<{ conversationId: string; events: unknown[] }>;
152
160
 
153
- function fakeLogRow(overrides: Partial<LogRow> = {}): LogRow {
161
+ function fakeLogMetaRow(overrides: Partial<LogMetaRow> = {}): LogMetaRow {
154
162
  return {
155
163
  id: "log-default",
156
164
  conversationId: "conv-default",
157
165
  messageId: null,
158
166
  provider: "anthropic",
159
- requestPayload: "{}",
160
- responsePayload: "{}",
161
167
  createdAt: 1000,
162
168
  agentLoopExitReason: null,
163
169
  callSite: null,
@@ -165,6 +171,17 @@ function fakeLogRow(overrides: Partial<LogRow> = {}): LogRow {
165
171
  };
166
172
  }
167
173
 
174
+ function fakeCompactionRow(
175
+ overrides: Partial<CompactionAgentLogRow> = {},
176
+ ): CompactionAgentLogRow {
177
+ return {
178
+ ...fakeLogMetaRow(),
179
+ responsePayload: "{}",
180
+ requestMessageCount: null,
181
+ ...overrides,
182
+ };
183
+ }
184
+
168
185
  function fakeCompactionLogEvent(
169
186
  overrides: Partial<CompactionLogEvent> = {},
170
187
  ): CompactionLogEvent {
@@ -203,12 +220,12 @@ function fakeCompactionLogEvent(
203
220
  beforeEach(() => {
204
221
  state.conversation = null;
205
222
  state.selectedCall = null;
206
- state.turnBounds = null;
223
+ state.previousNonCompactionCallCreatedAt = null;
207
224
  state.compactionLogs = [];
208
225
  state.compactionStoreEvents = null;
209
226
  state.compactionStoreError = null;
210
- sourceCalls.getRequestLogByIdArgs.length = 0;
211
- sourceCalls.getTurnTimeBoundsArgs.length = 0;
227
+ sourceCalls.getRequestLogMetaByIdArgs.length = 0;
228
+ sourceCalls.getPreviousNonCompactionCallCreatedAtArgs.length = 0;
212
229
  sourceCalls.getCompactionLogsBetweenArgs.length = 0;
213
230
  sourceCalls.getEventsBetweenArgs.length = 0;
214
231
  });
@@ -267,7 +284,7 @@ describe("handleGetCompactionTrail — request-shape errors", () => {
267
284
 
268
285
  test("throws BadRequestError when callId belongs to a different conversation", async () => {
269
286
  state.conversation = { id: "conv-1" };
270
- state.selectedCall = fakeLogRow({
287
+ state.selectedCall = fakeLogMetaRow({
271
288
  id: "call-x",
272
289
  conversationId: "conv-other",
273
290
  createdAt: 1234,
@@ -286,140 +303,129 @@ describe("handleGetCompactionTrail — request-shape errors", () => {
286
303
  // ---------------------------------------------------------------------
287
304
 
288
305
  describe("handleGetCompactionTrail — happy path", () => {
289
- test("forwards the turn window to the source (start - 1 as floor, end + 1 as ceiling)", async () => {
306
+ test("scopes the window to the previous real call (floor) and the selected call (ceiling)", async () => {
307
+ // GIVEN a selected call whose previous real (non-compactionAgent)
308
+ // call ran at 2000
290
309
  state.conversation = { id: "conv-1" };
291
- state.selectedCall = fakeLogRow({
310
+ state.selectedCall = fakeLogMetaRow({
292
311
  id: "call-selected",
293
312
  conversationId: "conv-1",
294
313
  createdAt: 5000,
295
314
  });
296
- // Selected call sits inside a turn that runs [2000, 9000].
297
- state.turnBounds = { startTime: 2000, endTime: 9000 };
315
+ state.previousNonCompactionCallCreatedAt = 2000;
298
316
  state.compactionLogs = [];
299
317
 
318
+ // WHEN the handler resolves the compactions for the call
300
319
  await handler({
301
320
  pathParams: { id: "conv-1" },
302
321
  queryParams: { callId: "call-selected" },
303
322
  });
304
323
 
305
- expect(sourceCalls.getRequestLogByIdArgs).toEqual(["call-selected"]);
306
- expect(sourceCalls.getTurnTimeBoundsArgs).toEqual([
307
- { conversationId: "conv-1", messageCreatedAt: 5000 },
324
+ // THEN it anchors the floor on the previous real call and the
325
+ // ceiling on the selected call's own createdAt — strictly, with no
326
+ // boundary fudging.
327
+ expect(sourceCalls.getRequestLogMetaByIdArgs).toEqual(["call-selected"]);
328
+ expect(sourceCalls.getPreviousNonCompactionCallCreatedAtArgs).toEqual([
329
+ { conversationId: "conv-1", beforeCreatedAt: 5000 },
308
330
  ]);
309
- // 1ms-shift around the exclusive `(>, <)` predicate so rows that
310
- // land on the bounds themselves come back.
311
- expect(sourceCalls.getCompactionLogsBetweenArgs).toEqual([
312
- { conversationId: "conv-1", afterCreatedAt: 1999, beforeCreatedAt: 9001 },
313
- ]);
314
- });
315
-
316
- test("scopes the trail to the whole turn — compactions after the selected call are in scope", async () => {
317
- // Selecting an early call in a turn that contains compactions
318
- // *after* the selected call must still surface those later events.
319
- // This is the core promise of turn-scoping: position within the
320
- // turn is irrelevant.
321
- state.conversation = { id: "conv-1" };
322
- state.selectedCall = fakeLogRow({
323
- id: "call-early",
324
- conversationId: "conv-1",
325
- createdAt: 2500,
326
- });
327
- state.turnBounds = { startTime: 2000, endTime: 9000 };
328
- state.compactionLogs = [];
329
-
330
- await handler({
331
- pathParams: { id: "conv-1" },
332
- queryParams: { callId: "call-early" },
333
- });
334
-
335
- // Ceiling is the *turn end*, not the selected call's createdAt.
336
331
  expect(sourceCalls.getCompactionLogsBetweenArgs).toEqual([
337
- { conversationId: "conv-1", afterCreatedAt: 1999, beforeCreatedAt: 9001 },
332
+ { conversationId: "conv-1", afterCreatedAt: 2000, beforeCreatedAt: 5000 },
338
333
  ]);
339
334
  });
340
335
 
341
- test("falls back to a null floor + selectedCall.createdAt ceiling when getTurnTimeBounds returns null", async () => {
342
- // The only-message-in-conversation edge case. Preserves the
343
- // pre-turn-scoping behavior for this degenerate input.
336
+ test("uses an open (null) floor when the selected call is the first real call", async () => {
337
+ // GIVEN a selected call with no earlier real call in the conversation
344
338
  state.conversation = { id: "conv-1" };
345
- state.selectedCall = fakeLogRow({
346
- id: "call-solo",
339
+ state.selectedCall = fakeLogMetaRow({
340
+ id: "call-first",
347
341
  conversationId: "conv-1",
348
342
  createdAt: 5000,
349
343
  });
350
- state.turnBounds = null;
344
+ state.previousNonCompactionCallCreatedAt = null;
351
345
  state.compactionLogs = [];
352
346
 
347
+ // WHEN the handler resolves the compactions for the call
353
348
  await handler({
354
349
  pathParams: { id: "conv-1" },
355
- queryParams: { callId: "call-solo" },
350
+ queryParams: { callId: "call-first" },
356
351
  });
357
352
 
353
+ // THEN the floor is dropped so every preceding compaction is in scope
358
354
  expect(sourceCalls.getCompactionLogsBetweenArgs).toEqual([
359
355
  { conversationId: "conv-1", afterCreatedAt: null, beforeCreatedAt: 5000 },
360
356
  ]);
361
357
  });
362
358
 
363
359
  test("returns an empty events list when no compactions ran in the window", async () => {
360
+ // GIVEN a selected call with no compactions attributed to it
364
361
  state.conversation = { id: "conv-1" };
365
- state.selectedCall = fakeLogRow({
362
+ state.selectedCall = fakeLogMetaRow({
366
363
  id: "call-1",
367
364
  conversationId: "conv-1",
368
365
  createdAt: 5000,
369
366
  });
370
- state.turnBounds = { startTime: 2000, endTime: 9000 };
367
+ state.previousNonCompactionCallCreatedAt = 2000;
371
368
  state.compactionLogs = [];
372
369
 
370
+ // WHEN the handler resolves the compactions for the call
373
371
  const result = await handler({
374
372
  pathParams: { id: "conv-1" },
375
373
  queryParams: { callId: "call-1" },
376
374
  });
375
+
376
+ // THEN it returns an empty list, not an error
377
377
  expect(result).toEqual({ conversationId: "conv-1", events: [] });
378
378
  });
379
379
 
380
- test("projects each compaction log to the wire shape", async () => {
380
+ test("projects each compaction log row to the wire shape", async () => {
381
+ // GIVEN two legacy compaction rows attributed to the selected call
381
382
  state.conversation = { id: "conv-1" };
382
- state.selectedCall = fakeLogRow({
383
+ state.selectedCall = fakeLogMetaRow({
383
384
  id: "call-1",
384
385
  conversationId: "conv-1",
385
386
  createdAt: 9000,
386
387
  });
387
- state.turnBounds = { startTime: 500, endTime: 10_000 };
388
+ state.previousNonCompactionCallCreatedAt = 500;
388
389
  state.compactionLogs = [
389
- fakeLogRow({
390
+ fakeCompactionRow({
390
391
  id: "compaction-1",
391
392
  conversationId: "conv-1",
392
393
  createdAt: 1000,
393
394
  }),
394
- fakeLogRow({
395
+ fakeCompactionRow({
395
396
  id: "compaction-2",
396
397
  conversationId: "conv-1",
397
398
  createdAt: 2000,
398
399
  }),
399
400
  ];
400
401
 
402
+ // WHEN the handler resolves the compactions for the call
401
403
  const result = await handler({
402
404
  pathParams: { id: "conv-1" },
403
405
  queryParams: { callId: "call-1" },
404
406
  });
405
407
 
408
+ // THEN it emits one event per row, each carrying the full wire shape
406
409
  expect(result.conversationId).toBe("conv-1");
407
410
  expect(result.events).toHaveLength(2);
408
- // Field-set check on the first event — values are validated by the
409
- // projection tests below; here we just confirm the route emits the
410
- // full wire shape.
411
411
  expect(Object.keys(result.events[0] as object).sort()).toEqual([
412
+ "compacted",
413
+ "compactedMessages",
414
+ "contextTokensAfter",
415
+ "contextTokensBefore",
412
416
  "createdAt",
413
417
  "durationMs",
414
- "estimatedCostUsd",
415
418
  "id",
416
- "inputTokens",
417
- "model",
418
- "outputTokens",
419
- "provider",
420
- "requestMessageCount",
421
- "responsePreview",
422
- "stopReason",
419
+ "messagesAfter",
420
+ "messagesBefore",
421
+ "preservedTailMessages",
422
+ "skipReason",
423
+ "summaryFailed",
424
+ "summaryInputTokens",
425
+ "summaryModel",
426
+ "summaryOutputTokens",
427
+ "summaryText",
428
+ "trigger",
423
429
  ]);
424
430
  });
425
431
  });
@@ -429,27 +435,30 @@ describe("handleGetCompactionTrail — happy path", () => {
429
435
  // ---------------------------------------------------------------------
430
436
 
431
437
  describe("handleGetCompactionTrail — compaction log store", () => {
432
- test("serves the trail from the store and skips the legacy projection", async () => {
438
+ test("serves the compactions from the store and skips the legacy projection", async () => {
439
+ // GIVEN the compaction-log store is configured and has a completed event
433
440
  state.conversation = { id: "conv-1" };
434
- state.selectedCall = fakeLogRow({
441
+ state.selectedCall = fakeLogMetaRow({
435
442
  id: "call-1",
436
443
  conversationId: "conv-1",
437
444
  createdAt: 5000,
438
445
  });
439
- state.turnBounds = { startTime: 2000, endTime: 9000 };
446
+ state.previousNonCompactionCallCreatedAt = 2000;
440
447
  state.compactionStoreEvents = [fakeCompactionLogEvent()];
441
448
 
449
+ // WHEN the handler resolves the compactions for the call
442
450
  const result = await handler({
443
451
  pathParams: { id: "conv-1" },
444
452
  queryParams: { callId: "call-1" },
445
453
  });
446
454
 
447
- // Same ±1ms-shifted turn window as the legacy path.
455
+ // THEN it queries the store with the same call window and never
456
+ // touches the legacy projection
448
457
  expect(sourceCalls.getEventsBetweenArgs).toEqual([
449
458
  {
450
459
  conversationId: "conv-1",
451
- afterStartedAt: 1999,
452
- beforeStartedAt: 9001,
460
+ afterStartedAt: 2000,
461
+ beforeStartedAt: 5000,
453
462
  },
454
463
  ]);
455
464
  expect(sourceCalls.getCompactionLogsBetweenArgs).toEqual([]);
@@ -457,37 +466,46 @@ describe("handleGetCompactionTrail — compaction log store", () => {
457
466
  {
458
467
  id: "comp-1",
459
468
  createdAt: 3000,
460
- model: "test-model",
461
- provider: null,
462
- inputTokens: 880,
463
- outputTokens: 120,
469
+ trigger: "budget",
470
+ compacted: true,
471
+ summaryFailed: false,
472
+ skipReason: "auto",
473
+ contextTokensBefore: 900,
474
+ contextTokensAfter: 300,
475
+ messagesBefore: 12,
476
+ messagesAfter: 4,
477
+ compactedMessages: 10,
478
+ preservedTailMessages: 2,
464
479
  durationMs: 500,
465
- responsePreview: "summary text",
466
- requestMessageCount: 12,
467
- stopReason: "auto",
468
- estimatedCostUsd: null,
480
+ summaryModel: "test-model",
481
+ summaryInputTokens: 880,
482
+ summaryOutputTokens: 120,
483
+ summaryText: "summary text",
469
484
  },
470
485
  ]);
471
486
  });
472
487
 
473
488
  test("falls back to the legacy projection when the store has no rows for the window", async () => {
489
+ // GIVEN the store is configured but returns no events for this call
474
490
  state.conversation = { id: "conv-1" };
475
- state.selectedCall = fakeLogRow({
491
+ state.selectedCall = fakeLogMetaRow({
476
492
  id: "call-1",
477
493
  conversationId: "conv-1",
478
494
  createdAt: 5000,
479
495
  });
480
- state.turnBounds = { startTime: 2000, endTime: 9000 };
496
+ state.previousNonCompactionCallCreatedAt = 2000;
481
497
  state.compactionStoreEvents = [];
482
498
  state.compactionLogs = [
483
- fakeLogRow({ id: "compaction-legacy", conversationId: "conv-1" }),
499
+ fakeCompactionRow({ id: "compaction-legacy", conversationId: "conv-1" }),
484
500
  ];
485
501
 
502
+ // WHEN the handler resolves the compactions for the call
486
503
  const result = await handler({
487
504
  pathParams: { id: "conv-1" },
488
505
  queryParams: { callId: "call-1" },
489
506
  });
490
507
 
508
+ // THEN it falls through to the legacy projection
491
509
  expect(sourceCalls.getEventsBetweenArgs).toHaveLength(1);
492
510
  expect(sourceCalls.getCompactionLogsBetweenArgs).toHaveLength(1);
493
511
  expect(result.events).toHaveLength(1);
@@ -495,36 +513,40 @@ describe("handleGetCompactionTrail — compaction log store", () => {
495
513
  });
496
514
 
497
515
  test("falls back to the legacy projection when the store read throws", async () => {
516
+ // GIVEN the store read fails (e.g. ClickHouse unreachable)
498
517
  state.conversation = { id: "conv-1" };
499
- state.selectedCall = fakeLogRow({
518
+ state.selectedCall = fakeLogMetaRow({
500
519
  id: "call-1",
501
520
  conversationId: "conv-1",
502
521
  createdAt: 5000,
503
522
  });
504
- state.turnBounds = { startTime: 2000, endTime: 9000 };
523
+ state.previousNonCompactionCallCreatedAt = 2000;
505
524
  state.compactionStoreError = new Error("clickhouse unreachable");
506
525
  state.compactionLogs = [
507
- fakeLogRow({ id: "compaction-legacy", conversationId: "conv-1" }),
526
+ fakeCompactionRow({ id: "compaction-legacy", conversationId: "conv-1" }),
508
527
  ];
509
528
 
529
+ // WHEN the handler resolves the compactions for the call
510
530
  const result = await handler({
511
531
  pathParams: { id: "conv-1" },
512
532
  queryParams: { callId: "call-1" },
513
533
  });
514
534
 
535
+ // THEN it falls through to the legacy projection
515
536
  expect(sourceCalls.getCompactionLogsBetweenArgs).toHaveLength(1);
516
537
  expect(result.events).toHaveLength(1);
517
538
  expect((result.events[0] as { id: string }).id).toBe("compaction-legacy");
518
539
  });
519
540
 
520
541
  test("falls back to the legacy projection when any event is missing its end row", async () => {
542
+ // GIVEN one of the store events never wrote its end row (no finishedAt)
521
543
  state.conversation = { id: "conv-1" };
522
- state.selectedCall = fakeLogRow({
544
+ state.selectedCall = fakeLogMetaRow({
523
545
  id: "call-1",
524
546
  conversationId: "conv-1",
525
547
  createdAt: 5000,
526
548
  });
527
- state.turnBounds = { startTime: 2000, endTime: 9000 };
549
+ state.previousNonCompactionCallCreatedAt = 2000;
528
550
  state.compactionStoreEvents = [
529
551
  fakeCompactionLogEvent(),
530
552
  fakeCompactionLogEvent({
@@ -535,14 +557,17 @@ describe("handleGetCompactionTrail — compaction log store", () => {
535
557
  }),
536
558
  ];
537
559
  state.compactionLogs = [
538
- fakeLogRow({ id: "compaction-legacy", conversationId: "conv-1" }),
560
+ fakeCompactionRow({ id: "compaction-legacy", conversationId: "conv-1" }),
539
561
  ];
540
562
 
563
+ // WHEN the handler resolves the compactions for the call
541
564
  const result = await handler({
542
565
  pathParams: { id: "conv-1" },
543
566
  queryParams: { callId: "call-1" },
544
567
  });
545
568
 
569
+ // THEN it falls through to the legacy projection rather than serving
570
+ // an event with null counts/duration
546
571
  expect(sourceCalls.getEventsBetweenArgs).toHaveLength(1);
547
572
  expect(sourceCalls.getCompactionLogsBetweenArgs).toHaveLength(1);
548
573
  expect(result.events).toHaveLength(1);
@@ -556,30 +581,58 @@ describe("handleGetCompactionTrail — compaction log store", () => {
556
581
 
557
582
  describe("projectCompactionLogEventToTrailEvent", () => {
558
583
  test("maps a completed event onto the wire shape", () => {
584
+ // GIVEN a completed compaction-log event
585
+ // WHEN it is projected to the wire shape
559
586
  const event = projectCompactionLogEventToTrailEvent(
560
587
  fakeCompactionLogEvent(),
561
588
  );
589
+
590
+ // THEN the headline figures are the context reduction and the
591
+ // summarizer's own usage is carried separately
562
592
  expect(event).toEqual({
563
593
  id: "comp-1",
564
594
  createdAt: 3000,
565
- model: "test-model",
566
- provider: null,
567
- inputTokens: 880,
568
- outputTokens: 120,
595
+ trigger: "budget",
596
+ compacted: true,
597
+ summaryFailed: false,
598
+ skipReason: "auto",
599
+ contextTokensBefore: 900,
600
+ contextTokensAfter: 300,
601
+ messagesBefore: 12,
602
+ messagesAfter: 4,
603
+ compactedMessages: 10,
604
+ preservedTailMessages: 2,
569
605
  durationMs: 500,
570
- responsePreview: "summary text",
571
- requestMessageCount: 12,
572
- stopReason: "auto",
573
- estimatedCostUsd: null,
606
+ summaryModel: "test-model",
607
+ summaryInputTokens: 880,
608
+ summaryOutputTokens: 120,
609
+ summaryText: "summary text",
574
610
  });
575
611
  });
576
612
 
577
- test("maps an incomplete (start-only) event with nulls for end-phase fields", () => {
613
+ test("maps an empty trigger to null", () => {
614
+ // GIVEN an event whose trigger column is the empty-string default
615
+ // WHEN it is projected
616
+ const event = projectCompactionLogEventToTrailEvent(
617
+ fakeCompactionLogEvent({ trigger: "" }),
618
+ );
619
+
620
+ // THEN the empty string is normalized to the "not known" sentinel
621
+ expect(event.trigger).toBeNull();
622
+ });
623
+
624
+ test("carries through null end-phase fields on an incomplete event", () => {
625
+ // GIVEN a start-only event (no end row written yet)
626
+ // WHEN it is projected
578
627
  const event = projectCompactionLogEventToTrailEvent(
579
628
  fakeCompactionLogEvent({
580
629
  completed: false,
581
630
  finishedAt: null,
582
631
  durationMs: null,
632
+ compacted: null,
633
+ summaryFailed: null,
634
+ resultMessageCount: null,
635
+ estimatedInputTokens: null,
583
636
  summaryInputTokens: null,
584
637
  summaryOutputTokens: null,
585
638
  summaryModel: null,
@@ -587,65 +640,66 @@ describe("projectCompactionLogEventToTrailEvent", () => {
587
640
  summaryText: null,
588
641
  }),
589
642
  );
643
+
644
+ // THEN the end-phase fields are null while the start-phase
645
+ // pre-compaction figures survive
590
646
  expect(event.durationMs).toBeNull();
591
- expect(event.model).toBeNull();
592
- expect(event.inputTokens).toBeNull();
593
- expect(event.outputTokens).toBeNull();
594
- expect(event.responsePreview).toBeNull();
595
- expect(event.stopReason).toBeNull();
596
- // The start row always carries the pre-compaction message count.
597
- expect(event.requestMessageCount).toBe(12);
647
+ expect(event.compacted).toBeNull();
648
+ expect(event.contextTokensAfter).toBeNull();
649
+ expect(event.messagesAfter).toBeNull();
650
+ expect(event.summaryModel).toBeNull();
651
+ expect(event.summaryText).toBeNull();
652
+ expect(event.contextTokensBefore).toBe(900);
653
+ expect(event.messagesBefore).toBe(12);
598
654
  });
599
655
  });
600
656
 
601
657
  describe("projectLogRowToCompactionTrailEvent", () => {
602
- test("returns null for every summary-derived field when payloads are empty", () => {
603
- const event = projectLogRowToCompactionTrailEvent(fakeLogRow());
658
+ test("returns null for every field the legacy row can't recover", () => {
659
+ // GIVEN a compaction-agent row with empty payloads
660
+ // WHEN it is projected
661
+ const event = projectLogRowToCompactionTrailEvent(fakeCompactionRow());
662
+
663
+ // THEN only id/createdAt survive; everything the row doesn't carry
664
+ // lands as null
604
665
  expect(event.id).toBe("log-default");
605
666
  expect(event.createdAt).toBe(1000);
606
- expect(event.model).toBeNull();
607
- expect(event.inputTokens).toBeNull();
608
- expect(event.outputTokens).toBeNull();
609
- expect(event.responsePreview).toBeNull();
610
- expect(event.requestMessageCount).toBeNull();
611
- expect(event.stopReason).toBeNull();
612
- expect(event.estimatedCostUsd).toBeNull();
613
- });
614
-
615
- test("always returns null for durationMs (column not yet recorded)", () => {
616
- // Even if a future payload shape carries duration info, the
617
- // projection deliberately drops it — the gap is what surfaces to
618
- // the UI as "Unavailable" and informs the data-model decision.
619
- const event = projectLogRowToCompactionTrailEvent(
620
- fakeLogRow({
621
- responsePayload: JSON.stringify({
622
- // A made-up shape with a duration value to confirm the
623
- // projection ignores it.
624
- durationMs: 1234,
625
- duration_ms: 1234,
626
- }),
627
- }),
628
- );
667
+ expect(event.trigger).toBeNull();
668
+ expect(event.compacted).toBeNull();
669
+ expect(event.summaryFailed).toBeNull();
670
+ expect(event.skipReason).toBeNull();
671
+ expect(event.contextTokensBefore).toBeNull();
672
+ expect(event.contextTokensAfter).toBeNull();
673
+ expect(event.messagesBefore).toBeNull();
674
+ expect(event.messagesAfter).toBeNull();
675
+ expect(event.compactedMessages).toBeNull();
676
+ expect(event.preservedTailMessages).toBeNull();
629
677
  expect(event.durationMs).toBeNull();
678
+ expect(event.summaryModel).toBeNull();
679
+ expect(event.summaryInputTokens).toBeNull();
680
+ expect(event.summaryOutputTokens).toBeNull();
681
+ expect(event.summaryText).toBeNull();
630
682
  });
631
683
 
632
- test("falls back to the stored provider when the normalizer can't infer one", () => {
633
- // Empty payloads => normalizer returns no summary => fallback to
634
- // the row's stored `provider` column.
684
+ test("maps the SQL-computed request message count to compactedMessages", () => {
685
+ // GIVEN a row whose request payload held 7 messages (fed to the
686
+ // summarizer, i.e. the messages that were compacted)
687
+ // WHEN it is projected
635
688
  const event = projectLogRowToCompactionTrailEvent(
636
- fakeLogRow({ provider: "anthropic" }),
689
+ fakeCompactionRow({ requestMessageCount: 7 }),
637
690
  );
638
- expect(event.provider).toBe("anthropic");
691
+
692
+ // THEN that count surfaces as compactedMessages
693
+ expect(event.compactedMessages).toBe(7);
639
694
  });
640
695
 
641
- test("extracts model + inputTokens + stopReason from a real Anthropic payload", () => {
696
+ test("extracts the summary model + token usage + text from an Anthropic payload", () => {
697
+ // GIVEN a row carrying a real Anthropic summarizer response
698
+ // WHEN it is projected
642
699
  const event = projectLogRowToCompactionTrailEvent(
643
- fakeLogRow({
700
+ fakeCompactionRow({
644
701
  provider: "anthropic",
645
- requestPayload: JSON.stringify({
646
- model: "claude-sonnet-4-5",
647
- messages: [{ role: "user", content: "Summarize the prior context." }],
648
- }),
702
+ requestMessageCount: 1,
649
703
  responsePayload: JSON.stringify({
650
704
  type: "message",
651
705
  model: "claude-sonnet-4-5",
@@ -656,22 +710,27 @@ describe("projectLogRowToCompactionTrailEvent", () => {
656
710
  }),
657
711
  }),
658
712
  );
659
- expect(event.model).toBe("claude-sonnet-4-5");
660
- expect(event.provider).toBe("anthropic");
661
- expect(event.inputTokens).toBe(12_000);
662
- expect(event.outputTokens).toBe(800);
663
- expect(event.stopReason).toBe("end_turn");
713
+
714
+ // THEN the summarizer's own model + usage + text are recovered
715
+ expect(event.summaryModel).toBe("claude-sonnet-4-5");
716
+ expect(event.summaryInputTokens).toBe(12_000);
717
+ expect(event.summaryOutputTokens).toBe(800);
718
+ expect(event.summaryText).toBe("Summary text…");
719
+ expect(event.compactedMessages).toBe(1);
664
720
  });
665
721
 
666
722
  test("tolerates non-JSON payloads without throwing (falls back to nulls)", () => {
723
+ // GIVEN a row with a malformed (non-JSON) response payload
724
+ // WHEN it is projected
667
725
  const event = projectLogRowToCompactionTrailEvent(
668
- fakeLogRow({
669
- requestPayload: "not-json{{{",
726
+ fakeCompactionRow({
670
727
  responsePayload: "<html>nope</html>",
671
728
  }),
672
729
  );
730
+
731
+ // THEN it degrades to nulls instead of throwing
673
732
  expect(event.id).toBe("log-default");
674
- expect(event.model).toBeNull();
675
- expect(event.inputTokens).toBeNull();
733
+ expect(event.summaryModel).toBeNull();
734
+ expect(event.summaryInputTokens).toBeNull();
676
735
  });
677
736
  });