@vellumai/assistant 0.8.8 → 0.8.9-dev.202606091926.ebb2d62

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 (490) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/bun.lock +2 -2
  3. package/docs/activation-funnel-telemetry.md +310 -0
  4. package/examples/plugins/echo/README.md +61 -60
  5. package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
  6. package/examples/plugins/echo/hooks/stop.ts +16 -0
  7. package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
  8. package/examples/plugins/echo/package.json +1 -2
  9. package/examples/plugins/echo/src/emit.ts +19 -0
  10. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
  11. package/openapi.yaml +273 -121
  12. package/package.json +2 -2
  13. package/scripts/sync-llm-catalog.ts +2 -0
  14. package/src/__tests__/activation-early-marking.test.ts +120 -0
  15. package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
  16. package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
  17. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
  18. package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
  19. package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
  20. package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
  21. package/src/__tests__/agent-loop-thinking.test.ts +36 -20
  22. package/src/__tests__/agent-loop.test.ts +441 -96
  23. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
  24. package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
  25. package/src/__tests__/anthropic-provider.test.ts +23 -8
  26. package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
  27. package/src/__tests__/app-control-flow.test.ts +1 -1
  28. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  29. package/src/__tests__/app-executors.test.ts +132 -0
  30. package/src/__tests__/approval-cascade.test.ts +6 -5
  31. package/src/__tests__/approval-routes-http.test.ts +4 -1
  32. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  33. package/src/__tests__/build-persisted-content.test.ts +75 -1
  34. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  35. package/src/__tests__/channel-approvals.test.ts +1 -1
  36. package/src/__tests__/compaction-circuit.test.ts +258 -0
  37. package/src/__tests__/compaction-direct.test.ts +146 -0
  38. package/src/__tests__/compaction-events.test.ts +7 -7
  39. package/src/__tests__/compaction.benchmark.test.ts +1 -1
  40. package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
  41. package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
  42. package/src/__tests__/context-overflow-reducer.test.ts +265 -125
  43. package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
  44. package/src/__tests__/conversation-abort-tool-results.test.ts +7 -5
  45. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
  46. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
  47. package/src/__tests__/conversation-agent-loop-overflow.test.ts +158 -234
  48. package/src/__tests__/conversation-agent-loop.test.ts +342 -623
  49. package/src/__tests__/conversation-clean-command.test.ts +5 -2
  50. package/src/__tests__/conversation-confirmation-signals.test.ts +6 -5
  51. package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
  52. package/src/__tests__/conversation-error.test.ts +15 -1
  53. package/src/__tests__/conversation-history-web-search.test.ts +6 -1
  54. package/src/__tests__/conversation-media-retry.test.ts +1 -1
  55. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +44 -4
  56. package/src/__tests__/conversation-process-callsite.test.ts +15 -15
  57. package/src/__tests__/conversation-provider-retry-repair.test.ts +66 -65
  58. package/src/__tests__/conversation-queue.test.ts +10 -10
  59. package/src/__tests__/conversation-runtime-assembly.test.ts +1059 -231
  60. package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
  61. package/src/__tests__/conversation-slash-queue.test.ts +7 -5
  62. package/src/__tests__/conversation-slash-unknown.test.ts +7 -5
  63. package/src/__tests__/conversation-speed-override.test.ts +11 -10
  64. package/src/__tests__/conversation-starter-routes.test.ts +14 -6
  65. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
  66. package/src/__tests__/conversation-surfaces-app-control.test.ts +48 -1
  67. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
  68. package/src/__tests__/conversation-undo.test.ts +2 -2
  69. package/src/__tests__/conversation-workspace-cache-state.test.ts +24 -21
  70. package/src/__tests__/conversation-workspace-injection.test.ts +69 -7
  71. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +15 -12
  72. package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
  73. package/src/__tests__/corrected-target.test.ts +93 -0
  74. package/src/__tests__/credential-execution-feature-gates.test.ts +5 -4
  75. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  76. package/src/__tests__/cu-unified-flow.test.ts +36 -0
  77. package/src/__tests__/db-acp-history.test.ts +101 -0
  78. package/src/__tests__/dynamic-page-surface.test.ts +31 -0
  79. package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
  80. package/src/__tests__/empty-response-hook.test.ts +1 -1
  81. package/src/__tests__/file-write-tool.test.ts +63 -0
  82. package/src/__tests__/gateway-only-guard.test.ts +17 -2
  83. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  84. package/src/__tests__/guardian-routing-invariants.test.ts +93 -4
  85. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  86. package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
  87. package/src/__tests__/heartbeat-service.test.ts +1 -0
  88. package/src/__tests__/history-repair-hook.test.ts +5 -1
  89. package/src/__tests__/host-app-control-proxy.test.ts +45 -0
  90. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  91. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  92. package/src/__tests__/inference-profile-reaper.test.ts +62 -0
  93. package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
  94. package/src/__tests__/injector-background-turn.test.ts +13 -23
  95. package/src/__tests__/injector-chain.test.ts +268 -44
  96. package/src/__tests__/injector-disk-pressure.test.ts +210 -52
  97. package/src/__tests__/injector-document-comments.test.ts +97 -114
  98. package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
  99. package/src/__tests__/injector-v3-suppression.test.ts +4 -4
  100. package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
  101. package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
  102. package/src/__tests__/llm-catalog-parity.test.ts +4 -0
  103. package/src/__tests__/llm-resolver.test.ts +73 -0
  104. package/src/__tests__/memory-retrieval-hook.test.ts +156 -38
  105. package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
  106. package/src/__tests__/notification-decision-strategy.test.ts +3 -3
  107. package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
  108. package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
  109. package/src/__tests__/persist-unsendable-image.test.ts +97 -1
  110. package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
  111. package/src/__tests__/plugin-api-shim.test.ts +6 -9
  112. package/src/__tests__/plugin-bootstrap.test.ts +91 -81
  113. package/src/__tests__/plugin-registry.test.ts +3 -49
  114. package/src/__tests__/plugin-skill-contribution.test.ts +15 -14
  115. package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
  116. package/src/__tests__/plugin-types.test.ts +0 -70
  117. package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
  118. package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
  119. package/src/__tests__/published-app-updater.test.ts +138 -0
  120. package/src/__tests__/reaction-persistence.test.ts +1 -1
  121. package/src/__tests__/retry-thinking-adaptive-only.test.ts +245 -0
  122. package/src/__tests__/send-endpoint-busy.test.ts +4 -1
  123. package/src/__tests__/server-history-render.test.ts +39 -0
  124. package/src/__tests__/skill-feature-flags-integration.test.ts +31 -0
  125. package/src/__tests__/steer-tool-repair.test.ts +1 -1
  126. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  127. package/src/__tests__/subagent-detail.test.ts +25 -7
  128. package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
  129. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  130. package/src/__tests__/subagent-manager-notify.test.ts +1 -3
  131. package/src/__tests__/subagent-notify-parent.test.ts +1 -3
  132. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  133. package/src/__tests__/title-generate-hook.test.ts +5 -1
  134. package/src/__tests__/tool-error-hook.test.ts +1 -1
  135. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
  136. package/src/__tests__/tool-start-timestamp.test.ts +218 -0
  137. package/src/__tests__/user-plugin-loader.test.ts +54 -286
  138. package/src/__tests__/web-fetch.test.ts +45 -0
  139. package/src/acp/__tests__/agent-process.test.ts +161 -0
  140. package/src/acp/__tests__/client-handler.test.ts +40 -0
  141. package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
  142. package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
  143. package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
  144. package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
  145. package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
  146. package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
  147. package/src/acp/agent-process.ts +61 -1
  148. package/src/acp/auto-install.test.ts +280 -0
  149. package/src/acp/auto-install.ts +232 -0
  150. package/src/acp/client-handler.ts +31 -0
  151. package/src/acp/prepare-agent-env.ts +80 -27
  152. package/src/acp/resolve-agent.test.ts +188 -28
  153. package/src/acp/resolve-agent.ts +119 -42
  154. package/src/acp/resume-hint.ts +23 -0
  155. package/src/acp/session-manager.ts +507 -73
  156. package/src/agent/compaction-circuit.ts +60 -102
  157. package/src/agent/loop.ts +403 -251
  158. package/src/api/events/tool-result.ts +9 -0
  159. package/src/api/events/tool-use-start.ts +7 -0
  160. package/src/api/responses/conversation-message.ts +72 -13
  161. package/src/api/responses/memory-v3-selection-log.ts +19 -10
  162. package/src/approvals/guardian-request-resolvers.ts +27 -1
  163. package/src/background-wake/next-wake.ts +1 -0
  164. package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
  165. package/src/cli/commands/channel-verification-sessions.ts +6 -6
  166. package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
  167. package/src/cli/commands/memory-v3.ts +57 -199
  168. package/src/cli/commands/plugins.ts +43 -37
  169. package/src/cli/lib/__tests__/install-from-github.test.ts +790 -111
  170. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
  171. package/src/cli/lib/__tests__/plugin-details.test.ts +381 -0
  172. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +270 -0
  173. package/src/cli/lib/__tests__/search-plugins.test.ts +233 -32
  174. package/src/cli/lib/install-from-github.ts +797 -60
  175. package/src/cli/lib/plugin-catalog-cache.ts +84 -0
  176. package/src/cli/lib/plugin-details.ts +416 -0
  177. package/src/cli/lib/plugin-marketplace.ts +215 -0
  178. package/src/cli/lib/search-plugins.ts +201 -29
  179. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  180. package/src/config/acp-defaults.test.ts +10 -0
  181. package/src/config/acp-defaults.ts +9 -3
  182. package/src/config/acp-schema.ts +1 -7
  183. package/src/config/bundled-skills/acp/SKILL.md +80 -41
  184. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  185. package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
  186. package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
  187. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
  188. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
  189. package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
  190. package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
  191. package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
  192. package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
  193. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  194. package/src/config/bundled-tool-registry.ts +4 -0
  195. package/src/config/call-site-defaults.ts +0 -3
  196. package/src/config/feature-flag-registry.json +10 -56
  197. package/src/config/llm-resolver.ts +39 -7
  198. package/src/config/loader.ts +5 -22
  199. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
  200. package/src/config/schemas/call-site-catalog.ts +0 -21
  201. package/src/config/schemas/heartbeat.ts +9 -0
  202. package/src/config/schemas/llm.ts +17 -3
  203. package/src/config/schemas/memory-v3.ts +58 -8
  204. package/src/config/seed-inference-profiles.ts +18 -0
  205. package/src/context/compactor.ts +22 -4
  206. package/src/context/post-turn-tool-result-truncation.ts +39 -1
  207. package/src/context/strip-injections.ts +8 -2
  208. package/src/daemon/conversation-agent-loop-handlers.ts +120 -42
  209. package/src/daemon/conversation-agent-loop.ts +308 -1297
  210. package/src/daemon/conversation-error.ts +31 -4
  211. package/src/daemon/conversation-history.ts +1 -1
  212. package/src/daemon/conversation-lifecycle.ts +11 -255
  213. package/src/daemon/conversation-media-retry.ts +19 -6
  214. package/src/daemon/conversation-messaging.ts +17 -0
  215. package/src/daemon/conversation-process.ts +22 -141
  216. package/src/daemon/conversation-queue-manager.ts +8 -0
  217. package/src/daemon/conversation-registry.ts +159 -0
  218. package/src/daemon/conversation-runtime-assembly.ts +361 -393
  219. package/src/daemon/conversation-store.ts +9 -90
  220. package/src/daemon/conversation-surfaces.ts +165 -11
  221. package/src/daemon/conversation-workspace.ts +17 -0
  222. package/src/daemon/conversation.ts +444 -62
  223. package/src/daemon/disk-pressure-policy.ts +0 -1
  224. package/src/daemon/external-plugins-bootstrap.ts +36 -46
  225. package/src/daemon/handlers/config-channels.ts +11 -2
  226. package/src/daemon/handlers/conversations.ts +3 -1
  227. package/src/daemon/handlers/shared.ts +9 -1
  228. package/src/daemon/handlers/skills.ts +4 -1
  229. package/src/daemon/host-app-control-proxy.ts +72 -57
  230. package/src/daemon/host-proxy-preactivation.ts +1 -3
  231. package/src/daemon/lifecycle.ts +21 -7
  232. package/src/daemon/persist-unsendable-image.ts +62 -25
  233. package/src/daemon/process-message.ts +1 -1
  234. package/src/daemon/server.ts +2 -0
  235. package/src/daemon/tool-side-effects.ts +15 -0
  236. package/src/daemon/wake-conversation-ops.ts +269 -0
  237. package/src/embedded/plugin-api.ts +2 -2
  238. package/src/export/__tests__/transcript-formatter.test.ts +5 -0
  239. package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
  240. package/src/heartbeat/heartbeat-run-store.ts +23 -1
  241. package/src/heartbeat/heartbeat-service.ts +26 -0
  242. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  243. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  244. package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
  245. package/src/ipc/skill-routes/memory.ts +4 -2
  246. package/src/memory/__tests__/activation-session-store.test.ts +41 -0
  247. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
  248. package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
  249. package/src/memory/activation-session-store.ts +43 -0
  250. package/src/memory/conversation-crud.ts +29 -19
  251. package/src/memory/conversation-starter-checkpoints.ts +1 -0
  252. package/src/memory/db-init.ts +6 -0
  253. package/src/memory/embedding-backend.ts +5 -22
  254. package/src/memory/job-handlers/conversation-starters.ts +13 -2
  255. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
  256. package/src/memory/jobs-worker.ts +25 -1
  257. package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
  258. package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
  259. package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
  260. package/src/memory/migrations/index.ts +3 -0
  261. package/src/memory/onboarding-events-store.ts +66 -18
  262. package/src/memory/schema/acp.ts +4 -0
  263. package/src/memory/schema/infrastructure.ts +13 -0
  264. package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
  265. package/src/memory/v2/__tests__/page-store.test.ts +22 -0
  266. package/src/memory/v2/consolidation-job.ts +2 -63
  267. package/src/memory/v2/types.ts +5 -0
  268. package/src/messaging/providers/telegram-bot/api.ts +14 -5
  269. package/src/notifications/__tests__/copy-composer.test.ts +244 -0
  270. package/src/notifications/access-request-copy.ts +298 -0
  271. package/src/notifications/adapters/slack.ts +3 -3
  272. package/src/notifications/adapters/telegram.ts +9 -2
  273. package/src/notifications/copy-composer.ts +49 -267
  274. package/src/notifications/decision-engine.ts +16 -35
  275. package/src/notifications/home-feed-side-effect.ts +1 -6
  276. package/src/plugin-api/constants.ts +4 -0
  277. package/src/plugin-api/index.ts +40 -11
  278. package/src/plugin-api/types.ts +108 -0
  279. package/src/plugins/defaults/compaction/compact.ts +106 -0
  280. package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +250 -42
  281. package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
  282. package/src/plugins/defaults/compaction/manager-store.ts +57 -0
  283. package/src/plugins/defaults/compaction/package.json +1 -2
  284. package/src/{context → plugins/defaults/compaction}/window-manager.ts +95 -25
  285. package/src/plugins/defaults/empty-response/package.json +0 -1
  286. package/src/plugins/defaults/history-repair/package.json +0 -1
  287. package/src/plugins/defaults/index.ts +156 -26
  288. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
  289. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +285 -0
  290. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
  291. package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
  292. package/src/plugins/defaults/memory-retrieval/package.json +14 -0
  293. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
  294. package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +71 -0
  295. package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
  296. package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
  297. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +147 -134
  298. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +456 -0
  299. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +662 -0
  300. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
  301. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +91 -0
  302. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
  303. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
  304. package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
  305. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +74 -19
  306. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
  307. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +592 -0
  308. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -24
  309. package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +91 -0
  310. package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
  311. package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
  312. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
  313. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
  314. package/src/plugins/defaults/memory-v3-shadow/injector.ts +76 -0
  315. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +555 -0
  316. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +266 -0
  317. package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
  318. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +71 -0
  319. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
  320. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +16 -8
  321. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
  322. package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
  323. package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
  324. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +156 -0
  325. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +382 -0
  326. package/src/plugins/defaults/memory-v3-shadow/types.ts +92 -0
  327. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +4 -1
  328. package/src/plugins/defaults/title-generate/package.json +0 -1
  329. package/src/plugins/defaults/tool-error/package.json +0 -1
  330. package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
  331. package/src/plugins/external-api.ts +12 -2
  332. package/src/plugins/pipeline.ts +6 -293
  333. package/src/plugins/registry.ts +9 -37
  334. package/src/plugins/types.ts +76 -381
  335. package/src/plugins/user-loader.ts +30 -127
  336. package/src/prompts/__tests__/system-prompt.test.ts +6 -0
  337. package/src/prompts/system-prompt.ts +61 -10
  338. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +71 -4
  339. package/src/providers/anthropic/client.ts +9 -10
  340. package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
  341. package/src/providers/inference/logit-bias.ts +55 -0
  342. package/src/providers/model-catalog.ts +57 -0
  343. package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
  344. package/src/providers/openai/chat-completions-provider.ts +44 -1
  345. package/src/providers/retry.ts +57 -7
  346. package/src/providers/types.ts +6 -0
  347. package/src/runtime/__tests__/agent-wake.test.ts +555 -691
  348. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  349. package/src/runtime/agent-wake.ts +108 -209
  350. package/src/runtime/assistant-event-hub.ts +1 -1
  351. package/src/runtime/channel-approvals.ts +1 -1
  352. package/src/runtime/interactive-ui.ts +1 -1
  353. package/src/runtime/routes/__tests__/acp-routes.test.ts +466 -55
  354. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  355. package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
  356. package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
  357. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
  358. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
  359. package/src/runtime/routes/acp-routes.test.ts +185 -32
  360. package/src/runtime/routes/acp-routes.ts +328 -30
  361. package/src/runtime/routes/app-management-routes.ts +3 -0
  362. package/src/runtime/routes/approval-routes.ts +1 -1
  363. package/src/runtime/routes/browser-routes.ts +1 -1
  364. package/src/runtime/routes/browser-tabs-routes.ts +6 -10
  365. package/src/runtime/routes/channel-verification-routes.ts +14 -5
  366. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  367. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  368. package/src/runtime/routes/conversation-query-routes.ts +1 -1
  369. package/src/runtime/routes/conversation-routes.ts +30 -2
  370. package/src/runtime/routes/conversation-starter-routes.ts +13 -7
  371. package/src/runtime/routes/conversations-import-routes.ts +24 -7
  372. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  373. package/src/runtime/routes/host-cu-routes.ts +1 -1
  374. package/src/runtime/routes/identity-routes.ts +18 -3
  375. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  376. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
  377. package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
  378. package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
  379. package/src/runtime/routes/memory-v3-routes.ts +65 -305
  380. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
  381. package/src/runtime/routes/playground/helpers.ts +1 -1
  382. package/src/runtime/routes/plugins-routes.ts +337 -35
  383. package/src/runtime/routes/stt-routes.ts +45 -12
  384. package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
  385. package/src/runtime/routes/work-items-routes.ts +2 -4
  386. package/src/runtime/routes/workspace-routes.ts +50 -15
  387. package/src/runtime/services/conversation-serializer.ts +1 -1
  388. package/src/runtime/verification-outbound-actions.ts +147 -2
  389. package/src/runtime/verification-templates.ts +29 -3
  390. package/src/services/published-app-updater.ts +30 -8
  391. package/src/signals/cancel.ts +2 -4
  392. package/src/subagent/manager.ts +21 -5
  393. package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
  394. package/src/telemetry/activation-funnel.ts +167 -0
  395. package/src/telemetry/types.ts +13 -0
  396. package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
  397. package/src/telemetry/usage-telemetry-reporter.ts +26 -1
  398. package/src/tools/acp/context.ts +20 -0
  399. package/src/tools/acp/list-agents.test.ts +9 -19
  400. package/src/tools/acp/list-agents.ts +3 -15
  401. package/src/tools/acp/spawn.test.ts +173 -202
  402. package/src/tools/acp/spawn.ts +37 -172
  403. package/src/tools/acp/steer.test.ts +105 -8
  404. package/src/tools/acp/steer.ts +48 -17
  405. package/src/tools/apps/executors.ts +166 -50
  406. package/src/tools/browser/browser-execution.ts +12 -2
  407. package/src/tools/filesystem/write.ts +34 -0
  408. package/src/tools/network/web-fetch.ts +65 -24
  409. package/src/tools/skills/load.ts +1 -1
  410. package/src/tools/subagent/spawn.ts +2 -4
  411. package/src/tools/ui-surface/definitions.ts +32 -5
  412. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
  413. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
  414. package/docs/plugins.md +0 -832
  415. package/examples/plugins/echo/register.ts +0 -143
  416. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
  417. package/src/__tests__/compaction-pipeline.test.ts +0 -210
  418. package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
  419. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
  420. package/src/__tests__/pipeline-runner.test.ts +0 -554
  421. package/src/daemon/wake-target-adapter.ts +0 -253
  422. package/src/memory/v3/__tests__/assign.test.ts +0 -242
  423. package/src/memory/v3/__tests__/capabilities.test.ts +0 -118
  424. package/src/memory/v3/__tests__/core.test.ts +0 -39
  425. package/src/memory/v3/__tests__/health.test.ts +0 -219
  426. package/src/memory/v3/__tests__/maintain-job.test.ts +0 -288
  427. package/src/memory/v3/__tests__/needle.test.ts +0 -107
  428. package/src/memory/v3/__tests__/orchestrate.test.ts +0 -436
  429. package/src/memory/v3/__tests__/provider-blocks.test.ts +0 -13
  430. package/src/memory/v3/__tests__/reconcile.test.ts +0 -274
  431. package/src/memory/v3/__tests__/render-injection.test.ts +0 -61
  432. package/src/memory/v3/__tests__/router.test.ts +0 -332
  433. package/src/memory/v3/__tests__/selector.test.ts +0 -470
  434. package/src/memory/v3/__tests__/shadow-plugin.test.ts +0 -432
  435. package/src/memory/v3/__tests__/snapshot.test.ts +0 -168
  436. package/src/memory/v3/__tests__/tree.test.ts +0 -192
  437. package/src/memory/v3/assign.ts +0 -268
  438. package/src/memory/v3/capabilities.ts +0 -124
  439. package/src/memory/v3/core.ts +0 -26
  440. package/src/memory/v3/health.ts +0 -0
  441. package/src/memory/v3/maintain-job.ts +0 -314
  442. package/src/memory/v3/needle.ts +0 -115
  443. package/src/memory/v3/orchestrate.ts +0 -126
  444. package/src/memory/v3/page-content.ts +0 -34
  445. package/src/memory/v3/provider-blocks.ts +0 -26
  446. package/src/memory/v3/reconcile.ts +0 -523
  447. package/src/memory/v3/router.ts +0 -190
  448. package/src/memory/v3/selection-log-store.ts +0 -84
  449. package/src/memory/v3/selector.ts +0 -226
  450. package/src/memory/v3/shadow-plugin.ts +0 -411
  451. package/src/memory/v3/snapshot.ts +0 -209
  452. package/src/memory/v3/tree.ts +0 -174
  453. package/src/memory/v3/types.ts +0 -59
  454. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
  455. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
  456. package/src/plugins/defaults/circuit-breaker/package.json +0 -15
  457. package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
  458. package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
  459. package/src/plugins/defaults/compaction/register.ts +0 -35
  460. package/src/plugins/defaults/compaction/terminal.ts +0 -73
  461. package/src/plugins/defaults/empty-response/register.ts +0 -23
  462. package/src/plugins/defaults/history-repair/register.ts +0 -24
  463. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +0 -216
  464. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
  465. package/src/plugins/defaults/overflow-reduce/package.json +0 -15
  466. package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
  467. package/src/plugins/defaults/title-generate/register.ts +0 -35
  468. package/src/plugins/defaults/tool-error/register.ts +0 -23
  469. package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
  470. package/src/proactive-artifact/aux-message-injector.ts +0 -97
  471. package/src/proactive-artifact/decision.test.ts +0 -226
  472. package/src/proactive-artifact/decision.ts +0 -165
  473. package/src/proactive-artifact/index.ts +0 -7
  474. package/src/proactive-artifact/job.test.ts +0 -962
  475. package/src/proactive-artifact/job.ts +0 -372
  476. package/src/proactive-artifact/message-copy.ts +0 -58
  477. package/src/proactive-artifact/trigger-state.test.ts +0 -286
  478. package/src/proactive-artifact/trigger-state.ts +0 -123
  479. package/src/util/map-limit.ts +0 -27
  480. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
  481. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
  482. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
  483. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
  484. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
  485. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
  486. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
  487. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
  488. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
  489. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
  490. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
