@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
@@ -2,6 +2,11 @@ import type {
2
2
  ConversationErrorCode,
3
3
  ConversationErrorEvent,
4
4
  } from "../api/events/conversation-error.js";
5
+ import {
6
+ ORDERING_ERROR_PATTERNS,
7
+ WEB_SEARCH_ORDERING_PATTERNS,
8
+ } from "../plugins/defaults/history-repair/terminal.js";
9
+ import { isImageDimensionsTooLargeError } from "../plugins/defaults/image-recovery/detect.js";
5
10
  import { ConnectionResolutionError } from "../providers/connection-resolution.js";
6
11
  import { getProviderRoutingSource } from "../providers/registry.js";
7
12
  import { isAbortReason } from "../util/abort-reasons.js";
@@ -118,21 +123,6 @@ const PROVIDER_API_PATTERNS = [
118
123
  /gateway timeout/i,
119
124
  ];
120
125
 
121
- // Provider ordering error patterns (tool_use/tool_result mismatches)
122
- const ORDERING_ERROR_PATTERNS = [
123
- /tool_result.*not immediately after.*tool_use/i,
124
- /tool_use.*must have.*tool_result/i,
125
- /tool_use_id.*without.*tool_result/i,
126
- /tool_result.*tool_use_id.*not found/i,
127
- /messages.*invalid.*order/i,
128
- ];
129
-
130
- // Web-search-specific ordering error patterns
131
- const WEB_SEARCH_ORDERING_PATTERNS = [
132
- /web_search.*tool_use.*without/i,
133
- /web_search.*tool_result/i,
134
- ];
135
-
136
126
  // Stale/invalid opaque `encrypted_content` token in replayed web-search results.
137
127
  // Anthropic's tokens have bounded validity; the daemon replaces historical
138
128
  // `web_search_tool_result` blocks with text summaries to avoid this, but this
@@ -149,18 +139,6 @@ const STREAMING_ERROR_PATTERNS = [
149
139
  /stream has ended.*this shouldn't happen/i,
150
140
  ];
151
141
 
