@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
@@ -0,0 +1,418 @@
1
+ /**
2
+ * ClickHouse compaction log writer.
3
+ *
4
+ * Consumes the agent loop's compaction start/end event pair
5
+ * (`context_compacting` / `compaction_completed`, correlated by
6
+ * `compactionId`) and writes one row per event to the ClickHouse table
7
+ * configured under `compactionLogs` in workspace config. Disabled unless
8
+ * `compactionLogs.destination === "clickhouse"` — there is no SQLite
9
+ * destination.
10
+ *
11
+ * The table is event-sourced rather than upserted: a `start` row records
12
+ * that an attempt began (so attempts that never complete — pipeline throw,
13
+ * turn abort — are still visible), and an `end` row carries the full
14
+ * `ContextWindowResult`. Consumers pair rows by `compaction_id`.
15
+ *
16
+ * Writes are strictly best-effort: every entry point swallows and logs
17
+ * failures so a ClickHouse outage can never abort a turn. URL and password
18
+ * come from the credential store (`clickhouse:url`, `clickhouse:password`),
19
+ * matching the `llmRequestLogs` mirror convention; database/table/user come
20
+ * from config. The table is created lazily with `CREATE TABLE IF NOT EXISTS`
21
+ * on first write so opting in requires no out-of-band DDL.
22
+ */
23
+ import type { AgentEvent } from "../agent/loop.js";
24
+ import { getConfig } from "../config/loader.js";
25
+ import type { CompactionLogsClickHouseConfig } from "../config/schemas/compaction-logs.js";
26
+ import { credentialKey } from "../security/credential-key.js";
27
+ import { getSecureKeyAsync } from "../security/secure-keys.js";
28
+ import { getLogger } from "../util/logger.js";
29
+
30
+ const log = getLogger("compaction-log-writer");
31
+
32
+ export type CompactionStartEvent = Extract<
33
+ AgentEvent,
34
+ { type: "context_compacting" }
35
+ >;
36
+ export type CompactionEndEvent = Extract<
37
+ AgentEvent,
38
+ { type: "compaction_completed" }
39
+ >;
40
+
41
+ /** Cap stored summary text so a pathological summary can't bloat rows. */
42
+ const SUMMARY_TEXT_MAX_CHARS = 4000;
43
+
44
+ /**
45
+ * Wire row written as one JSONEachRow line. Sentinels follow the
46
+ * llm_request_logs mirror convention: ClickHouse columns are non-Nullable,
47
+ * so `''` means "not set" for strings and `-1` means "unknown" for counts
48
+ * that only exist on one phase of the pair.
49
+ */
50
+ interface CompactionLogRow {
51
+ assistant_id: string;
52
+ conversation_id: string;
53
+ compaction_id: string;
54
+ request_id: string;
55
+ phase: "start" | "end";
56
+ trigger: string;
57
+ started_at: number;
58
+ finished_at: number;
59
+ duration_ms: number;
60
+ pre_message_count: number;
61
+ basis_message_count: number;
62
+ compacted: number;
63
+ previous_estimated_input_tokens: number;
64
+ estimated_input_tokens: number;
65
+ max_input_tokens: number;
66
+ threshold_tokens: number;
67
+ compacted_messages: number;
68
+ compacted_persisted_messages: number;
69
+ preserved_tail_messages: number;
70
+ summary_calls: number;
71
+ summary_input_tokens: number;
72
+ summary_output_tokens: number;
73
+ summary_model: string;
74
+ summary_failed: number;
75
+ reason: string;
76
+ exhausted: number;
77
+ injection_mode: string;
78
+ auto_compress_applied: number;
79
+ summary_text: string;
80
+ }
81
+
82
+ async function readCredentialOrNull(
83
+ service: string,
84
+ field: string,
85
+ ): Promise<string | null> {
86
+ const value = await getSecureKeyAsync(credentialKey(service, field));
87
+ return value ?? null;
88
+ }
89
+
90
+ /** Injectable fetch override for tests. Defaults to globalThis.fetch. */
91
+ export type CompactionLogFetch = typeof fetch;
92
+
93
+ export interface ClickHouseCompactionLogWriterDeps {
94
+ /** Override the credential read for `clickhouse:url`. */
95
+ resolveUrl?: () => Promise<string | null>;
96
+ /** Override the credential read for `clickhouse:password`. */
97
+ resolvePassword?: () => Promise<string | null>;
98
+ /** Override the credential read for `vellum:platform_assistant_id`. */
99
+ resolveAssistantId?: () => Promise<string | null>;
100
+ /** Override fetch for testing. */
101
+ fetchImpl?: CompactionLogFetch;
102
+ }
103
+
104
+ export class ClickHouseCompactionLogWriter {
105
+ private cachedUrl: string | null = null;
106
+ private cachedPassword: string | null = null;
107
+ private cachedAssistantId: string | null = null;
108
+ private ensureTablePromise: Promise<void> | null = null;
109
+
110
+ private readonly resolveUrl: () => Promise<string | null>;
111
+ private readonly resolvePassword: () => Promise<string | null>;
112
+ private readonly resolveAssistantId: () => Promise<string | null>;
113
+ private readonly fetchImpl: CompactionLogFetch;
114
+
115
+ constructor(
116
+ private readonly config: CompactionLogsClickHouseConfig,
117
+ deps: ClickHouseCompactionLogWriterDeps = {},
118
+ ) {
119
+ this.resolveUrl =
120
+ deps.resolveUrl ?? (() => readCredentialOrNull("clickhouse", "url"));
121
+ this.resolvePassword =
122
+ deps.resolvePassword ??
123
+ (() => readCredentialOrNull("clickhouse", "password"));
124
+ this.resolveAssistantId =
125
+ deps.resolveAssistantId ??
126
+ (() => readCredentialOrNull("vellum", "platform_assistant_id"));
127
+ this.fetchImpl = deps.fetchImpl ?? globalThis.fetch.bind(globalThis);
128
+ }
129
+
130
+ async writeStart(
131
+ conversationId: string,
132
+ event: CompactionStartEvent,
133
+ ): Promise<void> {
134
+ await this.insert({
135
+ ...emptyRow(await this.assistantId(), conversationId, event),
136
+ phase: "start",
137
+ pre_message_count: event.messages.length,
138
+ });
139
+ }
140
+
141
+ async writeEnd(
142
+ conversationId: string,
143
+ event: CompactionEndEvent,
144
+ ): Promise<void> {
145
+ const r = event.result;
146
+ await this.insert({
147
+ ...emptyRow(await this.assistantId(), conversationId, event),
148
+ phase: "end",
149
+ finished_at: event.finishedAt,
150
+ duration_ms: event.finishedAt - event.startedAt,
151
+ basis_message_count: event.messages.length,
152
+ compacted: r.compacted ? 1 : 0,
153
+ previous_estimated_input_tokens: r.previousEstimatedInputTokens,
154
+ estimated_input_tokens: r.estimatedInputTokens,
155
+ max_input_tokens: r.maxInputTokens,
156
+ threshold_tokens: r.thresholdTokens,
157
+ compacted_messages: r.compactedMessages,
158
+ compacted_persisted_messages: r.compactedPersistedMessages,
159
+ preserved_tail_messages: r.preservedTailMessages ?? -1,
160
+ summary_calls: r.summaryCalls,
161
+ summary_input_tokens: r.summaryInputTokens,
162
+ summary_output_tokens: r.summaryOutputTokens,
163
+ summary_model: r.summaryModel,
164
+ summary_failed:
165
+ r.summaryFailed === undefined ? -1 : r.summaryFailed ? 1 : 0,
166
+ reason: r.reason ?? "",
167
+ exhausted: r.exhausted ? 1 : 0,
168
+ injection_mode: r.injectionMode ?? "",
169
+ auto_compress_applied: r.autoCompressApplied ? 1 : 0,
170
+ summary_text: r.summaryText.slice(0, SUMMARY_TEXT_MAX_CHARS),
171
+ });
172
+ }
173
+
174
+ private async insert(row: CompactionLogRow): Promise<void> {
175
+ await this.ensureTable();
176
+ await this.exec(
177
+ `INSERT INTO ${this.tableRef()} FORMAT JSONEachRow`,
178
+ JSON.stringify(row),
179
+ );
180
+ }
181
+
182
+ /**
183
+ * Create the table on first write so opting in is self-serve. Idempotent
184
+ * (`IF NOT EXISTS`) and memoized per writer instance; a rejected attempt
185
+ * clears the memo so the next write retries.
186
+ */
187
+ private ensureTable(): Promise<void> {
188
+ if (!this.ensureTablePromise) {
189
+ this.ensureTablePromise = this.exec(
190
+ `CREATE TABLE IF NOT EXISTS ${this.tableRef()} (
191
+ assistant_id String,
192
+ conversation_id String,
193
+ compaction_id String,
194
+ request_id String,
195
+ phase String,
196
+ trigger String,
197
+ started_at DateTime64(3),
198
+ finished_at DateTime64(3),
199
+ duration_ms Int64,
200
+ pre_message_count Int64,
201
+ basis_message_count Int64,
202
+ compacted UInt8,
203
+ previous_estimated_input_tokens Int64,
204
+ estimated_input_tokens Int64,
205
+ max_input_tokens Int64,
206
+ threshold_tokens Int64,
207
+ compacted_messages Int64,
208
+ compacted_persisted_messages Int64,
209
+ preserved_tail_messages Int64,
210
+ summary_calls Int64,
211
+ summary_input_tokens Int64,
212
+ summary_output_tokens Int64,
213
+ summary_model String,
214
+ summary_failed Int8,
215
+ reason String,
216
+ exhausted UInt8,
217
+ injection_mode String,
218
+ auto_compress_applied UInt8,
219
+ summary_text String,
220
+ created_at DateTime64(3) DEFAULT now64(3)
221
+ ) ENGINE = MergeTree
222
+ ORDER BY (assistant_id, conversation_id, started_at, compaction_id)`,
223
+ ).catch((err: unknown) => {
224
+ this.ensureTablePromise = null;
225
+ throw err;
226
+ });
227
+ }
228
+ return this.ensureTablePromise;
229
+ }
230
+
231
+ private tableRef(): string {
232
+ // Database is set via the `database=` URL param in `exec`, so only the
233
+ // table identifier needs quoting. Backtick-quote to tolerate
234
+ // non-default names with special characters.
235
+ return `\`${this.config.table.replace(/`/g, "``")}\``;
236
+ }
237
+
238
+ private async exec(sql: string, body?: string): Promise<void> {
239
+ const baseUrl = await this.url();
240
+ const password = await this.password();
241
+
242
+ let target: URL;
243
+ try {
244
+ target = new URL(baseUrl);
245
+ } catch (err) {
246
+ throw new Error(
247
+ `clickhouse:url is not a valid URL: ${err instanceof Error ? err.message : String(err)}`,
248
+ );
249
+ }
250
+ target.searchParams.set("database", this.config.database);
251
+ // For INSERTs the statement goes in the `query` param and the row data
252
+ // in the body; DDL ships as the body itself.
253
+ // Integer timestamps land correctly in DateTime64(3) columns: ClickHouse
254
+ // treats integers inserted into DateTime64 as appropriately scaled Unix
255
+ // timestamps, so epoch-millis map 1:1 at scale 3.
256
+ if (body !== undefined) {
257
+ target.searchParams.set("query", sql);
258
+ }
259
+
260
+ const auth =
261
+ "Basic " +
262
+ Buffer.from(`${this.config.user}:${password}`, "utf8").toString("base64");
263
+
264
+ const res = await this.fetchImpl(target.toString(), {
265
+ method: "POST",
266
+ headers: { Authorization: auth, "Content-Type": "text/plain" },
267
+ body: body ?? sql,
268
+ });
269
+
270
+ if (!res.ok) {
271
+ const text = await res.text().catch(() => "");
272
+ throw new Error(
273
+ `ClickHouse compaction-log write failed (HTTP ${res.status}): ${text.slice(0, 500)}`,
274
+ );
275
+ }
276
+ // Drain the body so the connection can be reused.
277
+ await res.text().catch(() => "");
278
+ }
279
+
280
+ private async assistantId(): Promise<string> {
281
+ if (this.cachedAssistantId) return this.cachedAssistantId;
282
+ const val = await this.resolveAssistantId();
283
+ if (!val) {
284
+ throw new Error(
285
+ "vellum:platform_assistant_id credential is required when compactionLogs.destination=clickhouse",
286
+ );
287
+ }
288
+ this.cachedAssistantId = val;
289
+ return val;
290
+ }
291
+
292
+ private async url(): Promise<string> {
293
+ if (this.cachedUrl) return this.cachedUrl;
294
+ const val = await this.resolveUrl();
295
+ if (!val) {
296
+ throw new Error(
297
+ "clickhouse:url credential is required when compactionLogs.destination=clickhouse",
298
+ );
299
+ }
300
+ this.cachedUrl = val;
301
+ return val;
302
+ }
303
+
304
+ private async password(): Promise<string> {
305
+ if (this.cachedPassword) return this.cachedPassword;
306
+ const val = await this.resolvePassword();
307
+ if (!val) {
308
+ throw new Error(
309
+ "clickhouse:password credential is required when compactionLogs.destination=clickhouse",
310
+ );
311
+ }
312
+ this.cachedPassword = val;
313
+ return val;
314
+ }
315
+ }
316
+
317
+ /** Shared row scaffold with phase-dependent fields at their sentinels. */
318
+ function emptyRow(
319
+ assistantId: string,
320
+ conversationId: string,
321
+ event: CompactionStartEvent | CompactionEndEvent,
322
+ ): CompactionLogRow {
323
+ return {
324
+ assistant_id: assistantId,
325
+ conversation_id: conversationId,
326
+ compaction_id: event.compactionId,
327
+ request_id: event.requestId,
328
+ phase: "start",
329
+ trigger: event.trigger,
330
+ started_at: event.startedAt,
331
+ finished_at: 0,
332
+ duration_ms: -1,
333
+ pre_message_count: -1,
334
+ basis_message_count: -1,
335
+ compacted: 0,
336
+ previous_estimated_input_tokens: -1,
337
+ estimated_input_tokens: -1,
338
+ max_input_tokens: -1,
339
+ threshold_tokens: -1,
340
+ compacted_messages: -1,
341
+ compacted_persisted_messages: -1,
342
+ preserved_tail_messages: -1,
343
+ summary_calls: -1,
344
+ summary_input_tokens: -1,
345
+ summary_output_tokens: -1,
346
+ summary_model: "",
347
+ summary_failed: -1,
348
+ reason: "",
349
+ exhausted: 0,
350
+ injection_mode: "",
351
+ auto_compress_applied: 0,
352
+ summary_text: "",
353
+ };
354
+ }
355
+
356
+ /**
357
+ * Writer cache keyed by the serialized connection config so config edits
358
+ * take effect on the next event without restarting, while steady-state
359
+ * writes reuse one instance (and its cached credentials / ensured table).
360
+ */
361
+ let cachedWriter: {
362
+ key: string;
363
+ writer: ClickHouseCompactionLogWriter;
364
+ } | null = null;
365
+
366
+ function getWriter(): ClickHouseCompactionLogWriter | null {
367
+ const cfg = getConfig().compactionLogs;
368
+ if (!cfg || cfg.destination !== "clickhouse") return null;
369
+ const key = JSON.stringify(cfg.clickhouse);
370
+ if (!cachedWriter || cachedWriter.key !== key) {
371
+ cachedWriter = {
372
+ key,
373
+ writer: new ClickHouseCompactionLogWriter(cfg.clickhouse),
374
+ };
375
+ }
376
+ return cachedWriter.writer;
377
+ }
378
+
379
+ /** Test hook: drop the memoized writer so config/dep changes are picked up. */
380
+ export function resetCompactionLogWriterForTests(): void {
381
+ cachedWriter = null;
382
+ }
383
+
384
+ /**
385
+ * Record a compaction start event. Fire-and-forget: resolves immediately
386
+ * when logging is disabled and never throws.
387
+ */
388
+ export function recordCompactionStartBestEffort(
389
+ conversationId: string,
390
+ event: CompactionStartEvent,
391
+ ): void {
392
+ const writer = getWriter();
393
+ if (!writer) return;
394
+ writer.writeStart(conversationId, event).catch((err: unknown) => {
395
+ log.warn(
396
+ { err, conversationId, compactionId: event.compactionId },
397
+ "Failed to write compaction start log (non-fatal)",
398
+ );
399
+ });
400
+ }
401
+
402
+ /**
403
+ * Record a compaction end event. Fire-and-forget: resolves immediately
404
+ * when logging is disabled and never throws.
405
+ */
406
+ export function recordCompactionEndBestEffort(
407
+ conversationId: string,
408
+ event: CompactionEndEvent,
409
+ ): void {
410
+ const writer = getWriter();
411
+ if (!writer) return;
412
+ writer.writeEnd(conversationId, event).catch((err: unknown) => {
413
+ log.warn(
414
+ { err, conversationId, compactionId: event.compactionId },
415
+ "Failed to write compaction end log (non-fatal)",
416
+ );
417
+ });
418
+ }
@@ -28,6 +28,11 @@ import { findDisplayTurnEndIndex } from "../conversations/message-consolidation.
28
28
  import { conversationMetadataSyncTag } from "../daemon/message-types/sync.js";