@@ -11,6 +11,9 @@
11
11
  * - Skips (re)injection when the `<info>` block is already present in the
12
12
  * turn's working messages (presence detection — the block persists in
13
13
  * history between compactions).
14
+ * - Still injects when only a dynamic `<memory>` activation block is present
15
+ * (that wrapper must not be mistaken for the static block) — both on a
16
+ * normal turn and after compaction strips the prior `<info>` block.
14
17
  *
15
18
  * The injector sources its content itself via `readMemoryV2StaticContent()`
16
19
  * behind the personal-memory trust gate, so each test seeds the workspace
@@ -35,7 +38,7 @@ mock.module("../config/loader.js", () => ({
35
38
  }));
36
39
 
37
40
  const { defaultInjectors } =
38
- await import("../plugins/defaults/injectors/register.js");
41
+ await import("../plugins/defaults/memory-retrieval/injectors.js");
39
42
  import type { Injector, TurnContext } from "../plugins/types.js";
40
43
  import type { Message } from "../providers/types.js";
41
44
  import { getWorkspacePromptPath } from "../util/platform.js";
@@ -54,7 +57,6 @@ function makeContext(overrides: Partial<TurnContext> = {}): TurnContext {
54
57
  conversationId: "conv-test",
55
58
  turnIndex: 0,
56
59
  trust: { sourceChannel: "vellum", trustClass: "guardian" },
57
- injectionInputs: {},
58
60
  ...overrides,
59
61
  };
