@vellumai/assistant 0.8.12 → 0.9.0-staging.1

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 (414) 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/openapi.yaml +6935 -5545
  15. package/package.json +8 -4
  16. package/scripts/generate-openapi.ts +66 -114
  17. package/src/__tests__/access-request-seed-content-blocks.test.ts +213 -0
  18. package/src/__tests__/adaptive-thinking-repair.test.ts +32 -3
  19. package/src/__tests__/agent-loop-output-hooks.test.ts +183 -0
  20. package/src/__tests__/agent-loop-regrowth-guard.test.ts +506 -0
  21. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  22. package/src/__tests__/agent-wake-override-profile.test.ts +77 -0
  23. package/src/__tests__/app-dir-path-guard.test.ts +27 -3
  24. package/src/__tests__/approval-cascade.test.ts +0 -5
  25. package/src/__tests__/approval-routes-http.test.ts +91 -0
  26. package/src/__tests__/assistant-stream-state.test.ts +107 -0
  27. package/src/__tests__/browser-fill-credential.test.ts +3 -3
  28. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -1
  29. package/src/__tests__/compaction-events.test.ts +63 -7
  30. package/src/__tests__/compaction-trail-store.test.ts +74 -1
  31. package/src/__tests__/compaction.benchmark.test.ts +63 -41
  32. package/src/__tests__/compactor-low-watermark-cut.test.ts +349 -0
  33. package/src/__tests__/context-window-manager-compact-retry.test.ts +64 -0
  34. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -5
  35. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -5
  36. package/src/__tests__/conversation-history-web-search.test.ts +7 -0
  37. package/src/__tests__/conversation-process-callsite.test.ts +0 -5
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -5
  39. package/src/__tests__/conversation-queue.test.ts +0 -5
  40. package/src/__tests__/conversation-slash-queue.test.ts +0 -5
  41. package/src/__tests__/conversation-slash-unknown.test.ts +0 -5
  42. package/src/__tests__/conversation-speed-override.test.ts +0 -5
  43. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  44. package/src/__tests__/conversation-usage.test.ts +2 -0
  45. package/src/__tests__/conversation-workspace-injection.test.ts +0 -5
  46. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -5
  47. package/src/__tests__/credential-broker-browser-fill.test.ts +2 -2
  48. package/src/__tests__/credential-broker-server-use.test.ts +2 -2
  49. package/src/__tests__/credential-broker.test.ts +1 -1
  50. package/src/__tests__/credential-prompt-route.test.ts +417 -0
  51. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  52. package/src/__tests__/credential-vault.test.ts +37 -0
  53. package/src/__tests__/db-schedule-syntax-migration.test.ts +24 -0
  54. package/src/__tests__/dynamic-page-surface.test.ts +125 -0
  55. package/src/__tests__/empty-state-greeting-cache.test.ts +94 -0
  56. package/src/__tests__/gateway-flag-listener.test.ts +24 -7
  57. package/src/__tests__/guardian-action-sweep.test.ts +56 -219
  58. package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
  59. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  60. package/src/__tests__/list-messages-hidden-metadata.test.ts +99 -0
  61. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +87 -1
  62. package/src/__tests__/llm-resolver.test.ts +115 -0
  63. package/src/__tests__/managed-profile-guard.test.ts +6 -5
  64. package/src/__tests__/max-tokens-continue-hook.test.ts +184 -0
  65. package/src/__tests__/media-generate-image.test.ts +20 -9
  66. package/src/__tests__/model-intents.test.ts +1 -1
  67. package/src/__tests__/normalize-onboarding.test.ts +26 -0
  68. package/src/__tests__/notification-decision-strategy.test.ts +4 -2
  69. package/src/__tests__/notification-telegram-adapter.test.ts +21 -3
  70. package/src/__tests__/pending-interactions-resolved-event.test.ts +62 -0
  71. package/src/__tests__/post-turn-tool-result-truncation.test.ts +72 -18
  72. package/src/__tests__/require-fresh-approval.test.ts +425 -1
  73. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -0
  74. package/src/__tests__/schedule-routes-workflow-validation.test.ts +408 -0
  75. package/src/__tests__/schedule-routes.test.ts +257 -4
  76. package/src/__tests__/schedule-store.test.ts +60 -0
  77. package/src/__tests__/schedule-tools.test.ts +247 -2
  78. package/src/__tests__/skill-execute-input.test.ts +85 -0
  79. package/src/__tests__/skill-secret-handling-guard.test.ts +21 -20
  80. package/src/__tests__/skills.test.ts +3 -3
  81. package/src/__tests__/slack-app-setup-skill-regression.test.ts +1 -1
  82. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  83. package/src/__tests__/subagent-tool-gate-mode.test.ts +547 -0
  84. package/src/__tests__/system-prompt.test.ts +1 -1
  85. package/src/__tests__/task-progress-nudge-hook.test.ts +372 -0
  86. package/src/__tests__/task-scheduler.test.ts +299 -0
  87. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +209 -0
  88. package/src/__tests__/tool-result-spool.test.ts +3 -1
  89. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +181 -0
  90. package/src/__tests__/workspace-migration-103-upgrade-quality-profile-to-opus-4-8.test.ts +174 -0
  91. package/src/agent/compaction-circuit.ts +11 -0
  92. package/src/agent/loop.ts +181 -12
  93. package/src/api/constants/call-sites.ts +12 -0
  94. package/src/api/events/assistant-thinking-delta.ts +10 -0
  95. package/src/api/events/usage-update.ts +7 -0
  96. package/src/api/index.ts +4 -1
  97. package/src/api/responses/memory-v3-selection-log.ts +18 -11
  98. package/src/approvals/approval-primitive.ts +2 -2
  99. package/src/background-wake/background-wake-routes.test.ts +5 -2
  100. package/src/bundler/compiler-tools.ts +1 -1
  101. package/src/calls/call-domain.ts +1 -1
  102. package/src/calls/guardian-action-sweep.ts +16 -93
  103. package/src/cli/AGENTS.md +4 -0
  104. package/src/cli/commands/__tests__/schedules.test.ts +430 -1
  105. package/src/cli/commands/credentials.ts +28 -24
  106. package/src/cli/commands/image-generation.ts +23 -9
  107. package/src/cli/commands/notifications.ts +1 -1
  108. package/src/cli/commands/plugins.ts +89 -46
  109. package/src/cli/commands/schedules.ts +384 -11
  110. package/src/cli/lib/__tests__/inspect-plugin.test.ts +69 -5
  111. package/src/cli/lib/__tests__/install-from-github.test.ts +15 -0
  112. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +81 -4
  113. package/src/cli/lib/inspect-plugin.ts +62 -1
  114. package/src/cli/lib/install-from-github.ts +52 -5
  115. package/src/cli/lib/upgrade-plugin.ts +18 -0
  116. package/src/config/__tests__/workflows-schema.test.ts +60 -0
  117. package/src/config/bundled-skills/acp/SKILL.md +2 -2
  118. package/src/config/bundled-skills/image-studio/SKILL.md +66 -19
  119. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -6
  120. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +22 -3
  121. package/src/config/bundled-skills/personal-page/SKILL.md +57 -0
  122. package/src/config/bundled-skills/personal-page/TOOLS.json +27 -0
  123. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +17 -0
  124. package/src/config/bundled-skills/schedule/SKILL.md +7 -2
  125. package/src/config/bundled-skills/schedule/TOOLS.json +48 -4
  126. package/src/config/bundled-skills/workflows/SKILL.md +214 -0
  127. package/src/config/bundled-skills/workflows/TOOLS.json +84 -0
  128. package/src/config/bundled-skills/workflows/tools/manage-workflows.ts +12 -0
  129. package/src/config/bundled-skills/workflows/tools/run-workflow.ts +12 -0
  130. package/src/config/bundled-tool-registry.ts +14 -2
  131. package/src/config/call-site-defaults.ts +5 -0
  132. package/src/config/feature-flag-registry.json +12 -4
  133. package/src/config/llm-context-resolution.ts +8 -0
  134. package/src/config/llm-resolver.ts +30 -0
  135. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +22 -0
  136. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +16 -0
  137. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +77 -0
  138. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +57 -0
  139. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +28 -0
  140. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +234 -0
  141. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +48 -0
  142. package/src/config/preloaded-apps/personal-page/src/components/media.ts +16 -0
  143. package/src/config/preloaded-apps/personal-page/src/index.html +20 -0
  144. package/src/config/preloaded-apps/personal-page/src/main.tsx +7 -0
  145. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +82 -0
  146. package/src/config/preloaded-apps/personal-page/src/styles.css +759 -0
  147. package/src/config/schema.ts +2 -0
  148. package/src/config/schemas/call-site-catalog.ts +7 -0
  149. package/src/config/schemas/heartbeat.ts +4 -1
  150. package/src/config/schemas/llm.ts +33 -26
  151. package/src/config/schemas/memory-retrospective.ts +19 -0
  152. package/src/config/schemas/platform.ts +8 -0
  153. package/src/config/schemas/services.ts +5 -2
  154. package/src/config/schemas/workflows.ts +42 -0
  155. package/src/config/skills.ts +3 -3
  156. package/src/context/compactor.ts +273 -39
  157. package/src/context/post-turn-tool-result-truncation.ts +23 -6
  158. package/src/context/tool-result-spool.ts +12 -17
  159. package/src/credential-execution/executable-discovery.ts +1 -1
  160. package/src/credential-execution/process-manager.ts +37 -3
  161. package/src/credential-execution/prompted-credential.ts +205 -0
  162. package/src/daemon/conversation-agent-loop-handlers.ts +14 -0
  163. package/src/daemon/conversation-process.ts +11 -2
  164. package/src/daemon/conversation-surfaces.ts +75 -0
  165. package/src/daemon/conversation-tool-setup.ts +103 -26
  166. package/src/daemon/conversation-usage.ts +2 -0
  167. package/src/daemon/conversation.ts +115 -11
  168. package/src/daemon/handlers/shared.ts +26 -14
  169. package/src/daemon/host-cu-proxy.ts +15 -12
  170. package/src/daemon/host-file-proxy.ts +15 -12
  171. package/src/daemon/host-transfer-proxy.ts +30 -24
  172. package/src/daemon/lifecycle.ts +40 -3
  173. package/src/daemon/message-protocol.ts +3 -0
  174. package/src/daemon/message-types/messages.ts +2 -10
  175. package/src/daemon/message-types/workflows.ts +49 -0
  176. package/src/daemon/parse-actual-tokens-from-error.test.ts +62 -1
  177. package/src/daemon/parse-actual-tokens-from-error.ts +43 -4
  178. package/src/daemon/process-message.ts +6 -0
  179. package/src/daemon/tool-setup-types.ts +57 -0
  180. package/src/daemon/wake-conversation-ops.ts +18 -0
  181. package/src/heartbeat/heartbeat-run-store.ts +8 -2
  182. package/src/home/feed-types.ts +1 -1
  183. package/src/ipc/gateway-flag-listener.ts +28 -6
  184. package/src/mcp/mcp-auth-state.ts +8 -20
  185. package/src/media/__tests__/image-models.test.ts +57 -0
  186. package/src/media/image-models.ts +66 -0
  187. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +38 -0
  188. package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +12 -2
  189. package/src/memory/__tests__/memory-retrospective-job.test.ts +911 -34
  190. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +227 -5
  191. package/src/memory/__tests__/memory-retrospective-state.test.ts +195 -0
  192. package/src/memory/__tests__/preloaded-apps.test.ts +85 -0
  193. package/src/memory/auto-analysis-enqueue.ts +14 -1
  194. package/src/memory/compaction-log-store-clickhouse.ts +6 -4
  195. package/src/memory/conversation-crud.ts +9 -2
  196. package/src/memory/conversation-disk-view.ts +1 -1
  197. package/src/memory/conversation-queries.ts +22 -7
  198. package/src/memory/db-init.ts +20 -0
  199. package/src/memory/db-maintenance.ts +16 -0
  200. package/src/memory/embedding-runtime-manager.ts +1 -1
  201. package/src/memory/llm-request-log-source-clickhouse.ts +112 -14
  202. package/src/memory/llm-request-log-source-local.ts +19 -1
  203. package/src/memory/llm-request-log-source.ts +35 -6
  204. package/src/memory/llm-request-log-store.ts +90 -2
  205. package/src/memory/memory-retrospective-constants.ts +9 -0
  206. package/src/memory/memory-retrospective-enqueue.ts +3 -6
  207. package/src/memory/memory-retrospective-fork-boundary.ts +94 -0
  208. package/src/memory/memory-retrospective-job.ts +500 -208
  209. package/src/memory/memory-retrospective-startup-cleanup.ts +97 -19
  210. package/src/memory/memory-retrospective-state.ts +85 -2
  211. package/src/memory/migrations/281-memory-retrospective-remembered-log.ts +40 -0
  212. package/src/memory/migrations/282-schedule-inference-profile.test.ts +77 -0
  213. package/src/memory/migrations/282-schedule-inference-profile.ts +26 -0
  214. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.test.ts +102 -0
  215. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.ts +53 -0
  216. package/src/memory/migrations/284-workflow-runs.ts +51 -0
  217. package/src/memory/migrations/285-schedule-workflow-mode.ts +26 -0
  218. package/src/memory/migrations/286-workflow-run-trust.ts +27 -0
  219. package/src/memory/migrations/287-conversation-origin-channel-index.ts +15 -0
  220. package/src/memory/migrations/288-backfill-origin-channel-from-bindings.ts +43 -0
  221. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +115 -0
  222. package/src/memory/migrations/290-schedule-capabilities.test.ts +77 -0
  223. package/src/memory/migrations/290-schedule-capabilities.ts +25 -0
  224. package/src/memory/migrations/__tests__/281-memory-retrospective-remembered-log.test.ts +96 -0
  225. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +571 -0
  226. package/src/memory/migrations/index.ts +10 -0
  227. package/src/memory/preloaded-apps.ts +116 -0
  228. package/src/memory/schema/infrastructure.ts +4 -0
  229. package/src/memory/schema/memory-core.ts +4 -0
  230. package/src/memory/v2/__tests__/concept-page-frontmatter-schema.test.ts +45 -0
  231. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +11 -7
  232. package/src/memory/v2/__tests__/page-store.test.ts +13 -2
  233. package/src/memory/v2/__tests__/qdrant.test.ts +24 -0
  234. package/src/memory/v2/frontmatter-sweep.ts +7 -6
  235. package/src/memory/v2/page-store.ts +4 -3
  236. package/src/memory/v2/qdrant.ts +42 -3
  237. package/src/memory/v2/types.ts +16 -10
  238. package/src/messaging/draft-store.ts +1 -1
  239. package/src/notifications/access-request-copy.ts +200 -113
  240. package/src/notifications/adapters/slack.ts +250 -111
  241. package/src/notifications/adapters/telegram.ts +7 -44
  242. package/src/notifications/approval-card-builder.ts +93 -0
  243. package/src/notifications/broadcaster.ts +74 -0
  244. package/src/notifications/conversation-pairing.ts +8 -6
  245. package/src/notifications/copy-composer.ts +32 -26
  246. package/src/notifications/decision-engine.ts +59 -7
  247. package/src/notifications/guardian-question-mode.ts +145 -155
  248. package/src/notifications/home-feed-side-effect.ts +1 -1
  249. package/src/notifications/notification-utils.ts +66 -0
  250. package/src/notifications/signal.ts +6 -0
  251. package/src/notifications/tool-approval-copy.ts +142 -0
  252. package/src/notifications/types.ts +19 -0
  253. package/src/permissions/threshold.ts +11 -0
  254. package/src/plugin-api/types.ts +16 -4
  255. package/src/plugins/defaults/compaction/window-manager.ts +44 -0
  256. package/src/plugins/defaults/index.ts +46 -0
  257. package/src/plugins/defaults/max-tokens-continue/continue-state-store.ts +53 -0
  258. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +80 -0
  259. package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +20 -0
  260. package/src/plugins/defaults/max-tokens-continue/package.json +14 -0
  261. package/src/plugins/defaults/memory-retrieval/hooks/__tests__/user-prompt-submit.test.ts +37 -0
  262. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +29 -1
  263. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +8 -3
  264. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +4 -2
  265. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +28 -18
  266. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +67 -0
  267. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +122 -22
  268. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +2 -0
  269. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +63 -1
  270. package/src/plugins/defaults/memory-v3-shadow/injector.ts +61 -18
  271. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +1 -1
  272. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +39 -10
  273. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +34 -1
  274. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +112 -47
  275. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +78 -15
  276. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +206 -0
  277. package/src/plugins/defaults/task-progress-nudge/package.json +15 -0
  278. package/src/prompts/__tests__/system-prompt.test.ts +100 -1
  279. package/src/prompts/__tests__/task-progress-hint-section.test.ts +5 -7
  280. package/src/prompts/normalize-onboarding.ts +2 -0
  281. package/src/prompts/persona-resolver.ts +3 -0
  282. package/src/prompts/system-prompt.ts +51 -2
  283. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +3 -1
  284. package/src/prompts/templates/system-sections.ts +8 -3
  285. package/src/providers/call-site-routing.ts +6 -3
  286. package/src/providers/fireworks/client.ts +3 -0
  287. package/src/providers/inference/auth.ts +52 -46
  288. package/src/providers/model-intents.ts +2 -2
  289. package/src/providers/openai/__tests__/coerce-object-args.test.ts +105 -0
  290. package/src/providers/openai/chat-completions-provider.ts +47 -9
  291. package/src/providers/openai/coerce-object-args.ts +104 -0
  292. package/src/providers/retry.ts +8 -5
  293. package/src/providers/types.ts +10 -0
  294. package/src/runtime/__tests__/agent-wake.test.ts +629 -7
  295. package/src/runtime/access-request-helper.ts +16 -9
  296. package/src/runtime/agent-wake.ts +302 -51
  297. package/src/runtime/assistant-stream-state.ts +141 -8
  298. package/src/runtime/background-job-runner.ts +9 -0
  299. package/src/runtime/channel-approval-types.ts +1 -0
  300. package/src/runtime/guardian-action-message-composer.ts +0 -54
  301. package/src/runtime/http-server.ts +6 -14
  302. package/src/runtime/http-types.ts +0 -1
  303. package/src/runtime/message-composer-types.ts +0 -9
  304. package/src/runtime/middleware/__tests__/rate-limiter.test.ts +63 -0
  305. package/src/runtime/middleware/auth.ts +27 -3
  306. package/src/runtime/middleware/rate-limiter.ts +28 -1
  307. package/src/runtime/migrations/vbundle-builder.ts +6 -5
  308. package/src/runtime/pending-interactions.ts +20 -1
  309. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +232 -173
  310. package/src/runtime/routes/__tests__/plugins-routes.test.ts +18 -0
  311. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +436 -0
  312. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +11 -0
  313. package/src/runtime/routes/approval-routes.ts +35 -8
  314. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -1
  315. package/src/runtime/routes/btw-routes.ts +37 -1
  316. package/src/runtime/routes/channel-route-shared.ts +3 -1
  317. package/src/runtime/routes/consolidation-routes.ts +17 -13
  318. package/src/runtime/routes/conversation-compaction-routes.ts +159 -119
  319. package/src/runtime/routes/conversation-list-routes.ts +41 -4
  320. package/src/runtime/routes/conversation-query-routes.ts +196 -11
  321. package/src/runtime/routes/conversation-routes.ts +15 -1
  322. package/src/runtime/routes/credential-prompt-routes.ts +39 -17
  323. package/src/runtime/routes/empty-state-greeting-cache.ts +65 -0
  324. package/src/runtime/routes/heartbeat-routes.ts +18 -13
  325. package/src/runtime/routes/image-generation-routes.ts +20 -2
  326. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +26 -1
  327. package/src/runtime/routes/index.ts +6 -0
  328. package/src/runtime/routes/log-export/AGENTS.md +1 -1
  329. package/src/runtime/routes/log-export/workspace-allowlist.ts +1 -1
  330. package/src/runtime/routes/migration-routes.ts +5 -9
  331. package/src/runtime/routes/plugins-routes.ts +26 -0
  332. package/src/runtime/routes/ps-routes.ts +10 -8
  333. package/src/runtime/routes/retrospective-routes.ts +235 -0
  334. package/src/runtime/routes/runs-pagination.ts +75 -0
  335. package/src/runtime/routes/schedule-routes.ts +367 -59
  336. package/src/runtime/routes/sounds-config-routes.ts +239 -0
  337. package/src/runtime/routes/surface-action-routes.ts +84 -4
  338. package/src/runtime/routes/workflow-routes.test.ts +372 -0
  339. package/src/runtime/routes/workflow-routes.ts +363 -0
  340. package/src/runtime/routes/workspace-routes.test.ts +61 -1
  341. package/src/runtime/routes/workspace-routes.ts +26 -1
  342. package/src/runtime/services/__tests__/analyze-conversation.test.ts +38 -0
  343. package/src/runtime/services/analyze-conversation.ts +26 -13
  344. package/src/schedule/inference-profile.ts +28 -0
  345. package/src/schedule/schedule-store.ts +122 -4
  346. package/src/schedule/scheduler-types.ts +6 -0
  347. package/src/schedule/scheduler.ts +96 -0
  348. package/src/security/secret-allowlist.ts +1 -1
  349. package/src/skills/path-classifier.ts +1 -1
  350. package/src/tools/browser/browser-execution.ts +9 -11
  351. package/src/tools/credentials/broker.ts +4 -4
  352. package/src/tools/credentials/vault.ts +26 -137
  353. package/src/tools/executor.ts +69 -0
  354. package/src/tools/flag-gated-tools.test.ts +76 -0
  355. package/src/tools/permission-checker.ts +8 -1
  356. package/src/tools/registry.ts +51 -0
  357. package/src/tools/schedule/create.ts +77 -1
  358. package/src/tools/schedule/list.ts +1 -0
  359. package/src/tools/schedule/update.ts +73 -1
  360. package/src/tools/skills/execute.ts +56 -0
  361. package/src/tools/terminal/shell.ts +1 -1
  362. package/src/tools/ui-surface/definitions.ts +48 -2
  363. package/src/tools/workflows/manage-workflows.ts +183 -0
  364. package/src/tools/workflows/run-workflow.test.ts +442 -0
  365. package/src/tools/workflows/run-workflow.ts +88 -0
  366. package/src/types/onboarding-context.ts +2 -0
  367. package/src/usage/attribution.ts +24 -0
  368. package/src/util/canonicalize-identity.ts +12 -3
  369. package/src/util/platform.ts +17 -17
  370. package/src/watcher/__tests__/engine.test.ts +24 -0
  371. package/src/watcher/__tests__/telemetry.test.ts +135 -0
  372. package/src/watcher/engine.ts +7 -0
  373. package/src/watcher/telemetry.ts +74 -0
  374. package/src/workflows/capabilities.test.ts +365 -0
  375. package/src/workflows/capabilities.ts +359 -0
  376. package/src/workflows/deterministic-stringify.ts +27 -0
  377. package/src/workflows/engine-integration.test.ts +656 -0
  378. package/src/workflows/engine.test.ts +1144 -0
  379. package/src/workflows/engine.ts +1078 -0
  380. package/src/workflows/fanout-load.test.ts +168 -0
  381. package/src/workflows/journal-store.test.ts +369 -0
  382. package/src/workflows/journal-store.ts +470 -0
  383. package/src/workflows/leaf-runner.test.ts +704 -0
  384. package/src/workflows/leaf-runner.ts +589 -0
  385. package/src/workflows/library.test.ts +134 -0
  386. package/src/workflows/library.ts +124 -0
  387. package/src/workflows/run-manager.test.ts +711 -0
  388. package/src/workflows/run-manager.ts +593 -0
  389. package/src/workflows/sandbox-escape.test.ts +339 -0
  390. package/src/workflows/sandbox.test.ts +251 -0
  391. package/src/workflows/sandbox.ts +447 -0
  392. package/src/workspace/adaptive-thinking-repair.ts +33 -11
  393. package/src/workspace/migrations/021-move-signals-to-workspace.ts +1 -1
  394. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +1 -1
  395. package/src/workspace/migrations/026-backfill-install-meta.ts +1 -1
  396. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +2 -1
  397. package/src/workspace/migrations/031-drop-user-md.ts +1 -4
  398. package/src/workspace/migrations/048-remove-workspace-hooks.ts +1 -1
  399. package/src/workspace/migrations/056-release-notes-inference-profile-reordering.ts +5 -2
  400. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +1 -1
  401. package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +8 -2
  402. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +41 -14
  403. package/src/workspace/migrations/102-preserve-heartbeat-enabled-for-existing-workspaces.ts +69 -0
  404. package/src/workspace/migrations/103-upgrade-quality-profile-to-opus-4-8.ts +83 -0
  405. package/src/workspace/migrations/104-recheck-adaptive-thinking-model-implied-anthropic.ts +133 -0
  406. package/src/workspace/migrations/registry.ts +6 -0
  407. package/src/workspace/migrations/runner.ts +1 -1
  408. package/tsconfig.json +1 -1
  409. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -200
  410. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -579
  411. package/src/__tests__/guardian-action-store.test.ts +0 -106
  412. package/src/daemon/guardian-action-generators.ts +0 -71
  413. package/src/memory/guardian-action-store.ts +0 -484
  414. package/src/runtime/guardian-action-grant-minter.ts +0 -150
