@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
@@ -11,6 +11,8 @@ import { tmpdir } from "node:os";
11
11
  import { join } from "node:path";
12
12
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
13
13
 
14
+ import { parse as parseYaml } from "yaml";
15
+
14
16
  const TEST_DIR = process.env.VELLUM_WORKSPACE_DIR!;
15
17
 
16
18
  const noopLogger = {
@@ -252,6 +254,129 @@ describe("workspace skills", () => {
252
254
  });
253
255
  });
254
256
 
257
+ describe("plugin-resident skills", () => {
258
+ const pluginsDir = join(TEST_DIR, "plugins");
259
+
260
+ /**
261
+ * Materialize a skill on disk inside an installed plugin directory:
262
+ * `plugins/<plugin>/skills/<skillId>/SKILL.md`. A `package.json` is written
263
+ * for the plugin by default since that is the install gate the discovery
264
+ * scan keys on; pass `withPackageJson: false` to simulate a stray directory.
265
+ */
266
+ function writePluginSkill(
267
+ pluginName: string,
268
+ skillId: string,
269
+ name: string,
270
+ description: string,
271
+ body: string = "Plugin skill body",
272
+ {
273
+ withPackageJson = true,
274
+ packageName,
275
+ }: { withPackageJson?: boolean; packageName?: string } = {},
276
+ ): void {
277
+ const pluginDir = join(pluginsDir, pluginName);
278
+ mkdirSync(pluginDir, { recursive: true });
279
+ if (withPackageJson) {
280
+ writeFileSync(
281
+ join(pluginDir, "package.json"),
282
+ JSON.stringify({ name: packageName ?? pluginName, version: "1.0.0" }),
283
+ );
284
+ }
285
+ const skillDir = join(pluginDir, "skills", skillId);
286
+ mkdirSync(skillDir, { recursive: true });
287
+ writeFileSync(
288
+ join(skillDir, "SKILL.md"),
289
+ `---\nname: "${name}"\ndescription: "${description}"\n---\n\n${body}\n`,
290
+ );
291
+ }
292
+
293
+ beforeEach(() => {
294
+ mkdirSync(join(TEST_DIR, "skills"), { recursive: true });
295
+ });
296
+
297
+ afterEach(() => {
298
+ for (const dir of [join(TEST_DIR, "skills"), pluginsDir]) {
299
+ if (existsSync(dir)) rmSync(dir, { recursive: true, force: true });
300
+ }
301
+ });
302
+
303
+ test("discovers skills shipped inside an installed plugin, attributed to it", () => {
304
+ writePluginSkill("caveman", "caveman", "Caveman", "Terse mode");
305
+
306
+ const skill = loadSkillCatalog().find((s) => s.id === "caveman");
307
+ expect(skill).toBeDefined();
308
+ expect(skill!.source).toBe("plugin");
309
+ expect(skill!.owner).toEqual({ kind: "plugin", id: "caveman" });
310
+ });
311
+
312
+ test("loadSkillBySelector loads a plugin-resident skill body", () => {
313
+ writePluginSkill(
314
+ "caveman",
315
+ "caveman",
316
+ "Caveman",
317
+ "Terse mode",
318
+ "Full plugin skill body",
319
+ );
320
+
321
+ const result = loadSkillBySelector("caveman");
322
+ expect(result.error).toBeUndefined();
323
+ expect(result.skill).toBeDefined();
324
+ expect(result.skill!.source).toBe("plugin");
325
+ expect(result.skill!.body).toBe("Full plugin skill body");
326
+ expect(result.skill!.owner).toEqual({ kind: "plugin", id: "caveman" });
327
+ });
328
+
329
+ test("ignores plugin directories without a package.json (staging/stray dirs)", () => {
330
+ writePluginSkill(
331
+ "half-installed",
332
+ "ghost",
333
+ "Ghost",
334
+ "Should be skipped",
335
+ "body",
336
+ { withPackageJson: false },
337
+ );
338
+
339
+ const skill = loadSkillCatalog().find((s) => s.id === "ghost");
340
+ expect(skill).toBeUndefined();
341
+ });
342
+
343
+ test("ignores plugin dirs whose package.json name mismatches the directory", () => {
344
+ // Mirrors the loader's recognition gate: an un-adapted clone whose
345
+ // package.json declares `caveman-installer` in a `caveman` dir is skipped.
346
+ writePluginSkill("caveman", "caveman", "Caveman", "Terse mode", "body", {
347
+ packageName: "caveman-installer",
348
+ });
349
+
350
+ const skill = loadSkillCatalog().find((s) => s.id === "caveman");
351
+ expect(skill).toBeUndefined();
352
+ });
353
+
354
+ test("workspace skill overrides a plugin-resident skill with the same id", () => {
355
+ const WORKSPACE_DIR = join(
356
+ tmpdir(),
357
+ `vellum-workspace-test-${crypto.randomUUID()}`,
358
+ );
359
+ const workspaceSkillsDir = join(WORKSPACE_DIR, ".vellum", "skills");
360
+ mkdirSync(join(workspaceSkillsDir, "shared-id"), { recursive: true });
361
+ writeFileSync(
362
+ join(workspaceSkillsDir, "shared-id", "SKILL.md"),
363
+ `---\nname: "Workspace Wins"\ndescription: "Workspace version"\n---\n\nbody\n`,
364
+ );
365
+ writePluginSkill("caveman", "shared-id", "Plugin Loses", "Plugin version");
366
+
367
+ try {
368
+ const skill = loadSkillCatalog(workspaceSkillsDir).find(
369
+ (s) => s.id === "shared-id",
370
+ );
371
+ expect(skill).toBeDefined();
372
+ expect(skill!.source).toBe("workspace");
373
+ expect(skill!.name).toBe("Workspace Wins");
374
+ } finally {
375
+ rmSync(WORKSPACE_DIR, { recursive: true, force: true });
376
+ }
377
+ });
378
+ });
379
+
255
380
  describe("tool manifest detection", () => {
256
381
  beforeEach(() => {
257
382
  mkdirSync(join(TEST_DIR, "skills"), { recursive: true });
@@ -514,6 +639,83 @@ describe("includes frontmatter parsing", () => {
514
639
  });
515
640
  });
516
641
 
642
+ describe("category frontmatter parsing", () => {
643
+ beforeEach(() => {
644
+ mkdirSync(join(TEST_DIR, "skills"), { recursive: true });
645
+ });
646
+
647
+ afterEach(() => {
648
+ const skillsDir = join(TEST_DIR, "skills");
649
+ if (existsSync(skillsDir))
650
+ rmSync(skillsDir, { recursive: true, force: true });
651
+ });
652
+
653
+ function writeSkillWithCategory(skillId: string, category: string): void {
654
+ const skillDir = join(TEST_DIR, "skills", skillId);
655
+ mkdirSync(skillDir, { recursive: true });
656
+ const metadata = `{"vellum":{"category":${category}}}`;
657
+ writeFileSync(
658
+ join(skillDir, "SKILL.md"),
659
+ `---\nname: "${skillId}"\ndescription: "test"\nmetadata: ${metadata}\n---\n\nBody.\n`,
660
+ );
661
+ }
662
+
663
+ test("parses category from metadata.vellum", () => {
664
+ writeSkillWithCategory("categorized", '"productivity"');
665
+ const skill = loadUserSkillCatalog().find((s) => s.id === "categorized");
666
+ expect(skill!.category).toBe("productivity");
667
+ });
668
+
669
+ test("trims whitespace in category", () => {
670
+ writeSkillWithCategory("padded", '" email "');
671
+ const skill = loadUserSkillCatalog().find((s) => s.id === "padded");
672
+ expect(skill!.category).toBe("email");
673
+ });
674
+
675
+ test("returns undefined for empty category", () => {
676
+ writeSkillWithCategory("empty", '" "');
677
+ const skill = loadUserSkillCatalog().find((s) => s.id === "empty");
678
+ expect(skill!.category).toBeUndefined();
679
+ });
680
+
681
+ test("returns undefined for non-string category", () => {
682
+ writeSkillWithCategory("numeric", "42");
683
+ const skill = loadUserSkillCatalog().find((s) => s.id === "numeric");
684
+ expect(skill!.category).toBeUndefined();
685
+ });
686
+
687
+ test("skill without category has undefined category", () => {
688
+ writeSkill("no-category", "No Category", "Test");
689
+ const skill = loadUserSkillCatalog().find((s) => s.id === "no-category");
690
+ expect(skill!.category).toBeUndefined();
691
+ });
692
+ });
693
+
694
+ describe("bundled skill categories", () => {
695
+ test("every bundled skill declares a valid category slug", () => {
696
+ const yamlPath = join(
697
+ import.meta.dir,
698
+ "..",
699
+ "..",
700
+ "..",
701
+ "skills",
702
+ "skill-categories-catalog.yaml",
703
+ );
704
+ const parsed = parseYaml(readFileSync(yamlPath, "utf-8")) as {
705
+ categories: { slug: string }[];
706
+ };
707
+ const validSlugs = new Set(parsed.categories.map((c) => c.slug));
708
+ expect(validSlugs.size).toBeGreaterThan(0);
709
+
710
+ const bundled = loadSkillCatalog().filter((s) => s.bundled);
711
+ expect(bundled.length).toBeGreaterThan(0);
712
+ const invalid = bundled
713
+ .filter((s) => !s.category || !validSlugs.has(s.category))
714
+ .map((s) => `${s.id}: ${s.category ?? "(missing)"}`);
715
+ expect(invalid).toEqual([]);
716
+ });
717
+ });
718
+
517
719
  describe("managed browser skill", () => {
518
720
  const BROWSER_SKILL_MD = readFileSync(
519
721
  join(import.meta.dirname, "../../../skills/vellum-browser-use/SKILL.md"),
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Category precedence in toSlimSkillResponse (via listSkills):
3
+ * catalog entry > SKILL.md frontmatter > "system" fallback.
4
+ */
5
+ import { join } from "node:path";
6
+ import { describe, expect, mock, test } from "bun:test";
7
+
8
+ import type { SkillSummary } from "../config/skills.js";
9
+ import type { CatalogSkill } from "../skills/catalog-install.js";
10
+
11
+ let mockSummaries: SkillSummary[] = [];
12
+ let mockCachedCatalog: CatalogSkill[] = [];
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Module mocks — must be declared before importing the module under test
16
+ // ---------------------------------------------------------------------------
17
+
18
+ mock.module("../util/logger.js", () => ({
19
+ getLogger: () =>
20
+ new Proxy({} as Record<string, unknown>, {
21
+ get: () => () => {},
22
+ }),
23
+ }));
24
+
25
+ mock.module("../config/loader.js", () => ({
26
+ getConfig: () => ({}),
27
+ invalidateConfigCache: () => {},
28
+ loadRawConfig: () => ({}),
29
+ saveRawConfig: () => {},
30
+ }));
31
+
32
+ mock.module("../config/skills.js", () => ({
33
+ loadSkillCatalog: () => mockSummaries,
34
+ }));
35
+
36
+ mock.module("../config/skill-state.js", () => ({
37
+ resolveSkillStates: (catalog: SkillSummary[]) =>
38
+ catalog.map((summary) => ({ summary, state: "enabled" })),
39
+ skillFlagKey: () => null,
40
+ }));
41
+
42
+ mock.module("../config/assistant-feature-flags.js", () => ({
43
+ isAssistantFeatureFlagEnabled: () => true,
44
+ }));
45
+
46
+ mock.module("../skills/install-meta.js", () => ({
47
+ readInstallMeta: () => null,
48
+ }));
49
+
50
+ mock.module("../skills/catalog-cache.js", () => ({
51
+ getCatalog: async () => mockCachedCatalog,
52
+ getCachedCatalogSync: () => mockCachedCatalog,
53
+ }));
54
+
55
+ mock.module("../skills/catalog-files.js", () => ({
56
+ catalogSkillToSlim: () => ({}),
57
+ createVellumCatalogProvider: () => ({}),
58
+ hasHiddenOrSkippedSegment: () => false,
59
+ readCatalogSkillFiles: async () => null,
60
+ readCatalogSkillFileContent: async () => null,
61
+ sanitizeRelativePath: (p: string) => p,
62
+ SKIP_DIRS: new Set(["node_modules", "__pycache__", ".git"]),
63
+ }));
64
+
65
+ mock.module("../skills/skillssh-files.js", () => ({
66
+ createSkillsShProvider: () => ({}),
67
+ }));
68
+
69
+ mock.module("../skills/clawhub-files.js", () => ({
70
+ createClawhubProvider: () => ({}),
71
+ }));
72
+
73
+ mock.module("../skills/catalog-install.js", () => ({
74
+ installSkillLocally: async () => {},
75
+ }));
76
+
77
+ mock.module("../skills/catalog-search.js", () => ({
78
+ filterByQuery: () => [],
79
+ }));
80
+
81
+ mock.module("../skills/clawhub.js", () => ({
82
+ clawhubCheckUpdates: async () => [],
83
+ clawhubInspect: async () => ({}),
84
+ clawhubInstall: async () => ({ success: true }),
85
+ clawhubSearch: async () => ({ skills: [] }),
86
+ clawhubUpdate: async () => ({ success: true }),
87
+ }));
88
+
89
+ mock.module("../skills/skillssh-registry.js", () => ({
90
+ fetchSkillAudits: async () => [],
91
+ installExternalSkill: async () => {},
92
+ resolveSkillSource: () => ({ owner: "", repo: "", skillSlug: "" }),
93
+ searchSkillsRegistry: async () => [],
94
+ }));
95
+
96
+ mock.module("../skills/managed-store.js", () => ({
97
+ createManagedSkill: () => ({ created: true }),
98
+ deleteManagedSkill: () => ({ deleted: true }),
99
+ validateManagedSkillId: () => null,
100
+ }));
101
+
102
+ mock.module("../memory/graph/capability-seed.js", () => ({
103
+ deleteSkillCapabilityNode: () => {},
104
+ seedSkillGraphNodes: () => {},
105
+ seedUninstalledCatalogSkillMemories: async () => {},
106
+ }));
107
+
108
+ mock.module("../providers/provider-send-message.js", () => ({
109
+ createTimeout: () => ({
110
+ signal: AbortSignal.timeout(1000),
111
+ cleanup: () => {},
112
+ }),
113
+ extractText: () => "",
114
+ getConfiguredProvider: async () => null,
115
+ userMessage: () => ({}),
116
+ }));
117
+
118
+ mock.module("../util/platform.js", () => ({
119
+ getWorkspaceSkillsDir: () => "/tmp/test-skills-category",
120
+ }));
121
+
122
+ mock.module("../daemon/handlers/shared.js", () => ({
123
+ CONFIG_RELOAD_DEBOUNCE_MS: 100,
124
+ ensureSkillEntry: () => ({ enabled: false }),
125
+ log: {
126
+ info: () => {},
127
+ warn: () => {},
128
+ error: () => {},
129
+ debug: () => {},
130
+ },
131
+ }));
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // Import after mocks
135
+ // ---------------------------------------------------------------------------
136
+
137
+ import { listSkills } from "../daemon/handlers/skills.js";
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Helpers
141
+ // ---------------------------------------------------------------------------
142
+
143
+ function makeSummary(overrides: Partial<SkillSummary>): SkillSummary {
144
+ const directoryPath = overrides.directoryPath ?? "/tmp/nonexistent-skill-dir";
145
+ return {
146
+ id: overrides.id ?? "summary-id",
147
+ name: overrides.id ?? "summary-id",
148
+ displayName: overrides.displayName ?? "Summary",
149
+ description: "",
150
+ directoryPath,
151
+ skillFilePath: join(directoryPath, "SKILL.md"),
152
+ source: overrides.source ?? "bundled",
153
+ bundled: overrides.source !== "managed",
154
+ category: overrides.category,
155
+ };
156
+ }
157
+
158
+ function makeCatalogSkill(id: string, category: string): CatalogSkill {
159
+ return {
160
+ id,
161
+ name: id,
162
+ description: "",
163
+ metadata: { vellum: { category } },
164
+ };
165
+ }
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // Tests
169
+ // ---------------------------------------------------------------------------
170
+
171
+ describe("listSkills — category precedence", () => {
172
+ test("catalog category wins over frontmatter category", () => {
173
+ mockSummaries = [makeSummary({ id: "gmail", category: "messaging" })];
174
+ mockCachedCatalog = [makeCatalogSkill("gmail", "email")];
175
+
176
+ const [skill] = listSkills();
177
+ expect(skill.category).toBe("email");
178
+ });
179
+
180
+ test("frontmatter category used when skill is not in the catalog", () => {
181
+ mockSummaries = [makeSummary({ id: "schedule", category: "productivity" })];
182
+ mockCachedCatalog = [];
183
+
184
+ const [skill] = listSkills();
185
+ expect(skill.category).toBe("productivity");
186
+ });
187
+
188
+ test("falls back to system when neither catalog nor frontmatter declare a category", () => {
189
+ mockSummaries = [makeSummary({ id: "mystery" })];
190
+ mockCachedCatalog = [];
191
+
192
+ const [skill] = listSkills();
193
+ expect(skill.category).toBe("system");
194
+ });
195
+ });
@@ -1,9 +1,8 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
- import {
4
- stripAllMemoryInjections,
5
- stripExistingMemoryInjections,
6
- } from "../memory/graph/conversation-graph-memory.js";
3
+ import { stripSpotlightInjections } from "../context/strip-injections.js";
4
+ import { stripExistingMemoryInjections } from "../memory/graph/conversation-graph-memory.js";
5
+ import { wrapMemorySpotlightBlock } from "../memory/memory-marker.js";
7
6
  import type { ContentBlock, Message } from "../providers/types.js";
8
7
 
9
8
  // ---------------------------------------------------------------------------
@@ -204,8 +203,8 @@ describe("stripExistingMemoryInjections", () => {
204
203
  expect(result[0].content).toEqual([textBlock("hello")]);
205
204
  });
206
205
 
207
- // Regression guard: the v2 helper must remain last-message-only even after
208
- // the shared per-message strip is extracted for stripAllMemoryInjections.
206
+ // Regression guard: the helper must remain last-message-only — memory-v3's
207
+ // frozen-card carry depends on historical `<memory>` blocks staying intact.
209
208
  test("strips ONLY the last user message, leaving earlier ones injected", () => {
210
209
  const messages = [
211
210
  userMsg(memoryTextBlock, textBlock("first")),
@@ -221,55 +220,50 @@ describe("stripExistingMemoryInjections", () => {
221
220
  });
222
221
 
223
222
  // ---------------------------------------------------------------------------
224
- // stripAllMemoryInjections — removes memory-injected blocks from EVERY user
225
- // message (memory-v3 live mode). Reuses the same per-message strip logic.
223
+ // stripSpotlightInjections — the memory-v3 ephemeral spotlight is the ONLY
224
+ // block strip-and-replaced across every user message each turn. Frozen
225
+ // `<memory>` card blocks must never be touched by it (the cache contract);
226
+ // the old whole-layer stripAllMemoryInjections replace is gone.
226
227
  // ---------------------------------------------------------------------------
227
228
 
228
- describe("stripAllMemoryInjections", () => {
229
- test("strips memory blocks from multiple historical user messages", () => {
229
+ const spotlightBlock = (inner: string): ContentBlock => ({
230
+ type: "text",
231
+ text: wrapMemorySpotlightBlock(inner),
232
+ });
233
+
234
+ describe("stripSpotlightInjections", () => {
235
+ test("strips spotlight blocks from every user message, leaving <memory> blocks intact", () => {
230
236
  const messages = [
231
- userMsg(memoryTextBlock, textBlock("first")),
237
+ userMsg(memoryTextBlock, textBlock("first"), spotlightBlock("s1")),
232
238
  assistantMsg("ok"),
233
- userMsg(
234
- memoryImageMarker,
235
- memoryImage,
236
- memoryImageClose,
237
- memoryTextBlock,
238
- textBlock("second"),
239
- ),
240
- assistantMsg("sure"),
241
- userMsg(memoryTextBlock, textBlock("third")),
239
+ userMsg(memoryTextBlock, textBlock("second"), spotlightBlock("s2")),
242
240
  ];
243
- const result = stripAllMemoryInjections(messages);
244
- expect(result[0].content).toEqual([textBlock("first")]);
241
+ const result = stripSpotlightInjections(messages);
242
+ expect(result[0].content).toEqual([memoryTextBlock, textBlock("first")]);
245
243
  expect(result[1]).toBe(messages[1]); // assistant message untouched
246
- expect(result[2].content).toEqual([textBlock("second")]);
247
- expect(result[3]).toBe(messages[3]);
248
- expect(result[4].content).toEqual([textBlock("third")]);
244
+ expect(result[2].content).toEqual([memoryTextBlock, textBlock("second")]);
249
245
  });
250
246
 
251
- test("preserves non-memory content and user-attached images", () => {
252
- const messages = [
253
- userMsg(memoryTextBlock, imageBlock, textBlock("what is this?")),
254
- userMsg(textBlock("plain message")),
255
- ];
256
- const result = stripAllMemoryInjections(messages);
257
- expect(result[0].content).toEqual([imageBlock, textBlock("what is this?")]);
258
- expect(result[1].content).toEqual([textBlock("plain message")]);
247
+ test("requires the full wrapper: user text merely starting with the tag survives", () => {
248
+ const lookalike = textBlock("<memory_spotlight>\nnot really a block");
249
+ const messages = [userMsg(lookalike, textBlock("hello"))];
250
+ const result = stripSpotlightInjections(messages);
251
+ expect(result[0].content).toEqual([lookalike, textBlock("hello")]);
259
252
  });
260
253
 
261
- test("no-op (same reference) when no user message carries a memory block", () => {
254
+ test("content no-op when no spotlight blocks exist", () => {
262
255
  const messages = [
263
- userMsg(textBlock("hello")),
256
+ userMsg(memoryTextBlock, textBlock("hello")),
264
257
  assistantMsg("hi"),
265
258
  userMsg(imageBlock),
266
259
  ];
267
- const result = stripAllMemoryInjections(messages);
268
- expect(result).toBe(messages);
260
+ const result = stripSpotlightInjections(messages);
261
+ expect(result).toEqual(messages);
262
+ // Untouched messages keep their identity (cheap no-op detection upstream).
263
+ expect(result[0]).toBe(messages[0]);
269
264
  });
270
265
 
271
266
  test("no-op for empty messages array", () => {
272
- const result = stripAllMemoryInjections([]);
273
- expect(result).toEqual([]);
267
+ expect(stripSpotlightInjections([])).toEqual([]);
274
268
  });
275
269
  });
@@ -0,0 +1,79 @@
1
+ // Per the test-machinery isolation rule in assistant/AGENTS.md, this shared
2
+ // helper has no runtime imports from src/ — the importing test files (which
3
+ // may import production modules like any normal consumer) pass the db handle
4
+ // and table objects in. The imports below are type-only and fully erased at
5
+ // compile time, so loading this module has no side effects.
6
+ import type { getDb } from "../../memory/db-connection.js";
7
+ import type { conversations, toolInvocations } from "../../memory/schema.js";
8
+
9
+ /**
10
+ * Sentinel embedded in the seeded raw input/result payloads. Tests assert it
11
+ * never appears in any projection or wire payload — raw tool args/outputs
12
+ * must never leave the device.
13
+ */
14
+ export const TOOL_INVOCATION_PII_SENTINEL = "must never leave the device";
15
+
16
+ /** Production handles supplied by the importing test file. */
17
+ export interface ToolInvocationSeedDeps {
18
+ db: ReturnType<typeof getDb>;
19
+ conversations: typeof conversations;
20
+ toolInvocations: typeof toolInvocations;
21
+ }
22
+
23
+ export interface ToolInvocationSeedSpec {
24
+ id: string;
25
+ createdAt: number;
26
+ conversationId: string;
27
+ toolName?: string;
28
+ decision?: string;
29
+ durationMs?: number;
30
+ argBytes?: number | null;
31
+ resultBytes?: number | null;
32
+ provider?: string | null;
33
+ model?: string | null;
34
+ inferenceProfile?: string | null;
35
+ inferenceProfileSource?: string | null;
36
+ }
37
+
38
+ /**
39
+ * Seed a `tool_invocations` row (plus its FK conversation) for telemetry
40
+ * projection tests. Byte sizes default to non-null — post-migration writer
41
+ * paths always compute them; pass an explicit null to seed a legacy
42
+ * pre-migration-278 row.
43
+ */
44
+ export function seedToolInvocation(
45
+ deps: ToolInvocationSeedDeps,
46
+ spec: ToolInvocationSeedSpec,
47
+ ): void {
48
+ // tool_invocations has an enforced FK to conversations.
49
+ deps.db
50
+ .insert(deps.conversations)
51
+ .values({
52
+ id: spec.conversationId,
53
+ title: "test",
54
+ createdAt: 1000,
55
+ updatedAt: 1000,
56
+ })
57
+ .onConflictDoNothing()
58
+ .run();
59
+ deps.db
60
+ .insert(deps.toolInvocations)
61
+ .values({
62
+ id: spec.id,
63
+ conversationId: spec.conversationId,
64
+ toolName: spec.toolName ?? "calendar_list_events",
65
+ input: `{"secret":"raw tool args — ${TOOL_INVOCATION_PII_SENTINEL}"}`,
66
+ result: `{"secret":"raw tool output — ${TOOL_INVOCATION_PII_SENTINEL}"}`,
67
+ decision: spec.decision ?? "allow",
68
+ riskLevel: "low",
69
+ durationMs: spec.durationMs ?? 12,
70
+ createdAt: spec.createdAt,
71
+ argBytes: spec.argBytes !== undefined ? spec.argBytes : 2,
72
+ resultBytes: spec.resultBytes !== undefined ? spec.resultBytes : 9,
73
+ provider: spec.provider ?? null,
74
+ model: spec.model ?? null,
75
+ inferenceProfile: spec.inferenceProfile ?? null,
76
+ inferenceProfileSource: spec.inferenceProfileSource ?? null,
77
+ })
78
+ .run();
79
+ }
@@ -78,6 +78,8 @@ function makeCtx(
78
78
  conversationId: "conv-1",
79
79
  userMessageId: "msg-1",
80
80
  requestId: "req-1",
81
+ modelProfileKey: null,
82
+ isNonInteractive: false,
81
83
  prompt: "first message",
82
84
  originalMessages: messages,
83
85
  latestMessages: messages,
@@ -121,9 +123,7 @@ function makeStopCtx(overrides: Partial<StopContext> = {}): StopContext {
121
123
  return {
122
124
  conversationId: "conv-1",
123
125
  messages: historyWithUserTurns(3),
124
- responseContent: [{ type: "text", text: "reply 3" }],
125
- stopReason: "end_turn",
126
- decision: "stop",
126
+ exitReason: "no_tool_calls",
127
127
  logger: noopLogger,
128
128
  ...overrides,
129
129
  };
@@ -273,18 +273,20 @@ describe("title-generate stop hook", () => {
273
273
  expect(queueRegenerateConversationTitleMock).toHaveBeenCalledTimes(1);
274
274
  });
275
275
 
276
- test("defers when an earlier hook chose to continue the run", async () => {
277
- // GIVEN a third-user-turn stop that an earlier hook flipped to "continue"
276
+ test("does not regenerate on a non-success terminal exit", async () => {
277
+ // GIVEN a third-user-turn stop that ended on a provider error rather than
278
+ // a finalized no-tool reply
278
279
  const ctx = makeStopCtx({
279
280
  messages: historyWithUserTurns(3),
280
- decision: "continue",
281
+ exitReason: "error",
282
+ error: new Error("provider rejected"),
281
283
  });
282
284
 
283
285
  // WHEN the stop hook runs and any deferred work flushes
284
286
  await stop(ctx);
285
287
  await flushMacrotasks();
286
288
 
287
- // THEN it defers to the eventual terminal stop rather than re-titling now
289
+ // THEN no regeneration fires — there is no new topic to re-title from
288
290
  expect(queueRegenerateConversationTitleMock).toHaveBeenCalledTimes(0);
289
291
  });
290
292