@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,23 +1,41 @@
1
1
  /**
2
- * Tests for the memory-v3-live v2-suppression branch in
3
- * `applyRuntimeInjections`.
2
+ * Tests for the memory-v3 step-0 branch in `applyRuntimeInjections`:
3
+ * spotlight strip + v2 tail suppression.
4
4
  *
5
5
  * When the `memory-v3-live` flag is on AND the v3 injector (id `memory-v3`,
6
- * placement `after-memory-prefix`) actually produces a block, runtime
7
- * assembly strips the v2 `<memory>` prefix from EVERY user message before
8
- * splicing the v3 block — so v3 becomes the sole `<memory>` source and history
9
- * is byte-stable for prompt caching.
6
+ * placement `after-memory-prefix`) produces a block — possibly EMPTY-TEXT on
7
+ * an all-repeat turn — runtime assembly strips the v2 `<memory>` prefix from
8
+ * the TAIL user message only before splicing the v3 block. Historical user
9
+ * messages keep their memory blocks byte-identical (frozen v3 cards and
10
+ * pre-cutover v2 blocks both ride the cached prefix); the old whole-layer
11
+ * strip is gone.
10
12
  *
11
- * Keyed off whether v3 produced a block, NOT off the flag alone: a v3
12
- * failure (`produce()` → null) leaves v2's block intact (fallback-to-v2).
13
+ * The ephemeral `<memory_spotlight>` block is strip-and-replaced every turn:
14
+ * stale spotlights are removed from every user message unconditionally (a
15
+ * scoped, single-id strip), and the spotlight injector's `append-user-tail`
16
+ * block lands at the tail.
13
17
  *
14
- * The flag-off path must be byte-for-byte identical to today — that is the
15
- * load-bearing regression guard. `applyRuntimeInjections` reads the flag
16
- * itself, so these tests drive it through the override cache.
18
+ * v2 suppression stays keyed off whether v3 produced a block, NOT off the
19
+ * flag alone: a v3 failure (`produce()` → null) leaves v2's block intact
20
+ * (fallback-to-v2). The flag-off path must be byte-for-byte identical to
21
+ * today — that is the load-bearing regression guard. `applyRuntimeInjections`
22
+ * reads the flag itself, so these tests drive it through the override cache.
23
+ *
24
+ * The strip discriminates v2's dynamic block by IDENTITY, not by prefix: v2's
25
+ * `INJECTION_HEADER` and v3's `V3_CARDS_INJECTION_HEADER` are deliberately
26
+ * byte-identical, and v2's router block leads with that header whenever any
27
+ * summary section is present, so no prefix can tell the layers apart. The
28
+ * identity — the exact text v2 prepended this turn — is read off the live
29
+ * graph-memory handle, which these tests register and seed per test.
17
30
  */
18
31
 
19
- import { beforeEach, describe, expect, mock, test } from "bun:test";
32
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
20
33
 
