@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
@@ -28,8 +28,15 @@ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
28
28
  import { drizzle } from "drizzle-orm/bun-sqlite";
29
29
 
30
30
  import { migrateAddMemoryV3Selections } from "../../../../memory/migrations/268-add-memory-v3-selections.js";
31
+ import { migrateAddMemoryV3EverInjected } from "../../../../memory/migrations/277-add-memory-v3-ever-injected.js";
31
32
  import * as schema from "../../../../memory/schema.js";
32
- import type { CandidateLane, SectionIndex, SelectionSource } from "../types.js";
33
+ import type { HotSetEntry, HotSetOptions } from "../hot-set.js";
34
+ import type { OrchestrateResult } from "../orchestrate.js";
35
+ import {
36
+ MEMORY_V3_COMMIT_META_KEY,
37
+ type SectionIndex,
38
+ type SelectionSource,
39
+ } from "../types.js";
33
40
 
34
41
  // `mock.module` is process-global and, in Bun, neither `mock.restore()` nor a
35
42
  // re-mock in `afterAll` reverts it for files that load LATER in the same
@@ -62,6 +69,8 @@ const realSkillStore = {
62
69
  const realCliCommandStore = {
63
70
  ...(await import("../../../../memory/v2/cli-command-store.js")),
64
71
  };
72
+ const realCoreSet = { ...(await import("../core-set.js")) };
73
+ const realHotSet = { ...(await import("../hot-set.js")) };
65
74
 
66
75
  let shadowMockActive = false;
67
76
 
@@ -69,6 +78,7 @@ let shadowMockActive = false;
69
78
 
70
79
  let liveEnabled = false;
71
80
  let shadowEnabled = false;
81
+ let memoryEnabled = true;
72
82
  let messages: Array<{ role: string; content: string }> = [];
73
83
 
74
84
  // A synthetic skill capability slug the page index carries. Its rendered
@@ -77,30 +87,36 @@ let messages: Array<{ role: string; content: string }> = [];
77
87
  const CAPABILITY_SLUG = "skills/example";
78
88
  const CAPABILITY_CONTENT = "use the kumquat skill to do the thing";
79
89
 
80
- // The orchestrate result the spy returns. `laneBySlug` records the lane that
81
- // surfaced each pooled slug: page-1 → "needle", page-2 → "dense", page-3 →
82
- // "edge"; `attributeSelections` reads it directly. `carried` is a carry-forward
83
- // slug not re-selected this turn. `sectionBySlug` carries the matched section
84
- // for the slugs that had one (page-1/page-2) — consumed by the live injector's
85
- // progressive disclosure, independent of source attribution.
86
- const orchestrateSpy = mock(async () => ({
87
- currentSelections: [
88
- { slug: "page-1", pinned: true },
89
- { slug: "page-2", pinned: false },
90
- { slug: "page-3", pinned: false },
91
- ],
92
- workingSetUnion: new Set<string>(["page-1", "page-2", "page-3"]),
93
- finalInjection: ["page-1", "page-2", "page-3", "carried"],
94
- sectionBySlug: new Map([
95
- ["page-1", { article: "page-1", title: "", text: "x", ordinal: 0 }],
96
- ["page-2", { article: "page-2", title: "", text: "y", ordinal: 0 }],
97
- ]),
98
- laneBySlug: new Map<string, CandidateLane>([
99
- ["page-1", "needle"],
100
- ["page-2", "dense"],
101
- ["page-3", "edge"],
102
- ]),
103
- }));
90
+ // The orchestrate result the spy returns. `lanes` records where each pooled
91
+ // slug lived: page-core in the core lane, page-hot in the hot lane, and the
92
+ // finder entries page-1 → "needle", page-2 → "dense", page-3 → "edge";
93
+ // `attributeSelections` reads it directly. `matchedSections` carries the
94
+ // matched section for the slugs that had one (page-1/page-2) — consumed by the
95
+ // live injector's progressive disclosure, independent of source attribution.
96
+ const orchestrateSpy = mock(
97
+ async (): Promise<OrchestrateResult> => ({
98
+ selections: [
99
+ { slug: "page-core", pinned: false },
100
+ { slug: "page-hot", pinned: false },
101
+ { slug: "page-1", pinned: true },
102
+ { slug: "page-2", pinned: false },
103
+ { slug: "page-3", pinned: false },
104
+ ],
105
+ matchedSections: new Map([
106
+ ["page-1", { article: "page-1", title: "", text: "x", ordinal: 0 }],
107
+ ["page-2", { article: "page-2", title: "", text: "y", ordinal: 0 }],
108
+ ]),
109
+ lanes: {
110
+ core: ["page-core"],
111
+ hot: ["page-hot"],
112
+ finder: [
113
+ { slug: "page-1", descriptor: "", lane: "needle" },
114
+ { slug: "page-2", descriptor: "", lane: "dense" },
115
+ { slug: "page-3", descriptor: "", lane: "edge" },
116
+ ],
117
+ },
118
+ }),
119
+ );
104
120
 
105
121
  let sectionBuilds = 0;
106
122
  let needleBuilds = 0;
@@ -108,6 +124,14 @@ let edgeBuilds = 0;
108
124
  let ensureCollectionCalls = 0;
109
125
  let ensureCollectionThrows = false;
110
126
 
127
+ // Stable-prefix lane inputs, driven per test: what the curated core file
128
+ // yields and what the frecency hot set computes. `hotSetOpts` captures the
129
+ // options `initLanes` passed so the test can assert the core exclusion and the
130
+ // config plumbing.
131
+ let coreSetSlugs: string[] = [];
132
+ let hotSetResult: HotSetEntry[] = [];
133
+ let hotSetOpts: HotSetOptions | null = null;
134
+
111
135
  // The `pageBody` resolver `initLanes` passes to `buildSectionIndex` (its second
112
136
  // arg), captured by the stub below so a test can drive it directly: a capability
113
137
  // slug must resolve to its rendered capability content, an on-disk slug to its
@@ -122,12 +146,21 @@ function makeDb() {
122
146
  testSqlite.exec("PRAGMA journal_mode=WAL");
123
147
  const db = drizzle(testSqlite, { schema });
124
148
  migrateAddMemoryV3Selections(db);
149
+ // The live injector's net-new dedup reads/writes the everInjected store.
150
+ migrateAddMemoryV3EverInjected(db);
125
151
  return db;
126
152
  }
127
153
 
154
+ // The fake index lists the LIVE pages — `initLanes` filters the core and hot
155
+ // lanes against `byArticle`, so membership here is what "the page exists"
156
+ // means to the stable-prefix lanes.
128
157
  const FAKE_SECTION_INDEX: SectionIndex = {
129
158
  sections: [],
130
- byArticle: new Map(),
159
+ byArticle: new Map([
160
+ ["page-1", []],
161
+ ["page-2", []],
162
+ [CAPABILITY_SLUG, []],
163
+ ]),
131
164
  };
132
165
 
133
166
  // ─── module mocks (installed before the plugin import) ──────────────────────
@@ -144,8 +177,10 @@ mock.module("../../../../config/assistant-feature-flags.js", () => ({
144
177
  mock.module("../../../../config/loader.js", () => ({
145
178
  getConfig: () => ({
146
179
  memory: {
180
+ enabled: memoryEnabled,
147
181
  v3: {
148
- workingSet: { maxPages: 150, evictWindow: 5 },
182
+ hotSet: { k: 40, halfLifeDays: 14 },
183
+ spotlight: { n: 6, windowTurns: 2 },
149
184
  needleK: 100,
150
185
  denseK: 100,
151
186
  edge: { hubDegree: 30, seedCount: 18, perSeed: 6, cap: 45 },
@@ -155,6 +190,26 @@ mock.module("../../../../config/loader.js", () => ({
155
190
  }),
156
191
  }));
157
192
 
193
+ // Stable-prefix lanes: the curated core loader and the frecency hot set are
194
+ // stubbed to controllable values; `initLanes` owns the existence filtering and
195
+ // the core exclusion these tests assert.
196
+ mock.module("../core-set.js", () => ({
197
+ ...realCoreSet,
198
+ loadCoreSet: (...args: Parameters<typeof realCoreSet.loadCoreSet>) =>
199
+ shadowMockActive ? coreSetSlugs : realCoreSet.loadCoreSet(...args),
200
+ }));
201
+
202
+ mock.module("../hot-set.js", () => ({
203
+ ...realHotSet,
204
+ computeHotSet: (
205
+ ...args: Parameters<typeof realHotSet.computeHotSet>
206
+ ): HotSetEntry[] => {
207
+ if (!shadowMockActive) return realHotSet.computeHotSet(...args);
208
+ hotSetOpts = args[1];
209
+ return hotSetResult;
210
+ },
211
+ }));
212
+
158
213
  // Spread the real module so every export the live path transitively imports
159
214
  // stays present; only `getMessages` is overridden.
160
215
  mock.module("../../../../memory/conversation-crud.js", () => ({
@@ -305,9 +360,14 @@ mock.module("../orchestrate.js", () => ({
305
360
  }));
306
361
 
307
362
  // Import AFTER mocks so the plugin binds to them.
308
- const { runShadowObservation, resetShadowLanesForTests, invalidateLanes } =
309
- await import("../shadow-plugin.js");
310
- const { memoryV3Injector } = await import("../injector.js");
363
+ const {
364
+ runShadowObservation,
365
+ resetShadowLanesForTests,
366
+ invalidateLanes,
367
+ attributeSelections,
368
+ } = await import("../shadow-plugin.js");
369
+ const { memoryV3Injector, resetMemoryV3InjectorStateForTests } =
370
+ await import("../injector.js");
311
371
 
312
372
  afterAll(() => {
313
373
  shadowMockActive = false;
@@ -325,6 +385,7 @@ beforeEach(() => {
325
385
  shadowMockActive = true;
326
386
  liveEnabled = false;
327
387
  shadowEnabled = false;
388
+ memoryEnabled = true;
328
389
  messages = [
329
390
  {
330
391
  role: "user",
@@ -338,21 +399,45 @@ beforeEach(() => {
338
399
  ensureCollectionCalls = 0;
339
400
  ensureCollectionThrows = false;
340
401
  capturedPageBody = null;
402
+ coreSetSlugs = [];
403
+ hotSetResult = [];
404
+ hotSetOpts = null;
341
405
  testDb = makeDb();
342
406
  resetShadowLanesForTests();
407
+ // The injector memoizes one orchestration per (conversation, turn); clear it
408
+ // so tests reusing the same ids observe fresh orchestrations.
409
+ resetMemoryV3InjectorStateForTests();
343
410
  });
344
411
 
345
- /** Invoke the memory-v3 injector's `produce()` for a turn. */
346
- function produce(conversationId: string, turnIndex: number) {
347
- return memoryV3Injector.produce({
412
+ /** Invoke the memory-v3 injector's `produce()` for a turn and, when a block
413
+ * is produced, invoke its attachment-commit callback — simulating runtime
414
+ * assembly's user-tail commit point, where the everInjected-store write now
415
+ * happens. */
416
+ async function produce(conversationId: string, turnIndex: number) {
417
+ const block = await memoryV3Injector.produce({
348
418
  requestId: "r1",
349
419
  conversationId,
350
420
  turnIndex,
351
421
  trust: {} as never,
352
422
  });
423
+ const commit = block?.meta?.[MEMORY_V3_COMMIT_META_KEY];
424
+ if (typeof commit === "function") (commit as () => void)();
425
+ return block;
353
426
  }
354
427
 
355
428
  describe("memory-v3 shadow plugin", () => {
429
+ test("global memory disabled → observation is skipped even when v3 flags are on", async () => {
430
+ shadowEnabled = true;
431
+ liveEnabled = true;
432
+ memoryEnabled = false;
433
+
434
+ await runShadowObservation("conv-1", 0);
435
+
436
+ expect(orchestrateSpy).not.toHaveBeenCalled();
437
+ expect(sectionBuilds).toBe(0);
438
+ expect(readRows()).toHaveLength(0);
439
+ });
440
+
356
441
  test("shadow flag OFF → orchestrate not called, no DB writes", async () => {
357
442
  shadowEnabled = false;
358
443
  await runShadowObservation("conv-1", 0);
@@ -367,20 +452,38 @@ describe("memory-v3 shadow plugin", () => {
367
452
 
368
453
  expect(orchestrateSpy).toHaveBeenCalledTimes(1);
369
454
  const rows = readRows();
370
- // Each selection is attributed to the lane that surfaced it
371
- // (`result.laneBySlug`), not re-derived from section presence — so the
372
- // dense-only page-2 logs "dense", not "needle".
455
+ // Each selection is attributed to the lane that pooled it
456
+ // (`result.lanes`), not re-derived from section presence — so the
457
+ // dense-only page-2 logs "dense", not "needle". The result is
458
+ // current-turn selections only.
373
459
  expect(rows).toEqual([
374
- { slug: "carried", source: "carry-forward", pinned: 0 },
375
460
  // page-1 was surfaced by the needle lane → "needle", pinned.
376
461
  { slug: "page-1", source: "needle", pinned: 1 },
377
462
  // page-2 was surfaced by the dense lane → "dense".
378
463
  { slug: "page-2", source: "dense", pinned: 0 },
379
464
  // page-3 was surfaced by the edge lane → "edge".
380
465
  { slug: "page-3", source: "edge", pinned: 0 },
466
+ // page-core / page-hot sit in the stable prefix → "core" / "hot".
467
+ { slug: "page-core", source: "core", pinned: 0 },
468
+ { slug: "page-hot", source: "hot", pinned: 0 },
381
469
  ]);
382
470
  });
383
471
 
472
+ test("a selection of a core page a finder also hit attributes to core (pool position wins)", () => {
473
+ const rows = attributeSelections({
474
+ selections: [{ slug: "page-core", pinned: false }],
475
+ matchedSections: new Map(),
476
+ lanes: {
477
+ core: ["page-core"],
478
+ hot: [],
479
+ // The needle also hit the core page this turn — the row still logs
480
+ // "core" because that is where the candidate lived in the pool.
481
+ finder: [{ slug: "page-core", descriptor: "", lane: "needle" }],
482
+ },
483
+ });
484
+ expect(rows).toEqual([{ slug: "page-core", source: "core", pinned: 0 }]);
485
+ });
486
+
384
487
  test("the turn passed to orchestrate carries the latest user message", async () => {
385
488
  shadowEnabled = true;
386
489
  await runShadowObservation("conv-1", 0);
@@ -405,12 +508,38 @@ describe("memory-v3 shadow plugin", () => {
405
508
  sectionIndex?: unknown;
406
509
  needle?: unknown;
407
510
  edgeGraph?: unknown;
408
- workingSet?: unknown;
511
+ coreSlugs?: unknown;
512
+ hotSlugs?: unknown;
409
513
  };
410
514
  expect(deps.sectionIndex).toBeDefined();
411
515
  expect(deps.needle).toBeDefined();
412
516
  expect(deps.edgeGraph).toBeDefined();
413
- expect(deps.workingSet).toBeDefined();
517
+ expect(deps.coreSlugs).toEqual([]);
518
+ expect(deps.hotSlugs).toEqual([]);
519
+ });
520
+
521
+ test("initLanes filters core to existing pages and excludes core from the hot set", async () => {
522
+ shadowEnabled = true;
523
+ // The core file lists a live page and a dangling slug; the hot set returns
524
+ // a live page and a deleted one (selection rows can outlive their pages).
525
+ coreSetSlugs = ["page-1", "missing-page"];
526
+ hotSetResult = [
527
+ { slug: "page-2", score: 2 },
528
+ { slug: "gone-page", score: 1 },
529
+ ];
530
+ await runShadowObservation("conv-1", 0);
531
+
532
+ const deps = (
533
+ orchestrateSpy.mock.calls as unknown as unknown[][]
534
+ )[0]![1] as { coreSlugs: string[]; hotSlugs: string[] };
535
+ // Dangling core entries and deleted hot pages never reach the pool.
536
+ expect(deps.coreSlugs).toEqual(["page-1"]);
537
+ expect(deps.hotSlugs).toEqual(["page-2"]);
538
+ // The hot set was computed with the (filtered) core excluded and the
539
+ // configured k / half-life (14 days, in ms).
540
+ expect(hotSetOpts?.excludeSlugs).toEqual(new Set(["page-1"]));
541
+ expect(hotSetOpts?.k).toBe(40);
542
+ expect(hotSetOpts?.halfLifeMs).toBe(14 * 24 * 60 * 60 * 1000);
414
543
  });
415
544
 
416
545
  test("both flags OFF → produce returns null, no orchestrate, no writes", async () => {
@@ -431,7 +560,7 @@ describe("memory-v3 shadow plugin", () => {
431
560
  expect(readRows().length).toBeGreaterThan(0);
432
561
  });
433
562
 
434
- test("live on → produce returns the rendered <memory> block and logs", async () => {
563
+ test("live on → produce returns the net-new CARD block and logs", async () => {
435
564
  liveEnabled = true;
436
565
  shadowEnabled = false;
437
566
  const block = await produce("conv-1", 0);
@@ -439,25 +568,37 @@ describe("memory-v3 shadow plugin", () => {
439
568
  expect(block!.placement).toBe("after-memory-prefix");
440
569
  expect(block!.text.startsWith("<memory>\n")).toBe(true);
441
570
  expect(block!.text.endsWith("\n</memory>")).toBe(true);
442
- // Progressive disclosure: a slug with a matched section renders that
443
- // section's text (page-1 → "x"), NOT the full page body.
444
- expect(block!.text).toContain("# memory/concepts/page-1.md\nx");
445
- expect(block!.text).not.toContain("body for page-1");
446
- // A slug with no matched section (carry-forward) falls back to the full body.
447
- expect(block!.text).toContain("body for carried");
571
+ // Turn 1: every selection is net-new and renders as a compact card —
572
+ // the page header plus the page's head section (the fixture body has no
573
+ // `## ` headings, so the whole body is the head and no TOC line renders).
574
+ for (const slug of ["page-core", "page-hot", "page-1", "page-2"]) {
575
+ expect(block!.text).toContain(
576
+ `# memory/concepts/${slug}.md\nbody for ${slug}`,
577
+ );
578
+ }
448
579
  // Selections are still logged in live mode.
449
580
  expect(readRows().length).toBeGreaterThan(0);
450
581
  });
451
582
 
583
+ test("live on → a later turn re-selecting the same pages renders an EMPTY block (net-new dedup)", async () => {
584
+ liveEnabled = true;
585
+ shadowEnabled = false;
586
+ const first = await produce("conv-1", 0);
587
+ expect(first!.text.length).toBeGreaterThan(0);
588
+ // Same orchestrate fixture on the next turn → zero net-new cards. The
589
+ // block is still produced (its presence keys v2 suppression downstream).
590
+ const repeat = await produce("conv-1", 1);
591
+ expect(repeat).not.toBeNull();
592
+ expect(repeat!.text).toBe("");
593
+ });
594
+
452
595
  test("live on but empty selection → produce returns null", async () => {
453
596
  liveEnabled = true;
454
597
  shadowEnabled = false;
455
598
  orchestrateSpy.mockImplementationOnce(async () => ({
456
- currentSelections: [],
457
- workingSetUnion: new Set<string>(),
458
- finalInjection: [],
459
- sectionBySlug: new Map(),
460
- laneBySlug: new Map(),
599
+ selections: [],
600
+ matchedSections: new Map(),
601
+ lanes: { core: [], hot: [], finder: [] },
461
602
  }));
462
603
  const block = await produce("conv-1", 0);
463
604
  expect(block).toBeNull();
@@ -1,22 +1,10 @@
1
1
  import { expect, test } from "bun:test";
2
2
 
3
- import type {
4
- MemoryRoutingTurn,
5
- SelectionSource,
6
- Slug,
7
- WorkingSetEntry,
8
- } from "../types.js";
3
+ import type { MemoryRoutingTurn, SelectionSource, Slug } from "../types.js";
9
4
 
10
5
  test("v3 core types instantiate", () => {
11
6
  const slug: Slug = "page-123";
12
7
 
13
- const entry: WorkingSetEntry = {
14
- slug,
15
- selectedAtTurn: 1,
16
- pinned: false,
17
- lastSeenTurn: 2,
18
- };
19
-
20
8
  const turnContext: MemoryRoutingTurn = {
21
9
  conversationId: "conv-xyz",
22
10
  turnNumber: 3,
@@ -24,9 +12,9 @@ test("v3 core types instantiate", () => {
24
12
  recentContext: "prior turns",
25
13
  };
26
14
 
27
- const source: SelectionSource = "carry-forward";
15
+ const source: SelectionSource = "needle";
28
16
 
29
- expect(entry.slug).toBe(slug);
17
+ expect(slug).toBe("page-123");
30
18
  expect(turnContext.turnNumber).toBe(3);
31
- expect(source).toBe("carry-forward");
19
+ expect(source).toBe("needle");
32
20
  });
@@ -0,0 +1,173 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { cardBytes, renderCard } from "./card.js";
4
+
5
+ describe("renderCard", () => {
6
+ test("standard article renders head section + section TOC", () => {
7
+ const raw = [
8
+ "---",
9
+ "summary: A page about topic A.",
10
+ "---",
11
+ "# Topic A",
12
+ "",
13
+ "Lead paragraph one about topic A.",
14
+ "",
15
+ "Lead paragraph two with more detail.",
16
+ "",
17
+ "## History",
18
+ "history body",
19
+ "",
20
+ "## Design",
21
+ "design body",
22
+ "",
23
+ "## Open Questions",
24
+ "open questions body",
25
+ ].join("\n");
26
+
27
+ expect(renderCard("page-a", raw)).toBe(
28
+ [
29
+ "# memory/concepts/page-a.md",
30
+ "# Topic A",
31
+ "",
32
+ "Lead paragraph one about topic A.",
33
+ "",
34
+ "Lead paragraph two with more detail.",
35
+ "",
36
+ "[sections: §History · §Design · §Open Questions]",
37
+ ].join("\n"),
38
+ );
39
+ });
40
+
41
+ test("kind: index page renders its links: map instead of section names", () => {
42
+ const longNote = "x".repeat(100);
43
+ const raw = [
44
+ "---",
45
+ "kind: index",
46
+ "links:",
47
+ " - page-a — short note about page a",
48
+ ` - page-b — ${longNote}`,
49
+ " - page-c",
50
+ "---",
51
+ "# Index of Topics",
52
+ "",
53
+ "Lead line.",
54
+ "",
55
+ "## Pages",
56
+ "generic section body",
57
+ ].join("\n");
58
+
59
+ const card = renderCard("topics-index", raw);
60
+ expect(card).toBe(
61
+ [
62
+ "# memory/concepts/topics-index.md",
63
+ "# Index of Topics",
64
+ "",
65
+ "Lead line.",
66
+ "",
67
+ `[linked: page-a — short note about page a · page-b — ${"x".repeat(80)}… · page-c]`,
68
+ ].join("\n"),
69
+ );
70
+ // Section names never appear on an index card.
71
+ expect(card).not.toContain("§Pages");
72
+ });
73
+
74
+ test("kind: index without usable links falls back to section names", () => {
75
+ const raw = [
76
+ "---",
77
+ "kind: index",
78
+ "---",
79
+ "# Bare Index",
80
+ "",
81
+ "## Pages",
82
+ "body",
83
+ ].join("\n");
84
+
85
+ expect(renderCard("bare-index", raw)).toBe(
86
+ [
87
+ "# memory/concepts/bare-index.md",
88
+ "# Bare Index",
89
+ "",
90
+ "[sections: §Pages]",
91
+ ].join("\n"),
92
+ );
93
+ });
94
+
95
+ test("sectionless page omits the TOC line", () => {
96
+ const raw = [
97
+ "---",
98
+ "summary: s",
99
+ "---",
100
+ "# Sectionless",
101
+ "",
102
+ "Just a lead.",
103
+ ].join("\n");
104
+
105
+ expect(renderCard("page-b", raw)).toBe(
106
+ ["# memory/concepts/page-b.md", "# Sectionless", "", "Just a lead."].join(
107
+ "\n",
108
+ ),
109
+ );
110
+ });
111
+
112
+ test("frontmatter-less text renders head + TOC", () => {
113
+ const raw = [
114
+ "# No Frontmatter",
115
+ "",
116
+ "Lead.",
117
+ "",
118
+ "## Only Section",
119
+ "body",
120
+ ].join("\n");
121
+
122
+ expect(renderCard("page-c", raw)).toBe(
123
+ [
124
+ "# memory/concepts/page-c.md",
125
+ "# No Frontmatter",
126
+ "",
127
+ "Lead.",
128
+ "",
129
+ "[sections: §Only Section]",
130
+ ].join("\n"),
131
+ );
132
+ });
133
+
134
+ test("body starting at a ## heading renders header + TOC with no head block", () => {
135
+ const raw = ["## First", "body", "", "## Second", "body"].join("\n");
136
+
137
+ expect(renderCard("page-d", raw)).toBe(
138
+ ["# memory/concepts/page-d.md", "", "[sections: §First · §Second]"].join(
139
+ "\n",
140
+ ),
141
+ );
142
+ });
143
+
144
+ test("long lead injects whole (no length cap)", () => {
145
+ const longLead = `Long lead. ${"y".repeat(10_000)}`;
146
+ const raw = ["# Long", "", longLead, "", "## Section", "body"].join("\n");
147
+
148
+ expect(renderCard("page-e", raw)).toContain(longLead);
149
+ });
150
+
151
+ test("deterministic for the same input", () => {
152
+ const raw = ["# T", "", "Lead.", "", "## S", "b"].join("\n");
153
+ expect(renderCard("page-f", raw)).toBe(renderCard("page-f", raw));
154
+ });
155
+ });
156
+
157
+ describe("cardBytes", () => {
158
+ test("counts UTF-8 bytes, not characters", () => {
159
+ expect(cardBytes("abc")).toBe(3);
160
+ expect(cardBytes("§")).toBe(2); // U+00A7 is 2 bytes in UTF-8
161
+ });
162
+
163
+ test("accounts for a rendered card's full byte footprint", () => {
164
+ const card = renderCard(
165
+ "page-a",
166
+ ["# T", "", "Lead.", "", "## S", "b"].join("\n"),
167
+ );
168
+ // The card contains exactly one multibyte char (the TOC's "§"), so its
169
+ // byte footprint is one over its character length.
170
+ expect(card.match(/§/g)).toHaveLength(1);
171
+ expect(cardBytes(card)).toBe(card.length + 1);
172
+ });
173
+ });