152
- // User-initiated cancellation patterns — these should NOT produce conversation_error
153
- // Image-input validation patterns — Anthropic 400s with one of these messages
154
- // when an image block violates a hard limit. The first matches the per-side
155
- // pixel cap ("image dimensions exceed max allowed size"); the second matches the
156
- // base64 payload cap ("image exceeds 5 MB maximum: 7465044 bytes > 5242880
157
- // bytes"). Distinct classification matters because retrying with the same
158
- // oversized image is futile — the recovery path must strip or downscale it.
159
- const IMAGE_DIMENSIONS_TOO_LARGE_PATTERNS = [
160
- /image dimensions? exceeds? max allowed size/i,
161
- /image exceeds \d+\s*MB maximum/i,
162
- ];
163
-
164
142
  const VISION_NOT_SUPPORTED_PATTERNS = [
165
143
  /no endpoints found that support image input/i,
166
144
  /does not support image/i,
@@ -457,7 +435,7 @@ function classifyCore(
457
435
  // from "no key configured" (banner: "API key required").
458
436
  return invalidApiKeyClassification(attribution);
459
437
  }
460
- if (isImageDimensionsTooLarge(message)) {
438
+ if (isImageDimensionsTooLargeError(message)) {
461
439
  return {
462
440
  code: "IMAGE_TOO_LARGE",
463
441
  userMessage:
@@ -499,11 +477,6 @@ export function isContextTooLarge(message: string): boolean {
499
477
  return CONTEXT_TOO_LARGE_PATTERNS.some((p) => p.test(message));
500
478
  }
501
479
 
502
- /** Check whether an error message indicates an image-input dimension failure. */
503
- function isImageDimensionsTooLarge(message: string): boolean {
504
- return IMAGE_DIMENSIONS_TOO_LARGE_PATTERNS.some((p) => p.test(message));
505
- }
506
-
507
480
  function isVisionNotSupported(message: string): boolean {
508
481
  return VISION_NOT_SUPPORTED_PATTERNS.some((p) => p.test(message));
509
482
  }
@@ -244,7 +244,7 @@ export function disposeConversation(ctx: DisposeContext): void {
244
244
  ctx.workspaceTopLevelContext = null;
245
245
  // The compaction module owns the per-conversation ContextWindowManager, so
246
246
  // teardown releases it directly. Moving this behind a compaction-plugin hook
247
- // would let the module own disposal end-to-end, but the per-turn `stop` hook
247
+ // would let the module own disposal end-to-end, but the per-turn `stop` hook
248
248
  // would first require relocating the manager's only
249
249
  // cross-turn state — `nonPersistedPrefixCount` — off the manager so a
250
250
  // per-turn dispose/rebuild stays correct.
@@ -10,9 +10,14 @@ import { join } from "node:path";
10
10
 
11
11
  import { type ChannelId, parseInterfaceId } from "../channels/types.js";
12
12
  import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
13
+ import { resolveCallSiteConfig } from "../config/llm-resolver.js";
13
14
  import { getConfig } from "../config/loader.js";
14
- import type { LLMCallSite } from "../config/schemas/llm.js";
15
- import { stripUserTextBlocksByPrefix } from "../context/strip-injections.js";
15
+ import type { LLMCallSite, LLMConfig } from "../config/schemas/llm.js";
16
+ import {
17
+ NOW_SCRATCHPAD_STRIP_PREFIXES,
18
+ stripSpotlightInjections,
19
+ stripUserTextBlocksByPrefix,
20
+ } from "../context/strip-injections.js";
16
21
  import { getDocumentsForConversation } from "../documents/document-store.js";
17
22
  import {
18
23
  getApp,
@@ -28,8 +33,9 @@ import { isBackgroundConversationType } from "../memory/conversation-types.js";
28
33
  import {
29
34
  countMemoryPrefixBlocks,
30
35
  extractMemoryPrefixBlocks,
31
- stripAllMemoryInjections,
36
+ getLiveGraphMemory,
32
37
  } from "../memory/graph/conversation-graph-memory.js";
38
+ import { unwrapMemoryBlock, wrapMemoryBlock } from "../memory/memory-marker.js";
33
39
  import {
34
40
  readSlackMetadata,
35
41
  readSlackMetadataFromMessageMetadata,
@@ -44,7 +50,10 @@ import {
44
50
  } from "../messaging/providers/slack/render-transcript.js";
45
51
  import { createContextSummaryMessage } from "../plugins/defaults/compaction/window-manager.js";
46
52
  import { getInjectorChain } from "../plugins/defaults/memory-retrieval/injector-chain.js";
47
- import { MEMORY_V3_BLOCK_ID } from "../plugins/defaults/memory-v3-shadow/types.js";
53
+ import {
54
+ MEMORY_V3_BLOCK_ID,
55
+ MEMORY_V3_COMMIT_META_KEY,
56
+ } from "../plugins/defaults/memory-v3-shadow/types.js";
48
57
  import type {
49
58
  InjectionBlock,
50
59
  InjectionPlacement,
@@ -210,6 +219,43 @@ export function resolveTurnInboundActorContext(
210
219
  return resolved.trustClass === "guardian" ? null : resolved;
211
220
  }
212
221
 
222
+ /**
223
+ * Render the `model_profile:` turn-context label for a turn from its resolved
224
+ * inference profile key, for the unified `<turn_context>` block.
225
+ *
226
+ * Returns `null` when there is no key to announce (the caller gates this to the
227
+ * turns where the active profile changed since the one last delivered to the
228
+ * model). Otherwise the human-readable label comes from the profile's
229
+ * configured `label` (falling back to the key) and the model id from the
230
+ * call-site resolution keyed on that profile, yielding `Label (model)` — or
231
+ * just `Label` when no model resolves. Derives purely from the passed key,
232
+ * call site, and config, so callers thread the key (plain turn data) rather
233
+ * than the rendered string and self-resolve the call site from the live
234
+ * conversation.
235
+ *
236
+ * `selectionSeed` is the conversation id, threaded so that a key naming a `mix`
237
+ * profile expands to the same arm the turn's provider calls run on (which seed
238
+ * expansion with the same id). Without it the announced model would be a fresh
239
+ * random arm that can disagree with the model actually serving the turn.
240
+ */
241
+ export function resolveTurnModelProfileLabel(
242
+ modelProfileKey: string | null,
243
+ callSite: LLMCallSite,
244
+ llm: LLMConfig,
245
+ selectionSeed?: string,
246
+ ): string | null {
247
+ if (modelProfileKey == null) {
248
+ return null;
249
+ }
250
+ const profileEntry = llm.profiles?.[modelProfileKey];
251
+ const resolved = resolveCallSiteConfig(callSite, llm, {
252
+ overrideProfile: modelProfileKey,
253
+ selectionSeed,
254
+ });
255
+ const label = profileEntry?.label ?? modelProfileKey;
256
+ return resolved.model ? `${label} (${resolved.model})` : label;
257
+ }
258
+
213
259
  /** Derive channel capabilities from source channel + interface identifiers. */
214
260
  export function resolveChannelCapabilities(
215
261
  sourceChannel?: string | null,
@@ -631,12 +677,7 @@ function injectVoiceCallControlContext(
631
677
 
632
678
  /** Strip `<NOW.md>` blocks injected by the `now-md` default injector. */
633
679
  export function stripNowScratchpad(messages: Message[]): Message[] {
634
- return stripUserTextBlocksByPrefix(messages, [
635
- // Shared prefix catches both the current tag and any pre-line-limit
636
- // variant that may linger in in-flight histories during a rolling deploy.
637
- "<NOW.md Always keep this up to date",
638
- "<now_scratchpad>", // backward-compat: strip legacy blocks from pre-rename history
639
- ]);
680
+ return stripUserTextBlocksByPrefix(messages, NOW_SCRATCHPAD_STRIP_PREFIXES);
640
681
  }
641
682
 
642
683
  /**
@@ -1485,6 +1526,24 @@ export interface RuntimeInjectionBlocks {
1485
1526
  nowScratchpadBlock?: string;
1486
1527
  pkbContextBlock?: string;
1487
1528
  memoryV2StaticBlock?: string;
1529
+ /**
1530
+ * UNWRAPPED inner text of the memory-v3 frozen net-new card block the v3
1531
+ * injector attached this turn, mirroring v2's unwrapped `memoryInjectedBlock`
1532
+ * contract (rehydration re-wraps on use). Undefined when v3 attached no new
1533
+ * cards (all-repeat turn, v3 off, or v3 failure). Persisted by the
1534
+ * user-prompt-submit hook under `metadata.memoryV3InjectedBlock`
1535
+ * (`MEMORY_V3_INJECTED_BLOCK_METADATA_KEY`).
1536
+ */
1537
+ memoryV3InjectedBlock?: string;
1538
+ /**
1539
+ * True when memory-v3 superseded v2 as this turn's `<memory>` source — the
1540
+ * `memory-v3-live` flag is on AND the v3 injector produced a block (possibly
1541
+ * empty-text on an all-repeat turn), i.e. exactly when assembly stripped
1542
+ * v2's fresh tail block. The user-prompt-submit hook keys v2's
1543
+ * `memoryInjectedBlock` metadata persist off this so a stripped v2 block is
1544
+ * never rehydrated back into history on reload.
1545
+ */
1546
+ memoryV3Active?: boolean;
1488
1547
  /**
1489
1548
  * Composed output of every plugin-registered {@link Injector}, concatenated
1490
1549
  * in ascending `order`. Empty string when every injector opted out (returned
@@ -1637,6 +1696,63 @@ function applyInjectionBlock(
1637
1696
  }
1638
1697
  }
1639
1698
 
1699
+ /**
1700
+ * Strip v2's freshly-prepended DYNAMIC memory prefix from the tail user
1701
+ * message, preserving every other leading memory-prefix block. Exactly three
1702
+ * shapes are v2's to remove:
1703
+ *
1704
+ * - `v2WrappedBlock` — the wrapped (`wrapMemoryBlock`) form of the text the
1705
+ * graph-memory wiring prepended this turn, matched by full-block IDENTITY.
1706
+ * A prefix match cannot work here: v3's card-block instruction header is
1707
+ * deliberately byte-identical to v2's `INJECTION_HEADER`
1708
+ * (`memory/v2/injection.ts`), and v2's router block leads with that same
1709
+ * header whenever any summary section is present — the dominant case — so
1710
+ * the two layers' blocks are indistinguishable by any shared prefix.
1711
+ * - legacy `<memory __injected>` text blocks — only v2 ever produced them.
1712
+ * - memory-image groups (`<memory_image …>` opener text, the image block,
1713
+ * the `</memory_image>` closer) — only v2 injects images; v3 card blocks
1714
+ * are text-only.
1715
+ *
1716
+ * Everything else in the leading memory prefix survives:
1717
+ *
1718
+ * - memory-v3's frozen card blocks: a same-turn re-entry assembly (overflow
1719
+ * convergence) sees the tail it assembled on first entry, whose leading
1720
+ * block may be this turn's just-frozen v3 cards (while this re-entry's
1721
+ * injector run produces an EMPTY block — the cards are already claimed by
1722
+ * the everInjected store). Their bytes never equal the v2 identity, so
1723
+ * they are kept without needing to be recognized as v3.
1724
+ * - the `<info>` static block: it counts toward the memory prefix for
1725
+ * splice positioning but is never prepended by `prepareMemory`, so the v2
1726
+ * suppression strip has no business removing it.
1727
+ *
1728
+ * `v2WrappedBlock === null` (nothing injected this turn, or no live graph
1729
+ * handle) still strips the image groups and legacy blocks — both are
1730
+ * unambiguously v2's — but leaves every `<memory>` text block in place.
1731
+ */
1732
+ function stripTailV2DynamicMemoryPrefix(
1733
+ messages: Message[],
1734
+ v2WrappedBlock: string | null,
1735
+ ): Message[] {
1736
+ const last = messages[messages.length - 1];
1737
+ if (!last || last.role !== "user") return messages;
1738
+ const prefixCount = countMemoryPrefixBlocksOnContent(last.content);
1739
+ if (prefixCount === 0) return messages;
1740
+ const content = last.content.filter((block, i) => {
1741
+ if (i >= prefixCount) return true;
1742
+ // v2's memory-image groups: opener text, injected image, closer text.
1743
+ if (block.type === "image") return false;
1744
+ if (block.type !== "text") return true;
1745
+ if (block.text.startsWith("<memory_image")) return false;
1746
+ if (block.text === "</memory_image>") return false;
1747
+ // v2's legacy dynamic wrapper.
1748
+ if (block.text.startsWith("<memory __injected>\n")) return false;
1749
+ // v2's current dynamic block — identity match only (see doc comment).
1750
+ return v2WrappedBlock === null || block.text !== v2WrappedBlock;
1751
+ });
1752
+ if (content.length === last.content.length) return messages;
1753
+ return [...messages.slice(0, -1), { ...last, content }];
1754
+ }
1755
+
1640
1756
  /**
1641
1757
  * Per-turn options accepted by {@link applyRuntimeInjections}.
1642
1758
  *
@@ -1990,6 +2106,7 @@ export async function applyRuntimeInjections(
1990
2106
  let pkbContextCaptured: string | undefined;
1991
2107
  let pkbSystemReminderCaptured: string | undefined;
1992
2108
  let memoryV2StaticCaptured: string | undefined;
2109
+ let memoryV3Captured: string | undefined;
1993
2110
  const initialTail = runMessages[runMessages.length - 1];
1994
2111
  const initialTailIsUser = !!initialTail && initialTail.role === "user";
1995
2112
  if (initialTailIsUser) {
@@ -2013,6 +2130,26 @@ export async function applyRuntimeInjections(
2013
2130
  case "memory-v2-static":
2014
2131
  memoryV2StaticCaptured = block.text;
2015
2132
  break;
2133
+ case MEMORY_V3_BLOCK_ID: {
2134
+ // The v3 frozen card block is persisted UNWRAPPED (the v2
2135
+ // `memoryInjectedBlock` contract — rehydration re-wraps on use).
2136
+ // An empty-text block (all-repeat turn) attaches no content, so
2137
+ // nothing is captured for persistence either.
2138
+ if (block.text.length > 0) {
2139
+ memoryV3Captured = unwrapMemoryBlock(block.text);
2140
+ }
2141
+ // Attachment is guaranteed from here (user tail — the gate this
2142
+ // capture loop runs under), so commit the injector's deferred
2143
+ // everInjected-store write. On a non-user tail the block silently
2144
+ // no-ops in `applyInjectionBlock`, and skipping the commit keeps
2145
+ // the store from claiming cards that never attached (which would
2146
+ // suppress them until compaction).
2147
+ const commit = block.meta?.[MEMORY_V3_COMMIT_META_KEY];
2148
+ if (typeof commit === "function") {
2149
+ (commit as () => void)();
2150
+ }
2151
+ break;
2152
+ }
2016
2153
  }
2017
2154
  }
2018
2155
  }
@@ -2029,15 +2166,32 @@ export async function applyRuntimeInjections(
2029
2166
  ? injectorChainPieces.join("\n\n")
2030
2167
  : undefined;
2031
2168
 
2032
- // ── Step 0: memory-v3-live v2 suppression ──
2033
- // When the `memory-v3-live` flag is on AND the v3 injector actually produced
2034
- // a block this turn, v3 is the sole `<memory>` source. v2's `prepareMemory`
2035
- // already prepended its own `<memory>` block to the tail user message (and
2036
- // historical turns may carry v2 blocks from earlier turns). Strip every user
2037
- // message's memory prefix here so:
2038
- // 1. The v3 `after-memory-prefix` block (Step 2) lands at the top of the
2039
- // tail with no v2 prefix ahead of it — exactly one `<memory>` block.
2040
- // 2. History is byte-stable across turns for prompt caching.
2169
+ // ── Step 0: memory-v3 ephemeral-spotlight strip + v2 tail suppression ──
2170
+ //
2171
+ // Spotlight strip (unconditional): the `<memory_spotlight>` block is
2172
+ // ephemeral by contract — re-rendered at the current tail each turn — so any
2173
+ // spotlight riding history from a previous turn is stale and is removed
2174
+ // here. This is a SCOPED strip of only that block id: the frozen `<memory>`
2175
+ // card blocks on historical messages are untouched (the cache contract).
2176
+ // With the v3 flag off no spotlight blocks exist and this is a content
2177
+ // no-op, keeping the v2 path bit-for-bit identical.
2178
+ let runMessagesForAssembly = stripSpotlightInjections(runMessages);
2179
+
2180
+ // v2 suppression: when the `memory-v3-live` flag is on AND the v3 injector
2181
+ // produced a block this turn (possibly empty-text on an all-repeat turn), v3
2182
+ // owns the `<memory>` layer. v2's `prepareMemory` already prepended its own
2183
+ // fresh `<memory>` block to the tail user message — strip the TAIL's v2
2184
+ // dynamic prefix only, so the v3 `after-memory-prefix` block (Step 2) lands
2185
+ // at the top of the tail with no v2 prefix ahead of it. Historical user
2186
+ // messages keep their memory blocks byte-identical: frozen v3 card blocks
2187
+ // from prior turns AND pre-cutover v2 blocks both ride the cached prefix
2188
+ // (the old whole-layer `stripAllMemoryInjections` replace is gone). The
2189
+ // strip discriminates v2's dynamic block by IDENTITY ({@link
2190
+ // stripTailV2DynamicMemoryPrefix}): the live graph handle holds the exact
2191
+ // text the wiring layer prepended this turn, so a re-entry tail's
2192
+ // just-frozen v3 card block (and the `<info>` static block) survive even
2193
+ // though v2 and v3 blocks share identical wrapper + header bytes — the v2
2194
+ // prefix this strip exists to remove was already stripped on first entry.
2041
2195
  // Keyed off the v3 block being present (not the flag alone) so a v3 failure
2042
2196
  // (`produce()` → null) leaves v2's block intact — fallback rather than a
2043
2197
  // memory-less turn. Idempotent: re-injection sites that already stripped
@@ -2047,9 +2201,14 @@ export async function applyRuntimeInjections(
2047
2201
  getConfig(),
2048
2202
  );
2049
2203
  const v3ProducedBlock = afterMemory.some((b) => b.id === MEMORY_V3_BLOCK_ID);
2050
- let runMessagesForAssembly = runMessages;
2051
- if (suppressV2MemoryForV3 && v3ProducedBlock) {
2052
- runMessagesForAssembly = stripAllMemoryInjections(runMessages);
2204
+ const memoryV3Active = suppressV2MemoryForV3 && v3ProducedBlock;
2205
+ if (memoryV3Active) {
2206
+ const v2DynamicText =
2207
+ getLiveGraphMemory(conversationId)?.lastInjectedBlockText ?? null;
2208
+ runMessagesForAssembly = stripTailV2DynamicMemoryPrefix(
2209
+ runMessagesForAssembly,
2210
+ v2DynamicText === null ? null : wrapMemoryBlock(v2DynamicText),
2211
+ );
2053
2212
  }
2054
2213
 
2055
2214
  let result = runMessagesForAssembly;
@@ -2214,6 +2373,8 @@ export async function applyRuntimeInjections(
2214
2373
  nowScratchpadBlock: nowScratchpadCaptured,
2215
2374
  pkbContextBlock: pkbContextCaptured,
2216
2375
  memoryV2StaticBlock: memoryV2StaticCaptured,
2376
+ memoryV3InjectedBlock: memoryV3Captured,
2377
+ memoryV3Active,
2217
2378
  injectorChainBlock,
2218
2379
  },
2219
2380
  };
@@ -16,9 +16,12 @@ import { getConfig } from "../config/loader.js";
16
16
  import { skillFlagKey } from "../config/skill-state.js";
17
17
  import type { SkillSummary, SkillToolManifest } from "../config/skills.js";
18
18
  import { loadSkillCatalog } from "../config/skills.js";
19
+ import { recordSkillLoadedEvent } from "../memory/skill-loaded-events-store.js";
19
20
  import type { Message, ToolDefinition } from "../providers/types.js";
20
21
  import type { ActiveSkillEntry } from "../skills/active-skill-tools.js";
21
22
  import { deriveActiveSkills } from "../skills/active-skill-tools.js";
23
+ import { getCachedCatalogSync } from "../skills/catalog-cache.js";
24
+ import { readInstallMeta } from "../skills/install-meta.js";
22
25
  import { parseToolManifestFile } from "../skills/tool-manifest.js";
23
26
  import { computeSkillVersionHash } from "../skills/version-hash.js";
24
27
  import {
@@ -28,10 +31,34 @@ import {
28
31
  unregisterSkillTools,
29
32
  } from "../tools/registry.js";
30
33
  import { createSkillToolsFromManifest } from "../tools/skills/skill-tool-factory.js";
34
+ import type { UsageAttributionSnapshot } from "../usage/attribution.js";
35
+ import { toAttributionColumns } from "../usage/attribution.js";
31
36
  import { getLogger } from "../util/logger.js";
32
37
 
33
38
  const log = getLogger("conversation-skill-tools");
34
39
 
40
+ /**
41
+ * Sentinel "version hash" stored in the previously-active map for skills that
42
+ * are active without a loadable TOOLS.json (instruction-only skills — the
43
+ * common case for catalog installs — or skills whose manifest failed to
44
+ * parse). These entries registered no tools, so teardown paths must skip
45
+ * `unregisterSkillTools` for them to avoid decrementing refcounts held by
46
+ * other conversations.
47
+ */
48
+ const NO_TOOLS_VERSION = "__no_tools__";
49
+
50
+ /**
51
+ * Whether a tracked version-hash entry corresponds to tools that were actually
52
+ * registered. Absent entries and the no-tools sentinel registered nothing, so
53
+ * every teardown path must consult this before calling `unregisterSkillTools`
54
+ * — the registry refcounts, and a spurious unregister would decrement counts
55
+ * held by other conversations. This helper is the single owner of the
56
+ * sentinel check; do not compare against `NO_TOOLS_VERSION` elsewhere.
57
+ */
58
+ function hasRegisteredTools(trackedHash: string | undefined): boolean {
59
+ return trackedHash !== undefined && trackedHash !== NO_TOOLS_VERSION;
60
+ }
61
+
35
62
  // ---------------------------------------------------------------------------
36
63
  // Public types
37
64
  // ---------------------------------------------------------------------------
@@ -74,7 +101,8 @@ export interface ProjectSkillToolsOptions {
74
101
  preactivatedSkillIds?: string[];
75
102
  /**
76
103
  * Conversation-scoped tracking map of previously active skill IDs to their
77
- * version hashes. Each conversation should own its own map to prevent
104
+ * version hashes (or the no-tools sentinel for active skills without a
105
+ * loadable manifest). Each conversation should own its own map to prevent
78
106
  * cross-conversation state bleed when the daemon serves multiple concurrent
79
107
  * conversations. When a skill's hash changes between turns, its tools are
80
108
  * unregistered and re-registered with the updated definitions.
@@ -86,6 +114,11 @@ export interface ProjectSkillToolsOptions {
86
114
  * reads across agent turns.
87
115
  */
88
116
  cache?: SkillProjectionCache;
117
+ /** Telemetry context for skill_loaded events; absent disables recording. */
118
+ telemetry?: {
119
+ conversationId: string;
120
+ attribution: UsageAttributionSnapshot | null;
121
+ };
89
122
  }
90
123
 
91
124
  // ---------------------------------------------------------------------------
@@ -116,6 +149,50 @@ function loadManifestForSkill(skill: SkillSummary): SkillToolManifest | null {
116
149
  }
117
150
  }
118
151
 
152
+ /**
153
+ * Whether a skill is Vellum-produced for `skill_loaded` telemetry purposes:
154
+ * bundled skills always are; managed skills only when their install metadata
155
+ * records a `vellum` origin (legacy version.json installs are inferred as
156
+ * vellum by `readInstallMeta`). All other sources (workspace, extra, plugin)
157
+ * never emit.
158
+ */
159
+ function isVellumProducedSkill(skill: SkillSummary): boolean {
160
+ if (skill.source === "bundled") return true;
161
+ if (skill.source === "managed") {
162
+ return readInstallMeta(skill.directoryPath)?.origin === "vellum";
163
+ }
164
+ return false;
165
+ }
166
+
167
+ /**
168
+ * Record a `skill_loaded` telemetry event for a newly-activated Vellum-produced
169
+ * skill. Metadata only — never skill output or conversation content.
170
+ * `skill_updated_at` comes from the cached merged catalog (sync accessor —
171
+ * this runs on the hot tool-projection path). Failures are swallowed at
172
+ * debug level: recording must never break tool projection.
173
+ */
174
+ function recordSkillLoadedTelemetry(
175
+ skill: SkillSummary,
176
+ telemetry: ProjectSkillToolsOptions["telemetry"],
177
+ ): void {
178
+ if (!telemetry) return;
179
+ try {
180
+ if (!isVellumProducedSkill(skill)) return;
181
+ const catalogEntry = getCachedCatalogSync().find((s) => s.id === skill.id);
182
+ recordSkillLoadedEvent({
183
+ conversationId: telemetry.conversationId,
184
+ skillName: skill.id,
185
+ skillUpdatedAt: catalogEntry?.updatedAt,
186
+ ...toAttributionColumns(telemetry.attribution),
187
+ });
188
+ } catch (err) {
189
+ log.debug(
190
+ { err, skillId: skill.id },
191
+ "Failed to record skill_loaded telemetry event (non-fatal)",
192
+ );
193
+ }
194
+ }
195
+
119
196
  // ---------------------------------------------------------------------------
120
197
  // Cache helpers
121
198
  // ---------------------------------------------------------------------------
@@ -268,8 +345,11 @@ export function projectSkillTools(
268
345
  }
269
346
  }
270
347
 
271
- // Unregister tools for skills that are no longer active
348
+ // Unregister tools for skills that are no longer active. Skills tracked
349
+ // with the no-tools sentinel never registered anything — skip them so we
350
+ // don't decrement refcounts held by other conversations.
272
351
  for (const id of removedIds) {
352
+ if (!hasRegisteredTools(prevActive.get(id))) continue;
273
353
  log.info({ skillId: id }, "Unregistering tools for deactivated skill");
274
354
  unregisterSkillTools(id);
275
355
  }
@@ -294,8 +374,36 @@ export function projectSkillTools(
294
374
  continue;
295
375
  }
296
376
 
377
+ // A skill that newly became active counts as one `skill_loaded`, whether
378
+ // or not it ships a TOOLS.json — instruction-only skills (the common case
379
+ // for catalog installs) must be counted too. The event is recorded only
380
+ // once the skill lands in `successfulEntries`, so a failed registration
381
+ // is retried (and counted) on a later turn instead of emitting one row
382
+ // per failing turn. The once-per-activation guard (`prevActive`) is
383
+ // in-memory per-conversation state: after conversation disposal or a
384
+ // daemon restart, re-projection re-emits skill_loaded for already-active
385
+ // skills. That is intentional — a re-load after restart is a load.
386
+ // Downstream consumers that need activation-level uniqueness dedup on
387
+ // (conversation_id, skill_name).
388
+ const prevHash = prevActive.get(skillId);
389
+
297
390
  const manifest = loadManifestForSkill(skill);
298
391
  if (!manifest) {
392
+ // No loadable manifest — nothing to register, but keep tracking the
393
+ // activation so it isn't re-recorded every turn. If tools were
394
+ // registered on a previous turn (manifest removed or corrupted since),
395
+ // tear them down like the transiently-failed path would.
396
+ if (hasRegisteredTools(prevHash)) {
397
+ log.info(
398
+ { skillId },
399
+ "Unregistering tools for skill whose manifest is no longer loadable",
400
+ );
401
+ unregisterSkillTools(skillId);
402
+ }
403
+ successfulEntries.set(skillId, NO_TOOLS_VERSION);
404
+ if (prevHash === undefined) {
405
+ recordSkillLoadedTelemetry(skill, options?.telemetry);
406
+ }
299
407
  continue;
300
408
  }
301
409
 
@@ -321,10 +429,23 @@ export function projectSkillTools(
321
429
 
322
430
  if (tools.length > 0) {
323
431
  let accepted = tools;
324
- const prevHash = prevActive.get(skillId);
325
- if (prevHash === undefined) {
326
- // Newly active skill — register for the first time
327
- accepted = registerSkillTools(skillId, tools);
432
+ if (!hasRegisteredTools(prevHash)) {
433
+ // Newly active skill, or a previously manifest-less activation whose
434
+ // TOOLS.json has since appeared — register for the first time. There
435
+ // is nothing to unregister in the sentinel case: no tools were ever
436
+ // registered for it.
437
+ try {
438
+ accepted = registerSkillTools(skillId, tools);
439
+ } catch (err) {
440
+ log.error({ err, skillId }, "Failed to register skill tools");
441
+ // Not added to successfulEntries — registration (and the
442
+ // skill_loaded record below) is retried next turn.
443
+ continue;
444
+ }
445
+ // A first successful registration counts as the load; a manifest-less
446
+ // activation gaining a TOOLS.json re-registers, which — like a
447
+ // version-hash change — counts as a new load.
448
+ recordSkillLoadedTelemetry(skill, options?.telemetry);
328
449
  } else if (prevHash !== currentHash) {
329
450
  // Hash changed — unregister stale tools, then re-register with new definitions
330
451
  log.info(
@@ -343,6 +464,8 @@ export function projectSkillTools(
343
464
  // Don't add to successfulEntries — will be cleaned up as transiently-failed
344
465
  continue;
345
466
  }
467
+ // A version change that re-registers counts as a new skill load.
468
+ recordSkillLoadedTelemetry(skill, options?.telemetry);
346
469
  } else {
347
470
  // Hash unchanged — filter to only tools that are actually registered
348
471
  // for this skill. Some tools may have been skipped during initial
@@ -372,7 +495,9 @@ export function projectSkillTools(
372
495
  if (
373
496
  activeIds.has(id) &&
374
497
  !successfulEntries.has(id) &&
375
- !alreadyUnregistered.has(id)
498
+ !alreadyUnregistered.has(id) &&
499
+ // Sentinel entries registered no tools — nothing to unregister.
500
+ hasRegisteredTools(prevActive.get(id))
376
501
  ) {
377
502
  log.info(
378
503
  { skillId: id },
@@ -403,7 +528,11 @@ export function resetSkillToolProjection(
403
528
  trackedIds?: Map<string, string>,
404
529
  ): void {
405
530
  if (trackedIds) {
406
- for (const id of trackedIds.keys()) {
531
+ for (const [id, hash] of trackedIds) {
532
+ // Sentinel entries (active skills without a manifest) registered no
533
+ // tools — skip them so we don't decrement refcounts held by other
534
+ // conversations.
535
+ if (!hasRegisteredTools(hash)) continue;
407
536
  unregisterSkillTools(id);
408
537
  }
409
538
  trackedIds.clear();
@@ -458,17 +458,3 @@ export async function resolveSlash(
458
458
 
459
459
  return { kind: "passthrough", content };
460
460
  }
461
-
462
- // ── Provider Ordering Error Detection ────────────────────────────────
463
-
464
- const ORDERING_ERROR_PATTERNS = [
465
- /tool_result.*not immediately after.*tool_use/i,
466
- /tool_use.*must have.*tool_result/i,
467
- /tool_use_id.*without.*tool_result/i,
468
- /tool_result.*tool_use_id.*not found/i,
469
- /messages.*invalid.*order/i,
470
- ];
471
-
472
- export function isProviderOrderingError(message: string): boolean {
473
- return ORDERING_ERROR_PATTERNS.some((pattern) => pattern.test(message));
474
- }