@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
@@ -32,6 +32,21 @@ export const TRUNCATION_MARKER = "\u2014 full result:";
32
32
  */
33
33
  export const TRUNCATION_EXEMPT_TOOLS = new Set<string>(["skill_load"]);
34
34
 
35
+ /**
36
+ * File-read tools that page spooled `.tool-results/` content back into context.
37
+ * `file_read` reads inside the workspace; `host_file_read` reads anywhere on the
38
+ * host. Both are (a) exempt from the result-time spool pass — re-stubbing a read
39
+ * of a spooled file would hand back another stub, so the content could never be
40
+ * paged back at all — and (b) recognized by {@link derefToolResultReReads}, so a
41
+ * re-read of a spooled file collapses to a stub instead of duplicating content
42
+ * already in context. Sharing one set keeps the two passes from drifting if a
43
+ * new file-read tool is added.
44
+ */
45
+ export const FILE_READ_TOOL_NAMES = new Set<string>([
46
+ "file_read",
47
+ "host_file_read",
48
+ ]);
49
+
35
50
  /**
36
51
  * Build a map of tool_use_id -> originating tool name by walking the tool_use
37
52
  * blocks in assistant messages. A tool_result only carries `tool_use_id`, so
@@ -170,16 +185,18 @@ export const REREAD_STUB =
170
185
  * Deduplicate re-reads of saved tool results.
171
186
  *
172
187
  * When `postTurnTruncateToolResults` truncates a large result, it saves the full
173
- * content to a `.tool-results/` file. If the model later calls `file_read` on that
174
- * saved file, the result is a second copy of content whose truncated prefix/suffix
175
- * is already in context. This function detects those re-reads and replaces their
176
- * tool_result content with a short stub to avoid duplication.
188
+ * content to a `.tool-results/` file. If the model later calls `file_read` or
189
+ * `host_file_read` on that saved file, the result is a second copy of content
190
+ * whose truncated prefix/suffix is already in context. This function detects
191
+ * those re-reads and replaces their tool_result content with a short stub to
192
+ * avoid duplication.
177
193
  */