@@ -86,7 +86,40 @@ export async function ensureSectionCollection(
86
86
  const onDisk = config.memory.qdrant.onDisk;
87
87
 
88
88
  const exists = await client.collectionExists(SECTION_COLLECTION);
89
- if (!exists.exists) {
89
+ let needsCreate = !exists.exists;
90
+
91
+ // An existing collection sized to a different embedding dimension (e.g. a
92
+ // 384-dim collection now serving a 3072-dim embedder) carries the right
93
+ // vector at the wrong size — every upsert then fails with HTTP 400 and the
94
+ // collection is never repaired. Probe the schema and recreate on dimension
95
+ // drift; on a probe failure, assume compatible rather than risk a destructive
96
+ // recreate (mirrors the v2 concept-page collection posture). The next
97
+ // `backfillAllSections` regenerates the embeddings.
98
+ if (exists.exists) {
99
+ try {
100
+ const info = await client.getCollection(SECTION_COLLECTION);
101
+ const vectors = info.config?.params?.vectors;
102
+ const size =
103
+ vectors && typeof vectors === "object" && "size" in vectors
104
+ ? (vectors as { size?: unknown }).size
105
+ : undefined;
106
+ if (typeof size === "number" && size !== vectorSize) {
107
+ log.warn(
108
+ { collection: SECTION_COLLECTION, expected: vectorSize, found: size },
109
+ "Memory v3 section collection dimension drift — deleting and recreating; embeddings will be regenerated by the next backfill",
110
+ );
111
+ await client.deleteCollection(SECTION_COLLECTION);
112
+ needsCreate = true;
113
+ }
114
+ } catch (err) {
115
+ log.warn(
116
+ { err, collection: SECTION_COLLECTION },
117
+ "Failed to probe v3 section collection schema; assuming compatible",
118
+ );
119
+ }
120
+ }
121
+
122
+ if (needsCreate) {
90
123
  log.info(
91
124
  { collection: SECTION_COLLECTION, vectorSize },
92
125
  "Creating Qdrant collection for memory v3 sections",
@@ -1,25 +1,32 @@
1
1
  /**
2
- * Read-side store for the inspector's Memory V3 section. Reads the persisted
3
- * `memory_v3_selections` rows for a turn and re-renders an APPROXIMATE
4
- * `<memory>` block for what v3 selected, so the inspector can show the turn's
5
- * selection without re-running orchestration — which would be wrong anyway,
6
- * since the hot lane is frecency-stateful and can't be reproduced after the
7
- * fact.
2
+ * Read-side store for the inspector's Memory V3 panel. Reads the persisted
3
+ * `memory_v3_selections` rows for a turn (by the turn's message ids) and
4
+ * re-renders the `<memory>` block for what v3 selected, so the inspector can
5
+ * show the turn's selection without re-running orchestration — which would be
6
+ * wrong anyway, since the hot lane is frecency-stateful and can't be reproduced
7
+ * after the fact.
8
8
  *
9
- * The rendered text is inspector-only and NOT byte-identical to live
10
- * injection: the live injector freezes net-new compact CARDS into history
11
- * (`renderV3CardContent`) plus an ephemeral spotlight, while this store
12
- * re-renders the whole selection set as full/lead pages
13
- * (`renderV3SectionContent` with no persisted section identity — see the
14
- * inline note at `injectedText`).
9
+ * The rendered text is inspector-only and NOT byte-identical to live injection:
10
+ * the live injector freezes net-new compact CARDS into history
11
+ * (`renderV3CardContent`) plus an ephemeral spotlight. Here we re-render each
12
+ * selection's MATCHED SECTION — resolved from the persisted `(slug, ordinal)`
13
+ * against the current page — when one was recorded, falling back to the
14
+ * full/lead page otherwise. Section text is re-derived from the current page,
15
+ * so it reflects bounded page-drift if the page changed since the turn (the
16
+ * same approximation the v2 inspector accepts).
15
17
  */
16
18
 
17
19
  import type { MemoryV3SelectionLog } from "../../../api/responses/memory-v3-selection-log.js";
18
20
  import { isAssistantFeatureFlagEnabled } from "../../../config/assistant-feature-flags.js";
19
21
  import { getConfig } from "../../../config/loader.js";
20
22
  import { getDb, getSqliteFrom } from "../../../memory/db-connection.js";
23
+ import { readPage } from "../../../memory/v2/page-store.js";
24
+ import { getWorkspaceDir } from "../../../util/platform.js";
25
+ import { capabilityOrDiskBody } from "./capabilities.js";
26
+ import { sectionByOrdinal } from "./orchestrate.js";
21
27
  import { renderV3SectionContent } from "./page-content.js";
22
28
  import { renderMemoryBlock } from "./render-injection.js";
29
+ import { buildSectionIndex } from "./sections.js";
23
30
  import {
24
31
  type Section,
25
32
  SELECTION_SOURCES,
@@ -31,16 +38,21 @@ const MEMORY_V3_SHADOW = "memory-v3-shadow" as const;
31
38
  const MEMORY_V3_LIVE = "memory-v3-live" as const;
32
39
 
33
40
  interface SelectionRow {
41
+ turn: number;
34
42
  slug: string;
35
43
  source: string;
36
44
  pinned: number;
45
+ section_ordinal: number | null;
46
+ section_title: string | null;
37
47
  }
38
48
 
49
+ const SELECTION_COLUMNS = `turn, slug, source, pinned, section_ordinal, section_title`;
50
+
39
51
  function rowsForTurn(conversationId: string, turn: number): SelectionRow[] {
40
52
  return getSqliteFrom(getDb())
41
53
  .query(
42
54
  /*sql*/ `
43
- SELECT slug, source, pinned FROM memory_v3_selections
55
+ SELECT ${SELECTION_COLUMNS} FROM memory_v3_selections
44
56
  WHERE conversation_id = ? AND turn = ?
45
57
  ORDER BY rowid
46
58
  `,
@@ -48,32 +60,59 @@ function rowsForTurn(conversationId: string, turn: number): SelectionRow[] {
48
60
  .all(conversationId, turn) as SelectionRow[];
49
61
  }
50
62
 
63
+ function rowsForMessageIds(messageIds: string[]): SelectionRow[] {
64
+ if (messageIds.length === 0) return [];
65
+ const placeholders = messageIds.map(() => "?").join(", ");
66
+ return getSqliteFrom(getDb())
67
+ .query(
68
+ /*sql*/ `
69
+ SELECT ${SELECTION_COLUMNS} FROM memory_v3_selections
70
+ WHERE message_id IN (${placeholders})
71
+ ORDER BY rowid
72
+ `,
73
+ )
74
+ .all(...messageIds) as SelectionRow[];
75
+ }
76
+
51
77
  /**
52
- * Build the inspector's v3 selection log for one turn of a conversation.
53
- *
54
- * `turn` is the inspected message's turn (the v2-activation log's turn). The
55
- * selection is returned ONLY for that exact turn; when there are no v3 rows for
56
- * it — including when `turn` is null (the message has no v2-activation turn) —
57
- * this returns `null` rather than falling back to a different turn. A fallback
58
- * would attribute another turn's selection (e.g. a later turn's pages and
59
- * rendered block) to the inspected message, which corrupts shadow validation.
60
- *
61
- * Caveat: v3 rows are keyed by the orchestrator turn counter (`ctx.turnCount`)
62
- * while `turn` here is the v2 memory-tracker turn. They coincide for normal
63
- * turns, but if they diverge this simply yields `null` (no section) rather than
64
- * wrong data. Tying v3 rows to a message id for exact per-message attribution
65
- * regardless of counter drift is a documented follow-up.
66
- *
67
- * Selection rows are stored in selection order, so rendering them in row order
68
- * reproduces the block v3 would inject.
78
+ * Resolve each selection's persisted matched section `(slug, ordinal)` to the
79
+ * concrete `Section` in the CURRENT page, so the injected block renders the
80
+ * matched section rather than the full page. Only slugs with a recorded ordinal
81
+ * are resolved (core/hot/fresh/edge selections have none and render full-page).
82
+ * A page edited since the turn re-derives the current section at that ordinal,
83
+ * or falls back to full-page when the ordinal no longer exists.
69
84
  */
70
- export async function getMemoryV3SelectionForInspector(
71
- conversationId: string,
72
- turn: number | null | undefined,
73
- ): Promise<MemoryV3SelectionLog | null> {
74
- if (turn == null) return null;
85
+ async function reconstructMatchedSections(
86
+ rows: SelectionRow[],
87
+ ): Promise<Map<Slug, Section>> {
88
+ const sectionSlugs = rows
89
+ .filter((r) => r.section_ordinal != null)
90
+ .map((r) => r.slug);
91
+ if (sectionSlugs.length === 0) return new Map();
92
+
93
+ const workspaceDir = getWorkspaceDir();
94
+ const pageBody = (slug: Slug): Promise<string> =>
95
+ capabilityOrDiskBody(slug, async (s) => {
96
+ try {
97
+ return (await readPage(workspaceDir, s))?.body ?? "";
98
+ } catch {
99
+ return "";
100
+ }
101
+ });
102
+ const index = await buildSectionIndex(sectionSlugs, pageBody);
103
+
104
+ const sectionBySlug = new Map<Slug, Section>();
105
+ for (const row of rows) {
106
+ if (row.section_ordinal == null) continue;
107
+ const section = sectionByOrdinal(index, row.slug, row.section_ordinal);
108
+ if (section) sectionBySlug.set(row.slug, section);
109
+ }
110
+ return sectionBySlug;
111
+ }
75
112
 
76
- const rows = rowsForTurn(conversationId, turn);
113
+ async function buildSelectionLog(
114
+ rows: SelectionRow[],
115
+ ): Promise<MemoryV3SelectionLog | null> {
77
116
  if (rows.length === 0) return null;
78
117
 
79
118
  const config = getConfig();
@@ -81,25 +120,19 @@ export async function getMemoryV3SelectionForInspector(
81
120
  slug: r.slug,
82
121
  source: r.source,
83
122
  pinned: r.pinned === 1,
123
+ sectionOrdinal: r.section_ordinal,
124
+ sectionHeading: r.section_title,
84
125
  }));
85
126
  const slugs: Slug[] = selections.map((s) => s.slug);
86
- // NOTE: `injectedText` is APPROXIMATE — it renders the full/lead page for
87
- // every slug, not the exact matched section live injection used. The inspector
88
- // re-renders from persisted rows without re-running orchestration, and the
89
- // section identity is not persisted (`memory_v3_selections` stores
90
- // slug/source/pinned, not the section ordinal), so the section map is empty
91
- // and `renderV3SectionContent` falls back to the full page. Persisting the
92
- // injected section (a schema migration) is deliberately deferred: the A/B's
93
- // primary signal is slug-level recall via `summarizeSelections`, which is
94
- // exact — only this debug-oriented injected text is an approximation.
127
+ const sectionBySlug = await reconstructMatchedSections(rows);
95
128
  const injectedText = await renderMemoryBlock(
96
129
  slugs,
97
- new Map<Slug, Section>(),
130
+ sectionBySlug,
98
131
  renderV3SectionContent,
99
132
  );
100
133
 
101
134
  return {
102
- turn,
135
+ turn: rows[0]!.turn,
103
136
  live: isAssistantFeatureFlagEnabled(MEMORY_V3_LIVE, config),
104
137
  shadow: isAssistantFeatureFlagEnabled(MEMORY_V3_SHADOW, config),
105
138
  selections,
@@ -107,6 +140,38 @@ export async function getMemoryV3SelectionForInspector(
107
140
  };
108
141
  }
109
142
 
143
+ /**
144
+ * Build the inspector's v3 selection log for the inspected message's turn,
145
+ * keyed by the turn's message ids. This is the durable join: `writeSelections`
146
+ * logs rows with `message_id = NULL` and the turn-end backfill stamps them with
147
+ * the assistant message id, so a per-message lookup is robust against the drift
148
+ * between v2's tracker turn and v3's orchestrator `turnCount`. Returns `null`
149
+ * when no v3 rows match (e.g. a turn predating the message-id backfill, or a
150
+ * conversation with no v3 data). Message ids are globally unique, so no
151
+ * conversation scope is needed.
152
+ *
153
+ * Selection rows are stored in selection order, so rendering them in row order
154
+ * reproduces the block v3 would inject.
155
+ */
156
+ export async function getMemoryV3SelectionForInspectorByMessageIds(
157
+ messageIds: string[],
158
+ ): Promise<MemoryV3SelectionLog | null> {
159
+ return buildSelectionLog(rowsForMessageIds(messageIds));
160
+ }
161
+
162
+ /**
163
+ * Turn-keyed variant, retained for callers/tests that look up by an exact
164
+ * `(conversation, turn)`. Returns `null` when `turn` is null or there are no
165
+ * rows for it.
166
+ */
167
+ export async function getMemoryV3SelectionForInspector(
168
+ conversationId: string,
169
+ turn: number | null | undefined,
170
+ ): Promise<MemoryV3SelectionLog | null> {
171
+ if (turn == null) return null;
172
+ return buildSelectionLog(rowsForTurn(conversationId, turn));
173
+ }
174
+
110
175
  /**
111
176
  * Offline A/B aggregate over a conversation's logged v3 selections. Reads every
112
177
  * `memory_v3_selections` row for the conversation (all turns) and rolls them up
@@ -55,6 +55,7 @@ import { computeHotSet } from "./hot-set.js";
55
55
  import { computeLearnedEdgeGraph } from "./learned-edges.js";
56
56
  import type { OrchestrateResult } from "./orchestrate.js";
57
57
  import { orchestrate } from "./orchestrate.js";
58
+ import { MemoryV3RetrievalUnavailableError } from "./pool-select.js";
58
59
  import { ensureSectionCollection } from "./section-dense-store.js";
59
60
  import type { SectionNeedle } from "./section-needle.js";
60
61
  import { buildSectionNeedle } from "./section-needle.js";
@@ -418,6 +419,11 @@ interface SelectionRow {
418
419
  slug: Slug;
419
420
  source: SelectionSource;
420
421
  pinned: number;
422
+ /** Ordinal of the matched section a finder lane surfaced; null for
423
+ * core/hot/fresh/edge selections with no matched section. */
424
+ sectionOrdinal: number | null;
425
+ /** Heading of the matched section; null when there is no matched section. */
426
+ sectionTitle: string | null;
421
427
  }
422
428
 
423
429
  /**
@@ -438,17 +444,24 @@ export function attributeSelections(result: OrchestrateResult): SelectionRow[] {
438
444
  const finderLane = new Map(
439
445
  result.lanes.finder.map((c) => [c.slug, c.lane] as const),
440
446
  );
441
- return result.selections.map((sel) => ({
442
- slug: sel.slug,
443
- source: core.has(sel.slug)
444
- ? ("core" as const)
445
- : hot.has(sel.slug)
446
- ? ("hot" as const)
447
- : fresh.has(sel.slug)
448
- ? ("fresh" as const)
449
- : (finderLane.get(sel.slug) ?? "needle"),
450
- pinned: sel.pinned ? 1 : 0,
451
- }));
447
+ return result.selections.map((sel) => {
448
+ // The matched section is populated only for finder-lane hits (including
449
+ // hits on core/hot pages); core/hot/fresh/edge-only selections have none.
450
+ const section = result.matchedSections.get(sel.slug);
451
+ return {
452
+ slug: sel.slug,
453
+ source: core.has(sel.slug)
454
+ ? ("core" as const)
455
+ : hot.has(sel.slug)
456
+ ? ("hot" as const)
457
+ : fresh.has(sel.slug)
458
+ ? ("fresh" as const)
459
+ : (finderLane.get(sel.slug) ?? "needle"),
460
+ pinned: sel.pinned ? 1 : 0,
461
+ sectionOrdinal: section?.ordinal ?? null,
462
+ sectionTitle: section?.title ?? null,
463
+ };
464
+ });
452
465
  }
453
466
 
454
467
  /** Write the attributed selection rows to `memory_v3_selections`. */
@@ -461,17 +474,51 @@ export function writeSelections(
461
474
  const raw = getSqliteFrom(getDb());
462
475
  // PK is (conversation_id, turn, slug); OR REPLACE keeps the write
463
476
  // idempotent if the same turn is observed twice (e.g. a retried turn).
477
+ // `message_id` is written NULL here (the assistant message does not exist at
478
+ // injection time) and stamped at turn end by
479
+ // `backfillMemoryV3SelectionMessageId`.
464
480
  const stmt = raw.query(/*sql*/ `
465
481
  INSERT OR REPLACE INTO memory_v3_selections (
466
- conversation_id, turn, slug, source, pinned, created_at
467
- ) VALUES (?, ?, ?, ?, ?, ?)
482
+ conversation_id, turn, slug, source, pinned, created_at,
483
+ message_id, section_ordinal, section_title
484
+ ) VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?)
468
485
  `);
469
486
  const now = Date.now();
470
487
  for (const row of rows) {
471
- stmt.run(conversationId, turn, row.slug, row.source, row.pinned, now);
488
+ stmt.run(
489
+ conversationId,
490
+ turn,
491
+ row.slug,
492
+ row.source,
493
+ row.pinned,
494
+ now,
495
+ row.sectionOrdinal,
496
+ row.sectionTitle,
497
+ );
472
498
  }
473
499
  }
474
500
 
501
+ /**
502
+ * Stamp the turn's assistant message id onto the selection rows just written
503
+ * for it. Mirrors the v2 activation-log backfill: `writeSelections` writes
504
+ * `message_id = NULL` at injection time, and this runs at turn end once the
505
+ * assistant message exists. Relies on the single-threaded-per-conversation turn
506
+ * invariant — every NULL-`message_id` row for the conversation belongs to the
507
+ * turn that just finished. Lets the inspector look v3 selections up by the
508
+ * turn's message ids (robust against v2/v3 turn-counter drift).
509
+ */
510
+ export function backfillMemoryV3SelectionMessageId(
511
+ conversationId: string,
512
+ assistantMessageId: string,
513
+ ): void {
514
+ getSqliteFrom(getDb())
515
+ .query(
516
+ /*sql*/ `UPDATE memory_v3_selections SET message_id = ?
517
+ WHERE conversation_id = ? AND message_id IS NULL`,
518
+ )
519
+ .run(assistantMessageId, conversationId);
520
+ }
521
+
475
522
  /**
476
523
  * Run v3 orchestration for one turn and log the selection set, returning the
477
524
  * orchestrate result so a live caller can render it. Never throws — all
@@ -530,6 +577,16 @@ export async function observeTurn(
530
577
  writeSelections(conversationId, turnIndex, rows);
531
578
  return result;
532
579
  } catch (err) {
580
+ // An INFRASTRUCTURE failure (the selector lost its provider — e.g. a
581
+ // transient CES credential blip) must NOT be silently swallowed: re-throw
582
+ // so the LIVE injector hard-fails the turn (a clean, retryable failure)
583
+ // rather than shipping it with no `<memory>` block. The shadow/observation
584
+ // callers (the injector in shadow mode, runShadowObservation) catch this
585
+ // and swallow it, so observation never fails a turn. Other (non-infra)
586
+ // errors stay non-fatal and degrade to no v3 block, as before.
587
+ if (err instanceof MemoryV3RetrievalUnavailableError) {
588
+ throw err;
589
+ }
533
590
  log.warn(
534
591
  { err: err instanceof Error ? err.message : String(err), conversationId },
535
592
  "memory-v3 orchestration failed (non-fatal)",
@@ -553,5 +610,11 @@ export async function runShadowObservation(
553
610
  const config = getConfig();
554
611
  if (config.memory.enabled === false) return;
555
612
  if (!isAssistantFeatureFlagEnabled(MEMORY_V3_SHADOW, config)) return;
556
- await observeTurn(conversationId, turnIndex);
613
+ try {
614
+ await observeTurn(conversationId, turnIndex);
615
+ } catch {
616
+ // Shadow observation is fire-and-forget and must NEVER fail a turn.
617
+ // `observeTurn` now re-throws infra failures so the LIVE injector can
618
+ // hard-fail on them; here (observation only) we swallow them.
619
+ }
557
620
  }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Default `post-tool-use` hook: when an interactive turn accumulates several
3
+ * tool-call rounds without the model having shown a `task_progress` card,
4
+ * surface a soft notice via `additionalContext` reminding it to show progress.
5
+ *
6
+ * Motivation: the system prompt and the `ui_show` tool description both ask
7
+ * the model to show a `task_progress` card on long multi-step turns, but
8
+ * weaker models (e.g. MiniMax M3) disregard the static instruction and run
9
+ * 30+ tool calls with no user-visible progress. A reminder injected mid-turn,
10
+ * right after a tool result, is far more salient than static prompt text.
11
+ *
12
+ * The nudge is strictly best-effort:
13
+ * - It fires at most once per turn (no nagging).
14
+ * - It is advisory — the model may decline if it is about to finish or judges
15
+ * a card unnecessary.
16
+ * - A failed or ignored card never blocks the turn.
17
+ *
18
+ * It is scoped to weaker open models (Kimi, DeepSeek, MiniMax) that disregard
19
+ * the static progress-card instruction; capable models follow the prompt and
20
+ * never need the reminder. It also self-targets: a model that already showed a
21
+ * card this turn is never nudged.
22
+ *
23
+ * The turn state is derived from conversation history on every call (mirroring
24
+ * the tool-error and exploration-drift plugins) so the signal survives
25
+ * mid-turn compaction rewriting the array. The current turn is the trailing
26
+ * window bounded by the last genuine user message (a user row with no
27
+ * tool_result blocks). Within it we count tool-use rounds and detect whether a
28
+ * `ui_show` task_progress card was issued.
29
+ *
30
+ * Gating:
31
+ * - weaker open models only (Kimi, DeepSeek, MiniMax) — checked first on the
32
+ * hot path so capable-model turns short-circuit immediately.
33
+ * - mainAgent call site only — background turns (wake, title-gen, memory) and
34
+ * subagents have no live user watching, so a progress card is pointless.
35
+ * - the client must support dynamic UI — otherwise `ui_show` is blocked
36
+ * client-side and the nudge would only provoke a wasted, erroring call.
37
+ *
38
+ * The call-site, capability, and subagent gates are resolved lazily on the
39
+ * would-nudge path; the model gate is a cheap regex test run up front.
40
+ *
41
+ * Dedup uses a per-conversation high-water mark of the round count at the last
42
+ * nudge: a non-zero mark means "already nudged this turn", which also dedupes
43
+ * the parallel tool results of one batch (they observe identical history and
44
+ * compute the same round count). The mark resets when the round count drops
45
+ * below it (a new turn restarts counting low).
46
+ */
47
+
48
+ import type { PluginHookFn, PostToolUseContext } from "@vellumai/plugin-api";
49
+
50
+ import type { ContentBlock, Message } from "../../../../providers/types.js";
51
+
52
+ /**
53
+ * Canonical nudge notice. Module-level constant so tests and wrapping plugins
54
+ * can match it without duplicating the string. Shown to the model as
55
+ * provider-only context, never to the user. Deliberately soft: coarse steps
56
+ * are fine and the model may skip it when wrapping up.
57
+ */
58
+ export const TASK_PROGRESS_NUDGE_TEXT =
59
+ '<system_notice>You are several tool calls into this turn and have not shown the user a progress card. If you are doing multi-step work, call ui_show now with surface_type "card" and template "task_progress" (coarse steps are fine — a rough "Working on X" beats no signal) so the user can see what is happening, and keep it updated with ui_update as you go. Skip this if you are about to finish; never let it interrupt the actual work.</system_notice>';
60
+
61
+ /**
62
+ * Number of tool-use rounds in a turn, with no task_progress card shown, that
63
+ * triggers the nudge. Tuned conservatively: a turn of one or two rounds (the
64
+ * common simple case) is never touched; only a clearly multi-step turn is
65
+ * nudged, and only once. Lower from telemetry if cards still arrive too late.
66
+ */
67
+ export const TASK_PROGRESS_NUDGE_ROUND_THRESHOLD = 3;
68
+
69
+ /**
70
+ * Weaker open models that disregard the static progress-card instruction and
71
+ * so get the mid-turn nudge: Kimi, DeepSeek, and MiniMax. Family-level matching
72
+ * spans provider naming conventions (OpenRouter `moonshotai/kimi-k2.6`,
73
+ * `deepseek/deepseek-chat`, `minimax/minimax-m3`; Fireworks
74
+ * `accounts/fireworks/models/minimax-m3`, `kimi-k2p6`). Extend as other models
75
+ * show the same gap. Capable models (Claude, GPT) follow the prompt and are
76
+ * intentionally excluded.
77
+ */
78
+ const WEAK_MODEL_PATTERN = /kimi|deepseek|minimax/i;
79
+
80
+ /**
81
+ * Round count at the last nudge, per conversation. A non-zero entry means the
82
+ * turn has already been nudged; it resets when the round count drops below the
83
+ * mark (a new turn). Mirrors the exploration-drift high-water mark so parallel
84
+ * results of one batch dedupe to a single notice.
85
+ */
86
+ const lastNudgedRoundsByConversation = new Map<string, number>();
87
+
88
+ /** Test-only: clear the per-conversation nudge high-water marks. */
89
+ export function resetTaskProgressNudgeStateForTests(): void {
90
+ lastNudgedRoundsByConversation.clear();
91
+ }
92
+
93
+ /**
94
+ * True when a `ui_show` tool input shows a `task_progress` card — accepting the
95
+ * template either at the top level or nested under `data`, mirroring the
96
+ * server-side normalization tolerance.
97
+ */
98
+ function isTaskProgressShowInput(input: unknown): boolean {
99
+ if (input === null || typeof input !== "object") return false;
100
+ const record = input as Record<string, unknown>;
101
+ if (record.template === "task_progress") return true;
102
+ const data = record.data;
103
+ return (
104
+ data !== null &&
105
+ typeof data === "object" &&
106
+ (data as Record<string, unknown>).template === "task_progress"
107
+ );
108
+ }
109
+
110
+ /**
111
+ * Scan the trailing turn (walking back to the last genuine user message) for
112
+ * the number of tool-use rounds and whether a task_progress card was shown.
113
+ * The current round's assistant tool_use is already in history; its result is
114
+ * not, so the count includes the current round.
115
+ */
116
+ function scanTurn(messages: ReadonlyArray<Message>): {
117
+ rounds: number;
118
+ taskProgressShown: boolean;
119
+ } {
120
+ let rounds = 0;
121
+ let taskProgressShown = false;
122
+ for (let i = messages.length - 1; i >= 0; i--) {
123
+ const message = messages[i];
124
+ if (message.role === "user") {
125
+ const carriesToolResult = message.content.some(
126
+ (block: ContentBlock) =>
127
+ block.type === "tool_result" ||
128
+ block.type === "web_search_tool_result",
129
+ );
130
+ if (!carriesToolResult) break; // genuine user prompt — turn boundary
131
+ continue;
132
+ }
133
+ if (message.role !== "assistant") continue;
134
+ let hasToolUse = false;
135
+ for (const block of message.content) {
136
+ if (block.type !== "tool_use") continue;
137
+ hasToolUse = true;
138
+ if (block.name === "ui_show" && isTaskProgressShowInput(block.input)) {
139
+ taskProgressShown = true;
140
+ }
141
+ }
142
+ if (hasToolUse) rounds++;
143
+ }
144
+ return { rounds, taskProgressShown };
145
+ }
146
+
147
+ const postToolUse: PluginHookFn<PostToolUseContext> = async (ctx) => {
148
+ if (!WEAK_MODEL_PATTERN.test(ctx.model)) return;
149
+
150
+ const { rounds, taskProgressShown } = scanTurn(ctx.messages);
151
+
152
+ let lastNudged = lastNudgedRoundsByConversation.get(ctx.conversationId) ?? 0;
153
+ if (rounds < lastNudged) {
154
+ // New turn (round count restarted low) — drop the stale mark.
155
+ lastNudgedRoundsByConversation.delete(ctx.conversationId);
156
+ lastNudged = 0;
157
+ }
158
+
159
+ // A card now exists this turn: clear any stale mark and never nudge.
160
+ if (taskProgressShown) {
161
+ if (lastNudged !== 0) {
162
+ lastNudgedRoundsByConversation.delete(ctx.conversationId);
163
+ }
164
+ return;
165
+ }
166
+
167
+ if (rounds < TASK_PROGRESS_NUDGE_ROUND_THRESHOLD) return;
168
+ if (lastNudged !== 0) return; // already nudged this turn
169
+
170
+ // Resolve gating lazily on the rare would-nudge path to keep the hot path
171
+ // free of the conversation-registry and subagent module graphs.
172
+ const { findConversation } =
173
+ await import("../../../../daemon/conversation-registry.js");
174
+ const conversation = findConversation(ctx.conversationId);
175
+ if (!conversation) return;
176
+
177
+ if (
178
+ conversation.currentCallSite &&
179
+ conversation.currentCallSite !== "mainAgent"
180
+ ) {
181
+ return; // background call site — no live user watching
182
+ }
183
+
184
+ const capabilities =
185
+ conversation.currentTurnChannelCapabilities ??
186
+ conversation.channelCapabilities;
187
+ if (capabilities && capabilities.supportsDynamicUi === false) {
188
+ return; // client cannot render surfaces — a nudge would only error
189
+ }
190
+
191
+ const { getSubagentManager } = await import("../../../../subagent/index.js");
192
+ if (getSubagentManager().getParentInfo(ctx.conversationId) !== undefined) {
193
+ return; // subagents have no live user
194
+ }
195
+
196
+ lastNudgedRoundsByConversation.set(ctx.conversationId, rounds);
197
+ ctx.logger.info(
198
+ { plugin: "task-progress-nudge", rounds },
199
+ "Multi-step turn with no task_progress card — nudging the model to show progress",
200
+ );
201
+ ctx.additionalContext = ctx.additionalContext
202
+ ? `${ctx.additionalContext}\n${TASK_PROGRESS_NUDGE_TEXT}`
203
+ : TASK_PROGRESS_NUDGE_TEXT;
204
+ };
205
+
206
+ export default postToolUse;
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "default-task-progress-nudge",
3
+ "version": "1.0.0",
4
+ "description": "First-party default plugin contributing a post-tool-use hook that nudges the model to show a task_progress card when an interactive turn accumulates several tool-call rounds without one.",
5
+ "private": true,
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./register.ts",
9
+ "engines": {
10
+ "node": ">=20.12.0"
11
+ },
12
+ "peerDependencies": {
13
+ "@vellumai/plugin-api": "^0.8.0"
14
+ }
15
+ }