60
62
  }
@@ -96,7 +98,7 @@ describe("memory-v2-static injector", () => {
96
98
 
97
99
  test("returns null in minimal mode even with content", async () => {
98
100
  seedEssentials("Alice prefers VS Code.");
99
- const ctx = makeContext({ injectionInputs: { mode: "minimal" } });
101
+ const ctx = makeContext({ mode: "minimal" });
100
102
  expect(await memoryV2StaticInjector.produce(ctx)).toBeNull();
101
103
  });
102
104
 
@@ -140,10 +142,12 @@ describe("memory-v2-static injector", () => {
140
142
  expect(await memoryV2StaticInjector.produce(ctx, runMessages)).toBeNull();
141
143
  });
142
144
 
143
- test("skips (re)injection when a legacy <memory>-wrapped static block is present", async () => {
144
- // Rows persisted before the `<info>` switch rehydrate the static block as
145
- // `<memory>…</memory>`. Re-injecting a fresh `<info>` copy alongside it
146
- // would duplicate the content until the next compaction.
145
+ test("injects the <info> block even when a dynamic <memory> activation block is present", async () => {
146
+ // Regression (#33612): the v2 *dynamic* activation block uses the
147
+ // `<memory>…</memory>` wrapper and `prepareMemory` prepends it to the tail
148
+ // user message every turn, before this injector runs. The static injector
149
+ // must NOT treat that as its own `<info>` block — doing so suppressed the
150
+ // static block on essentially every turn.
147
151
  seedEssentials("Alice prefers VS Code.");
148
152
  const ctx = makeContext();
149
153
  const runMessages: Message[] = [
@@ -152,12 +156,86 @@ describe("memory-v2-static injector", () => {
152
156
  content: [
153
157
  {
154
158
  type: "text",
155
- text: "<memory>\n## Essentials\n\nAlice prefers VS Code.\n</memory>",
159
+ text: "<memory>\n# memory/concepts/foo.md\nactivated page summary\n</memory>",
156
160
  },
157
161
  { type: "text", text: "What next?" },
158
162
  ],
159
163
  },
160
164
  ];
165
+ const block = await memoryV2StaticInjector.produce(ctx, runMessages);
166
+ expect(block).not.toBeNull();
167
+ expect(block!.id).toBe("memory-v2-static");
168
+ expect(block!.text).toBe(
169
+ "<info>\n## Essentials\n\nAlice prefers VS Code.\n</info>",
170
+ );
171
+ });
172
+
173
+ test("reinjects the <info> block after compaction strips it, alongside a fresh <memory> block", async () => {
174
+ // Post-compaction state: `stripInjectionsForCompaction` removed the prior
175
+ // `<info>` block, and the next turn's `prepareMemory` re-added a fresh
176
+ // dynamic `<memory>` block ahead of the chain. With no `<info>` present, the
177
+ // injector must reinject it rather than skip on the `<memory>` block.
178
+ seedEssentials("Alice prefers VS Code.");
179
+ const ctx = makeContext();
180
+ const postCompactMessages: Message[] = [
181
+ {
182
+ role: "user",
183
+ content: [
184
+ { type: "text", text: "<memory>\nactivated page summary\n</memory>" },
185
+ { type: "text", text: "Continue please." },
186
+ ],
187
+ },
188
+ ];
189
+ const block = await memoryV2StaticInjector.produce(
190
+ ctx,
191
+ postCompactMessages,
192
+ );
193
+ expect(block).not.toBeNull();
194
+ expect(block!.id).toBe("memory-v2-static");
195
+ expect(block!.text).toContain("## Essentials");
196
+ });
197
+
198
+ test("skips (re)injection when the <info> block is carried in a flattened history block", async () => {
199
+ // Regression: when a historical user message's separate injection content
200
+ // blocks get collapsed into one text block (observed after a daemon restart
201
+ // / plugin hot-reload rebuilt the rendered history mid-conversation — the
202
+ // blocks are joined with "\n"), the `<info>` wrapper is no longer its own
203
+ // block but a newline-delimited section inside a larger block. Presence
204
+ // detection must still recognize it; otherwise the injector re-injects a
205
+ // duplicate `<info>` onto the new tail (the double-injection bug).
206
+ seedEssentials("Alice prefers VS Code.");
207
+ const ctx = makeContext();
208
+ const flattenedFirstUser = [
209
+ "<workspace>\nRoot: /ws\nFiles: a.md\n</workspace>",
210
+ "<turn_context>\ncurrent_time: now\n</turn_context>",
211
+ "<memory>\nactivated page summary\n</memory>",
212
+ "<info>\n## Essentials\n\nAlice prefers VS Code.\n</info>",
213
+ "what should we do next?",
214
+ ].join("\n");
215
+ const runMessages: Message[] = [
216
+ { role: "user", content: [{ type: "text", text: flattenedFirstUser }] },
217
+ { role: "assistant", content: [{ type: "text", text: "On it." }] },
218
+ { role: "user", content: [{ type: "text", text: "great, continue" }] },
219
+ ];
161
220
  expect(await memoryV2StaticInjector.produce(ctx, runMessages)).toBeNull();
162
221
  });
222
+
223
+ test("still injects when a user message opens an <info> tag without closing it", async () => {
224
+ // False-positive guard: the wrapper matcher requires BOTH the opening and
225
+ // closing tags, so user prose that merely opens an injection-like tag must
226
+ // not be mistaken for a carried block and suppress injection.
227
+ seedEssentials("Alice prefers VS Code.");
228
+ const ctx = makeContext();
229
+ const runMessages: Message[] = [
230
+ {
231
+ role: "user",
232
+ content: [
233
+ { type: "text", text: "<info>\n is this how the tag works?" },
234
+ ],
235
+ },
236
+ ];
237
+ const block = await memoryV2StaticInjector.produce(ctx, runMessages);
238
+ expect(block).not.toBeNull();
239
+ expect(block!.id).toBe("memory-v2-static");
240
+ });
163
241
  });
@@ -9,17 +9,17 @@
9
9
 
10
10
  import { describe, expect, test } from "bun:test";
11
11
 
12
- import type { ConversationCandidateSet } from "../notifications/conversation-candidates.js";
13
12
  import {
14
13
  buildAccessRequestContractText,
15
14
  buildAccessRequestIdentityLine,
16
- composeFallbackCopy,
17
15
  hasAccessRequestInstructions,
18
16
  hasInviteFlowDirective,
19
17
  normalizeForDirectiveMatching,
20
18
  sanitizeIdentityField,
21
19
  sanitizeMessagePreview,
22
- } from "../notifications/copy-composer.js";
20
+ } from "../notifications/access-request-copy.js";
21
+ import type { ConversationCandidateSet } from "../notifications/conversation-candidates.js";
22
+ import { composeFallbackCopy } from "../notifications/copy-composer.js";
23
23
  import {
24
24
  enforceGuardianCallConversationAffinity,
25
25
  validateConversationActions,
@@ -128,12 +128,19 @@ describe("Parallel tool execution benchmarks", () => {
128
128
  return { content: "ok", isError: false };
129
129
  };
130
130
 
131
- const loop = new AgentLoop(provider, "system", {
131
+ const loop = new AgentLoop({
132
+ provider: provider,
133
+ systemPrompt: "system",
134
+ conversationId: "test-conversation",
132
135
  tools: dummyTools,
133
136
  toolExecutor: toolExecutor,
134
137
  });
135
138
  const start = Date.now();
136
- await loop.run([userMessage], () => {});
139
+ await loop.run({
140
+ messages: [userMessage],
141
+ onEvent: () => {},
142
+ trust: { sourceChannel: "vellum", trustClass: "unknown" },
143
+ });
137
144
  const elapsed = Date.now() - start;
138
145
 
139
146
  // Parallel: ~50ms + overhead. Sequential would be ~250ms.
@@ -169,12 +176,19 @@ describe("Parallel tool execution benchmarks", () => {
169
176
  return { content: "ok", isError: false };
170
177
  };
171
178
 
172
- const loop = new AgentLoop(provider, "system", {
179
+ const loop = new AgentLoop({
180
+ provider: provider,
181
+ systemPrompt: "system",
182
+ conversationId: "test-conversation",
173
183
  tools: dummyTools,
174
184
  toolExecutor: toolExecutor,
175
185
  });
176
186
  const start = Date.now();
177
- await loop.run([userMessage], () => {});
187
+ await loop.run({
188
+ messages: [userMessage],
189
+ onEvent: () => {},
190
+ trust: { sourceChannel: "vellum", trustClass: "unknown" },
191
+ });
178
192
  const elapsed = Date.now() - start;
179
193
 
180
194
  // All 10 tools should have executed
@@ -222,13 +236,20 @@ describe("Parallel tool execution benchmarks", () => {
222
236
  return { content: "ok", isError: false };
223
237
  };
224
238
 
225
- const loop = new AgentLoop(provider, "system", {
239
+ const loop = new AgentLoop({
240
+ provider: provider,
241
+ systemPrompt: "system",
242
+ conversationId: "test-conversation",
226
243
  tools: dummyTools,
227
244
  toolExecutor: toolExecutor,
228
245
  });
229
246
  const events: AgentEvent[] = [];
230
247
  const start = Date.now();
231
- await loop.run([userMessage], collectEvents(events));
248
+ await loop.run({
249
+ messages: [userMessage],
250
+ onEvent: collectEvents(events),
251
+ trust: { sourceChannel: "vellum", trustClass: "unknown" },
252
+ });
232
253
  const elapsed = Date.now() - start;
233
254
 
234
255
  // Parallel: ~2000ms (dominated by slow tool). Sequential: ~2400ms (2000 + 4*100).
@@ -289,12 +310,18 @@ describe("Parallel tool execution benchmarks", () => {
289
310
  return { content: "should not return", isError: false };
290
311
  };
291
312
 
292
- const loop = new AgentLoop(provider, "system", {
313
+ const loop = new AgentLoop({
314
+ provider: provider,
315
+ systemPrompt: "system",
316
+ conversationId: "test-conversation",
293
317
  tools: dummyTools,
294
318
  toolExecutor: toolExecutor,
295
319
  });
296
320
  const start = Date.now();
297
- const { history } = await loop.run([userMessage], () => {}, {
321
+ const { history } = await loop.run({
322
+ messages: [userMessage],
323
+ onEvent: () => {},
324
+ trust: { sourceChannel: "vellum", trustClass: "unknown" },
298
325
  signal: controller.signal,
299
326
  });
300
327
  const elapsed = Date.now() - start;
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Regression test for the durable-downscale path of the image-too-large
3
+ * recovery (JARVIS-1041 review follow-up).
4
+ *
5
+ * When an oversized stored image *can* be shrunk on this host (the common macOS
6
+ * path where `sips` is available), `persistUnsendableImageDowngrades` must write
7
+ * the downscaled bytes back to the DB — not leave the original in place. The
8
+ * latest tool-result media is intentionally kept in context, so leaving the
9
+ * full-size block would rehydrate and re-reject on every later turn instead of
10
+ * durably self-healing the conversation.
11
+ *
12
+ * `optimizeImageForTransport` needs `sips` and a decodable image to actually
13
+ * downscale, which is not portable to CI, so it is mocked here to simulate a
14
+ * successful shrink. The mock is process-global, so this case lives in its own
15
+ * file (the test runner isolates each file in its own process) to avoid
16
+ * disturbing the no-op-resize cases in persist-unsendable-image.test.ts.
17
+ */
18
+
19
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
20
+
21
+ // ── Module mock (must precede the import of the module under test) ────
22
+ // Simulate a host where resizing succeeds: any oversized image is shrunk to a
23
+ // small, distinct JPEG payload. durableImageReplacement checks the provider
24
+ // caps before calling this, so in-limit images never reach the mock.
25
+ const SHRUNK_DATA = "c2hydW5r"; // base64 for "shrunk"
26
+ mock.module("../agent/image-optimize.js", () => ({
27
+ optimizeImageForTransport: () => ({
28
+ data: SHRUNK_DATA,
29
+ mediaType: "image/jpeg",
30
+ }),
31
+ }));
32
+
33
+ import { persistUnsendableImageDowngrades } from "../daemon/persist-unsendable-image.js";
34
+ import {
35
+ addMessage,
36
+ createConversation,
37
+ getMessages,
38
+ } from "../memory/conversation-crud.js";
39
+ import { getDb } from "../memory/db-connection.js";
40
+ import { initializeDb } from "../memory/db-init.js";
41
+ import type { ContentBlock } from "../providers/types.js";
42
+
43
+ initializeDb();
44
+
45
+ function resetTables(): void {
46
+ const db = getDb();
47
+ db.run("DELETE FROM message_attachments");
48
+ db.run("DELETE FROM attachments");
49
+ db.run("DELETE FROM memory_segments");
50
+ db.run("DELETE FROM memory_embeddings");
51
+ db.run("DELETE FROM messages");
52
+ db.run("DELETE FROM conversations");
53
+ }
54
+
55
+ /** Minimal PNG whose IHDR declares dimensions past the 8000px provider cap. */
56
+ function oversizedPngBase64(): string {
57
+ const width = 12000;
58
+ const height = 9000;
59
+ return Buffer.from(
60
+ Uint8Array.from([
61
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, // PNG signature
62
+ 0x00, 0x00, 0x00, 0x0d, // IHDR length (13)
63
+ 0x49, 0x48, 0x44, 0x52, // "IHDR"
64
+ (width >>> 24) & 0xff,
65
+ (width >>> 16) & 0xff,
66
+ (width >>> 8) & 0xff,
67
+ width & 0xff,
68
+ (height >>> 24) & 0xff,
69
+ (height >>> 16) & 0xff,
70
+ (height >>> 8) & 0xff,
71
+ height & 0xff,
72
+ 0x08, 0x06, 0x00, 0x00, 0x00,
73
+ ]),
74
+ ).toString("base64");
75
+ }
76
+
77
+ function toolResultWithImage(data: string): ContentBlock {
78
+ return {
79
+ type: "tool_result",
80
+ tool_use_id: "toolu_123",
81
+ content: "Screenshot captured",
82
+ contentBlocks: [
83
+ { type: "image", source: { type: "base64", media_type: "image/png", data } },
84
+ ],
85
+ };
86
+ }
87
+
88
+ function storedContent(conversationId: string): ContentBlock[][] {
89
+ return getMessages(conversationId).map(
90
+ (row) => JSON.parse(row.content) as ContentBlock[],
91
+ );
92
+ }
93
+
94
+ describe("persistUnsendableImageDowngrades (downscalable host)", () => {
95
+ beforeEach(() => {
96
+ resetTables();
97
+ });
98
+
99
+ /** JARVIS-1041: an oversized screenshot that CAN be shrunk must persist the
100
+ * downscaled bytes, not the note and not the original. */
101
+ test("persists the downscaled image for a shrinkable tool_result screenshot", async () => {
102
+ // GIVEN a tool_result holding an oversized but shrinkable screenshot
103
+ const conv = createConversation();
104
+ await addMessage(
105
+ conv.id,
106
+ "user",
107
+ JSON.stringify([toolResultWithImage(oversizedPngBase64())]),
108
+ { skipIndexing: true },
109
+ );
110
+
111
+ // WHEN the downgrade is persisted
112
+ const rewritten = persistUnsendableImageDowngrades(conv.id);
113
+
114
+ // THEN the nested block stays an image, rewritten to the downscaled payload
115
+ expect(rewritten).toBe(1);
116
+ const [content] = storedContent(conv.id);
117
+ const toolResult = content.find((b) => b.type === "tool_result") as {
118
+ contentBlocks?: ContentBlock[];
119
+ };
120
+ const nested = toolResult.contentBlocks?.[0];
121
+ expect(nested?.type).toBe("image");
122
+ expect((nested as Extract<ContentBlock, { type: "image" }>).source.data).toBe(
123
+ SHRUNK_DATA,
124
+ );
125
+ });
126
+
127
+ /** Re-running is a no-op: the downscaled payload is within limits. */
128
+ test("is idempotent after a downscale rewrite", async () => {
129
+ // GIVEN a conversation whose oversized screenshot was already downscaled
130
+ const conv = createConversation();
131
+ await addMessage(
132
+ conv.id,
133
+ "user",
134
+ JSON.stringify([toolResultWithImage(oversizedPngBase64())]),
135
+ { skipIndexing: true },
136
+ );
137
+ expect(persistUnsendableImageDowngrades(conv.id)).toBe(1);
138
+
139
+ // WHEN the downgrade runs again
140
+ const secondRun = persistUnsendableImageDowngrades(conv.id);
141
+
142
+ // THEN nothing further is rewritten
143
+ expect(secondRun).toBe(0);
144
+ });
145
+ });
@@ -14,7 +14,10 @@
14
14
 
15
15
  import { beforeEach, describe, expect, test } from "bun:test";
16
16
 
17
- import { persistUnsendableImageDowngrades } from "../daemon/persist-unsendable-image.js";
17
+ import {
18
+ oversizedImageReplacement,
19
+ persistUnsendableImageDowngrades,
20
+ } from "../daemon/persist-unsendable-image.js";
18
21
  import {
19
22
  addMessage,
20
23
  createConversation,
@@ -87,6 +90,20 @@ function imageBlock(data: string): ContentBlock {
87
90
  };
88
91
  }
89
92
 
93
+ /**
94
+ * A tool_result carrying a nested image in its contentBlocks, mirroring what a
95
+ * browser screenshot produces. This is the JARVIS-1041 shape: the oversized
96
+ * image lives at tool_result.contentBlocks, never as a top-level block.
97
+ */
98
+ function toolResultWithImage(data: string): ContentBlock {
99
+ return {
100
+ type: "tool_result",
101
+ tool_use_id: "toolu_123",
102
+ content: "Screenshot captured",
103
+ contentBlocks: [imageBlock(data)],
104
+ };
105
+ }
106
+
90
107
  function storedContent(conversationId: string): ContentBlock[][] {
91
108
  return getMessages(conversationId).map(
92
109
  (row) => JSON.parse(row.content) as ContentBlock[],
@@ -194,6 +211,61 @@ describe("persistUnsendableImageDowngrades", () => {
194
211
  expect(content.some((b) => b.type === "image")).toBe(false);
195
212
  });
196
213
 
214
+ /** JARVIS-1041: the oversized image is nested inside a tool_result (e.g. a
215
+ * browser screenshot), not a top-level block. The downgrade must descend
216
+ * into tool_result.contentBlocks and swap the nested image for a note, while
217
+ * keeping the tool_result itself intact so tool_use/tool_result pairing
218
+ * survives. */
219
+ test("downgrades an oversized image nested in tool_result.contentBlocks", async () => {
220
+ // GIVEN an assistant turn whose tool_result holds an oversized screenshot
221
+ const conv = createConversation();
222
+ await addMessage(
223
+ conv.id,
224
+ "user",
225
+ JSON.stringify([toolResultWithImage(makePngBase64(12000, 9000))]),
226
+ { skipIndexing: true },
227
+ );
228
+
229
+ // WHEN the downgrade is persisted
230
+ const rewritten = persistUnsendableImageDowngrades(conv.id);
231
+
232
+ // THEN the message is rewritten with the nested image swapped for a note
233
+ expect(rewritten).toBe(1);
234
+ const [content] = storedContent(conv.id);
235
+ const toolResult = content.find((b) => b.type === "tool_result");
236
+ expect(toolResult).toBeDefined();
237
+ // AND the tool_result is preserved (pairing intact) with no image left
238
+ const nested = (toolResult as { contentBlocks?: ContentBlock[] })
239
+ .contentBlocks;
240
+ expect(nested?.some((b) => b.type === "image")).toBe(false);
241
+ expect(nested?.some((b) => b.type === "text")).toBe(true);
242
+ });
243
+
244
+ /** A sendable nested screenshot is never disturbed. */
245
+ test("leaves a normally-sized tool_result image untouched", async () => {
246
+ // GIVEN a tool_result with an image well within provider limits
247
+ const conv = createConversation();
248
+ await addMessage(
249
+ conv.id,
250
+ "user",
251
+ JSON.stringify([toolResultWithImage(makePngBase64(1024, 768))]),
252
+ { skipIndexing: true },
253
+ );
254
+
255
+ // WHEN the downgrade is persisted
256
+ const rewritten = persistUnsendableImageDowngrades(conv.id);
257
+
258
+ // THEN nothing is rewritten and the nested image remains
259
+ expect(rewritten).toBe(0);
260
+ const [content] = storedContent(conv.id);
261
+ const toolResult = content.find((b) => b.type === "tool_result") as {
262
+ contentBlocks?: ContentBlock[];
263
+ };
264
+ expect(toolResult.contentBlocks?.some((b) => b.type === "image")).toBe(
265
+ true,
266
+ );
267
+ });
268
+
197
269
  /** Re-running after a rewrite is a safe no-op (no image blocks remain). */
198
270
  test("is idempotent — a second run rewrites nothing", async () => {
199
271
  // GIVEN a conversation whose oversized image has already been downgraded
@@ -213,3 +285,27 @@ describe("persistUnsendableImageDowngrades", () => {
213
285
  expect(secondRun).toBe(0);
214
286
  });
215
287
  });
288
+
289
+ describe("oversizedImageReplacement", () => {
290
+ /** A still-sendable image must be left alone — never replaced with a note.
291
+ * This is the gate that keeps the in-memory recovery from discarding valid
292
+ * screenshots when only one image in the turn was actually oversized. */
293
+ test("returns null for an image within the provider caps", () => {
294
+ const sendable = imageBlock(makePngBase64(1024, 768)) as Extract<
295
+ ContentBlock,
296
+ { type: "image" }
297
+ >;
298
+ expect(oversizedImageReplacement(sendable)).toBeNull();
299
+ });
300
+
301
+ /** An image past the provider caps that cannot be shrunk on this host (fake
302
+ * PNG that sips cannot decode) collapses to the unsendable note. */
303
+ test("returns the unsendable note when an oversized image cannot be shrunk", () => {
304
+ const oversized = imageBlock(makePngBase64(12000, 9000)) as Extract<
305
+ ContentBlock,
306
+ { type: "image" }
307
+ >;
308
+ const replacement = oversizedImageReplacement(oversized);
309
+ expect(replacement?.type).toBe("text");
310
+ });
311
+ });
@@ -370,3 +370,81 @@ describe("persistence-layer secret redaction", () => {
370
370
  expect(textBlock?.text).toBe(text);
371
371
  });
372
372
  });
373
+
374
+ describe("thinking timing persistence", () => {
375
+ let state: EventHandlerState;
376
+
377
+ beforeEach(() => {
378
+ addMessageCalls.length = 0;
379
+ state = createEventHandlerState();
380
+ state.turnStartedAt = 1_700_000_000_000;
381
+ });
382
+
383
+ afterEach(() => {
384
+ addMessageCalls.length = 0;
385
+ });
386
+
387
+ function makeThinkingCompleteEvent(): Extract<
388
+ AgentEvent,
389
+ { type: "message_complete" }
390
+ > {
391
+ return {
392
+ type: "message_complete",
393
+ message: {
394
+ role: "assistant",
395
+ content: [
396
+ { type: "thinking", thinking: "let me reason", signature: "sig" },
397
+ { type: "text", text: "the answer" },
398
+ ],
399
+ },
400
+ };
401
+ }
402
+
403
+ test("stamps per-block thinking timing onto the persisted thinking block", async () => {
404
+ // GIVEN a turn whose row was reserved at llm_call_started
405
+ await handleLlmCallStarted(state, makeDeps());
406
+
407
+ // AND streaming then captured timing for one thinking block (startedAt when
408
+ // it opened, completedAt at its last reasoning delta). The reserve resets
409
+ // the accumulator, so deltas — and thus the timing — land after it.
410
+ state.currentThinkingTimestamps = [{ startedAt: 1000, completedAt: 1750 }];
411
+
412
+ // WHEN the message completes and content is persisted
413
+ await handleMessageComplete(state, makeDeps(), makeThinkingCompleteEvent());
414
+
415
+ // THEN the persisted thinking block carries the internal `_`-prefixed
416
+ // timing so a history reload can surface the duration + "Started at" hover
417
+ const persisted = lastPersisted("assistant");
418
+ const blocks = JSON.parse(persisted.content) as Array<{
419
+ type: string;
420
+ _startedAt?: number;
421
+ _completedAt?: number;
422
+ }>;
423
+ const thinkingBlock = blocks.find((b) => b.type === "thinking");
424
+ expect(thinkingBlock?._startedAt).toBe(1000);
425
+ expect(thinkingBlock?._completedAt).toBe(1750);
426
+ });
427
+
428
+ test("persists no thinking timing when none was captured this turn", async () => {
429
+ // GIVEN a turn that produced a thinking block but captured no timing
430
+ // (thinking streaming disabled, so the timing list stays empty)
431
+ expect(state.currentThinkingTimestamps).toEqual([]);
432
+
433
+ // WHEN the message completes and content is persisted
434
+ await handleLlmCallStarted(state, makeDeps());
435
+ await handleMessageComplete(state, makeDeps(), makeThinkingCompleteEvent());
436
+
437
+ // THEN the thinking block is persisted without timing, so the client hides
438
+ // the duration exactly as a tool call with no timing
439
+ const persisted = lastPersisted("assistant");
440
+ const blocks = JSON.parse(persisted.content) as Array<{
441
+ type: string;
442
+ _startedAt?: number;
443
+ _completedAt?: number;
444
+ }>;
445
+ const thinkingBlock = blocks.find((b) => b.type === "thinking");
446
+ expect(thinkingBlock).toBeDefined();
447
+ expect(thinkingBlock?._startedAt).toBeUndefined();
448
+ expect(thinkingBlock?._completedAt).toBeUndefined();
449
+ });
450
+ });
@@ -34,22 +34,19 @@ describe("buildShimSource", () => {
34
34
  test("emits a globalThis trampoline + one binding per export", () => {
35
35
  const source = buildShimSource(
36
36
  ["foo", "bar"],
37
- Symbol.for("vellum.plugin-api.v1"),
37
+ Symbol.for("vellum.plugin-api"),
38
38
  );
39
39
  expect(source).toBe(
40
- `const api = globalThis[Symbol.for("vellum.plugin-api.v1")];\n` +
40
+ `const api = globalThis[Symbol.for("vellum.plugin-api")];\n` +
41
41
  `export const foo = api.foo;\n` +
42
42
  `export const bar = api.bar;\n`,
43
43
  );
44
44
  });
45
45
 
46
46
  test("handles an empty export list (today's types-only surface)", () => {
47
- const source = buildShimSource(
48
- [],
49
- Symbol.for("vellum.plugin-api.v1"),
50
- );
47
+ const source = buildShimSource([], Symbol.for("vellum.plugin-api"));
51
48
  expect(source).toBe(
52
- `const api = globalThis[Symbol.for("vellum.plugin-api.v1")];\n`,
49
+ `const api = globalThis[Symbol.for("vellum.plugin-api")];\n`,
53
50
  );
54
51
  });
55
52
  });
@@ -109,7 +106,7 @@ describe("ensurePluginApiShim", () => {
109
106
  const pluginDir = join(workspaceDir, "plugins", "fake-plugin");
110
107
  await mkdir(pluginDir, { recursive: true });
111
108
  await writeFile(
112
- join(pluginDir, "register.js"),
109
+ join(pluginDir, "probe.js"),
113
110
  `import * as api from "@vellumai/plugin-api";\nexport { api };\n`,
114
111
  );
115
112
 
@@ -118,7 +115,7 @@ describe("ensurePluginApiShim", () => {
118
115
  // → plugin-api namespace. If any link in that chain is broken, this
119
116
  // import throws.
120
117
  const mod: { api: Record<string, unknown> } = await import(
121
- join(pluginDir, "register.js")
118
+ join(pluginDir, "probe.js")
122
119
  );
123
120
  expect(mod.api).toBeDefined();
124
121
  });