@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
@@ -7,7 +7,8 @@
7
7
  * double typed as `Conversation` (`makeWakeConversation`) that stubs only the
8
8
  * handful of members the wake touches — `getMessages`, `messages.push`,
9
9
  * `isProcessing`/`setProcessing`, `setTrustContext`, `setSubagentAllowedTools`,
10
- * `drainQueue`, and a scripted `agentLoop.run()`.
10
+ * `drainQueue`, `maybeCompact`, `contextWindowManager.estimateInputTokens`,
11
+ * and a scripted `agentLoop.run()`.
11
12
  *
12
13
  * The wake's side effects flow through the daemon boundary, so the
13
14
  * instrumentation is captured at that boundary: event emission and the
@@ -49,6 +50,12 @@ interface WakeConversationProbe {
49
50
  requestId?: string;
50
51
  trust?: unknown;
51
52
  allowedTools?: string[];
53
+ /**
54
+ * `conversation.wakePersonaOverride` as observed at run start — the
55
+ * field the conversation's resolveSystemPrompt callback reads when
56
+ * building the wake's system prompt.
57
+ */
58
+ personaOverride?: unknown;
52
59
  order: number;
53
60
  }>;
54
61
  /** Every `setProcessing` value, in call order. */
@@ -76,8 +83,27 @@ interface WakeConversationProbe {
76
83
  allowedToolSnapshots: Array<string[] | undefined>;
77
84
  /** `setTrustContext` calls, with the value and a monotonic order tag. */
78
85
  setTrustContextCalls: Array<{ ctx: unknown; order: number }>;
86
+ /**
87
+ * Every assignment to `conversation.wakePersonaOverride`, in order. A
88
+ * wake that applies an override records `[override, undefined]` — the
89
+ * trailing `undefined` proves the wake restored the field before
90
+ * releasing the conversation.
91
+ */
92
+ personaOverrideSets: unknown[];
79
93
  /** Number of persisted tail messages at the moment each frame emitted. */
80
94
  persistedAtEachEmit: number[];
95
+ /**
96
+ * `conversation.maybeCompact()` invocations (the wake's pre-run
97
+ * auto-compaction gate), tagged with the same monotonic order counter as
98
+ * `runCalls` so tests can prove the gate ran before the agent loop.
99
+ */
100
+ maybeCompactOrders: number[];
101
+ /**
102
+ * The sizing argument passed to each `conversation.maybeCompact()` call —
103
+ * the wake threads its resolved callSite/profile so the gate's threshold
104
+ * sizes against the wake's window instead of mainAgent's.
105
+ */
106
+ maybeCompactSizings: unknown[];
81
107
  }
82
108
 
83
109
  const wakeConvRegistry = new Map<string, WakeConversationProbe>();
@@ -89,8 +115,15 @@ const wakeConvRegistry = new Map<string, WakeConversationProbe>();
89
115
  // `persistWakeTailMessage` (createdAt for the disk-view sync). `addMessage`
90
116
  // is the persistence boundary the wake's tail persistence flows through —
91
117
  // it records into the originating conversation's probe.