178
194
  export function derefToolResultReReads(messages: Message[]): {
179
195
  messages: Message[];
180
196
  dereferencedCount: number;
181
197
  } {
182
- // Build a set of tool_use_ids that are file_read calls targeting .tool-results/ paths.
198
+ // Build a set of tool_use_ids that are file-read calls (file_read or
199
+ // host_file_read) targeting .tool-results/ paths.
183
200
  const reReadToolUseIds = new Set<string>();
184
201
 
185
202
  for (const msg of messages) {
@@ -187,7 +204,7 @@ export function derefToolResultReReads(messages: Message[]): {
187
204
  for (const block of msg.content) {
188
205
  if (block.type !== "tool_use") continue;
189
206
  const tu = block as ToolUseContent;
190
- if (tu.name !== "file_read") continue;
207
+ if (!FILE_READ_TOOL_NAMES.has(tu.name)) continue;
191
208
  const filePath = tu.input.path ?? tu.input.file_path;
192
209
  if (typeof filePath !== "string") continue;
193
210
  if (filePath.includes(`/${TOOL_RESULT_DIR}/`)) {
@@ -4,6 +4,7 @@ import { join } from "node:path";
4
4
  import type { ContentBlock, ToolResultContent } from "../providers/types.js";
5
5
  import {
6
6
  buildTruncatedContent,
7
+ FILE_READ_TOOL_NAMES,
7
8
  getToolResultFilePath,
8
9
  isTruncationEligible,
9
10
  TOOL_RESULT_DIR,
@@ -18,27 +19,21 @@ import {
18
19
  const AX_TREE_TAG = "<ax-tree>";
19
20
 
20
21
  /**
21
- * Tools whose results keep their full content at result time even when
22
- * oversized. `file_read` is the model's only way to page spooled content back
23
- * into context: stubbing a read of a `.tool-results/` file would spool a
24
- * fresh copy and hand back another stub, so oversized content could never be
25
- * read at all. Explicit reads are therefore honored in full; the post-turn
26
- * pass still truncates them at turn end, after the model has consumed the
27
- * content.
28
- */
29
- const RESULT_TIME_STUB_EXEMPT_TOOLS = new Set<string>(["file_read"]);
30
-
31
- /**
32
- * Whether a tool result is eligible for the result-time spool/stub pass:
33
- * the post-turn pass's shared rules plus the AX-tree and `file_read`
34
- * exemptions above.
22
+ * Whether a tool result is eligible for the result-time spool/stub pass: the
23
+ * post-turn pass's shared rules plus the AX-tree exemption, minus the file-read
24
+ * tools ({@link FILE_READ_TOOL_NAMES}). The file-read tools are the model's only
25
+ * way to page spooled content back into context: stubbing a read of a
26
+ * `.tool-results/` file would spool a fresh copy and hand back another stub, so
27
+ * oversized content could never be read at all. Explicit reads are therefore
28
+ * honored in full; the post-turn pass still truncates them at turn end, after
29
+ * the model has consumed the content.
35
30
  */
36
31
  function isSpoolEligible(
37
32
  tr: ToolResultContent,
38
33
  toolName: string | undefined,
39
34
  ): boolean {
40
35
  if (!isTruncationEligible(tr, toolName)) return false;
41
- if (toolName !== undefined && RESULT_TIME_STUB_EXEMPT_TOOLS.has(toolName)) {
36
+ if (toolName !== undefined && FILE_READ_TOOL_NAMES.has(toolName)) {
42
37
  return false;
43
38
  }
44
39
  if (tr.content.includes(AX_TREE_TAG)) return false;
@@ -56,8 +51,8 @@ function isSpoolEligible(
56
51
  * rewriting an earlier message between calls would invalidate the cache from
57
52
  * that point on every iteration. The model still gets the head/tail preview
58
53
  * plus the on-disk path, so it can page the full content back in with
59
- * `file_read` (whose results are exempt from this pass) when it actually
60
- * needs it.
54
+ * `file_read` or `host_file_read` (whose results are exempt from this pass)
55
+ * when it actually needs it.
61
56
  *
62
57
  * Uses the same file paths, stub bytes, and eligibility rules as
63
58
  * `postTurnTruncateToolResults`, whose `TRUNCATION_MARKER` guard then skips
@@ -63,7 +63,7 @@ function getManagedBootstrapSocketPath(): string {
63
63
  * in order. The first existing path wins.
64
64
  *
65
65
  * Only paths outside the sandbox working directory are eligible.
66
- * `getDataDir()` (under `~/.vellum/workspace/data`) was previously included
66
+ * `getDataDir()` (under `$VELLUM_WORKSPACE_DIR/data`) was previously included
67
67
  * but is inside the sandbox write boundary, so a sandboxed tool could plant
68
68
  * a malicious binary there. Removed to close the sandbox-escape vector.
69
69
  *
@@ -327,6 +327,8 @@ function createStdioTransport(proc: Subprocess): CesTransport {
327
327
  const decoder = new TextDecoder();
328
328
 
329
329
  void (async () => {
330
+ let endReason: "eof" | "error" = "eof";
331
+ let readError: unknown;
330
332
  try {
331
333
  while (true) {
332
334
  const { value, done } = await reader.read();
@@ -344,17 +346,49 @@ function createStdioTransport(proc: Subprocess): CesTransport {
344
346
  }
345
347
  }
346
348
  }
347
- } catch {
348
- // Process ended
349
+ } catch (err) {
350
+ endReason = "error";
351
+ readError = err;
349
352
  } finally {
353
+ // Preserve existing semantics: the transport reports dead the moment
354
+ // the stdout stream ends, regardless of why.
350
355
  alive = false;
356
+
357
+ // DIAGNOSTIC (observation only). The reconnection path (secure-keys.ts)
358
+ // bounces CES whenever isAlive() goes false. This classifies WHY the
359
+ // transport died: a real process exit, or a stdout stream that
360
+ // ended/errored while the process is STILL RUNNING — the spurious case
361
+ // that needlessly restarts a healthy CES. Let `proc.exited` settle one
362
+ // tick so a near-simultaneous real exit isn't misread as spurious, then
363
+ // snapshot the exit code (`null` = still running).
364
+ await new Promise((resolve) => setTimeout(resolve, 0));
365
+ const exitCode = proc.exitCode;
366
+ if (exitCode === null) {
367
+ log.warn(
368
+ {
369
+ pid: proc.pid,
370
+ endReason,
371
+ err: readError instanceof Error ? readError.message : readError,
372
+ },
373
+ "CES stdio transport: stdout stream ended while the process is still alive (spurious transport death)",
374
+ );
375
+ } else {
376
+ log.debug(
377
+ { pid: proc.pid, endReason, exitCode },
378
+ "CES stdio transport: stdout stream ended after process exit",
379
+ );
380
+ }
351
381
  }
352
382
  })();
353
383
  }
354
384
 
355
385
  // Track process exit
356
- proc.exited.then(() => {
386
+ void proc.exited.then((exitCode) => {
357
387
  alive = false;
388
+ log.info(
389
+ { pid: proc.pid, exitCode },
390
+ "CES stdio transport: process exited",
391
+ );
358
392
  });
359
393
 
360
394
  return {
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Persistence for credentials collected through a secure prompt.
3
+ *
4
+ * A prompted credential can be delivered in three shapes:
5
+ * - one-time send: injected into the broker for the next operation, never
6
+ * written to the vault;
7
+ * - a Slack channel token: routed through the Slack channel config so the
8
+ * channel actually connects, not just stored;
9
+ * - any other credential: written to secure storage.
10
+ *
11
+ * Both the `credential_store` tool's prompt action and the CLI
12
+ * `credentials prompt` route share this logic so the two entry points behave
13
+ * identically.
14
+ */
15
+
16
+ import { getConfig } from "../config/loader.js";
17
+ import {
18
+ setSlackChannelConfig,
19
+ type SlackChannelConfigResult,
20
+ } from "../daemon/handlers/config-slack-channel.js";
21
+ import { syncManualTokenConnection } from "../oauth/manual-token-connection.js";
22
+ import type { SecretDelivery } from "../permissions/secret-prompter.js";
23
+ import { credentialKey } from "../security/credential-key.js";
24
+ import { setSecureKeyAsync } from "../security/secure-keys.js";
25
+ import { credentialBroker } from "../tools/credentials/broker.js";
26
+ import {
27
+ getCredentialMetadata,
28
+ upsertCredentialMetadata,
29
+ } from "../tools/credentials/metadata-store.js";
30
+ import type { CredentialInjectionTemplate } from "../tools/credentials/policy-types.js";
31
+ import { getLogger } from "../util/logger.js";
32
+
33
+ const log = getLogger("prompted-credential");
34
+
35
+ /** Slack channel credentials connect the channel rather than just storing a value. */
36
+ export function isSlackChannelCredential(
37
+ service: string,
38
+ field: string,
39
+ ): field is "bot_token" | "app_token" | "user_token" {
40
+ return (
41
+ service === "slack_channel" &&
42
+ (field === "bot_token" || field === "app_token" || field === "user_token")
43
+ );
44
+ }
45
+
46
+ export async function storeSlackChannelCredential(
47
+ field: "bot_token" | "app_token" | "user_token",
48
+ value: string,
49
+ ): Promise<SlackChannelConfigResult> {
50
+ if (field === "bot_token") {
51
+ return setSlackChannelConfig(value, undefined);
52
+ }
53
+ if (field === "app_token") {
54
+ return setSlackChannelConfig(undefined, value);
55
+ }
56
+ return setSlackChannelConfig(undefined, undefined, value);
57
+ }
58
+
59
+ export function formatSlackChannelStatus(
60
+ result: SlackChannelConfigResult,
61
+ ): string {
62
+ if (result.connected) {
63
+ const teamLabel = result.teamName ?? "Slack";
64
+ const botLabel = result.botUsername ? ` (@${result.botUsername})` : "";
65
+ return ` Slack channel connected to ${teamLabel}${botLabel}.`;
66
+ }
67
+ if (result.warning) {
68
+ return ` ${result.warning}`;
69
+ }
70
+ return "";
71
+ }
72
+
73
+ /**
74
+ * Usage policy carried alongside a prompted credential.
75
+ *
76
+ * `allowedTools` / `allowedDomains` are forwarded to the metadata store as-is:
77
+ * `undefined` leaves an existing credential's list untouched and defaults to
78
+ * deny-all (empty) when creating a new credential, while an explicit empty
79
+ * array sets a deny-all policy. This matches the store's partial-update
80
+ * contract, so re-prompting a credential without policy flags preserves the
81
+ * policy it was originally stored with.
82
+ */
83
+ interface PromptedCredentialPolicy {
84
+ allowedTools?: string[];
85
+ allowedDomains?: string[];
86
+ usageDescription?: string;
87
+ injectionTemplates?: CredentialInjectionTemplate[];
88
+ }
89
+
90
+ /**
91
+ * Outcome of persisting a prompted credential.
92
+ *
93
+ * `error` carries a human-readable message (without an "Error:" prefix);
94
+ * `stored` carries the Slack connection result when the value was a Slack
95
+ * channel token.
96
+ */
97
+ type PersistPromptedCredentialResult =
98
+ | { outcome: "transient" }
99
+ | { outcome: "stored"; slackChannel?: SlackChannelConfigResult }
100
+ | { outcome: "error"; message: string };
101
+
102
+ /**
103
+ * Persist a credential value obtained from a secure prompt.
104
+ *
105
+ * The caller is responsible for issuing the prompt and handling a missing
106
+ * value (cancellation / unsupported channel); this function owns everything
107
+ * that happens once a value is in hand.
108
+ */
109
+ export async function persistPromptedCredential(args: {
110
+ service: string;
111
+ field: string;
112
+ value: string;
113
+ delivery: SecretDelivery;
114
+ policy: PromptedCredentialPolicy;
115
+ }): Promise<PersistPromptedCredentialResult> {
116
+ const { service, field, value, delivery, policy } = args;
117
+
118
+ if (delivery === "transient_send") {
119
+ if (isSlackChannelCredential(service, field)) {
120
+ return {
121
+ outcome: "error",
122
+ message:
123
+ "Slack channel credentials must be saved to secure storage. Re-run the secure prompt and choose to store the token.",
124
+ };
125
+ }
126
+ const config = getConfig();
127
+ if (!config.secretDetection.allowOneTimeSend) {
128
+ log.warn(
129
+ { service, field },
130
+ "One-time send requested but not enabled in config",
131
+ );
132
+ return {
133
+ outcome: "error",
134
+ message:
135
+ "one-time send is not enabled. Set secretDetection.allowOneTimeSend to true in config.",
136
+ };
137
+ }
138
+ // Ensure metadata exists so broker policy checks work, but don't overwrite
139
+ // an existing record - a stored credential's policy must not be silently
140
+ // replaced by the transient prompt's policy. Metadata is written before the
141
+ // transient value so no dangling value can fail policy checks.
142
+ if (!getCredentialMetadata(service, field)) {
143
+ try {
144
+ upsertCredentialMetadata(service, field, {
145
+ allowedTools: policy.allowedTools,
146
+ allowedDomains: policy.allowedDomains,
147
+ usageDescription: policy.usageDescription,
148
+ injectionTemplates: policy.injectionTemplates,
149
+ });
150
+ } catch (err) {
151
+ log.error(
152
+ { service, field, err },
153
+ "metadata write failed for transient credential",
154
+ );
155
+ return {
156
+ outcome: "error",
157
+ message: `failed to write credential metadata for ${service}/${field}; the one-time value was discarded.`,
158
+ };
159
+ }
160
+ }
161
+ credentialBroker.injectTransient(service, field, value);
162
+ log.info(
163
+ { service, field, delivery: "transient_send" },
164
+ "One-time secret delivery used",
165
+ );
166
+ return { outcome: "transient" };
167
+ }
168
+
169
+ let slackChannel: SlackChannelConfigResult | undefined;
170
+ if (isSlackChannelCredential(service, field)) {
171
+ slackChannel = await storeSlackChannelCredential(field, value);
172
+ if (!slackChannel.success) {
173
+ return {
174
+ outcome: "error",
175
+ message: slackChannel.error ?? "failed to configure Slack channel",
176
+ };
177
+ }
178
+ } else {
179
+ const key = credentialKey(service, field);
180
+ const ok = await setSecureKeyAsync(key, value);
181
+ if (!ok) {
182
+ return { outcome: "error", message: "failed to store credential" };
183
+ }
184
+ }
185
+
186
+ try {
187
+ upsertCredentialMetadata(service, field, {
188
+ allowedTools: policy.allowedTools,
189
+ allowedDomains: policy.allowedDomains,
190
+ usageDescription: policy.usageDescription,
191
+ injectionTemplates: policy.injectionTemplates,
192
+ });
193
+ } catch (err) {
194
+ log.warn(
195
+ { service, field, err },
196
+ "metadata write failed after storing credential",
197
+ );
198
+ }
199
+
200
+ if (!isSlackChannelCredential(service, field)) {
201
+ await syncManualTokenConnection(service);
202
+ }
203
+
204
+ return { outcome: "stored", slackChannel };
205
+ }
@@ -51,6 +51,7 @@ import {
51
51
  writeSlackMetadata,
52
52
  } from "../messaging/providers/slack/message-metadata.js";
53
53
  import type { ContextWindowResult } from "../plugins/defaults/compaction/window-manager.js";
54
+ import { backfillMemoryV3SelectionMessageId } from "../plugins/defaults/memory-v3-shadow/shadow-plugin.js";
54
55
  import type {
55
56
  ContentBlock,
56
57
  ImageContent,
@@ -867,6 +868,7 @@ function handleThinkingDelta(
867
868
  thinking: event.thinking,
868
869
  conversationId: deps.ctx.conversationId,
869
870
  messageId: state.lastAssistantMessageId,
871
+ timestampMs: Date.now(),
870
872
  });
871
873
  // Mirror thinking into the same running view as text so the debounced
872
874
  // partial flush persists it mid-turn -- long reasoning streams survive a
@@ -1945,6 +1947,18 @@ export async function handleMessageComplete(
1945
1947
  );
1946
1948
  }
1947
1949
 
1950
+ try {
1951
+ backfillMemoryV3SelectionMessageId(
1952
+ deps.ctx.conversationId,
1953
+ assistantMessageId,
1954
+ );
1955
+ } catch (err) {
1956
+ deps.rlog.warn(
1957
+ { err },
1958
+ "Failed to backfill memory v3 selection messageId (non-fatal)",
1959
+ );
1960
+ }
1961
+
1948
1962
  deps.ctx.currentTurnSurfaces = [];
1949
1963
 
1950
1964
  // Emit trace event. Char count is computed from the cleaned +
@@ -222,8 +222,7 @@ async function buildPassthroughBatch(
222
222
  // otherwise diverge.
223
223
  if (candIf?.userMessageInterface !== headInterface?.userMessageInterface)
224
224
  break;
225
- if (candidate.sourceActorPrincipalId !== head.sourceActorPrincipalId)
226
- break;
225
+ if (candidate.sourceActorPrincipalId !== head.sourceActorPrincipalId) break;
227
226
  if (classifySlash(candidate.content) !== "passthrough") break;
228
227
  if (
229
228
  resolveVerificationSessionIntent(candidate.content).kind ===
@@ -1358,6 +1357,12 @@ export interface ProcessMessageOptions {
1358
1357
  currentPage?: string;
1359
1358
  isInteractive?: boolean;
1360
1359
  callSite?: LLMCallSite;
1360
+ /**
1361
+ * Optional ad-hoc inference-profile override applied to every LLM call
1362
+ * this turn issues (e.g. a schedule's pinned profile). Forwarded to
1363
+ * {@link Conversation.runAgentLoop}.
1364
+ */
1365
+ overrideProfile?: string;
1361
1366
  displayContent?: string;
1362
1367
  }
1363
1368
 
@@ -1380,6 +1385,7 @@ export async function processMessage(
1380
1385
  currentPage,
1381
1386
  isInteractive,
1382
1387
  callSite,
1388
+ overrideProfile,
1383
1389
  displayContent,
1384
1390
  } = options;
1385
1391
  await conversation.ensureActorScopedHistory();
@@ -1852,11 +1858,14 @@ export async function processMessage(
1852
1858
  isUserMessage?: boolean;
1853
1859
  titleText?: string;
1854
1860
  callSite?: LLMCallSite;
1861
+ overrideProfile?: string;
1855
1862
  } = { isUserMessage: true };
1856
1863
  if (isInteractive !== undefined) loopOptions.isInteractive = isInteractive;
1857
1864
  if (agentLoopContent !== resolvedContent)
1858
1865
  loopOptions.titleText = resolvedContent;
1859
1866
  if (callSite !== undefined) loopOptions.callSite = callSite;
1867
+ if (overrideProfile !== undefined)
1868
+ loopOptions.overrideProfile = overrideProfile;
1860
1869
 
1861
1870
  await conversation.runAgentLoop(agentLoopContent, userMessageId, {
1862
1871
  ...loopOptions,
@@ -7,6 +7,7 @@ import {
7
7
  getAppDirPath,
8
8
  getAppPreview,
9
9
  isMultifileApp,
10
+ listAppsByConversation,
10
11
  resolveAppDir,
11
12
  resolveEffectiveAppHtml,
12
13
  updateApp,
@@ -179,6 +180,18 @@ export function flushSurfaceDataPersist(surfaceId: string): void {
179
180
  persistSurfaceData(pending.conversationId, surfaceId, pending.data);
180
181
  }
181
182
 
183
+ /**
184
+ * Discard (without writing) any pending debounced persist for `surfaceId`.
185
+ * Called on dismissal so an in-flight `ui_update` snapshot cannot land after
186
+ * the surface block has been removed.
187
+ */
188
+ export function cancelSurfaceDataPersist(surfaceId: string): void {
189
+ const pending = pendingSurfacePersists.get(surfaceId);
190
+ if (!pending) return;
191
+ clearTimeout(pending.timer);
192
+ pendingSurfacePersists.delete(surfaceId);
193
+ }
194
+
182
195
  /**
183
196
  * Cancel all pending debounced persists. Called on conversation
184
197
  * teardown to avoid timers firing against torn-down state.
@@ -277,8 +290,133 @@ export function markSurfaceCompleted(
277
290
  log.warn({ err, surfaceId }, "Failed to persist surface completion to DB");
278
291
  }
279
292
  }
293
+
294
+ /**
295
+ * Remove a `ui_surface` content block from history so a passively dismissed
296
+ * surface does not survive a reload. The live client drops a dismissed surface
297
+ * entirely; this converges persisted state with that behaviour. Cancels any
298
+ * pending debounced data persist first so a late `ui_update` snapshot cannot
299
+ * re-add the block, then strips the block from in-memory messages and the DB.
300
+ */
301
+ export function removeSurfaceBlock(
302
+ ctx: { conversationId: string; messages?: Array<{ content: unknown }> },
303
+ surfaceId: string,
304
+ ): void {
305
+ cancelSurfaceDataPersist(surfaceId);
306
+
307
+ if (ctx.messages) {
308
+ for (let i = ctx.messages.length - 1; i >= 0; i--) {
309
+ const msg = ctx.messages[i];
310
+ if (!Array.isArray(msg.content)) continue;
311
+ const idx = msg.content.findIndex((block) => {
312
+ const b = block as Record<string, unknown>;
313
+ return b.type === "ui_surface" && b.surfaceId === surfaceId;
314
+ });
315
+ if (idx !== -1) {
316
+ msg.content.splice(idx, 1);
317
+ break;
318
+ }
319
+ }
320
+ }
321
+
322
+ try {
323
+ const rows = getMessages(ctx.conversationId);
324
+ for (let r = rows.length - 1; r >= 0; r--) {
325
+ let parsed: unknown[];
326
+ try {
327
+ const result = JSON.parse(rows[r].content);
328
+ if (!Array.isArray(result)) continue;
329
+ parsed = result;
330
+ } catch {
331
+ continue;
332
+ }
333
+ const idx = parsed.findIndex((pb) => {
334
+ const rb = pb as Record<string, unknown>;
335
+ return rb.type === "ui_surface" && rb.surfaceId === surfaceId;
336
+ });
337
+ if (idx !== -1) {
338
+ parsed.splice(idx, 1);
339
+ updateMessageContent(rows[r].id, JSON.stringify(parsed));
340
+ return;
341
+ }
342
+ }
343
+ } catch (err) {
344
+ log.warn({ err, surfaceId }, "Failed to remove dismissed surface from DB");
345
+ }
346
+ }
280
347
  const TASK_PROGRESS_TEMPLATE_FIELDS = ["title", "status", "steps"] as const;
281
348
 
349
+ const TASK_PROGRESS_CARD_STATUSES = new Set([
350
+ "in_progress",
351
+ "completed",
352
+ "failed",
353
+ ]);
354
+ const TASK_PROGRESS_STEP_STATUSES = new Set([
355
+ "pending",
356
+ "in_progress",
357
+ "completed",
358
+ "failed",
359
+ ]);
360
+
361
+ /**
362
+ * Coerce a model-supplied `steps` value into a renderable array. Drops
363
+ * non-object and label-less entries, accepts `title` as a `label` alias, and
364
+ * defaults a missing/invalid per-step status to "pending". Returns `[]` for a
365
+ * missing or non-array input so an indeterminate card still renders.
366
+ */
367
+ function normalizeTaskProgressSteps(
368
+ value: unknown,
369
+ ): Array<Record<string, unknown>> {
370
+ if (!Array.isArray(value)) return [];
371
+ return value
372
+ .filter((step): step is Record<string, unknown> => isPlainObject(step))
373
+ .map((step) => {
374
+ const label =
375
+ typeof step.label === "string"
376
+ ? step.label
377
+ : typeof step.title === "string"
378
+ ? step.title
379
+ : "";
380
+ const status =
381
+ typeof step.status === "string" &&
382
+ TASK_PROGRESS_STEP_STATUSES.has(step.status)
383
+ ? step.status
384
+ : "pending";
385
+ return { ...step, label, status };
386
+ })
387
+ .filter((step) => (step.label as string).trim().length > 0);
388
+ }
389
+
390
+ /**
391
+ * Guarantee a task_progress card reaches the client with a well-formed
392
+ * `templateData` object so a coarse or indeterminate attempt (missing steps,
393
+ * missing status) renders instead of being silently dropped. Fills only
394
+ * missing fields — a fully-specified card is left intact.
395
+ */
396
+ function ensureTaskProgressTemplateData(
397
+ normalized: Record<string, unknown>,
398
+ ): void {
399
+ const templateData: Record<string, unknown> = isPlainObject(
400
+ normalized.templateData,
401
+ )
402
+ ? { ...normalized.templateData }
403
+ : {};
404
+ if (
405
+ typeof templateData.title !== "string" &&
406
+ typeof normalized.title === "string"
407
+ ) {
408
+ templateData.title = normalized.title;
409
+ }
410
+ if (
411
+ typeof templateData.status !== "string" ||
412
+ !TASK_PROGRESS_CARD_STATUSES.has(templateData.status)
413
+ ) {
414
+ templateData.status = "in_progress";
415
+ }
416
+ templateData.steps = normalizeTaskProgressSteps(templateData.steps);
417
+ normalized.templateData = templateData;
418
+ }
419
+
282
420
  /**
283
421
  * Migrate dynamic_page fields from the top-level tool input into `data`.
284
422
  *
@@ -365,6 +503,10 @@ function normalizeCardShowData(
365
503
  normalized.body = "";
366
504
  }
367
505
 
506
+ if (normalized.template === "task_progress") {
507
+ ensureTaskProgressTemplateData(normalized);
508
+ }
509
+
368
510
  return normalized as unknown as CardSurfaceData;
369
511
  }
370
512
 
@@ -2783,6 +2925,15 @@ export async function surfaceProxyResolver(
2783
2925
  conversationId: ctx.conversationId,
2784
2926
  surfaceId,
2785
2927
  });
2928
+ // The live client drops a dismissed surface entirely. Mirror that in
2929
+ // persisted state: pull it from the pending turn snapshot (appended to
2930
+ // the message at turn completion) and strip any already-persisted block,
2931
+ // so a reload does not resurrect a half-finished progress card.
2932
+ const turnIdx = ctx.currentTurnSurfaces.findIndex(
2933
+ (s) => s.surfaceId === surfaceId,
2934
+ );
2935
+ if (turnIdx !== -1) ctx.currentTurnSurfaces.splice(turnIdx, 1);
2936
+ removeSurfaceBlock(ctx, surfaceId);
2786
2937
  }
2787
2938
  ctx.pendingSurfaceActions.delete(surfaceId);
2788
2939
  ctx.surfaceState.delete(surfaceId);
@@ -2796,11 +2947,24 @@ export async function surfaceProxyResolver(
2796
2947
  }
2797
2948
 
2798
2949
  if (toolName === "app_open") {
2799
- const appId = input.app_id as string;
2950
+ // Weaker models routinely omit app_id even though the active app is in
2951
+ // context. Fall back to the conversation's most-recently-updated app
2952
+ // rather than failing with "Invalid ID: undefined".
2953
+ let appId = input.app_id as string;
2954
+ if (typeof appId !== "string" || appId.trim().length === 0) {
2955
+ appId = listAppsByConversation(ctx.conversationId)[0]?.id ?? "";
2956
+ }
2800
2957
  const preview = input.preview as DynamicPageSurfaceData["preview"];
2801
2958
  const openMode = input.open_mode as string | undefined;
2802
- const app = getApp(appId);
2803
- if (!app) return { content: `App not found: ${appId}`, isError: true };
2959
+ const app = appId ? getApp(appId) : null;
2960
+ if (!app) {
2961
+ return {
2962
+ content: appId
2963
+ ? `App not found: ${appId}`
2964
+ : "app_id is required and no active app exists in this conversation. Call app_create first, or pass app_id explicitly.",
2965
+ isError: true,
2966
+ };
2967
+ }
2804
2968
 
2805
2969
  // Track conversation association (best-effort — failures must not break open flow).
2806
2970
  try {