29
29
  import type { TrustContext } from "../daemon/trust-context.js";
30
30
  import { clearAllConversationIds } from "../home/feed-writer.js";
31
+ import {
32
+ forkEverInjected,
33
+ MEMORY_V3_INJECTED_BLOCK_METADATA_KEY,
34
+ seedEverInjectedFromSlugs,
35
+ } from "../plugins/defaults/memory-v3-shadow/ever-injected-store.js";
31
36
  import { publishSyncInvalidation } from "../runtime/sync/sync-publisher.js";
32
37
  import { UserError } from "../util/errors.js";
33
38
  import { safeParseRecord } from "../util/json.js";
@@ -67,10 +72,15 @@ import {
67
72
  memorySummaries,
68
73
  messageAttachments,
69
74
  messages,
75
+ skillLoadedEvents,
70
76
  toolInvocations,
71
77
  } from "./schema.js";
72
78
  import { cancelPendingJobsForConversation } from "./task-memory-cleanup.js";
73
- import { forkActivationState } from "./v2/activation-store.js";
79
+ import {
80
+ forkActivationState,
81
+ seedForkActivationState,
82
+ } from "./v2/activation-store.js";
83
+ import { extractInjectedConceptSlugs } from "./v2/injected-block-slugs.js";
74
84
 
