@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
@@ -193,6 +193,19 @@ mock.module("../skills/version-hash.js", () => ({
193
193
  },
194
194
  }));
195
195
 
196
+ // Mock the skill_loaded telemetry dependencies of conversation-skill-tools so
197
+ // their heavy transitive imports (catalog-install → CLI program, sqlite) stay
198
+ // out of this import-light test.
199
+ mock.module("../skills/catalog-cache.js", () => ({
200
+ getCachedCatalogSync: () => [],
201
+ }));
202
+ mock.module("../skills/install-meta.js", () => ({
203
+ readInstallMeta: () => null,
204
+ }));
205
+ mock.module("../memory/skill-loaded-events-store.js", () => ({
206
+ recordSkillLoadedEvent: () => {},
207
+ }));
208
+
196
209
  // ---------------------------------------------------------------------------
197
210
  // Imports under test (after mocks)
198
211
  // ---------------------------------------------------------------------------
@@ -174,6 +174,7 @@ mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
174
174
  async maybeCompact() {
175
175
  return { compacted: false };
176
176
  }
177
+ resetOverflowRecovery() {}
177
178
  },
178
179
  createContextSummaryMessage: () => ({
179
180
  role: "user",
@@ -63,6 +63,12 @@ import {
63
63
  toolInvocations,
64
64
  } from "../memory/schema.js";
65
65
  import { hydrate as hydrateActivationState } from "../memory/v2/activation-store.js";
66
+ import {
67
+ getInjected as getV3Injected,
68
+ markPruned as markV3Pruned,
69
+ MEMORY_V3_INJECTED_BLOCK_METADATA_KEY,
70
+ recordInjected as recordV3Injected,
71
+ } from "../plugins/defaults/memory-v3-shadow/ever-injected-store.js";
66
72
 
67
73
  initializeDb();
68
74
 
@@ -77,6 +83,7 @@ function resetTables(): void {
77
83
  db.delete(llmRequestLogs).run();
78
84
  db.delete(toolInvocations).run();
79
85
  db.delete(memoryJobs).run();
86
+ db.run("DELETE FROM memory_v3_ever_injected");
80
87
  db.run("DELETE FROM message_attachments");
81
88
  db.run("DELETE FROM attachments");
82
89
  db.run("DELETE FROM messages");
@@ -922,6 +929,258 @@ describe("forkConversation", () => {
922
929
  expect(loadGraphMemoryState(fork.id)).toBeNull();
923
930
  });
924
931
 
932
+ test("truncated fork seeds everInjected from inherited memory attachments", async () => {
933
+ const source = createConversation("Truncated seed thread");
934
+ await addMessage(source.id, "user", "first turn", {
935
+ metadata: {
936
+ memoryInjectedBlock:
937
+ "# memory/concepts/topics/page-a.md\nSummary A\n\n# memory/concepts/topics/page-b.md\nSummary B",
938
+ },
939
+ skipIndexing: true,
940
+ });
941
+ await addMessage(source.id, "assistant", "first reply", {
942
+ skipIndexing: true,
943
+ });
944
+ const boundaryMessage = await addMessage(source.id, "user", "second turn", {
945
+ metadata: {
946
+ memoryInjectedBlock: "# memory/concepts/topics/page-c.md\nSummary C",
947
+ },
948
+ skipIndexing: true,
949
+ });
950
+ await addMessage(source.id, "assistant", "second reply", {
951
+ skipIndexing: true,
952
+ });
953
+ // Past the fork boundary — its attachment must NOT be claimed.
954
+ await addMessage(source.id, "user", "third turn", {
955
+ metadata: {
956
+ memoryInjectedBlock: "# memory/concepts/topics/page-d.md\nSummary D",
957
+ },
958
+ skipIndexing: true,
959
+ });
960
+
961
+ const db = getDb();
962
+ db.insert(activationState)
963
+ .values({
964
+ conversationId: source.id,
965
+ messageId: "parent-msg",
966
+ stateJson: JSON.stringify({ "topics/page-d": 0.9 }),
967
+ everInjectedJson: JSON.stringify([
968
+ { slug: "topics/page-a", turn: 1 },
969
+ { slug: "topics/page-b", turn: 1 },
970
+ { slug: "topics/page-c", turn: 2 },
971
+ { slug: "topics/page-d", turn: 3 },
972
+ ]),
973
+ currentTurn: 3,
974
+ updatedAt: 1_700_000_000_000,
975
+ })
976
+ .run();
977
+
978
+ const fork = forkConversation({
979
+ conversationId: source.id,
980
+ throughMessageId: boundaryMessage.id,
981
+ });
982
+
983
+ const childState = await hydrateActivationState(db, fork.id);
984
+ expect(childState).not.toBeNull();
985
+ // Exactly the slugs whose attachments live in the copied history —
986
+ // page-d (injected past the boundary) stays re-injectable.
987
+ expect(childState?.everInjected.map((e) => e.slug).sort()).toEqual([
988
+ "topics/page-a",
989
+ "topics/page-b",
990
+ "topics/page-c",
991
+ ]);
992
+ expect(childState?.everInjected.every((e) => e.turn === 0)).toBe(true);
993
+ // Activation scores and the turn counter start fresh, matching the
994
+ // graph tracker (also not copied for truncated forks).
995
+ expect(childState?.state).toEqual({});
996
+ expect(childState?.currentTurn).toBe(0);
997
+ });
998
+
999
+ test("truncated fork ignores attachments behind an inherited compaction boundary", async () => {
1000
+ const source = createConversation("Compacted truncated thread");
1001
+ await addMessage(source.id, "user", "compacted turn", {
1002
+ metadata: {
1003
+ memoryInjectedBlock:
1004
+ "# memory/concepts/topics/page-compacted.md\nOld summary",
1005
+ },
1006
+ skipIndexing: true,
1007
+ });
1008
+ await addMessage(source.id, "assistant", "compacted reply", {
1009
+ skipIndexing: true,
1010
+ });
1011
+ const boundaryMessage = await addMessage(source.id, "user", "live turn", {
1012
+ metadata: {
1013
+ memoryInjectedBlock: "# memory/concepts/topics/page-live.md\nSummary",
1014
+ },
1015
+ skipIndexing: true,
1016
+ });
1017
+ await addMessage(source.id, "assistant", "live reply", {
1018
+ skipIndexing: true,
1019
+ });
1020
+ await addMessage(source.id, "user", "past boundary", {
1021
+ skipIndexing: true,
1022
+ });
1023
+ // First two messages sit behind the compaction boundary: their
1024
+ // attachments are not rendered, so the fork must not claim them.
1025
+ getDb()
1026
+ .update(conversations)
1027
+ .set({ contextCompactedMessageCount: 2 })
1028
+ .where(eq(conversations.id, source.id))
1029
+ .run();
1030
+
1031
+ const fork = forkConversation({
1032
+ conversationId: source.id,
1033
+ throughMessageId: boundaryMessage.id,
1034
+ });
1035
+
1036
+ const childState = await hydrateActivationState(getDb(), fork.id);
1037
+ expect(childState?.everInjected.map((e) => e.slug)).toEqual([
1038
+ "topics/page-live",
1039
+ ]);
1040
+ });
1041
+
1042
+ test("copies the parent's memory-v3 everInjected record into a full fork", async () => {
1043
+ const source = createConversation("V3 carry thread");
1044
+ await addMessage(source.id, "user", "first turn", { skipIndexing: true });
1045
+
1046
+ recordV3Injected(
1047
+ source.id,
1048
+ [
1049
+ { slug: "topics/page-a", bytes: 120 },
1050
+ { slug: "topics/page-b", bytes: 340 },
1051
+ ],
1052
+ 1_700_000_000_000,
1053
+ );
1054
+ markV3Pruned(source.id, ["topics/page-b"], 1_700_000_001_000);
1055
+
1056
+ const fork = forkConversation({ conversationId: source.id });
1057
+
1058
+ // Full-row copy, pruned state included.
1059
+ expect(getV3Injected(fork.id)).toEqual(
1060
+ new Map([
1061
+ ["topics/page-a", { bytes: 120, prunedAt: null }],
1062
+ ["topics/page-b", { bytes: 340, prunedAt: 1_700_000_001_000 }],
1063
+ ]),
1064
+ );
1065
+ // Parent record is untouched.
1066
+ expect(getV3Injected(source.id).size).toBe(2);
1067
+ });
1068
+
1069
+ test("leaves the fork's memory-v3 record empty when the parent has none", async () => {
1070
+ const source = createConversation("V3 pristine thread");
1071
+ await addMessage(source.id, "user", "first message", {
1072
+ skipIndexing: true,
1073
+ });
1074
+
1075
+ const fork = forkConversation({ conversationId: source.id });
1076
+
1077
+ expect(getV3Injected(fork.id).size).toBe(0);
1078
+ });
1079
+
1080
+ test("truncated fork seeds the memory-v3 record from inherited v3 card blocks", async () => {
1081
+ const source = createConversation("V3 truncated seed thread");
1082
+ await addMessage(source.id, "user", "first turn", {
1083
+ metadata: {
1084
+ [MEMORY_V3_INJECTED_BLOCK_METADATA_KEY]:
1085
+ "# memory/concepts/topics/page-a.md\nCard A\n\n# memory/concepts/topics/page-b.md\nCard B",
1086
+ // A v2 block on the same message must seed only the v2 record.
1087
+ memoryInjectedBlock: "# memory/concepts/topics/page-v2.md\nSummary",
1088
+ },
1089
+ skipIndexing: true,
1090
+ });
1091
+ await addMessage(source.id, "assistant", "first reply", {
1092
+ skipIndexing: true,
1093
+ });
1094
+ const boundaryMessage = await addMessage(source.id, "user", "second turn", {
1095
+ skipIndexing: true,
1096
+ });
1097
+ await addMessage(source.id, "assistant", "second reply", {
1098
+ skipIndexing: true,
1099
+ });
1100
+ // Past the fork boundary — its card must NOT be claimed.
1101
+ await addMessage(source.id, "user", "third turn", {
1102
+ metadata: {
1103
+ [MEMORY_V3_INJECTED_BLOCK_METADATA_KEY]:
1104
+ "# memory/concepts/topics/page-c.md\nCard C",
1105
+ },
1106
+ skipIndexing: true,
1107
+ });
1108
+
1109
+ recordV3Injected(
1110
+ source.id,
1111
+ [
1112
+ { slug: "topics/page-a", bytes: 120 },
1113
+ { slug: "topics/page-b", bytes: 340 },
1114
+ { slug: "topics/page-c", bytes: 90 },
1115
+ ],
1116
+ 1_700_000_000_000,
1117
+ );
1118
+
1119
+ const fork = forkConversation({
1120
+ conversationId: source.id,
1121
+ throughMessageId: boundaryMessage.id,
1122
+ });
1123
+
1124
+ // Exactly the slugs whose card blocks live in the copied history,
1125
+ // dedup-only (`bytes = 0` — resident accounting restarts on the child).
1126
+ expect(getV3Injected(fork.id)).toEqual(
1127
+ new Map([
1128
+ ["topics/page-a", { bytes: 0, prunedAt: null }],
1129
+ ["topics/page-b", { bytes: 0, prunedAt: null }],
1130
+ ]),
1131
+ );
1132
+ // The v2 seed picked up only the v2 block, not the v3 cards.
1133
+ const childState = await hydrateActivationState(getDb(), fork.id);
1134
+ expect(childState?.everInjected.map((e) => e.slug)).toEqual([
1135
+ "topics/page-v2",
1136
+ ]);
1137
+ });
1138
+
1139
+ test("truncated fork carries the parent's pruned tombstones for inherited v3 slugs", async () => {
1140
+ // Pruning never rewrites the persisted metadata block, so the fork scan
1141
+ // sees pruned cards' sections too — the seed must tombstone them, or the
1142
+ // child's rehydration would resurrect cards the parent's live view lost.
1143
+ const source = createConversation("V3 truncated pruned thread");
1144
+ await addMessage(source.id, "user", "first turn", {
1145
+ metadata: {
1146
+ [MEMORY_V3_INJECTED_BLOCK_METADATA_KEY]:
1147
+ "# memory/concepts/topics/page-a.md\nCard A\n\n# memory/concepts/topics/page-b.md\nCard B",
1148
+ },
1149
+ skipIndexing: true,
1150
+ });
1151
+ const boundaryMessage = await addMessage(
1152
+ source.id,
1153
+ "assistant",
1154
+ "first reply",
1155
+ { skipIndexing: true },
1156
+ );
1157
+ // Past the fork boundary — forces the truncated-seed path (a fork through
1158
+ // the final message takes the wholesale-copy path instead).
1159
+ await addMessage(source.id, "user", "second turn", { skipIndexing: true });
1160
+
1161
+ recordV3Injected(
1162
+ source.id,
1163
+ [
1164
+ { slug: "topics/page-a", bytes: 120 },
1165
+ { slug: "topics/page-b", bytes: 340 },
1166
+ ],
1167
+ 1_700_000_000_000,
1168
+ );
1169
+ markV3Pruned(source.id, ["topics/page-b"], 1_700_000_005_000);
1170
+
1171
+ const fork = forkConversation({
1172
+ conversationId: source.id,
1173
+ throughMessageId: boundaryMessage.id,
1174
+ });
1175
+
1176
+ expect(getV3Injected(fork.id)).toEqual(
1177
+ new Map([
1178
+ ["topics/page-a", { bytes: 0, prunedAt: null }],
1179
+ ["topics/page-b", { bytes: 0, prunedAt: 1_700_000_005_000 }],
1180
+ ]),
1181
+ );
1182
+ });
1183
+
925
1184
  test("defaults conversationType to standard and inherits the parent's group", async () => {
926
1185
  const source = createConversation("Default inheritance thread");
927
1186
  await addMessage(source.id, "user", "first message", {
@@ -812,7 +812,7 @@ describe("web_search_tool_result structural guard", () => {
812
812
  // Downgrades permanently-unsendable images nested in tool_result.contentBlocks
813
813
  // (e.g. an oversized browser screenshot). web_search_tool_result has opaque
814
814
  // content with no contentBlocks property, so it cannot hold such an image.
815
- "daemon/persist-unsendable-image.ts",
815
+ "plugins/defaults/image-recovery/recover.ts",
816
816
 
817
817
  // Final orphan-pair safety pass in the Slack transcript renderer.
818
818
  // Server-side block types (`server_tool_use`, `web_search_tool_result`)
@@ -1,4 +1,4 @@
1
- import { beforeEach, describe, expect, mock, test } from "bun:test";
1
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  // Stub out heavy dependencies before importing Conversation
4
4
  mock.module("../util/logger.js", () => ({
@@ -110,8 +110,38 @@ mock.module("../memory/conversation-queries.js", () => ({
110
110
  listConversations: () => [],
111
111
  }));
112
112
 
113
+ // `loadFromDb` reads the conversation's pruned v3 card slugs (prune valve)
114
+ // when a row carries a `memoryV3InjectedBlock`. Stub the store read so these
115
+ // tests never touch a real DB; the stub DELEGATES to the real implementation
116
+ // unless this file is actively running (`mock.module` is process-global and
117
+ // would otherwise leak into sibling files that use the real store).
118
+ const realEverInjectedStore = {
119
+ ...(await import("../plugins/defaults/memory-v3-shadow/ever-injected-store.js")),
120
+ };
121
+ let lifecycleStoreMockActive = false;
122
+ let mockPrunedSlugs = new Set<string>();
123
+ mock.module(
124
+ "../plugins/defaults/memory-v3-shadow/ever-injected-store.js",
125
+ () => ({
126
+ ...realEverInjectedStore,
127
+ getPrunedSlugs: (conversationId: string) =>
128
+ lifecycleStoreMockActive
129
+ ? mockPrunedSlugs
130
+ : realEverInjectedStore.getPrunedSlugs(conversationId),
131
+ }),
132
+ );
133
+
113
134
  import { Conversation } from "../daemon/conversation.js";
114
135
 
136
+ beforeEach(() => {
137
+ lifecycleStoreMockActive = true;
138
+ mockPrunedSlugs = new Set();
139
+ });
140
+
141
+ afterAll(() => {
142
+ lifecycleStoreMockActive = false;
143
+ });
144
+
115
145
  function makeConversation(): Conversation {
116
146
  const provider = {
117
147
  name: "mock",
@@ -346,6 +376,156 @@ describe("loadFromDb metadata injection rehydration", () => {
346
376
  expect(firstBlock.text.match(/<memory>/g)?.length).toBe(1);
347
377
  });
348
378
 
379
+ test("memoryV3InjectedBlock rehydrates wrapped on ALL rows (tail included)", async () => {
380
+ // The memory-v3 frozen card block persists UNWRAPPED under its own key and
381
+ // must come back wrapped on every row — including the tail, since the next
382
+ // turn injects only NET-NEW cards (deduped via the v3 store) and never
383
+ // re-renders this row's cards.
384
+ mockConversation = defaultConv();
385
+ mockDbMessages = [
386
+ {
387
+ id: "m1",
388
+ role: "user",
389
+ content: JSON.stringify([{ type: "text", text: "First turn" }]),
390
+ metadata: JSON.stringify({
391
+ memoryV3InjectedBlock:
392
+ "header line\n\n# memory/concepts/page-a.md\nhead a",
393
+ }),
394
+ },
395
+ {
396
+ id: "m2",
397
+ role: "assistant",
398
+ content: JSON.stringify([{ type: "text", text: "Reply" }]),
399
+ },
400
+ {
401
+ id: "m3",
402
+ role: "user",
403
+ content: JSON.stringify([{ type: "text", text: "Tail turn" }]),
404
+ metadata: JSON.stringify({
405
+ memoryV3InjectedBlock: "# memory/concepts/page-b.md\nhead b",
406
+ }),
407
+ },
408
+ ];
409
+
410
+ const conversation = makeConversation();
411
+ await conversation.loadFromDb();
412
+ const messages = conversation.getMessages();
413
+
414
+ expect(messages).toHaveLength(3);
415
+ expect(messages[0].content).toEqual([
416
+ {
417
+ type: "text",
418
+ text: "<memory>\nheader line\n\n# memory/concepts/page-a.md\nhead a\n</memory>",
419
+ },
420
+ { type: "text", text: "First turn" },
421
+ ]);
422
+ // Tail row rehydrates too (unlike turn_context / system_reminder).
423
+ expect(messages[2].content).toEqual([
424
+ {
425
+ type: "text",
426
+ text: "<memory>\n# memory/concepts/page-b.md\nhead b\n</memory>",
427
+ },
428
+ { type: "text", text: "Tail turn" },
429
+ ]);
430
+ });
431
+
432
+ test("pruned slugs' card sections are skipped at v3 rehydration (prune valve persistence)", async () => {
433
+ // The prune valve marks cards pruned in the everInjected store instead of
434
+ // rewriting the persisted metadata; the rehydration splice re-filters on
435
+ // every load, which is what makes a prune survive restarts.
436
+ mockConversation = defaultConv();
437
+ mockPrunedSlugs = new Set(["page-a"]);
438
+ mockDbMessages = [
439
+ {
440
+ id: "m1",
441
+ role: "user",
442
+ content: JSON.stringify([{ type: "text", text: "First turn" }]),
443
+ metadata: JSON.stringify({
444
+ memoryV3InjectedBlock:
445
+ "header line\n\n# memory/concepts/page-a.md\nhead a\n\n# memory/concepts/page-b.md\nhead b",
446
+ }),
447
+ },
448
+ {
449
+ id: "m2",
450
+ role: "assistant",
451
+ content: JSON.stringify([{ type: "text", text: "Reply" }]),
452
+ },
453
+ ];
454
+
455
+ const conversation = makeConversation();
456
+ await conversation.loadFromDb();
457
+ const messages = conversation.getMessages();
458
+
459
+ expect(messages[0].content).toEqual([
460
+ {
461
+ type: "text",
462
+ text: "<memory>\nheader line\n\n# memory/concepts/page-b.md\nhead b\n</memory>",
463
+ },
464
+ { type: "text", text: "First turn" },
465
+ ]);
466
+ });
467
+
468
+ test("a fully-pruned memoryV3InjectedBlock is skipped entirely at rehydration", async () => {
469
+ mockConversation = defaultConv();
470
+ mockPrunedSlugs = new Set(["page-a"]);
471
+ mockDbMessages = [
472
+ {
473
+ id: "m1",
474
+ role: "user",
475
+ content: JSON.stringify([{ type: "text", text: "First turn" }]),
476
+ metadata: JSON.stringify({
477
+ memoryV3InjectedBlock:
478
+ "header line\n\n# memory/concepts/page-a.md\nhead a",
479
+ }),
480
+ },
481
+ {
482
+ id: "m2",
483
+ role: "assistant",
484
+ content: JSON.stringify([{ type: "text", text: "Reply" }]),
485
+ },
486
+ ];
487
+
488
+ const conversation = makeConversation();
489
+ await conversation.loadFromDb();
490
+ const messages = conversation.getMessages();
491
+
492
+ // No memory block at all — an instruction header with zero cards carries
493
+ // no content (matches the live strip, which removes the emptied block).
494
+ expect(messages[0].content).toEqual([{ type: "text", text: "First turn" }]);
495
+ });
496
+
497
+ test("defensively-wrapped memoryV3InjectedBlock rehydrates singly-wrapped", async () => {
498
+ mockConversation = defaultConv();
499
+ mockDbMessages = [
500
+ {
501
+ id: "m1",
502
+ role: "user",
503
+ content: JSON.stringify([{ type: "text", text: "Hi" }]),
504
+ metadata: JSON.stringify({
505
+ memoryV3InjectedBlock:
506
+ "<memory>\n# memory/concepts/page-a.md\nhead a\n</memory>",
507
+ }),
508
+ },
509
+ {
510
+ id: "m2",
511
+ role: "assistant",
512
+ content: JSON.stringify([{ type: "text", text: "Hello" }]),
513
+ },
514
+ ];
515
+
516
+ const conversation = makeConversation();
517
+ await conversation.loadFromDb();
518
+ const messages = conversation.getMessages();
519
+
520
+ const firstBlock = messages[0].content[0];
521
+ expect(firstBlock).toEqual({
522
+ type: "text",
523
+ text: "<memory>\n# memory/concepts/page-a.md\nhead a\n</memory>",
524
+ });
525
+ if (firstBlock.type !== "text") throw new Error("unexpected block type");
526
+ expect(firstBlock.text.match(/<memory>/g)?.length).toBe(1);
527
+ });
528
+
349
529
  test("malformed metadata is tolerated: load does not throw, content unchanged", async () => {
350
530
  mockConversation = defaultConv();
351
531
  mockDbMessages = [
@@ -606,6 +786,82 @@ describe("loadFromDb metadata injection rehydration", () => {
606
786
  ]);
607
787
  });
608
788
 
789
+ test("rehydration order matches live assembly when memoryV2StaticBlock and memoryV3InjectedBlock co-occur", async () => {
790
+ // First-turn and first-post-compaction rows of a memory-v3-live
791
+ // conversation persist BOTH `memoryV2StaticBlock` and the v3 card block.
792
+ // Live assembly applies after-memory-prefix splices in ascending
793
+ // injector order (pkb-context 30, pkb-reminder 35, memory-v2-static 38,
794
+ // now-md 40, memory-v3-shadow 1000); each splice lands at the
795
+ // memory-prefix boundary (`countMemoryPrefixBlocks` counts `<memory>`
796
+ // AND `<info>` blocks), so the v3 block — spliced last and itself
797
+ // `<memory>`-wrapped — lands AFTER the `<info>` static block but before
798
+ // now-md's earlier splice:
799
+ // [<workspace>, <turn_context>, <memory>dynamic</memory>,
800
+ // <info>v2static</info>, <memory>v3cards</memory>, <NOW.md>,
801
+ // <system_reminder>, <knowledge_base>, ...original]
802
+ // Rehydration must reproduce this byte-for-byte or every daemon restart
803
+ // busts the provider prefix cache for the whole conversation history.
804
+ // (`memoryInjectedBlock` and the v3 key are mutually exclusive on real
805
+ // rows; both are included here to pin the relative order of all splices.)
806
+ mockConversation = defaultConv();
807
+ mockDbMessages = [
808
+ {
809
+ id: "m1",
810
+ role: "user",
811
+ content: JSON.stringify([{ type: "text", text: "First turn" }]),
812
+ metadata: JSON.stringify({
813
+ workspaceBlock: "<workspace>\nworkspace body\n</workspace>",
814
+ turnContextBlock: "<turn_context>\nctx payload\n</turn_context>",
815
+ memoryInjectedBlock: "mem payload",
816
+ memoryV2StaticBlock:
817
+ "<info>\n## Essentials\n\nAlice prefers VS Code.\n</info>",
818
+ memoryV3InjectedBlock:
819
+ "header line\n\n# memory/concepts/page-a.md\nhead a",
820
+ nowScratchpadBlock: "<NOW.md>\nnow body\n</NOW.md>",
821
+ pkbSystemReminderBlock:
822
+ "<system_reminder>\npkb reminder body\n</system_reminder>",
823
+ pkbContextBlock: "<knowledge_base>\nkb body\n</knowledge_base>",
824
+ }),
825
+ },
826
+ {
827
+ id: "m2",
828
+ role: "assistant",
829
+ content: JSON.stringify([{ type: "text", text: "Reply" }]),
830
+ },
831
+ {
832
+ id: "m3",
833
+ role: "user",
834
+ content: JSON.stringify([{ type: "text", text: "Tail" }]),
835
+ },
836
+ ];
837
+
838
+ const conversation = makeConversation();
839
+ await conversation.loadFromDb();
840
+ const messages = conversation.getMessages();
841
+
842
+ expect(messages).toHaveLength(3);
843
+ expect(messages[0].content).toEqual([
844
+ { type: "text", text: "<workspace>\nworkspace body\n</workspace>" },
845
+ { type: "text", text: "<turn_context>\nctx payload\n</turn_context>" },
846
+ { type: "text", text: "<memory>\nmem payload\n</memory>" },
847
+ {
848
+ type: "text",
849
+ text: "<info>\n## Essentials\n\nAlice prefers VS Code.\n</info>",
850
+ },
851
+ {
852
+ type: "text",
853
+ text: "<memory>\nheader line\n\n# memory/concepts/page-a.md\nhead a\n</memory>",
854
+ },
855
+ { type: "text", text: "<NOW.md>\nnow body\n</NOW.md>" },
856
+ {
857
+ type: "text",
858
+ text: "<system_reminder>\npkb reminder body\n</system_reminder>",
859
+ },
860
+ { type: "text", text: "<knowledge_base>\nkb body\n</knowledge_base>" },
861
+ { type: "text", text: "First turn" },
862
+ ]);
863
+ });
864
+
609
865
  test("untrusted-actor view does not rehydrate memoryV2StaticBlock", async () => {
610
866
  mockConversation = defaultConv();
611
867
  // Rows with `trusted_contact` / `unknown` provenance survive the
@@ -259,6 +259,7 @@ mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
259
259
  async maybeCompact() {
260
260
  return { compacted: false };
261
261
  }
262
+ resetOverflowRecovery() {}
262
263
  },
263
264
  createContextSummaryMessage: () => ({
264
265
  role: "user",