118
+ // Overridable per test (e.g. to simulate a throwing DB read); reset in
119
+ // beforeEach.
120
+ let mockGetConversationOverrideProfile: (
121
+ conversationId: string,
122
+ ) => string | undefined = () => undefined;
123
+
92
124
  mock.module("../../memory/conversation-crud.js", () => ({
93
- getConversationOverrideProfile: () => undefined,
125
+ getConversationOverrideProfile: (conversationId: string) =>
126
+ mockGetConversationOverrideProfile(conversationId),
94
127
  getConversation: () => ({
95
128
  archivedAt: null,
96
129
  createdAt: "2026-01-01T00:00:00.000Z",
@@ -209,6 +242,7 @@ const recordRequestLogCalls: Array<{
209
242
  responsePayload: string;
210
243
  messageId?: string;
211
244
  provider?: string;
245
+ callSite?: string;
212
246
  }> = [];
213
247
  mock.module("../../memory/llm-request-log-store.js", () => ({
214
248
  recordRequestLog: (
@@ -217,6 +251,7 @@ mock.module("../../memory/llm-request-log-store.js", () => ({
217
251
  responsePayload: string,
218
252
  messageId?: string,
219
253
  provider?: string,
254
+ callSite?: string,
220
255
  ) => {
221
256
  recordRequestLogCalls.push({
222
257
  conversationId,
@@ -224,6 +259,7 @@ mock.module("../../memory/llm-request-log-store.js", () => ({
224
259
  responsePayload,
225
260
  messageId,
226
261
  provider,
262
+ callSite,
227
263
  });
228
264
  return "log-id-test";
229
265
  },
@@ -236,7 +272,7 @@ import type {
236
272
  AgentLoopRunResult,
237
273
  } from "../../agent/loop.js";
238
274
  import type { Conversation } from "../../daemon/conversation.js";
239
- import type { Message } from "../../providers/types.js";
275
+ import { ContextOverflowError, type Message } from "../../providers/types.js";
240
276
  import {
241
277
  __resetWakeChainForTests,
242
278
  wakeAgentForOpportunity,
@@ -276,6 +312,13 @@ function makeWakeConversation(options: {
276
312
  initialAllowedTools?: Set<string>;
277
313
  /** Replaces the default scripted `agentLoop.run` body entirely. */
278
314
  runImpl?: ScriptedRun;
315
+ /**
316
+ * Token estimate returned by the double's
317
+ * `contextWindowManager.estimateInputTokens` (consumed by the wake's
318
+ * over-window pre-flight on compaction-suppressed wakes). Defaults to 0
319
+ * (always under the mocked 200k window).
320
+ */
321
+ estimatedInputTokens?: number;
279
322
  }): WakeConversation {
280
323
  const conversationId = options.conversationId ?? "conv-test";
281
324
  const probe: WakeConversationProbe = {
@@ -290,13 +333,17 @@ function makeWakeConversation(options: {
290
333
  processingDuringDrain: [],
291
334
  allowedToolSnapshots: [],
292
335
  setTrustContextCalls: [],
336
+ personaOverrideSets: [],
293
337
  persistedAtEachEmit: [],
338
+ maybeCompactOrders: [],
339
+ maybeCompactSizings: [],
294
340
  };
295
341
  wakeConvRegistry.set(conversationId, probe);
296
342
 
297
343
  let processing = options.isProcessing ?? false;
298
344
  let order = 0;
299
345
  let activeAllowedTools = options.initialAllowedTools;
346
+ let wakePersonaOverride: unknown;
300
347
  const snapshotAllowedTools = (): string[] | undefined =>
301
348
  activeAllowedTools ? [...activeAllowedTools].sort() : undefined;
302
349
 
@@ -359,6 +406,13 @@ function makeWakeConversation(options: {
359
406
  `tools:${snapshotAllowedTools()?.join(",") ?? "all"}`,
360
407
  );
361
408
  },
409
+ get wakePersonaOverride() {
410
+ return wakePersonaOverride;
411
+ },
412
+ set wakePersonaOverride(value: unknown) {
413
+ wakePersonaOverride = value;
414
+ probe.personaOverrideSets.push(value);
415
+ },
362
416
  agentLoop: {
363
417
  run: async (options: AgentLoopRunOptions) => {
364
418
  const { messages: input, onEvent } = options;
@@ -367,6 +421,7 @@ function makeWakeConversation(options: {
367
421
  requestId: options.requestId,
368
422
  trust: options.trust,
369
423
  allowedTools: snapshotAllowedTools(),
424
+ personaOverride: wakePersonaOverride,
370
425
  order: order++,
371
426
  });
372
427
  return runBody(input, onEvent, options);
@@ -383,6 +438,19 @@ function makeWakeConversation(options: {
383
438
  setTrustContext: (ctx: unknown) => {
384
439
  probe.setTrustContextCalls.push({ ctx, order: order++ });
385
440
  },
441
+ // Pre-run auto-compaction gate. The double only records the call (and
442
+ // the sizing argument) — compaction side effects are exercised in the
443
+ // Conversation tests.
444
+ maybeCompact: async (sizing?: unknown) => {
445
+ probe.maybeCompactOrders.push(order++);
446
+ probe.maybeCompactSizings.push(sizing);
447
+ probe.callSequence.push("maybeCompact");
448
+ return null;
449
+ },
450
+ // Consumed by the wake's over-window pre-flight (suppressed wakes only).
451
+ contextWindowManager: {
452
+ estimateInputTokens: () => options.estimatedInputTokens ?? 0,
453
+ },
386
454
  getTurnChannelContext: () => null,
387
455
  getTurnInterfaceContext: () => null,
388
456
  trustContext: undefined,
@@ -398,6 +466,7 @@ beforeEach(() => {
398
466
  recordRequestLogCalls.length = 0;
399
467
  mockGetOrCreateConversationCalls.length = 0;
400
468
  mockResolverTarget = null;
469
+ mockGetConversationOverrideProfile = () => undefined;
401
470
  mockDiskPressureStatus = {
402
471
  enabled: false,
403
472
  state: "disabled",
@@ -553,6 +622,116 @@ describe("wakeAgentForOpportunity", () => {
553
622
  });
554
623
  });
555
624
 
625
+ test("personaOverride is applied to the conversation for the run and restored after", async () => {
626
+ const conversation = makeWakeConversation({
627
+ scriptedAssistant: {
628
+ role: "assistant",
629
+ content: [{ type: "text", text: "reviewed." }],
630
+ },
631
+ });
632
+ const override = { userSlug: "alice", channelSlug: "telegram" };
633
+
634
+ const result = await wakeAgentForOpportunity(
635
+ {
636
+ conversationId: conversation.conversationId,
637
+ hint: "retrospective pass",
638
+ source: "memory-retrospective",
639
+ trustContext: { sourceChannel: "vellum", trustClass: "guardian" },
640
+ personaOverride: override,
641
+ },
642
+ { resolveTarget: async () => conversation },
643
+ );
644
+
645
+ expect(result.invoked).toBe(true);
646
+ // The override was live on the conversation when the loop ran — the
647
+ // resolveSystemPrompt callback reads this field at prompt-build time.
648
+ expect(conversation.runCalls[0]!.personaOverride).toEqual(override);
649
+ // Applied exactly once and cleared before the wake released the
650
+ // conversation, so a queued user turn can't build under it.
651
+ expect(conversation.personaOverrideSets).toEqual([override, undefined]);
652
+ });
653
+
654
+ test("personaOverride is cleared even when the agent loop throws", async () => {
655
+ const conversation = makeWakeConversation({
656
+ runImpl: async () => {
657
+ throw new Error("loop exploded");
658
+ },
659
+ });
660
+ const override = { userSlug: "alice" };
661
+
662
+ const result = await wakeAgentForOpportunity(
663
+ {
664
+ conversationId: conversation.conversationId,
665
+ hint: "retrospective pass",
666
+ source: "memory-retrospective",
667
+ personaOverride: override,
668
+ },
669
+ { resolveTarget: async () => conversation },
670
+ );
671
+
672
+ expect(result).toEqual({ invoked: true, producedToolCalls: false });
673
+ expect(conversation.personaOverrideSets).toEqual([override, undefined]);
674
+ });
675
+
676
+ test("a throwing profile read never strands the persona override on the conversation", async () => {
677
+ // The override-profile lookup (and the config/window reads next to it)
678
+ // run BEFORE the try/finally that clears the wake's persona override. If
679
+ // the override were assigned before those reads, a throw there would
680
+ // strand it on the cached Conversation and corrupt every later prompt
681
+ // build. The wake must assign the override only after the reads succeed.
682
+ const conversation = makeWakeConversation({
683
+ scriptedAssistant: {
684
+ role: "assistant",
685
+ content: [{ type: "text", text: "never reached" }],
686
+ },
687
+ });
688
+ mockGetConversationOverrideProfile = () => {
689
+ throw new Error("profile read failed");
690
+ };
691
+
692
+ await expect(
693
+ wakeAgentForOpportunity(
694
+ {
695
+ conversationId: conversation.conversationId,
696
+ hint: "retrospective pass",
697
+ source: "memory-retrospective",
698
+ personaOverride: { userSlug: "alice" },
699
+ },
700
+ { resolveTarget: async () => conversation },
701
+ ),
702
+ ).rejects.toThrow("profile read failed");
703
+
704
+ // The override was never assigned (not assigned-then-cleared) and the
705
+ // conversation field is clean for the next turn's prompt build.
706
+ expect(conversation.personaOverrideSets).toEqual([]);
707
+ expect(conversation.wakePersonaOverride).toBeUndefined();
708
+ // The processing flag was never stranded either — the reads run before
709
+ // setProcessing(true).
710
+ expect(conversation.processingToggles).toEqual([]);
711
+ expect(conversation.runCalls).toHaveLength(0);
712
+ });
713
+
714
+ test("no personaOverride → the conversation's persona field is never touched", async () => {
715
+ const conversation = makeWakeConversation({
716
+ scriptedAssistant: {
717
+ role: "assistant",
718
+ content: [{ type: "text", text: "hello" }],
719
+ },
720
+ });
721
+
722
+ await wakeAgentForOpportunity(
723
+ {
724
+ conversationId: conversation.conversationId,
725
+ hint: "plain wake",
726
+ source: "background-tool",
727
+ },
728
+ { resolveTarget: async () => conversation },
729
+ );
730
+
731
+ expect(conversation.runCalls[0]!.personaOverride).toBeUndefined();
732
+ expect(conversation.personaOverrideSets).toEqual([]);
733
+ });
734
+
556
735
  test("silent no-op when agent produces no tool calls and no text", async () => {
557
736
  const conversation = makeWakeConversation({
558
737
  baseline: [
@@ -647,6 +826,99 @@ describe("wakeAgentForOpportunity", () => {
647
826
  expect(processingFalseIndex).toBeLessThan(drainIndex);
648
827
  });
649
828
 
829
+ test("applies toolGateMode: 'execution' alongside the allowlist and restores it after the wake", async () => {
830
+ let gateModeDuringRun: string | undefined;
831
+ const conversation = makeWakeConversation({
832
+ runImpl: async (input) => {
833
+ gateModeDuringRun = conversation.subagentToolGateMode;
834
+ return runResult([
835
+ ...input,
836
+ { role: "assistant", content: [{ type: "text", text: "Saved." }] },
837
+ ]);
838
+ },
839
+ });
840
+
841
+ const result = await wakeAgentForOpportunity(
842
+ {
843
+ conversationId: conversation.conversationId,
844
+ hint: "review for memories",
845
+ source: "memory-retrospective",
846
+ allowedTools: ["remember"],
847
+ toolGateMode: "execution",
848
+ },
849
+ { resolveTarget: async () => conversation },
850
+ );
851
+
852
+ expect(result).toEqual({ invoked: true, producedToolCalls: false });
853
+ // The gate mode is live on the conversation for the duration of the run
854
+ // (the tool resolver and executor closures read it there)...
855
+ expect(gateModeDuringRun).toBe("execution");
856
+ expect(conversation.runCalls[0]!.allowedTools).toEqual(["remember"]);
857
+ // ...and restored alongside the allowlist after the wake.
858
+ expect(conversation.subagentToolGateMode).toBeUndefined();
859
+ });
860
+
861
+ test("applies toolContextPin alongside the allowlist and restores it after the wake", async () => {
862
+ let pinDuringRun: unknown;
863
+ const conversation = makeWakeConversation({
864
+ runImpl: async (input) => {
865
+ pinDuringRun = conversation.toolContextPin;
866
+ return runResult([
867
+ ...input,
868
+ { role: "assistant", content: [{ type: "text", text: "Saved." }] },
869
+ ]);
870
+ },
871
+ });
872
+
873
+ const result = await wakeAgentForOpportunity(
874
+ {
875
+ conversationId: conversation.conversationId,
876
+ hint: "review for memories",
877
+ source: "memory-retrospective",
878
+ allowedTools: ["remember"],
879
+ toolGateMode: "execution",
880
+ toolContextPin: { hasNoClient: false, transportInterface: "macos" },
881
+ },
882
+ { resolveTarget: async () => conversation },
883
+ );
884
+
885
+ expect(result).toEqual({ invoked: true, producedToolCalls: false });
886
+ // The pin is live on the conversation for the duration of the run (the
887
+ // tool resolver reads it there for wire-definition parity)...
888
+ expect(pinDuringRun).toEqual({
889
+ hasNoClient: false,
890
+ transportInterface: "macos",
891
+ });
892
+ // ...and restored alongside the allowlist + gate mode after the wake.
893
+ expect(conversation.toolContextPin).toBeUndefined();
894
+ });
895
+
896
+ test("defaults to the wire gate mode when toolGateMode is absent", async () => {
897
+ let gateModeDuringRun: string | undefined;
898
+ const conversation = makeWakeConversation({
899
+ runImpl: async (input) => {
900
+ gateModeDuringRun = conversation.subagentToolGateMode;
901
+ return runResult([
902
+ ...input,
903
+ { role: "assistant", content: [{ type: "text", text: "Saved." }] },
904
+ ]);
905
+ },
906
+ });
907
+
908
+ await wakeAgentForOpportunity(
909
+ {
910
+ conversationId: conversation.conversationId,
911
+ hint: "review for memories",
912
+ source: "memory-retrospective",
913
+ allowedTools: ["remember"],
914
+ },
915
+ { resolveTarget: async () => conversation },
916
+ );
917
+
918
+ expect(gateModeDuringRun).toBe("wire");
919
+ expect(conversation.subagentToolGateMode).toBeUndefined();
920
+ });
921
+
650
922
  test("restores allowed tools before drain when the wake is a silent no-op", async () => {
651
923
  const conversation = makeWakeConversation({
652
924
  scriptedAssistant: {
@@ -1233,11 +1505,13 @@ describe("wakeAgentForOpportunity", () => {
1233
1505
  { resolveTarget: async () => conversation },
1234
1506
  );
1235
1507
 
1236
- // Full call sequence: processing toggled true → 3 pushes
1237
- // 3 persists → processing toggled false → drain. Specifically,
1238
- // every push and every persist must precede the single drain.
1508
+ // Full call sequence: processing toggled true → compaction gate
1509
+ // 3 pushes → 3 persists → processing toggled false → drain.
1510
+ // Specifically, every push and every persist must precede the
1511
+ // single drain.
1239
1512
  expect(conversation.callSequence).toEqual([
1240
1513
  "processing:true",
1514
+ "maybeCompact",
1241
1515
  "push",
1242
1516
  "push",
1243
1517
  "push",
@@ -1294,9 +1568,11 @@ describe("wakeAgentForOpportunity", () => {
1294
1568
  expect(conversation.emittedEvents).toHaveLength(0);
1295
1569
 
1296
1570
  // But drain still ran exactly once, after processing flipped to
1297
- // false. Sequence: toggle true → toggle false → drain.
1571
+ // false. Sequence: toggle true → compaction gate → toggle false →
1572
+ // drain.
1298
1573
  expect(conversation.callSequence).toEqual([
1299
1574
  "processing:true",
1575
+ "maybeCompact",
1300
1576
  "processing:false",
1301
1577
  "drain",
1302
1578
  ]);
@@ -1578,7 +1854,46 @@ describe("wakeAgentForOpportunity", () => {
1578
1854
  conversationId: conversation.conversationId,
1579
1855
  provider: "test-provider",
1580
1856
  messageId: undefined,
1857
+ callSite: "mainAgent",
1858
+ });
1859
+ });
1860
+
1861
+ test("wake with an explicit callSite records logs under that call site", async () => {
1862
+ const usageEvent: AgentEvent = {
1863
+ type: "usage",
1864
+ inputTokens: 100,
1865
+ outputTokens: 5,
1866
+ model: "test-model",
1867
+ actualProvider: "test-provider",
1868
+ providerDurationMs: 10,
1869
+ rawRequest: { request: "retrospective wake" },
1870
+ rawResponse: { response: "real reply" },
1871
+ };
1872
+ const conversation = makeWakeConversation({
1873
+ baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
1874
+ scriptedEvents: [usageEvent],
1875
+ scriptedAssistant: {
1876
+ role: "assistant",
1877
+ content: [{ type: "text", text: "real reply" }],
1878
+ },
1581
1879
  });
1880
+
1881
+ const result = await wakeAgentForOpportunity(
1882
+ {
1883
+ conversationId: conversation.conversationId,
1884
+ hint: "do reply",
1885
+ source: "unit-test",
1886
+ callSite: "memoryRetrospective",
1887
+ },
1888
+ { resolveTarget: async () => conversation },
1889
+ );
1890
+
1891
+ expect(result).toEqual({ invoked: true, producedToolCalls: false });
1892
+ expect(recordRequestLogCalls).toHaveLength(1);
1893
+ // Regression guard: persistLog used to hardcode "mainAgent", which
1894
+ // mislabeled retrospective/consolidation wake rows in llm_request_logs
1895
+ // and polluted call-site filtering during prompt diagnostics.
1896
+ expect(recordRequestLogCalls[0]?.callSite).toBe("memoryRetrospective");
1582
1897
  });
1583
1898
 
1584
1899
  test("non-serializable usage payload does not abort the wake", async () => {
@@ -1774,4 +2089,311 @@ describe("wakeAgentForOpportunity", () => {
1774
2089
  },
1775
2090
  );
1776
2091
  });
2092
+
2093
+ describe("suppressAutoCompaction + over-window policy", () => {
2094
+ const reply: Message = {
2095
+ role: "assistant",
2096
+ content: [{ type: "text", text: "done" }],
2097
+ };
2098
+
2099
+ test("default wake runs the pre-run compaction gate before the agent loop", async () => {
2100
+ const conversation = makeWakeConversation({
2101
+ scriptedAssistant: reply,
2102
+ });
2103
+
2104
+ const result = await wakeAgentForOpportunity(
2105
+ {
2106
+ conversationId: conversation.conversationId,
2107
+ hint: "do work",
2108
+ source: "unit-test",
2109
+ },
2110
+ { resolveTarget: async () => conversation },
2111
+ );
2112
+
2113
+ expect(result.invoked).toBe(true);
2114
+ expect(conversation.maybeCompactOrders).toHaveLength(1);
2115
+ expect(conversation.runCalls).toHaveLength(1);
2116
+ // The gate fired strictly before agentLoop.run (shared order counter).
2117
+ expect(conversation.maybeCompactOrders[0]!).toBeLessThan(
2118
+ conversation.runCalls[0]!.order,
2119
+ );
2120
+ // And under the processing marker, so a racing user send queues
2121
+ // instead of starting a concurrent turn during the summary call.
2122
+ expect(conversation.callSequence.indexOf("maybeCompact")).toBeGreaterThan(
2123
+ conversation.callSequence.indexOf("processing:true"),
2124
+ );
2125
+ });
2126
+
2127
+ test("suppressAutoCompaction: true skips the compaction gate on an otherwise identical wake", async () => {
2128
+ const conversation = makeWakeConversation({
2129
+ scriptedAssistant: reply,
2130
+ });
2131
+
2132
+ const result = await wakeAgentForOpportunity(
2133
+ {
2134
+ conversationId: conversation.conversationId,
2135
+ hint: "do work",
2136
+ source: "unit-test",
2137
+ suppressAutoCompaction: true,
2138
+ },
2139
+ { resolveTarget: async () => conversation },
2140
+ );
2141
+
2142
+ expect(result.invoked).toBe(true);
2143
+ expect(conversation.maybeCompactOrders).toHaveLength(0);
2144
+ expect(conversation.runCalls).toHaveLength(1);
2145
+ });
2146
+
2147
+ test("over-window suppressed wake fails fast without invoking the loop or compacting", async () => {
2148
+ const conversation = makeWakeConversation({
2149
+ scriptedAssistant: reply,
2150
+ // Mocked resolveEffectiveContextWindow yields maxInputTokens 200k.
2151
+ estimatedInputTokens: 250_000,
2152
+ });
2153
+
2154
+ const result = await wakeAgentForOpportunity(
2155
+ {
2156
+ conversationId: conversation.conversationId,
2157
+ hint: "do work",
2158
+ source: "unit-test",
2159
+ suppressAutoCompaction: true,
2160
+ },
2161
+ { resolveTarget: async () => conversation },
2162
+ );
2163
+
2164
+ expect(result).toEqual({
2165
+ invoked: false,
2166
+ producedToolCalls: false,
2167
+ reason: "context_overflow",
2168
+ });
2169
+ expect(conversation.runCalls).toHaveLength(0);
2170
+ expect(conversation.maybeCompactOrders).toHaveLength(0);
2171
+ // The processing marker is released and the queue drained despite the
2172
+ // early failure.
2173
+ expect(conversation.processingToggles).toEqual([true, false]);
2174
+ expect(conversation.isProcessing()).toBe(false);
2175
+ expect(conversation.drainQueueCalls).toBe(1);
2176
+ });
2177
+
2178
+ test("identical over-window wake without suppression compacts and runs instead of failing", async () => {
2179
+ const conversation = makeWakeConversation({
2180
+ scriptedAssistant: reply,
2181
+ estimatedInputTokens: 250_000,
2182
+ });
2183
+
2184
+ const result = await wakeAgentForOpportunity(
2185
+ {
2186
+ conversationId: conversation.conversationId,
2187
+ hint: "do work",
2188
+ source: "unit-test",
2189
+ },
2190
+ { resolveTarget: async () => conversation },
2191
+ );
2192
+
2193
+ expect(result.invoked).toBe(true);
2194
+ expect(conversation.maybeCompactOrders).toHaveLength(1);
2195
+ expect(conversation.runCalls).toHaveLength(1);
2196
+ });
2197
+
2198
+ test("suppressed wake maps a provider context-overflow rejection to a failed result", async () => {
2199
+ // The estimator under-counted (pre-flight passed) but the provider
2200
+ // still rejected the call as over-window. The loop swallows provider
2201
+ // errors into a graceful no-output stop, which without the mapping
2202
+ // would read as a successful silent no-op.
2203
+ const conversation = makeWakeConversation({
2204
+ estimatedInputTokens: 0,
2205
+ runImpl: async (input, onEvent) => {
2206
+ await onEvent({
2207
+ type: "error",
2208
+ error: new ContextOverflowError(
2209
+ "prompt is too long: 250000 tokens > 200000 maximum",
2210
+ "test-provider",
2211
+ ),
2212
+ });
2213
+ return runResult([...input]);
2214
+ },
2215
+ });
2216
+
2217
+ const result = await wakeAgentForOpportunity(
2218
+ {
2219
+ conversationId: conversation.conversationId,
2220
+ hint: "do work",
2221
+ source: "unit-test",
2222
+ suppressAutoCompaction: true,
2223
+ },
2224
+ { resolveTarget: async () => conversation },
2225
+ );
2226
+
2227
+ expect(result).toEqual({
2228
+ invoked: false,
2229
+ producedToolCalls: false,
2230
+ reason: "context_overflow",
2231
+ });
2232
+ // Cleanup still ran.
2233
+ expect(conversation.processingToggles).toEqual([true, false]);
2234
+ expect(conversation.drainQueueCalls).toBe(1);
2235
+ });
2236
+
2237
+ test("suppressed wake maps a REWRAPPED (untyped) provider overflow error to a failed result", async () => {
2238
+ // Managed-proxy/adapter paths rewrap provider overflow rejections into
2239
+ // plain Errors, defeating the typed `instanceof` check. The wake's
2240
+ // capture must fall back to the message-level heuristic — otherwise
2241
+ // the overflow reads as a successful silent no-op and the
2242
+ // fork-retrospective job advances `lastProcessedMessageId` past a
2243
+ // slice that was never reviewed.
2244
+ const conversation = makeWakeConversation({
2245
+ estimatedInputTokens: 0,
2246
+ runImpl: async (input, onEvent) => {
2247
+ await onEvent({
2248
+ type: "error",
2249
+ error: new Error(
2250
+ "Provider API error (400): prompt is too long: 250000 tokens > 200000 maximum",
2251
+ ),
2252
+ });
2253
+ return runResult([...input]);
2254
+ },
2255
+ });
2256
+
2257
+ const result = await wakeAgentForOpportunity(
2258
+ {
2259
+ conversationId: conversation.conversationId,
2260
+ hint: "do work",
2261
+ source: "unit-test",
2262
+ suppressAutoCompaction: true,
2263
+ },
2264
+ { resolveTarget: async () => conversation },
2265
+ );
2266
+
2267
+ expect(result).toEqual({
2268
+ invoked: false,
2269
+ producedToolCalls: false,
2270
+ reason: "context_overflow",
2271
+ });
2272
+ });
2273
+
2274
+ test("suppressed wake maps a rewrapped overflow THROW to a failed result", async () => {
2275
+ const conversation = makeWakeConversation({
2276
+ estimatedInputTokens: 0,
2277
+ runImpl: async () => {
2278
+ throw new Error("too many input tokens: 250000 > 200000");
2279
+ },
2280
+ });
2281
+
2282
+ const result = await wakeAgentForOpportunity(
2283
+ {
2284
+ conversationId: conversation.conversationId,
2285
+ hint: "do work",
2286
+ source: "unit-test",
2287
+ suppressAutoCompaction: true,
2288
+ },
2289
+ { resolveTarget: async () => conversation },
2290
+ );
2291
+
2292
+ expect(result).toEqual({
2293
+ invoked: false,
2294
+ producedToolCalls: false,
2295
+ reason: "context_overflow",
2296
+ });
2297
+ // Cleanup still ran.
2298
+ expect(conversation.processingToggles).toEqual([true, false]);
2299
+ expect(conversation.drainQueueCalls).toBe(1);
2300
+ });
2301
+
2302
+ test("suppressed wake treats an unrelated rewrapped error as a generic no-op, not an overflow", async () => {
2303
+ const conversation = makeWakeConversation({
2304
+ estimatedInputTokens: 0,
2305
+ runImpl: async () => {
2306
+ throw new Error("socket hang up");
2307
+ },
2308
+ });
2309
+
2310
+ const result = await wakeAgentForOpportunity(
2311
+ {
2312
+ conversationId: conversation.conversationId,
2313
+ hint: "do work",
2314
+ source: "unit-test",
2315
+ suppressAutoCompaction: true,
2316
+ },
2317
+ { resolveTarget: async () => conversation },
2318
+ );
2319
+
2320
+ expect(result).toEqual({ invoked: true, producedToolCalls: false });
2321
+ });
2322
+
2323
+ test("the compaction gate is sized with the wake's call site and forced profile", async () => {
2324
+ const conversation = makeWakeConversation({
2325
+ scriptedAssistant: reply,
2326
+ });
2327
+
2328
+ await wakeAgentForOpportunity(
2329
+ {
2330
+ conversationId: conversation.conversationId,
2331
+ hint: "do work",
2332
+ source: "memory-retrospective",
2333
+ callSite: "memoryRetrospective",
2334
+ forceOverrideProfile: "forced",
2335
+ },
2336
+ { resolveTarget: async () => conversation },
2337
+ );
2338
+
2339
+ // The wake threads its own resolution inputs so the gate's threshold
2340
+ // sizes against the wake's window instead of mainAgent's.
2341
+ expect(conversation.maybeCompactSizings).toEqual([
2342
+ {
2343
+ callSite: "memoryRetrospective",
2344
+ overrideProfile: "forced",
2345
+ forceOverrideProfile: true,
2346
+ },
2347
+ ]);
2348
+ });
2349
+
2350
+ test("a default wake sizes the compaction gate against mainAgent with no forced profile", async () => {
2351
+ const conversation = makeWakeConversation({
2352
+ scriptedAssistant: reply,
2353
+ });
2354
+
2355
+ await wakeAgentForOpportunity(
2356
+ {
2357
+ conversationId: conversation.conversationId,
2358
+ hint: "do work",
2359
+ source: "unit-test",
2360
+ },
2361
+ { resolveTarget: async () => conversation },
2362
+ );
2363
+
2364
+ expect(conversation.maybeCompactSizings).toEqual([
2365
+ {
2366
+ callSite: "mainAgent",
2367
+ overrideProfile: undefined,
2368
+ forceOverrideProfile: false,
2369
+ },
2370
+ ]);
2371
+ });
2372
+
2373
+ test("non-suppressed wake treats a provider context-overflow rejection as a silent no-op (existing behavior)", async () => {
2374
+ const conversation = makeWakeConversation({
2375
+ runImpl: async (input, onEvent) => {
2376
+ await onEvent({
2377
+ type: "error",
2378
+ error: new ContextOverflowError(
2379
+ "prompt is too long: 250000 tokens > 200000 maximum",
2380
+ "test-provider",
2381
+ ),
2382
+ });
2383
+ return runResult([...input]);
2384
+ },
2385
+ });
2386
+
2387
+ const result = await wakeAgentForOpportunity(
2388
+ {
2389
+ conversationId: conversation.conversationId,
2390
+ hint: "do work",
2391
+ source: "unit-test",
2392
+ },
2393
+ { resolveTarget: async () => conversation },
2394
+ );
2395
+
2396
+ expect(result).toEqual({ invoked: true, producedToolCalls: false });
2397
+ });
2398
+ });
1777
2399
  });