34
+ import { ConversationGraphMemory } from "../memory/graph/conversation-graph-memory.js";
35
+ import { wrapMemorySpotlightBlock } from "../memory/memory-marker.js";
36
+ import { INJECTION_HEADER } from "../memory/v2/injection.js";
37
+ import { V3_CARDS_INJECTION_HEADER } from "../plugins/defaults/memory-v3-shadow/render-injection.js";
38
+ import { MEMORY_V3_COMMIT_META_KEY } from "../plugins/defaults/memory-v3-shadow/types.js";
21
39
  import type {
22
40
  InjectionBlock,
23
41
  Injector,
@@ -28,7 +46,7 @@ import { setOverridesForTesting } from "./feature-flag-test-helpers.js";
28
46
 
29
47
  // Drive the suppression branch by controlling the static injector chain that
30
48
  // `applyRuntimeInjections` walks. The slot is mutated per-test to stand in for
31
- // the memory-v3 injector producing (or not producing) a block.
49
+ // the memory-v3 injectors producing (or not producing) blocks.
32
50
  const injectorChainSlot: Injector[] = [];
33
51
  mock.module("../plugins/defaults/memory-retrieval/injector-chain.js", () => ({
34
52
  getInjectorChain: () => injectorChainSlot,
@@ -46,13 +64,31 @@ function makeTurnContext(): TurnContext {
46
64
  };
47
65
  }
48
66
 
67
+ /** Live graph handles registered by the current test (disposed afterwards). */
68
+ const seededGraphs: ConversationGraphMemory[] = [];
69
+
49
70
  /**
50
- * A fake v3 injector that mirrors the real one's id + placement. The real
51
- * injector's `renderMemoryBlock` already wraps its content in
52
- * `<memory>\n…\n</memory>`, so the fake does too — `inner === null` means the
53
- * injector produced nothing this turn (error/empty selection).
71
+ * Register a live graph-memory handle for `conv-test-1` whose last retrieval
72
+ * injected `text` — the identity the suppression strip resolves via
73
+ * `getLiveGraphMemory(conversationId)?.lastInjectedBlockText`. The production
74
+ * setter is `prepareMemory` (which needs a live DB), so the cached block is
75
+ * seeded directly through the private field.
54
76
  */
55
- function v3Injector(inner: string | null): Injector {
77
+ function seedV2Identity(text: string | null): void {
78
+ const graph = new ConversationGraphMemory("conv-test-1");
79
+ (graph as unknown as { lastInjectedBlock: string | null }).lastInjectedBlock =
80
+ text;
81
+ seededGraphs.push(graph);
82
+ }
83
+
84
+ /**
85
+ * A fake v3 cards injector that mirrors the real one's id + placement. The
86
+ * real injector wraps its content in `<memory>\n…\n</memory>`, so the fake
87
+ * does too. `inner === null` means the injector produced nothing this turn
88
+ * (error/empty selection); `inner === ""` mirrors an all-repeat turn (block
89
+ * produced, empty text — nothing attached, but v2 is still suppressed).
90
+ */
91
+ function v3Injector(inner: string | null, commit?: () => void): Injector {
56
92
  return {
57
93
  name: "memory-v3-shadow",
58
94
  order: 1000,
@@ -60,8 +96,24 @@ function v3Injector(inner: string | null): Injector {
60
96
  if (inner === null) return null;
61
97
  return {
62
98
  id: "memory-v3",
63
- text: `<memory>\n${inner}\n</memory>`,
99
+ text: inner === "" ? "" : `<memory>\n${inner}\n</memory>`,
64
100
  placement: "after-memory-prefix",
101
+ ...(commit ? { meta: { [MEMORY_V3_COMMIT_META_KEY]: commit } } : {}),
102
+ };
103
+ },
104
+ };
105
+ }
106
+
107
+ /** A fake v3 spotlight injector mirroring the real id + tail placement. */
108
+ function spotlightInjector(inner: string): Injector {
109
+ return {
110
+ name: "memory-v3-spotlight",
111
+ order: 1001,
112
+ async produce(): Promise<InjectionBlock | null> {
113
+ return {
114
+ id: "memory-v3-spotlight",
115
+ text: wrapMemorySpotlightBlock(inner),
116
+ placement: "append-user-tail",
65
117
  };
66
118
  },
67
119
  };
@@ -94,14 +146,25 @@ describe("memory-v3-live v2 suppression", () => {
94
146
  setOverridesForTesting({});
95
147
  });
96
148
 
97
- test("flag ON + v3 produced a block → v2 stripped from all turns, exactly one <memory> (the v3 block)", async () => {
149
+ afterEach(() => {
150
+ for (const graph of seededGraphs) graph.dispose();
151
+ seededGraphs.length = 0;
152
+ });
153
+
154
+ test("flag ON + v3 produced a block → TAIL v2 stripped, historical memory blocks frozen in place", async () => {
98
155
  setOverridesForTesting({ "memory-v3-live": true });
99
- injectorChainSlot.push(v3Injector("v3 working set"));
156
+ injectorChainSlot.push(v3Injector("net-new cards"));
157
+ seedV2Identity("fresh recalled fact");
100
158
 
101
- // History: a prior user turn that still carries a v2 block (rehydrated),
102
- // plus the current tail user turn with its own v2 block.
159
+ // History: a prior user turn carrying a frozen memory block (a v3 card
160
+ // block or a pre-cutover v2 block — same wrapper), plus the current tail
161
+ // user turn with v2's freshly-prepended block.
162
+ const historicalUser = userMsgWithV2Memory(
163
+ "frozen card from turn 1",
164
+ "earlier question",
165
+ );
103
166
  const runMessages: Message[] = [
104
- userMsgWithV2Memory("old recalled fact", "earlier question"),
167
+ historicalUser,
105
168
  {
106
169
  role: "assistant",
107
170
  content: [{ type: "text", text: "earlier answer" }],
@@ -113,31 +176,292 @@ describe("memory-v3-live v2 suppression", () => {
113
176
  ...makeTurnContext(),
114
177
  });
115
178
 
116
- // Exactly one <memory> source across the WHOLE assembled context.
117
- const allTexts = result.messages.flatMap((m) =>
118
- m.content
119
- .filter((b): b is { type: "text"; text: string } => b.type === "text")
120
- .map((b) => b.text),
121
- );
122
- const memoryBlocks = allTexts.filter((t) => t.startsWith("<memory>"));
123
- expect(memoryBlocks).toHaveLength(1);
124
- // And it is the v3 block, not a v2 one.
125
- expect(memoryBlocks[0]).toBe("<memory>\nv3 working set\n</memory>");
179
+ // The HISTORICAL user turn keeps its memory block byte-identical — the
180
+ // frozen-card cache contract (no whole-layer strip).
181
+ expect(result.messages[0].content).toEqual(historicalUser.content);
126
182
 
127
- // The historical user turn no longer carries its v2 block (all-turns strip).
128
- const firstUser = result.messages[0];
129
- expect(
130
- firstUser.content.every(
131
- (b) => !(b.type === "text" && b.text.startsWith("<memory>")),
132
- ),
133
- ).toBe(true);
134
-
135
- // The v3 block lands at the head of the tail user message; original user
183
+ // The tail's fresh v2 block is replaced by the v3 block; original user
136
184
  // text is preserved right after it.
137
185
  const texts = tailTexts(result.messages);
138
- expect(texts[0]).toBe("<memory>\nv3 working set\n</memory>");
186
+ expect(texts[0]).toBe("<memory>\nnet-new cards\n</memory>");
139
187
  expect(texts[1]).toBe("current question");
140
188
  expect(texts).toHaveLength(2);
189
+
190
+ // The v3 block is captured for metadata persistence (UNWRAPPED) and the
191
+ // turn is marked v3-active so the hook skips v2's metadata write.
192
+ expect(result.blocks.memoryV3InjectedBlock).toBe("net-new cards");
193
+ expect(result.blocks.memoryV3Active).toBe(true);
194
+ });
195
+
196
+ test("flag ON + EMPTY-TEXT v3 block (all-repeat turn) → tail v2 stripped, nothing attached", async () => {
197
+ setOverridesForTesting({ "memory-v3-live": true });
198
+ injectorChainSlot.push(v3Injector(""));
199
+ seedV2Identity("fresh recalled fact");
200
+
201
+ const runMessages: Message[] = [
202
+ userMsgWithV2Memory("fresh recalled fact", "current question"),
203
+ ];
204
+
205
+ const result = await applyRuntimeInjections(runMessages, {
206
+ ...makeTurnContext(),
207
+ });
208
+
209
+ // v2's fresh block is gone and NO v3 content replaced it — the frozen
210
+ // cards from prior turns already carry the memory.
211
+ const texts = tailTexts(result.messages);
212
+ expect(texts).toEqual(["current question"]);
213
+ // No bytes to persist, but the turn is still v3-active (gates v2's
214
+ // metadata write so the stripped block is not rehydrated on reload).
215
+ expect(result.blocks.memoryV3InjectedBlock).toBeUndefined();
216
+ expect(result.blocks.memoryV3Active).toBe(true);
217
+ });
218
+
219
+ test("stale spotlight blocks are stripped from EVERY user message; the new spotlight lands at the tail", async () => {
220
+ setOverridesForTesting({ "memory-v3-live": true });
221
+ injectorChainSlot.push(v3Injector(""), spotlightInjector("fresh sections"));
222
+
223
+ const staleSpotlight = {
224
+ type: "text" as const,
225
+ text: wrapMemorySpotlightBlock("stale sections"),
226
+ };
227
+ const runMessages: Message[] = [
228
+ {
229
+ role: "user",
230
+ content: [{ type: "text", text: "earlier question" }, staleSpotlight],
231
+ },
232
+ {
233
+ role: "assistant",
234
+ content: [{ type: "text", text: "earlier answer" }],
235
+ },
236
+ { role: "user", content: [{ type: "text", text: "current question" }] },
237
+ ];
238
+
239
+ const result = await applyRuntimeInjections(runMessages, {
240
+ ...makeTurnContext(),
241
+ });
242
+
243
+ // The stale spotlight is gone from the historical turn…
244
+ expect(result.messages[0].content).toEqual([
245
+ { type: "text", text: "earlier question" },
246
+ ]);
247
+ // …and exactly one fresh spotlight sits at the tail.
248
+ const texts = tailTexts(result.messages);
249
+ expect(texts).toEqual([
250
+ "current question",
251
+ wrapMemorySpotlightBlock("fresh sections"),
252
+ ]);
253
+ });
254
+
255
+ test("convergence re-entry: a tail leading with this turn's frozen v3 cards (and <info>) is NOT stripped", async () => {
256
+ setOverridesForTesting({ "memory-v3-live": true });
257
+ // Re-entry shape: the memo returns the same selections, every slug is now
258
+ // in the everInjected store, so the injector produces an EMPTY block —
259
+ // while the tail still carries the v3 card block frozen on first entry
260
+ // (leading the content because no workspace / <turn_context> prepend
261
+ // fired this turn) plus the <info> static block. The graph handle still
262
+ // holds this turn's v2 identity (stripped on first entry, so absent from
263
+ // the tail) — it must not match the frozen cards.
264
+ injectorChainSlot.push(v3Injector(""));
265
+ seedV2Identity("fresh recalled fact");
266
+
267
+ const frozenV3Block = `<memory>\n${V3_CARDS_INJECTION_HEADER}\n\n# memory/concepts/page-a.md\nhead\n</memory>`;
268
+ const infoBlock = "<info>\nstatic memory\n</info>";
269
+ const runMessages: Message[] = [
270
+ {
271
+ role: "user",
272
+ content: [
273
+ { type: "text", text: infoBlock },
274
+ { type: "text", text: frozenV3Block },
275
+ { type: "text", text: "current question" },
276
+ ],
277
+ },
278
+ ];
279
+
280
+ const result = await applyRuntimeInjections(runMessages, {
281
+ ...makeTurnContext(),
282
+ });
283
+
284
+ // The just-frozen cards and the static block survive re-entry — only
285
+ // v2's fresh dynamic prefix (absent here; stripped on first entry) is
286
+ // ever removed.
287
+ expect(tailTexts(result.messages)).toEqual([
288
+ infoBlock,
289
+ frozenV3Block,
290
+ "current question",
291
+ ]);
292
+ });
293
+
294
+ test("first entry with a v2 prefix AND a frozen v3 block strips ONLY the v2 block", async () => {
295
+ setOverridesForTesting({ "memory-v3-live": true });
296
+ injectorChainSlot.push(v3Injector(""));
297
+ seedV2Identity("fresh recalled fact");
298
+
299
+ const frozenV3Block = `<memory>\n${V3_CARDS_INJECTION_HEADER}\n\n# memory/concepts/page-a.md\nhead\n</memory>`;
300
+ const runMessages: Message[] = [
301
+ {
302
+ role: "user",
303
+ content: [
304
+ { type: "text", text: "<memory>\nfresh recalled fact\n</memory>" },
305
+ { type: "text", text: frozenV3Block },
306
+ { type: "text", text: "current question" },
307
+ ],
308
+ },
309
+ ];
310
+
311
+ const result = await applyRuntimeInjections(runMessages, {
312
+ ...makeTurnContext(),
313
+ });
314
+
315
+ expect(tailTexts(result.messages)).toEqual([
316
+ frozenV3Block,
317
+ "current question",
318
+ ]);
319
+ });
320
+
321
+ test("REGRESSION: a v2 block leading with the REAL summary header (byte-identical to v3's) is stripped; v3 cards and <info> survive (first entry)", async () => {
322
+ setOverridesForTesting({ "memory-v3-live": true });
323
+ injectorChainSlot.push(v3Injector(""));
324
+
325
+ // The collision this guards: v2's router block leads with
326
+ // INJECTION_HEADER whenever any summary section is present — the dominant
327
+ // production case — and v3's card header is deliberately the same bytes.
328
+ expect(INJECTION_HEADER).toBe(V3_CARDS_INJECTION_HEADER);
329
+
330
+ const v2Inner = `${INJECTION_HEADER}\n\n## memory/concepts/page-b.md\nsummary of page b`;
331
+ seedV2Identity(v2Inner);
332
+
333
+ const frozenV3Block = `<memory>\n${V3_CARDS_INJECTION_HEADER}\n\n# memory/concepts/page-a.md\nhead\n</memory>`;
334
+ const infoBlock = "<info>\nstatic memory\n</info>";
335
+ const runMessages: Message[] = [
336
+ {
337
+ role: "user",
338
+ content: [
339
+ { type: "text", text: `<memory>\n${v2Inner}\n</memory>` },
340
+ { type: "text", text: frozenV3Block },
341
+ { type: "text", text: infoBlock },
342
+ { type: "text", text: "current question" },
343
+ ],
344
+ },
345
+ ];
346
+
347
+ const result = await applyRuntimeInjections(runMessages, {
348
+ ...makeTurnContext(),
349
+ });
350
+
351
+ // Despite sharing v3's header prefix, v2's block is gone (identity
352
+ // match); the v3 cards and <info> blocks are byte-identical in place.
353
+ expect(tailTexts(result.messages)).toEqual([
354
+ frozenV3Block,
355
+ infoBlock,
356
+ "current question",
357
+ ]);
358
+ });
359
+
360
+ test("REGRESSION: re-entry with a header-bearing v2 identity keeps the first entry's frozen v3 cards", async () => {
361
+ setOverridesForTesting({ "memory-v3-live": true });
362
+ // Re-entry: produce() returns the EMPTY block (cards already claimed by
363
+ // the store) while the tail carries the FIRST entry's v3 block — and the
364
+ // graph handle still holds the summary-bearing v2 identity from this
365
+ // turn's retrieval. Identity must be matched against the v2 block, never
366
+ // against "whatever leads with the shared header".
367
+ injectorChainSlot.push(v3Injector(""));
368
+
369
+ const v2Inner = `${INJECTION_HEADER}\n\n## memory/concepts/page-b.md\nsummary of page b`;
370
+ seedV2Identity(v2Inner);
371
+
372
+ const frozenV3Block = `<memory>\n${V3_CARDS_INJECTION_HEADER}\n\n# memory/concepts/page-a.md\nhead\n</memory>`;
373
+ const runMessages: Message[] = [
374
+ {
375
+ role: "user",
376
+ content: [
377
+ { type: "text", text: frozenV3Block },
378
+ { type: "text", text: "current question" },
379
+ ],
380
+ },
381
+ ];
382
+
383
+ const result = await applyRuntimeInjections(runMessages, {
384
+ ...makeTurnContext(),
385
+ });
386
+
387
+ expect(tailTexts(result.messages)).toEqual([
388
+ frozenV3Block,
389
+ "current question",
390
+ ]);
391
+ });
392
+
393
+ test("v2 memory-image groups and legacy <memory __injected> blocks are stripped even without an identity", async () => {
394
+ setOverridesForTesting({ "memory-v3-live": true });
395
+ injectorChainSlot.push(v3Injector("net-new cards"));
396
+ // No live graph handle: identity unknown (null). Image groups and legacy
397
+ // blocks are unambiguously v2's and are stripped regardless; the shared
398
+ // `<memory>` wrapper is left alone without an identity to match.
399
+ const runMessages: Message[] = [
400
+ {
401
+ role: "user",
402
+ content: [
403
+ { type: "text", text: "<memory_image __injected>\na chart" },
404
+ {
405
+ type: "image",
406
+ source: { type: "base64", media_type: "image/png", data: "aGk=" },
407
+ },
408
+ { type: "text", text: "</memory_image>" },
409
+ { type: "text", text: "<memory __injected>\nlegacy recalled fact" },
410
+ { type: "text", text: "current question" },
411
+ ],
412
+ },
413
+ ];
414
+
415
+ const result = await applyRuntimeInjections(runMessages, {
416
+ ...makeTurnContext(),
417
+ });
418
+
419
+ const texts = tailTexts(result.messages);
420
+ expect(texts).toEqual([
421
+ "<memory>\nnet-new cards\n</memory>",
422
+ "current question",
423
+ ]);
424
+ // The injected image is gone too (the 3-block group strips as a unit).
425
+ expect(
426
+ result.messages[result.messages.length - 1].content.some(
427
+ (b) => b.type === "image",
428
+ ),
429
+ ).toBe(false);
430
+ });
431
+
432
+ test("the v3 block's attachment-commit callback fires on a user tail", async () => {
433
+ setOverridesForTesting({ "memory-v3-live": true });
434
+ const commit = mock(() => {});
435
+ injectorChainSlot.push(v3Injector("net-new cards", commit));
436
+
437
+ const runMessages: Message[] = [
438
+ userMsgWithV2Memory("fresh recalled fact", "current question"),
439
+ ];
440
+ await applyRuntimeInjections(runMessages, { ...makeTurnContext() });
441
+
442
+ expect(commit).toHaveBeenCalledTimes(1);
443
+ });
444
+
445
+ test("the commit callback is SKIPPED when the tail is not a user message (block never attaches)", async () => {
446
+ setOverridesForTesting({ "memory-v3-live": true });
447
+ const commit = mock(() => {});
448
+ injectorChainSlot.push(v3Injector("net-new cards", commit));
449
+
450
+ const runMessages: Message[] = [
451
+ { role: "user", content: [{ type: "text", text: "question" }] },
452
+ { role: "assistant", content: [{ type: "text", text: "answer" }] },
453
+ ];
454
+ const result = await applyRuntimeInjections(runMessages, {
455
+ ...makeTurnContext(),
456
+ });
457
+
458
+ // Neither attached nor captured nor committed: the store must not claim
459
+ // cards that never reached history.
460
+ expect(commit).not.toHaveBeenCalled();
461
+ expect(result.blocks.memoryV3InjectedBlock).toBeUndefined();
462
+ expect(result.messages[1].content).toEqual([
463
+ { type: "text", text: "answer" },
464
+ ]);
141
465
  });
142
466
 
143
467
  test("flag ON but v3 produced NOTHING → v2 block left intact (fallback-to-v2)", async () => {
@@ -158,10 +482,11 @@ describe("memory-v3-live v2 suppression", () => {
158
482
  expect(texts[1]).toBe("current question");
159
483
  // No v3 block was added.
160
484
  expect(texts).toHaveLength(2);
485
+ expect(result.blocks.memoryV3Active).toBe(false);
161
486
  });
162
487
 
163
488
  test("flag OFF → byte-for-byte identical to today even when v3 would have produced a block", async () => {
164
- injectorChainSlot.push(v3Injector("v3 working set"));
489
+ injectorChainSlot.push(v3Injector("v3 cards"));
165
490
 
166
491
  const runMessages: Message[] = [
167
492
  userMsgWithV2Memory("old recalled fact", "earlier question"),
@@ -185,14 +510,15 @@ describe("memory-v3-live v2 suppression", () => {
185
510
  // behavior change and it is fully gated off here.
186
511
  const texts = tailTexts(offResult.messages);
187
512
  expect(texts[0]).toBe("<memory>\nfresh recalled fact\n</memory>");
188
- expect(texts[1]).toBe("<memory>\nv3 working set\n</memory>");
513
+ expect(texts[1]).toBe("<memory>\nv3 cards\n</memory>");
189
514
  expect(texts[2]).toBe("current question");
190
515
 
191
- // Historical user turn keeps its v2 block (no all-turns strip when off).
516
+ // Historical user turn keeps its v2 block.
192
517
  const firstUserTexts = offResult.messages[0].content
193
518
  .filter((b): b is { type: "text"; text: string } => b.type === "text")
194
519
  .map((b) => b.text);
195
520
  expect(firstUserTexts[0]).toBe("<memory>\nold recalled fact\n</memory>");
521
+ expect(offResult.blocks.memoryV3Active).toBe(false);
196
522
  });
197
523
 
198
524
  test("no v3 injector registered + flag ON → no stripping, messages untouched", async () => {
@@ -83,6 +83,13 @@ describe("schedule_create tool description", () => {
83
83
  expect(scheduleCreateDef.description).toContain("cron");
84
84
  });
85
85
 
86
+ test("requires an authored description", () => {
87
+ expect(scheduleCreateDef.input_schema.required).toContain("description");
88
+ expect(
89
+ scheduleCreateDef.input_schema.properties.description.description,
90
+ ).toContain("what it does and why");
91
+ });
92
+
86
93
  test('warns against using for "add to my tasks" requests', () => {
87
94
  expect(scheduleCreateDef.description).toContain(
88
95
  'Do NOT use this for "add to my tasks"',