@vellumai/assistant 0.8.10 → 0.8.11-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 (400) hide show
  1. package/bun.lock +62 -1
  2. package/docs/workspace-tools.md +196 -0
  3. package/examples/plugins/echo/README.md +3 -3
  4. package/knip.json +1 -0
  5. package/openapi.yaml +460 -128
  6. package/package.json +2 -1
  7. package/scripts/build-plugin-api.ts +299 -0
  8. package/src/__tests__/agent-loop-callsite-precedence.test.ts +7 -0
  9. package/src/__tests__/agent-loop-compaction-events.test.ts +197 -0
  10. package/src/__tests__/agent-loop-exit-reason.test.ts +93 -96
  11. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +2 -0
  12. package/src/__tests__/agent-loop-output-hooks.test.ts +274 -1
  13. package/src/__tests__/agent-loop-override-profile.test.ts +3 -0
  14. package/src/__tests__/agent-loop-provider-error-recording.test.ts +4 -0
  15. package/src/__tests__/agent-loop-thinking.test.ts +4 -0
  16. package/src/__tests__/agent-loop.test.ts +578 -5
  17. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +0 -1
  18. package/src/__tests__/approval-cascade.test.ts +1 -0
  19. package/src/__tests__/background-workers-disk-pressure.test.ts +0 -2
  20. package/src/__tests__/btw-routes.test.ts +0 -1
  21. package/src/__tests__/build-persisted-content.test.ts +75 -1
  22. package/src/__tests__/catalog-install-normalize.test.ts +141 -0
  23. package/src/__tests__/ces-startup-timeout.test.ts +60 -0
  24. package/src/__tests__/compaction-events.test.ts +1 -0
  25. package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
  26. package/src/__tests__/context-overflow-reducer.test.ts +264 -124
  27. package/src/__tests__/context-window-manager-overflow-rung.test.ts +351 -0
  28. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  29. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +13 -5
  30. package/src/__tests__/conversation-agent-loop-overflow.test.ts +284 -455
  31. package/src/__tests__/conversation-agent-loop.test.ts +131 -551
  32. package/src/__tests__/conversation-app-control-instantiation.test.ts +13 -0
  33. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
  34. package/src/__tests__/conversation-fork-crud.test.ts +259 -0
  35. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  36. package/src/__tests__/conversation-lifecycle.test.ts +257 -1
  37. package/src/__tests__/conversation-process-callsite.test.ts +1 -0
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +38 -377
  39. package/src/__tests__/conversation-queue.test.ts +1 -39
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +119 -8
  41. package/src/__tests__/conversation-skill-tools.test.ts +491 -5
  42. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  43. package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
  44. package/src/__tests__/conversation-speed-override.test.ts +1 -0
  45. package/src/__tests__/conversation-store.test.ts +74 -0
  46. package/src/__tests__/conversation-surfaces-app-control.test.ts +4 -1
  47. package/src/__tests__/conversation-tool-setup-attribution.test.ts +323 -0
  48. package/src/__tests__/conversation-tool-setup-tools-disabled.test.ts +34 -0
  49. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
  50. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  51. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -0
  52. package/src/__tests__/corrected-target.test.ts +93 -0
  53. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -5
  54. package/src/__tests__/credential-execution-tools.test.ts +23 -11
  55. package/src/__tests__/credential-security-invariants.test.ts +6 -1
  56. package/src/__tests__/db-schedule-syntax-migration.test.ts +80 -0
  57. package/src/__tests__/device-id.test.ts +70 -1
  58. package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
  59. package/src/__tests__/empty-response-hook.test.ts +242 -66
  60. package/src/__tests__/external-plugin-loader.test.ts +0 -31
  61. package/src/__tests__/get-skill-detail-audit.test.ts +43 -1
  62. package/src/__tests__/guardian-routing-invariants.test.ts +91 -0
  63. package/src/__tests__/history-repair-hook.test.ts +228 -3
  64. package/src/__tests__/host-app-control-proxy.test.ts +45 -0
  65. package/src/__tests__/host-browser-proxy.test.ts +254 -9
  66. package/src/__tests__/identity-routes.test.ts +1 -0
  67. package/src/__tests__/image-recovery-hook.test.ts +387 -0
  68. package/src/__tests__/injector-chain.test.ts +5 -4
  69. package/src/__tests__/injector-v3-suppression.test.ts +373 -47
  70. package/src/__tests__/intent-routing.test.ts +7 -0
  71. package/src/__tests__/memory-retrieval-hook.test.ts +117 -15
  72. package/src/__tests__/notification-decision-strategy.test.ts +3 -3
  73. package/src/__tests__/oauth-store.test.ts +0 -85
  74. package/src/__tests__/{context-overflow-policy.test.ts → overflow-policy.test.ts} +1 -1
  75. package/src/__tests__/parallel-tool.benchmark.test.ts +4 -0
  76. package/src/__tests__/persist-unsendable-image-downscale.test.ts +29 -9
  77. package/src/__tests__/persist-unsendable-image.test.ts +4 -4
  78. package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
  79. package/src/__tests__/plugin-bootstrap.test.ts +82 -73
  80. package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
  81. package/src/__tests__/plugin-types.test.ts +0 -8
  82. package/src/__tests__/provider-catalog-visibility.test.ts +1 -9
  83. package/src/__tests__/prune-old-conversations-job.test.ts +99 -0
  84. package/src/__tests__/registry.test.ts +240 -1
  85. package/src/__tests__/require-fresh-approval.test.ts +3 -0
  86. package/src/__tests__/schedule-routes.test.ts +116 -1
  87. package/src/__tests__/schedule-store.test.ts +28 -0
  88. package/src/__tests__/schedule-tools.test.ts +94 -1
  89. package/src/__tests__/server-history-render.test.ts +39 -0
  90. package/src/__tests__/skill-projection-feature-flag.test.ts +13 -0
  91. package/src/__tests__/skill-projection.benchmark.test.ts +25 -7
  92. package/src/__tests__/skills.test.ts +202 -0
  93. package/src/__tests__/slim-skill-category.test.ts +195 -0
  94. package/src/__tests__/strip-memory-injections.test.ts +33 -39
  95. package/src/__tests__/test-support/tool-invocation-seed.ts +79 -0
  96. package/src/__tests__/title-generate-hook.test.ts +9 -7
  97. package/src/__tests__/tool-audit-listener.test.ts +264 -1
  98. package/src/__tests__/tool-error-hook.test.ts +4 -3
  99. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
  100. package/src/__tests__/tool-executor-lifecycle-events.test.ts +273 -0
  101. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  102. package/src/__tests__/tool-start-timestamp.test.ts +218 -0
  103. package/src/__tests__/tools-get-route.test.ts +202 -0
  104. package/src/__tests__/workspace-tool-loader.test.ts +319 -0
  105. package/src/__tests__/workspace-tools-watcher-flag.test.ts +70 -0
  106. package/src/agent/loop.ts +569 -319
  107. package/src/api/events/tool-result.ts +9 -0
  108. package/src/api/events/tool-use-start.ts +7 -0
  109. package/src/api/index.ts +10 -0
  110. package/src/api/responses/conversation-message.ts +135 -27
  111. package/src/api/responses/memory-v3-selection-log.ts +4 -4
  112. package/src/approvals/guardian-request-resolvers.ts +26 -0
  113. package/src/browser-session/backends/host-bridge.ts +29 -0
  114. package/src/browser-session/index.ts +1 -0
  115. package/src/browser-session/types.ts +5 -1
  116. package/src/cli/commands/__tests__/schedules.test.ts +62 -4
  117. package/src/cli/commands/__tests__/skills.test.ts +53 -0
  118. package/src/cli/commands/channel-verification-sessions.ts +6 -6
  119. package/src/cli/commands/inference-providers.ts +0 -8
  120. package/src/cli/commands/plugins.ts +2 -2
  121. package/src/cli/commands/schedules.ts +27 -4
  122. package/src/cli/commands/skills.ts +187 -146
  123. package/src/cli/commands/tools.ts +106 -0
  124. package/src/cli/lib/__tests__/install-from-github.test.ts +256 -328
  125. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +6 -2
  126. package/src/cli/lib/__tests__/plugin-details.test.ts +10 -16
  127. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +2 -2
  128. package/src/cli/lib/__tests__/search-plugins.test.ts +145 -240
  129. package/src/cli/lib/install-from-github.ts +187 -117
  130. package/src/cli/lib/plugin-catalog-cache.ts +9 -9
  131. package/src/cli/lib/plugin-details.ts +38 -68
  132. package/src/cli/lib/plugin-marketplace.ts +42 -14
  133. package/src/cli/lib/search-plugins.ts +29 -129
  134. package/src/cli/program.ts +2 -0
  135. package/src/config/bundled-skills/acp/SKILL.md +1 -0
  136. package/src/config/bundled-skills/app-builder/SKILL.md +1 -0
  137. package/src/config/bundled-skills/app-control/SKILL.md +1 -0
  138. package/src/config/bundled-skills/computer-use/SKILL.md +1 -0
  139. package/src/config/bundled-skills/contacts/SKILL.md +1 -0
  140. package/src/config/bundled-skills/document-editor/SKILL.md +1 -0
  141. package/src/config/bundled-skills/followups/SKILL.md +1 -0
  142. package/src/config/bundled-skills/image-studio/SKILL.md +1 -0
  143. package/src/config/bundled-skills/media-processing/SKILL.md +1 -0
  144. package/src/config/bundled-skills/messaging/SKILL.md +1 -0
  145. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -0
  146. package/src/config/bundled-skills/playbooks/SKILL.md +1 -0
  147. package/src/config/bundled-skills/schedule/SKILL.md +1 -0
  148. package/src/config/bundled-skills/schedule/TOOLS.json +11 -3
  149. package/src/config/bundled-skills/sequences/SKILL.md +1 -0
  150. package/src/config/bundled-skills/settings/SKILL.md +1 -0
  151. package/src/config/bundled-skills/skill-management/SKILL.md +101 -1
  152. package/src/config/bundled-skills/subagent/SKILL.md +1 -0
  153. package/src/config/bundled-skills/transcribe/SKILL.md +1 -0
  154. package/src/config/env-registry.ts +23 -0
  155. package/src/config/feature-flag-registry.json +17 -81
  156. package/src/config/loader.ts +5 -22
  157. package/src/config/schema.ts +2 -0
  158. package/src/config/schemas/__tests__/compaction-logs.test.ts +56 -0
  159. package/src/config/schemas/__tests__/memory-v2.test.ts +0 -1
  160. package/src/config/schemas/__tests__/memory-v3.test.ts +61 -1
  161. package/src/config/schemas/compaction-logs.ts +79 -0
  162. package/src/config/schemas/memory-v2.ts +0 -8
  163. package/src/config/schemas/memory-v3.ts +104 -33
  164. package/src/config/seed-inference-profiles.ts +1 -1
  165. package/src/config/skills.ts +117 -47
  166. package/src/context/compactor.ts +11 -0
  167. package/src/context/strip-injections.ts +38 -4
  168. package/src/credential-execution/feature-gates.ts +0 -21
  169. package/src/credential-execution/startup-timeout.ts +32 -4
  170. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +18 -0
  171. package/src/daemon/conversation-agent-loop-handlers.ts +140 -91
  172. package/src/daemon/conversation-agent-loop.ts +123 -658
  173. package/src/daemon/conversation-error.ts +6 -33
  174. package/src/daemon/conversation-lifecycle.ts +1 -1
  175. package/src/daemon/conversation-runtime-assembly.ts +183 -22
  176. package/src/daemon/conversation-skill-tools.ts +137 -8
  177. package/src/daemon/conversation-slash.ts +0 -14
  178. package/src/daemon/conversation-store.ts +2 -19
  179. package/src/daemon/conversation-tool-setup.ts +87 -1
  180. package/src/daemon/conversation.ts +119 -50
  181. package/src/daemon/external-plugins-bootstrap.ts +36 -95
  182. package/src/daemon/handlers/config-channels.ts +11 -2
  183. package/src/daemon/handlers/shared.ts +9 -1
  184. package/src/daemon/handlers/skills.ts +10 -4
  185. package/src/daemon/host-app-control-proxy.ts +72 -57
  186. package/src/daemon/host-browser-proxy.ts +117 -22
  187. package/src/daemon/lifecycle.ts +34 -5
  188. package/src/daemon/message-protocol.ts +0 -7
  189. package/src/daemon/message-types/schedules.ts +1 -0
  190. package/src/daemon/message-types/skills.ts +17 -0
  191. package/src/daemon/providers-setup.ts +3 -0
  192. package/src/daemon/server.ts +3 -3
  193. package/src/daemon/tool-setup-types.ts +9 -3
  194. package/src/daemon/trust-context.ts +23 -0
  195. package/src/daemon/workspace-tools-watcher.ts +324 -0
  196. package/src/events/tool-audit-listener.ts +78 -16
  197. package/src/events/tool-metrics-listener.ts +2 -5
  198. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +227 -0
  199. package/src/memory/__tests__/conversation-queries.test.ts +176 -0
  200. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +20 -32
  201. package/src/memory/compaction-log-writer-clickhouse.ts +418 -0
  202. package/src/memory/conversation-crud.ts +134 -3
  203. package/src/memory/conversation-queries.ts +64 -7
  204. package/src/memory/db-init.ts +14 -0
  205. package/src/memory/embedding-backend.test.ts +130 -1
  206. package/src/memory/embedding-backend.ts +79 -106
  207. package/src/memory/embedding-gemini.ts +5 -0
  208. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +12 -0
  209. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +19 -0
  210. package/src/memory/graph/conversation-graph-memory.ts +36 -25
  211. package/src/memory/graph/tool-handlers.ts +3 -0
  212. package/src/memory/job-handlers/cleanup.ts +3 -1
  213. package/src/memory/jobs-store.ts +0 -28
  214. package/src/memory/jobs-worker.ts +10 -22
  215. package/src/memory/memory-marker.ts +29 -0
  216. package/src/memory/memory-retrospective-startup-cleanup.ts +1 -1
  217. package/src/memory/migrations/268-add-memory-v3-selections.ts +6 -0
  218. package/src/memory/migrations/270-schedule-description.ts +36 -0
  219. package/src/memory/migrations/275-tool-invocations-add-skill-id.test.ts +81 -0
  220. package/src/memory/migrations/275-tool-invocations-add-skill-id.ts +20 -0
  221. package/src/memory/migrations/276-tool-invocations-created-at-id-index.test.ts +68 -0
  222. package/src/memory/migrations/276-tool-invocations-created-at-id-index.ts +20 -0
  223. package/src/memory/migrations/277-add-memory-v3-ever-injected.ts +29 -0
  224. package/src/memory/migrations/278-tool-invocations-telemetry-columns.test.ts +96 -0
  225. package/src/memory/migrations/278-tool-invocations-telemetry-columns.ts +39 -0
  226. package/src/memory/migrations/279-create-skill-loaded-events.test.ts +84 -0
  227. package/src/memory/migrations/279-create-skill-loaded-events.ts +26 -0
  228. package/src/memory/migrations/280-conversations-surfaced-at.test.ts +88 -0
  229. package/src/memory/migrations/280-conversations-surfaced-at.ts +24 -0
  230. package/src/memory/migrations/index.ts +10 -0
  231. package/src/memory/migrations/registry.ts +8 -0
  232. package/src/memory/schema/conversations.ts +16 -0
  233. package/src/memory/schema/infrastructure.ts +26 -0
  234. package/src/memory/skill-loaded-events-store.test.ts +160 -0
  235. package/src/memory/skill-loaded-events-store.ts +95 -0
  236. package/src/memory/tool-executed-events-store.test.ts +219 -0
  237. package/src/memory/tool-executed-events-store.ts +102 -0
  238. package/src/memory/tool-usage-store.ts +15 -3
  239. package/src/memory/v2/__tests__/consolidation-job.test.ts +117 -12
  240. package/src/memory/v2/__tests__/consolidation-prompt-flag-gating-guard.test.ts +189 -0
  241. package/src/memory/v2/__tests__/injected-block-slugs.test.ts +90 -0
  242. package/src/memory/v2/__tests__/page-store.test.ts +33 -0
  243. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +88 -15
  244. package/src/memory/v2/activation-store.ts +50 -1
  245. package/src/memory/v2/consolidation-job.ts +113 -29
  246. package/src/memory/v2/injected-block-slugs.ts +79 -0
  247. package/src/memory/v2/injection.ts +6 -1
  248. package/src/memory/v2/prompts/consolidation.ts +414 -13
  249. package/src/memory/v2/router.ts +2 -28
  250. package/src/memory/v2/static-context.ts +1 -1
  251. package/src/memory/v2/types.ts +16 -0
  252. package/src/notifications/__tests__/copy-composer.test.ts +244 -0
  253. package/src/notifications/access-request-copy.ts +298 -0
  254. package/src/notifications/adapters/slack.ts +3 -3
  255. package/src/notifications/adapters/telegram.ts +2 -1
  256. package/src/notifications/copy-composer.ts +49 -267
  257. package/src/notifications/decision-engine.ts +16 -35
  258. package/src/notifications/home-feed-side-effect.ts +1 -6
  259. package/src/oauth/oauth-store.ts +0 -9
  260. package/src/permissions/checker.test.ts +83 -1
  261. package/src/permissions/checker.ts +25 -2
  262. package/src/permissions/gateway-threshold-reader.test.ts +182 -0
  263. package/src/permissions/gateway-threshold-reader.ts +80 -0
  264. package/src/platform/client.ts +1 -3
  265. package/src/platform/feature-gate.ts +3 -12
  266. package/src/plugin-api/constants.ts +4 -2
  267. package/src/plugin-api/index.ts +63 -11
  268. package/src/plugin-api/types.ts +236 -71
  269. package/src/plugins/defaults/compaction/compact.ts +66 -2
  270. package/src/plugins/defaults/compaction/context-overflow-reducer.ts +240 -32
  271. package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
  272. package/src/{daemon/context-overflow-policy.ts → plugins/defaults/compaction/overflow-policy.ts} +1 -1
  273. package/src/plugins/defaults/compaction/window-manager.ts +303 -1
  274. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +173 -0
  275. package/src/plugins/defaults/empty-response/hooks/stop.ts +11 -115
  276. package/src/plugins/defaults/empty-response/nudge-state-store.ts +46 -0
  277. package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +50 -0
  278. package/src/plugins/defaults/history-repair/hooks/stop.ts +22 -0
  279. package/src/plugins/defaults/history-repair/repair-state-store.ts +51 -0
  280. package/src/plugins/defaults/history-repair/terminal.ts +39 -2
  281. package/src/plugins/defaults/image-recovery/detect.ts +25 -0
  282. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +73 -0
  283. package/src/plugins/defaults/image-recovery/hooks/stop.ts +22 -0
  284. package/src/plugins/defaults/image-recovery/image-recovery-state-store.ts +48 -0
  285. package/src/plugins/defaults/image-recovery/package.json +14 -0
  286. package/src/{daemon/persist-unsendable-image.ts → plugins/defaults/image-recovery/recover.ts} +67 -14
  287. package/src/plugins/defaults/index.ts +71 -5
  288. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +76 -112
  289. package/src/plugins/defaults/memory-retrieval/hooks/{user-prompt-submit-temp.ts → user-prompt-submit.ts} +83 -74
  290. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +14 -8
  291. package/src/plugins/defaults/memory-retrieval/injectors.ts +2 -18
  292. package/src/plugins/defaults/memory-retrieval/package.json +14 -0
  293. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1157 -0
  294. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +683 -0
  295. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +161 -140
  296. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +160 -0
  297. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +335 -316
  298. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +145 -53
  299. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +38 -1
  300. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +18 -8
  301. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +112 -71
  302. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +192 -51
  303. package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +4 -16
  304. package/src/plugins/defaults/memory-v3-shadow/card.test.ts +173 -0
  305. package/src/plugins/defaults/memory-v3-shadow/card.ts +116 -0
  306. package/src/plugins/defaults/memory-v3-shadow/core-set.test.ts +104 -0
  307. package/src/plugins/defaults/memory-v3-shadow/core-set.ts +59 -0
  308. package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.test.ts +305 -0
  309. package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.ts +278 -0
  310. package/src/plugins/defaults/memory-v3-shadow/hot-set.test.ts +138 -0
  311. package/src/plugins/defaults/memory-v3-shadow/hot-set.ts +85 -0
  312. package/src/plugins/defaults/memory-v3-shadow/injector.ts +331 -24
  313. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +119 -13
  314. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +169 -114
  315. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +47 -16
  316. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +144 -66
  317. package/src/plugins/defaults/memory-v3-shadow/prune.test.ts +758 -0
  318. package/src/plugins/defaults/memory-v3-shadow/prune.ts +471 -0
  319. package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +68 -16
  320. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +19 -12
  321. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +96 -43
  322. package/src/plugins/defaults/memory-v3-shadow/types.ts +34 -17
  323. package/src/plugins/defaults/title-generate/hooks/stop.ts +9 -11
  324. package/src/plugins/pipeline.ts +8 -5
  325. package/src/plugins/types.ts +5 -51
  326. package/src/providers/cache-control.ts +26 -0
  327. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +1 -2
  328. package/src/providers/model-catalog.ts +13 -1
  329. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +147 -0
  330. package/src/providers/openai/chat-completions-provider.ts +46 -0
  331. package/src/providers/openai/responses-provider.ts +45 -0
  332. package/src/providers/registry.ts +0 -8
  333. package/src/runtime/__tests__/agent-wake.test.ts +0 -1
  334. package/src/runtime/agent-wake.ts +13 -0
  335. package/src/runtime/routes/__tests__/acp-routes.test.ts +151 -0
  336. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +12 -50
  337. package/src/runtime/routes/__tests__/conversation-surface-routes.test.ts +322 -0
  338. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +0 -62
  339. package/src/runtime/routes/__tests__/plugins-routes.test.ts +31 -11
  340. package/src/runtime/routes/acp-routes.test.ts +106 -0
  341. package/src/runtime/routes/acp-routes.ts +248 -2
  342. package/src/runtime/routes/browser-tabs-routes.ts +1 -1
  343. package/src/runtime/routes/channel-verification-routes.ts +14 -5
  344. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +0 -14
  345. package/src/runtime/routes/consolidation-routes.ts +6 -82
  346. package/src/runtime/routes/conversation-list-routes.ts +6 -0
  347. package/src/runtime/routes/conversation-management-routes.ts +71 -0
  348. package/src/runtime/routes/identity-routes.ts +8 -0
  349. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
  350. package/src/runtime/routes/inference-provider-connection-routes.ts +0 -45
  351. package/src/runtime/routes/plugins-routes.ts +34 -45
  352. package/src/runtime/routes/schedule-routes.ts +43 -5
  353. package/src/runtime/routes/settings-routes.ts +140 -15
  354. package/src/runtime/routes/skills-routes.ts +18 -6
  355. package/src/runtime/services/__tests__/conversation-serializer.test.ts +140 -0
  356. package/src/runtime/services/conversation-serializer.ts +38 -1
  357. package/src/runtime/verification-outbound-actions.ts +147 -2
  358. package/src/runtime/verification-templates.ts +29 -3
  359. package/src/schedule/schedule-store.ts +19 -0
  360. package/src/skills/catalog-install.ts +77 -13
  361. package/src/tasks/task-scheduler.ts +1 -0
  362. package/src/telemetry/types.ts +66 -1
  363. package/src/telemetry/usage-telemetry-reporter.test.ts +542 -13
  364. package/src/telemetry/usage-telemetry-reporter.ts +213 -20
  365. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +49 -2
  366. package/src/tools/browser/__tests__/browser-status.test.ts +29 -5
  367. package/src/tools/browser/browser-execution.ts +27 -9
  368. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +380 -4
  369. package/src/tools/browser/cdp-client/__tests__/host-bridge-cdp-client.test.ts +107 -0
  370. package/src/tools/browser/cdp-client/__tests__/types.test.ts +6 -1
  371. package/src/tools/browser/cdp-client/factory.ts +217 -17
  372. package/src/tools/browser/cdp-client/host-bridge-cdp-client.ts +67 -0
  373. package/src/tools/browser/cdp-client/types.ts +22 -2
  374. package/src/tools/credential-execution/make-authenticated-request.ts +2 -1
  375. package/src/tools/credential-execution/manage-secure-command-tool.ts +169 -164
  376. package/src/tools/credential-execution/run-authenticated-command.ts +2 -1
  377. package/src/tools/executor.ts +39 -7
  378. package/src/tools/registry.ts +387 -5
  379. package/src/tools/schedule/create.ts +16 -0
  380. package/src/tools/schedule/list.ts +12 -4
  381. package/src/tools/schedule/update.ts +12 -0
  382. package/src/tools/skills/load.ts +11 -6
  383. package/src/tools/terminal/safe-env.ts +2 -0
  384. package/src/tools/types.ts +65 -9
  385. package/src/tools/workspace-tools/loader.ts +673 -0
  386. package/src/usage/attribution.ts +28 -0
  387. package/src/util/device-id.ts +17 -3
  388. package/src/util/platform.ts +16 -0
  389. package/tsconfig.plugin-api.json +13 -0
  390. package/src/__tests__/plugin-external-api.test.ts +0 -68
  391. package/src/__tests__/plugin-skill-contribution.test.ts +0 -355
  392. package/src/daemon/message-types/browser.ts +0 -10
  393. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
  394. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/eval-turns.json +0 -36
  395. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/live-turns.json +0 -37
  396. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-eviction.test.ts +0 -106
  397. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-skeleton.test.ts +0 -44
  398. package/src/plugins/defaults/memory-v3-shadow/working-set.ts +0 -91
  399. package/src/plugins/external-api.ts +0 -114
  400. package/src/plugins/plugin-skill-contributions.ts +0 -292
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Tests for the default `user-prompt-submit-temp` hook (memory retrieval +
2
+ * Tests for the default `user-prompt-submit` hook (memory retrieval +
3
3
  * runtime injection).
4
4
  *
5
5
  * Covers the retrieval behavior, the side effects the hook owns (injected-block
@@ -37,6 +37,7 @@ const applyRuntimeInjectionsMock = mock(async (messages: unknown) => ({
37
37
  mock.module("../daemon/conversation-runtime-assembly.js", () => ({
38
38
  applyRuntimeInjections: applyRuntimeInjectionsMock,
39
39
  resolveTurnInboundActorContext: () => null,
40
+ resolveTurnModelProfileLabel: () => null,
40
41
  }));
41
42
 
42
43
  // The hook self-resolves the live conversation and its trust class; both are
@@ -56,14 +57,22 @@ mock.module("../config/loader.js", () => ({
56
57
  getConfig: () => ({}) as AssistantConfig,
57
58
  }));
58
59
 
60
+ // The hook publishes `memory_recalled` (and forwards the sink into
61
+ // `prepareMemory` for `memory_status`) through the shared `broadcastMessage`
62
+ // hub. Stub it so tests can assert what the hook emits.
63
+ const broadcastMessageMock = mock((_msg: unknown) => {});
64
+ mock.module("../runtime/assistant-event-hub.js", () => ({
65
+ broadcastMessage: broadcastMessageMock,
66
+ }));
67
+
68
+ import type { UserPromptSubmitContext } from "@vellumai/plugin-api";
69
+
59
70
  import type { AssistantConfig } from "../config/schema.js";
60
71
  import type { Conversation } from "../daemon/conversation.js";
61
72
  import type { ServerMessage } from "../daemon/message-protocol.js";
62
73
  import type { ConversationGraphMemory } from "../memory/graph/conversation-graph-memory.js";
63
74
  import type { QdrantSparseVector } from "../memory/qdrant-client.js";
64
- import userPromptSubmitMemoryRetrieval, {
65
- type MemoryRetrievalHookContext,
66
- } from "../plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.js";
75
+ import userPromptSubmitMemoryRetrieval from "../plugins/defaults/memory-retrieval/hooks/user-prompt-submit.js";
67
76
  import type { Message } from "../providers/types.js";
68
77
 
69
78
  /** Canonical metrics payload the graph retriever attaches to a real hit. */
@@ -136,19 +145,20 @@ function makeFakeGraphMemory(overrides?: {
136
145
  }
137
146
 
138
147
  function makeHookCtx(
139
- overrides: Partial<MemoryRetrievalHookContext> = {},
140
- ): MemoryRetrievalHookContext {
148
+ overrides: Partial<UserPromptSubmitContext> = {},
149
+ ): UserPromptSubmitContext {
141
150
  return {
142
- onEvent: () => {},
143
151
  conversationId: "conv-test",
144
152
  userMessageId: "msg-test",
145
153
  logger: {
146
154
  warn: () => {},
147
- } as unknown as MemoryRetrievalHookContext["logger"],
155
+ } as unknown as UserPromptSubmitContext["logger"],
148
156
  latestMessages: [],
149
157
  requestId: "req-test",
150
158
  isNonInteractive: false,
151
- modelProfile: null,
159
+ modelProfileKey: null,
160
+ prompt: "",
161
+ originalMessages: [],
152
162
  ...overrides,
153
163
  };
154
164
  }
@@ -160,7 +170,10 @@ function makeHookCtx(
160
170
  */
161
171
  function installConversation(
162
172
  graphMemory: ConversationGraphMemory,
163
- opts?: { trusted?: boolean; signal?: AbortSignal },
173
+ opts?: {
174
+ trusted?: boolean;
175
+ signal?: AbortSignal;
176
+ },
164
177
  ): void {
165
178
  currentTrustClass = opts?.trusted === false ? "unknown" : "guardian";
166
179
  currentConversation = {
@@ -175,11 +188,12 @@ beforeEach(() => {
175
188
  recordMemoryRecallLogMock.mockReset();
176
189
  applyRuntimeInjectionsMock.mockClear();
177
190
  findConversationOrSubagentMock.mockClear();
191
+ broadcastMessageMock.mockReset();
178
192
  currentConversation = undefined;
179
193
  currentTrustClass = "guardian";
180
194
  });
181
195
 
182
- describe("user-prompt-submit-temp hook (memory retrieval)", () => {
196
+ describe("user-prompt-submit hook (memory retrieval)", () => {
183
197
  test("adopts the injected run messages when the actor is trusted", async () => {
184
198
  const injected: Message[] = [
185
199
  { role: "user", content: [{ type: "text", text: "injected" }] },
@@ -299,14 +313,12 @@ describe("user-prompt-submit-temp hook (memory retrieval)", () => {
299
313
  });
300
314
 
301
315
  test("persists injected block, recall log, and emits memory_recalled", async () => {
302
- const received: ServerMessage[] = [];
303
316
  const { memory } = makeFakeGraphMemory({
304
317
  injectedBlockText: "injected-block",
305
318
  metrics: makeMetrics(),
306
319
  });
307
320
  installConversation(memory, { trusted: true });
308
321
  const ctx = makeHookCtx({
309
- onEvent: (msg) => received.push(msg),
310
322
  userMessageId: "msg-42",
311
323
  conversationId: "conv-42",
312
324
  });
@@ -323,8 +335,98 @@ describe("user-prompt-submit-temp hook (memory retrieval)", () => {
323
335
  };
324
336
  expect(logEntry.conversationId).toBe("conv-42");
325
337
  expect(logEntry.reason).toBe("graph:none");
326
- expect(received).toHaveLength(1);
327
- expect(received[0]?.type).toBe("memory_recalled");
338
+ // The `memory_recalled` summary publishes through the shared broadcast hub.
339
+ expect(broadcastMessageMock).toHaveBeenCalledTimes(1);
340
+ const emitted = broadcastMessageMock.mock.calls[0]?.[0] as ServerMessage;
341
+ expect(emitted.type).toBe("memory_recalled");
342
+ });
343
+
344
+ test("memory-v3 active → v2 persists immediately, then the combined update removes it and persists the v3 card block", async () => {
345
+ // The v2 block is persisted RIGHT AFTER retrieval (no loss window: a
346
+ // crash between retrieval and assembly must not leave v2's activation
347
+ // store claiming pages whose block never reached metadata). Assembly then
348
+ // reports memoryV3Active (v3 superseded/stripped v2's tail block), so the
349
+ // post-assembly combined update REMOVES the v2 key — persisting it would
350
+ // rehydrate a block that is not in the live history.
351
+ const { memory } = makeFakeGraphMemory({
352
+ injectedBlockText: "v2-block-superseded",
353
+ metrics: makeMetrics(),
354
+ });
355
+ installConversation(memory, { trusted: true });
356
+ applyRuntimeInjectionsMock.mockImplementationOnce(
357
+ async (messages: unknown) => ({
358
+ messages,
359
+ blocks: {
360
+ memoryV3Active: true,
361
+ memoryV3InjectedBlock: "header\n\n# memory/concepts/page-a.md\nhead",
362
+ },
363
+ }),
364
+ );
365
+ const ctx = makeHookCtx({ userMessageId: "msg-43" });
366
+
367
+ await userPromptSubmitMemoryRetrieval(ctx);
368
+
369
+ expect(updateMessageMetadataMock.mock.calls).toEqual([
370
+ ["msg-43", { memoryInjectedBlock: "v2-block-superseded" }],
371
+ [
372
+ "msg-43",
373
+ {
374
+ // `undefined` deletes the key (JSON.stringify drops it) — the turn
375
+ // ends with the two memory layers mutually exclusive per row.
376
+ memoryInjectedBlock: undefined,
377
+ memoryV3InjectedBlock: "header\n\n# memory/concepts/page-a.md\nhead",
378
+ },
379
+ ],
380
+ ]);
381
+ });
382
+
383
+ test("memory-v3 active with EMPTY net-new → the immediate v2 persist is removed again", async () => {
384
+ // All-repeat turn: v2 was stripped (superseded) and v3 attached nothing
385
+ // new — the row must rehydrate with no memory block, matching live state.
386
+ const { memory } = makeFakeGraphMemory({
387
+ injectedBlockText: "v2-block-superseded",
388
+ metrics: makeMetrics(),
389
+ });
390
+ installConversation(memory, { trusted: true });
391
+ applyRuntimeInjectionsMock.mockImplementationOnce(
392
+ async (messages: unknown) => ({
393
+ messages,
394
+ blocks: { memoryV3Active: true },
395
+ }),
396
+ );
397
+ const ctx = makeHookCtx({ userMessageId: "msg-44" });
398
+
399
+ await userPromptSubmitMemoryRetrieval(ctx);
400
+
401
+ expect(updateMessageMetadataMock.mock.calls).toEqual([
402
+ ["msg-44", { memoryInjectedBlock: "v2-block-superseded" }],
403
+ ["msg-44", { memoryInjectedBlock: undefined }],
404
+ ]);
405
+ });
406
+
407
+ test("v2 block is already persisted when runtime assembly throws (no loss window)", async () => {
408
+ // Regression guard for the v2 path: with the v3 shadow flag on, assembly
409
+ // includes a seconds-long selector LLM call. If the process dies or the
410
+ // turn aborts in that window, the v2 block must already be in metadata —
411
+ // v2's activation store claimed its pages during retrieval, and a
412
+ // claimed-but-never-persisted block is absent from history on reload AND
413
+ // suppressed until compaction.
414
+ const { memory } = makeFakeGraphMemory({
415
+ injectedBlockText: "v2-block",
416
+ metrics: makeMetrics(),
417
+ });
418
+ installConversation(memory, { trusted: true });
419
+ applyRuntimeInjectionsMock.mockImplementationOnce(async () => {
420
+ throw new Error("assembly failed");
421
+ });
422
+ const ctx = makeHookCtx({ userMessageId: "msg-45" });
423
+
424
+ await expect(userPromptSubmitMemoryRetrieval(ctx)).rejects.toThrow(
425
+ "assembly failed",
426
+ );
427
+ expect(updateMessageMetadataMock).toHaveBeenCalledWith("msg-45", {
428
+ memoryInjectedBlock: "v2-block",
429
+ });
328
430
  });
329
431
 
330
432
  test("skips metadata persist when no block text is injected", async () => {
@@ -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,
@@ -58,7 +58,6 @@ import {
58
58
  getAppByProviderAndClientId,
59
59
  getConnection,
60
60
  getConnectionByProvider,
61
- getConnectionByProviderAndAccount,
62
61
  getProvider,
63
62
  isProviderConnected,
64
63
  listActiveConnectionsByProvider,
@@ -1689,90 +1688,6 @@ describe("connection operations", () => {
1689
1688
  });
1690
1689
  });
1691
1690
 
1692
- describe("getConnectionByProviderAndAccount", () => {
1693
- test("returns the connection matching the given account", async () => {
1694
- const app = await createTestApp("github", "client-1");
1695
-
1696
- const conn1 = createConnection({
1697
- oauthAppId: app.id,
1698
- provider: "github",
1699
- accountInfo: "user1@example.com",
1700
- grantedScopes: ["repo"],
1701
- hasRefreshToken: false,
1702
- createdAt: 1000,
1703
- });
1704
-
1705
- createConnection({
1706
- oauthAppId: app.id,
1707
- provider: "github",
1708
- accountInfo: "user2@example.com",
1709
- grantedScopes: ["repo"],
1710
- hasRefreshToken: false,
1711
- createdAt: 2000,
1712
- });
1713
-
1714
- const result = getConnectionByProviderAndAccount(
1715
- "github",
1716
- "user1@example.com",
1717
- );
1718
- expect(result).toBeDefined();
1719
- expect(result!.id).toBe(conn1.id);
1720
- });
1721
-
1722
- test("falls back to getConnectionByProvider when accountInfo is undefined", async () => {
1723
- const app = await createTestApp("github", "client-1");
1724
-
1725
- const conn = createConnection({
1726
- oauthAppId: app.id,
1727
- provider: "github",
1728
- accountInfo: "user@example.com",
1729
- grantedScopes: ["repo"],
1730
- hasRefreshToken: false,
1731
- });
1732
-
1733
- const result = getConnectionByProviderAndAccount("github", undefined);
1734
- expect(result).toBeDefined();
1735
- expect(result!.id).toBe(conn.id);
1736
- });
1737
-
1738
- test("returns undefined when no connection matches the account", async () => {
1739
- const app = await createTestApp("github", "client-1");
1740
-
1741
- createConnection({
1742
- oauthAppId: app.id,
1743
- provider: "github",
1744
- accountInfo: "user@example.com",
1745
- grantedScopes: ["repo"],
1746
- hasRefreshToken: false,
1747
- });
1748
-
1749
- const result = getConnectionByProviderAndAccount(
1750
- "github",
1751
- "other@example.com",
1752
- );
1753
- expect(result).toBeUndefined();
1754
- });
1755
-
1756
- test("skips revoked connections", async () => {
1757
- const app = await createTestApp("github", "client-1");
1758
-
1759
- const conn = createConnection({
1760
- oauthAppId: app.id,
1761
- provider: "github",
1762
- accountInfo: "user@example.com",
1763
- grantedScopes: ["repo"],
1764
- hasRefreshToken: false,
1765
- });
1766
- updateConnection(conn.id, { status: "revoked" });
1767
-
1768
- const result = getConnectionByProviderAndAccount(
1769
- "github",
1770
- "user@example.com",
1771
- );
1772
- expect(result).toBeUndefined();
1773
- });
1774
- });
1775
-
1776
1691
  describe("listActiveConnectionsByProvider", () => {
1777
1692
  test("returns all active connections for a provider", async () => {
1778
1693
  const app = await createTestApp("github", "client-1");
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import type { ContextOverflowRecoveryConfig } from "../config/schemas/inference.js";
4
4
  import { ContextOverflowRecoveryConfigSchema } from "../config/schemas/inference.js";
5
- import { resolveOverflowAction } from "../daemon/context-overflow-policy.js";
5
+ import { resolveOverflowAction } from "../plugins/defaults/compaction/overflow-policy.js";
6
6
 
7
7
  /** Parse an empty object to get all defaults. */
8
8
  const DEFAULTS: ContextOverflowRecoveryConfig =
@@ -137,6 +137,7 @@ describe("Parallel tool execution benchmarks", () => {
137
137
  });
138
138
  const start = Date.now();
139
139
  await loop.run({
140
+ requestId: "test-request",
140
141
  messages: [userMessage],
141
142
  onEvent: () => {},
142
143
  trust: { sourceChannel: "vellum", trustClass: "unknown" },
@@ -185,6 +186,7 @@ describe("Parallel tool execution benchmarks", () => {
185
186
  });
186
187
  const start = Date.now();
187
188
  await loop.run({
189
+ requestId: "test-request",
188
190
  messages: [userMessage],
189
191
  onEvent: () => {},
190
192
  trust: { sourceChannel: "vellum", trustClass: "unknown" },
@@ -246,6 +248,7 @@ describe("Parallel tool execution benchmarks", () => {
246
248
  const events: AgentEvent[] = [];
247
249
  const start = Date.now();
248
250
  await loop.run({
251
+ requestId: "test-request",
249
252
  messages: [userMessage],
250
253
  onEvent: collectEvents(events),
251
254
  trust: { sourceChannel: "vellum", trustClass: "unknown" },
@@ -319,6 +322,7 @@ describe("Parallel tool execution benchmarks", () => {
319
322
  });
320
323
  const start = Date.now();
321
324
  const { history } = await loop.run({
325
+ requestId: "test-request",
322
326
  messages: [userMessage],
323
327
  onEvent: () => {},
324
328
  trust: { sourceChannel: "vellum", trustClass: "unknown" },
@@ -30,7 +30,6 @@ mock.module("../agent/image-optimize.js", () => ({
30
30
  }),
31
31
  }));
32
32
 
33
- import { persistUnsendableImageDowngrades } from "../daemon/persist-unsendable-image.js";
34
33
  import {
35
34
  addMessage,
36
35
  createConversation,
@@ -38,6 +37,7 @@ import {
38
37
  } from "../memory/conversation-crud.js";
39
38
  import { getDb } from "../memory/db-connection.js";
40
39
  import { initializeDb } from "../memory/db-init.js";
40
+ import { persistUnsendableImageDowngrades } from "../plugins/defaults/image-recovery/recover.js";
41
41
  import type { ContentBlock } from "../providers/types.js";
42
42
 
43
43
  initializeDb();
@@ -58,9 +58,22 @@ function oversizedPngBase64(): string {
58
58
  const height = 9000;
59
59
  return Buffer.from(
60
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"
61
+ 0x89,
62
+ 0x50,
63
+ 0x4e,
64
+ 0x47,
65
+ 0x0d,
66
+ 0x0a,
67
+ 0x1a,
68
+ 0x0a, // PNG signature
69
+ 0x00,
70
+ 0x00,
71
+ 0x00,
72
+ 0x0d, // IHDR length (13)
73
+ 0x49,
74
+ 0x48,
75
+ 0x44,
76
+ 0x52, // "IHDR"
64
77
  (width >>> 24) & 0xff,
65
78
  (width >>> 16) & 0xff,
66
79
  (width >>> 8) & 0xff,
@@ -69,7 +82,11 @@ function oversizedPngBase64(): string {
69
82
  (height >>> 16) & 0xff,
70
83
  (height >>> 8) & 0xff,
71
84
  height & 0xff,
72
- 0x08, 0x06, 0x00, 0x00, 0x00,
85
+ 0x08,
86
+ 0x06,
87
+ 0x00,
88
+ 0x00,
89
+ 0x00,
73
90
  ]),
74
91
  ).toString("base64");
75
92
  }
@@ -80,7 +97,10 @@ function toolResultWithImage(data: string): ContentBlock {
80
97
  tool_use_id: "toolu_123",
81
98
  content: "Screenshot captured",
82
99
  contentBlocks: [
83
- { type: "image", source: { type: "base64", media_type: "image/png", data } },
100
+ {
101
+ type: "image",
102
+ source: { type: "base64", media_type: "image/png", data },
103
+ },
84
104
  ],
85
105
  };
86
106
  }
@@ -119,9 +139,9 @@ describe("persistUnsendableImageDowngrades (downscalable host)", () => {
119
139
  };
120
140
  const nested = toolResult.contentBlocks?.[0];
121
141
  expect(nested?.type).toBe("image");
122
- expect((nested as Extract<ContentBlock, { type: "image" }>).source.data).toBe(
123
- SHRUNK_DATA,
124
- );
142
+ expect(
143
+ (nested as Extract<ContentBlock, { type: "image" }>).source.data,
144
+ ).toBe(SHRUNK_DATA);
125
145
  });
126
146
 
127
147
  /** Re-running is a no-op: the downscaled payload is within limits. */
@@ -14,10 +14,6 @@
14
14
 
15
15
  import { beforeEach, describe, expect, test } from "bun:test";
16
16
 
17
- import {
18
- oversizedImageReplacement,
19
- persistUnsendableImageDowngrades,
20
- } from "../daemon/persist-unsendable-image.js";
21
17
  import {
22
18
  addMessage,
23
19
  createConversation,
@@ -25,6 +21,10 @@ import {
25
21
  } from "../memory/conversation-crud.js";
26
22
  import { getDb } from "../memory/db-connection.js";
27
23
  import { initializeDb } from "../memory/db-init.js";
24
+ import {
25
+ oversizedImageReplacement,
26
+ persistUnsendableImageDowngrades,
27
+ } from "../plugins/defaults/image-recovery/recover.js";
28
28
  import type { ContentBlock } from "../providers/types.js";
29
29
 
30
30
  initializeDb();
@@ -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
+ });