75
85
  const log = getLogger("conversation-store");
76
86
 
@@ -129,6 +139,9 @@ export const messageMetadataSchema = z
129
139
  /** Image source paths from desktop attachments, keyed by filename. */
130
140
  imageSourcePaths: z.record(z.string(), z.string()).optional(),
131
141
  memoryInjectedBlock: z.string().optional(),
142
+ /** Memory-v3 frozen net-new card block (unwrapped) — the v3 counterpart
143
+ * of `memoryInjectedBlock`. A row carries at most one of the two. */
144
+ [MEMORY_V3_INJECTED_BLOCK_METADATA_KEY]: z.string().optional(),
132
145
  turnContextBlock: z.string().optional(),
133
146
  pkbSystemReminderBlock: z.string().optional(),
134
147
  workspaceBlock: z.string().optional(),
@@ -166,6 +179,30 @@ function cloneForkMessageMetadata(
166
179
  return JSON.stringify({ forkSourceMessageId: sourceMessageId });
167
180
  }
168
181
 
182
+ /**
183
+ * Read a persisted memory-injection block off a message's metadata JSON, or
184
+ * `null` when absent/malformed. `key` selects the injection layer: v2's
185
+ * `memoryInjectedBlock` or memory-v3's card block
186
+ * (`MEMORY_V3_INJECTED_BLOCK_METADATA_KEY`). The block is what the request
187
+ * builder re-attaches as the message's `<memory>` content each turn.
188
+ */
189
+ function readInjectedBlock(
190
+ metadata: string | null,
191
+ key: string,
192
+ ): string | null {
193
+ if (!metadata) return null;
194
+ try {
195
+ const parsed: unknown = JSON.parse(metadata);
196
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
197
+ const block = (parsed as Record<string, unknown>)[key];
198
+ if (typeof block === "string") return block;
199
+ }
200
+ } catch {
201
+ // Malformed metadata — treat as no block.
202
+ }
203
+ return null;
204
+ }
205
+
169
206
  /**
170
207
  * Extract provenance metadata fields from a TrustContext.
171
208
  * When no guardian context is provided, defaults to 'unknown' because the
@@ -227,6 +264,7 @@ export interface ConversationRow {
227
264
  scheduleJobId: string | null;
228
265
  lastMessageAt: number | null;
229
266
  archivedAt: number | null;
267
+ surfacedAt: number | null;
230
268
  inferenceProfile: string | null;
231
269
  inferenceProfileSessionId: string | null;
232
270
  inferenceProfileExpiresAt: number | null;
@@ -261,6 +299,7 @@ export const parseConversation = createRowMapper<
261
299
  scheduleJobId: "scheduleJobId",
262
300
  lastMessageAt: "lastMessageAt",
263
301
  archivedAt: "archivedAt",
302
+ surfacedAt: "surfacedAt",
264
303
  inferenceProfile: "inferenceProfile",
265
304
  inferenceProfileSessionId: "inferenceProfileSessionId",
266
305
  inferenceProfileExpiresAt: "inferenceProfileExpiresAt",
@@ -1033,7 +1072,52 @@ export function forkConversation(params: {
1033
1072
  const isFullHistoryFork = copyBoundaryIndex === sourceMessages.length - 1;
1034
1073
  if (isFullHistoryFork) {
1035
1074
  forkActivationState(db, sourceConversation.id, fc.id);
1075
+ forkEverInjected(db, sourceConversation.id, fc.id);
1036
1076
  forkGraphMemoryState(sourceConversation.id, fc.id);
1077
+ } else {
1078
+ // Truncated fork: the wholesale copy above would over-claim, but
1079
+ // seeding nothing makes the child re-select and re-attach every page
1080
+ // whose `<memory>` attachment it already inherited (observed in
1081
+ // production: 89 duplicate page injections on one fork). Derive
1082
+ // `everInjected` from the inherited attachments themselves — scoped to
1083
+ // the child's visible window, since attachments behind an inherited
1084
+ // compaction boundary are not rendered and must stay re-injectable.
1085
+ // The v2 and v3 layers persist under separate metadata keys with the
1086
+ // same `# memory/concepts/<slug>.md` header convention, so each seeds
1087
+ // its own dedup record from its own blocks.
1088
+ const visibleStartIndex = preserveSourceCompactionState
1089
+ ? visibleWindowStartIndex
1090
+ : 0;
1091
+ const inheritedSlugs = new Set<string>();
1092
+ const inheritedV3Slugs = new Set<string>();
1093
+ for (const message of messagesToCopy.slice(visibleStartIndex)) {
1094
+ const block = readInjectedBlock(
1095
+ message.metadata,
1096
+ "memoryInjectedBlock",
1097
+ );
1098
+ if (block) {
1099
+ for (const slug of extractInjectedConceptSlugs(block)) {
1100
+ inheritedSlugs.add(slug);
1101
+ }
1102
+ }
1103
+ const v3Block = readInjectedBlock(
1104
+ message.metadata,
1105
+ MEMORY_V3_INJECTED_BLOCK_METADATA_KEY,
1106
+ );
1107
+ if (v3Block) {
1108
+ for (const slug of extractInjectedConceptSlugs(v3Block)) {
1109
+ inheritedV3Slugs.add(slug);
1110
+ }
1111
+ }
1112
+ }
1113
+ seedForkActivationState(db, fc.id, [...inheritedSlugs]);
1114
+ seedEverInjectedFromSlugs(
1115
+ db,
1116
+ sourceConversation.id,
1117
+ fc.id,
1118
+ [...inheritedV3Slugs],
1119
+ Date.now(),
1120
+ );
1037
1121
  }
1038
1122
  forkRetrospectiveState({
1039
1123
  database: db,
@@ -1104,6 +1188,9 @@ export function deleteConversation(id: string): DeletedMemoryIds {
1104
1188
  tx.delete(toolInvocations)
1105
1189
  .where(eq(toolInvocations.conversationId, id))
1106
1190
  .run();
1191
+ tx.delete(skillLoadedEvents)
1192
+ .where(eq(skillLoadedEvents.conversationId, id))
1193
+ .run();
1107
1194
  // Cascade deletes memory_segments, message_attachments.
1108
1195
  tx.delete(messages).where(eq(messages.conversationId, id)).run();
1109
1196
 
@@ -1126,6 +1213,9 @@ export function deleteConversation(id: string): DeletedMemoryIds {
1126
1213
  tx.delete(toolInvocations)
1127
1214
  .where(eq(toolInvocations.conversationId, id))
1128
1215
  .run();
1216
+ tx.delete(skillLoadedEvents)
1217
+ .where(eq(skillLoadedEvents.conversationId, id))
1218
+ .run();
1129
1219
  }
1130
1220
 
1131
1221
  tx.delete(conversations).where(eq(conversations.id, id)).run();
@@ -1183,7 +1273,7 @@ export function wipeConversation(id: string): WipeConversationResult {
1183
1273
 
1184
1274
  // Step D — Delegate to deleteConversation which handles messages (cascade
1185
1275
  // segments, attachments), llmRequestLogs, toolInvocations,
1186
- // embeddings, and the conversation row.
1276
+ // skillLoadedEvents, embeddings, and the conversation row.
1187
1277
  const deletedMemoryIds = deleteConversation(id);
1188
1278
 
1189
1279
  // Step E — Return the combined result.
@@ -1771,6 +1861,36 @@ export function unarchiveConversation(id: string): boolean {
1771
1861
  return true;
1772
1862
  }
1773
1863
 
1864
+ /**
1865
+ * Set or clear the `surfaced_at` promotion marker for a conversation.
1866
+ *
1867
+ * A non-null `surfaced_at` promotes a background/scheduled conversation
1868
+ * into the default ("standard") conversation listing so clients show it in
1869
+ * the Recents sidebar grouping. Promotion is always explicit — callers are
1870
+ * product flows that decide a background run deserves foreground visibility
1871
+ * (e.g. the user sent a follow-up message in it). Nothing sets this
1872
+ * automatically.
1873
+ *
1874
+ * Returns `null` when the conversation does not exist; otherwise the new
1875
+ * `surfacedAt` value (`number` when surfacing, `null` when clearing).
1876
+ */
1877
+ export function setConversationSurfaced(
1878
+ id: string,
1879
+ surfaced: boolean,
1880
+ ): { surfacedAt: number | null } | null {
1881
+ const conv = getConversation(id);
1882
+ if (!conv) return null;
1883
+ const now = Date.now();
1884
+ const surfacedAt = surfaced ? now : null;
1885
+ rawRun(
1886
+ "UPDATE conversations SET surfaced_at = ?, updated_at = ? WHERE id = ?",
1887
+ surfacedAt,
1888
+ now,
1889
+ id,
1890
+ );
1891
+ return { surfacedAt };
1892
+ }
1893
+
1774
1894
  /**
1775
1895
  * Set or clear the inference profile override for a conversation.
1776
1896
  * Pass `null` to clear the override and fall back to the workspace
@@ -2024,6 +2144,7 @@ export async function clearAll(): Promise<{
2024
2144
  await runOrThrow("DELETE FROM message_attachments");
2025
2145
  await runOrThrow("DELETE FROM attachments");
2026
2146
  await runOrThrow("DELETE FROM tool_invocations");
2147
+ await runOrThrow("DELETE FROM skill_loaded_events");
2027
2148
  let messagesFtsCorrupted = false;
2028
2149
  const ftsResult = await runAsyncSqlite("DELETE FROM messages_fts");
2029
2150
  if (!ftsResult.ok) {
@@ -2492,8 +2613,18 @@ export function batchSetDisplayOrders(
2492
2613
  ) {
2493
2614
  safeGroupId = "system:all";
2494
2615
  }
2616
+ // Moving a conversation into the Scheduled/Background system groups
2617
+ // is an explicit demotion out of Recents, so clear any `surfaced_at`
2618
+ // promotion in the same write — otherwise the surfaced marker would
2619
+ // keep the row in the standard listing and the move would appear to
2620
+ // do nothing.
2621
+ const clearsSurfaced =
2622
+ safeGroupId === "system:background" ||
2623
+ safeGroupId === "system:scheduled";
2495
2624
  rawRun(
2496
- "UPDATE conversations SET display_order = ?, is_pinned = ?, group_id = ? WHERE id = ?",
2625
+ `UPDATE conversations SET display_order = ?, is_pinned = ?, group_id = ?${
2626
+ clearsSurfaced ? ", surfaced_at = NULL" : ""
2627
+ } WHERE id = ?`,
2497
2628
  update.displayOrder,
2498
2629
  safeGroupId === "system:pinned" ? 1 : 0,
2499
2630
  safeGroupId,