@vellumai/assistant 0.9.0 → 0.10.0-staging.2

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 (572) hide show
  1. package/ARCHITECTURE.md +18 -34
  2. package/bun.lock +7 -8
  3. package/docs/activation-funnel-telemetry.md +28 -22
  4. package/docs/architecture/security.md +29 -28
  5. package/docs/stt-provider-onboarding.md +3 -5
  6. package/docs/workflows-testing.md +13 -44
  7. package/docs/workflows.md +3 -5
  8. package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
  9. package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
  10. package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
  11. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +97 -0
  12. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
  13. package/node_modules/@vellumai/gateway-client/src/index.ts +32 -6
  14. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
  15. package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
  16. package/openapi.yaml +976 -63
  17. package/package.json +2 -1
  18. package/scripts/sync-llm-catalog.ts +6 -15
  19. package/scripts/sync-web-search-catalog.ts +3 -11
  20. package/src/__tests__/access-request-card-view.test.ts +98 -0
  21. package/src/__tests__/access-request-seed-content-blocks.test.ts +2 -4
  22. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +72 -32
  23. package/src/__tests__/agent-loop-compaction-strip.test.ts +241 -0
  24. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +16 -13
  25. package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
  26. package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
  27. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
  28. package/src/__tests__/app-compiler.test.ts +15 -1
  29. package/src/__tests__/app-dir-path-guard.test.ts +0 -1
  30. package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
  31. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
  32. package/src/__tests__/auth-fallback-events-store.test.ts +6 -14
  33. package/src/__tests__/avatar-identity-sync.test.ts +2 -27
  34. package/src/__tests__/btw-routes.test.ts +6 -8
  35. package/src/__tests__/call-pointer-messages.test.ts +28 -0
  36. package/src/__tests__/cancel-clears-processing.test.ts +89 -0
  37. package/src/__tests__/channel-approval-routes.test.ts +0 -4
  38. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -15
  39. package/src/__tests__/checker.test.ts +0 -3
  40. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +3 -4
  41. package/src/__tests__/compactor-image-manifest-trust.test.ts +21 -1
  42. package/src/__tests__/compactor-summary-call-truncation.test.ts +223 -0
  43. package/src/__tests__/config-loader-backfill.test.ts +268 -27
  44. package/src/__tests__/config-schema.test.ts +35 -0
  45. package/src/__tests__/config-watcher.test.ts +0 -18
  46. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -2
  47. package/src/__tests__/contact-store-user-file.test.ts +0 -6
  48. package/src/__tests__/contacts-tools.test.ts +29 -0
  49. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
  50. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  51. package/src/__tests__/conversation-agent-loop.test.ts +58 -0
  52. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  53. package/src/__tests__/conversation-lifecycle.test.ts +7 -9
  54. package/src/__tests__/conversation-load-history-repair.test.ts +101 -0
  55. package/src/__tests__/conversation-routes-guardian-reply.test.ts +15 -12
  56. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -3
  57. package/src/__tests__/conversation-title-service.test.ts +62 -0
  58. package/src/__tests__/credential-broker.test.ts +449 -1
  59. package/src/__tests__/credential-execution-shell-lockdown.test.ts +18 -11
  60. package/src/__tests__/credential-execution-tools.test.ts +0 -1
  61. package/src/__tests__/credential-prompt-route.test.ts +4 -4
  62. package/src/__tests__/credential-routes.test.ts +360 -0
  63. package/src/__tests__/credential-security-invariants.test.ts +4 -13
  64. package/src/__tests__/disk-pressure-policy.test.ts +12 -0
  65. package/src/__tests__/disk-usage.test.ts +65 -0
  66. package/src/__tests__/dynamic-page-surface.test.ts +152 -1
  67. package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
  68. package/src/__tests__/gateway-flag-listener.test.ts +110 -1
  69. package/src/__tests__/gateway-only-guard.test.ts +3 -7
  70. package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
  71. package/src/__tests__/guardian-card-withdrawal.test.ts +403 -0
  72. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  73. package/src/__tests__/guardian-grant-minting.test.ts +3 -35
  74. package/src/__tests__/guardian-routing-invariants.test.ts +64 -26
  75. package/src/__tests__/guardian-routing-state.test.ts +0 -1
  76. package/src/__tests__/headless-browser-mode.test.ts +10 -0
  77. package/src/__tests__/headless-browser-navigate.test.ts +8 -3
  78. package/src/__tests__/helpers/create-guardian-binding.ts +0 -1
  79. package/src/__tests__/host-browser-proxy.test.ts +87 -0
  80. package/src/__tests__/identity-routes.test.ts +0 -189
  81. package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
  82. package/src/__tests__/injector-v3-suppression.test.ts +27 -20
  83. package/src/__tests__/internal-telemetry-routes.test.ts +6 -14
  84. package/src/__tests__/invite-redemption-service.test.ts +4 -7
  85. package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
  86. package/src/__tests__/llm-catalog-parity.test.ts +30 -23
  87. package/src/__tests__/llm-resolver.test.ts +70 -24
  88. package/src/__tests__/llm-schema.test.ts +1 -0
  89. package/src/__tests__/managed-profile-guard.test.ts +163 -4
  90. package/src/__tests__/mcp-health-check.test.ts +6 -7
  91. package/src/__tests__/media-stream-server-integration.test.ts +317 -13
  92. package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
  93. package/src/__tests__/onboarding-persona-write.test.ts +1 -1
  94. package/src/__tests__/path-policy.test.ts +34 -0
  95. package/src/__tests__/persona-resolver.test.ts +49 -14
  96. package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
  97. package/src/__tests__/plugin-api-provider.test.ts +24 -0
  98. package/src/__tests__/plugin-tool-contribution.test.ts +6 -3
  99. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +214 -0
  100. package/src/__tests__/provider-send-message-override-profile.test.ts +76 -0
  101. package/src/__tests__/reaction-persistence.test.ts +150 -29
  102. package/src/__tests__/registry.test.ts +2 -7
  103. package/src/__tests__/relay-server.test.ts +285 -0
  104. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  105. package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
  106. package/src/__tests__/schedule-routes.test.ts +0 -30
  107. package/src/__tests__/schedule-tools.test.ts +2 -18
  108. package/src/__tests__/scheduler-reuse-conversation.test.ts +8 -5
  109. package/src/__tests__/skill-execute-input.test.ts +51 -1
  110. package/src/__tests__/skill-runtime-path.test.ts +2 -3
  111. package/src/__tests__/skills.test.ts +51 -0
  112. package/src/__tests__/slack-notification-approval-card.test.ts +176 -0
  113. package/src/__tests__/slack-reaction-canonical-approval.test.ts +285 -0
  114. package/src/__tests__/subagent-tools.test.ts +266 -0
  115. package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
  116. package/src/__tests__/task-progress-nudge-hook.test.ts +1 -1
  117. package/src/__tests__/title-generate-hook.test.ts +100 -3
  118. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
  119. package/src/__tests__/token-manager.test.ts +519 -0
  120. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +1 -1
  121. package/src/__tests__/tool-audit-listener.test.ts +7 -7
  122. package/src/__tests__/tool-executor-lifecycle-events.test.ts +6 -3
  123. package/src/__tests__/tool-executor.test.ts +0 -79
  124. package/src/__tests__/trusted-contact-approval-notifier.test.ts +4 -2
  125. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +220 -3
  126. package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
  127. package/src/__tests__/trusted-contact-verification.test.ts +8 -10
  128. package/src/__tests__/twilio-routes.test.ts +81 -1
  129. package/src/__tests__/voice-invite-redemption.test.ts +2 -3
  130. package/src/__tests__/weak-open-model.test.ts +30 -0
  131. package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
  132. package/src/__tests__/workspace-greetings.test.ts +152 -0
  133. package/src/__tests__/workspace-migration-105-enable-memory-v3-live-for-new-workspaces.test.ts +149 -0
  134. package/src/__tests__/workspace-migration-108-drop-balanced-economy-profile.test.ts +285 -0
  135. package/src/__tests__/workspace-migration-add-send-diagnostics.test.ts +1 -1
  136. package/src/__tests__/workspace-migration-drop-collect-usage-data.test.ts +118 -0
  137. package/src/__tests__/workspace-migration-drop-send-diagnostics.test.ts +118 -0
  138. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +0 -4
  139. package/src/agent/loop.ts +49 -29
  140. package/src/api/README.md +6 -6
  141. package/src/api/events/tool-result.ts +6 -0
  142. package/src/api/events/workflow-completed.ts +53 -0
  143. package/src/api/events/workflow-leaf-finished.ts +38 -0
  144. package/src/api/events/workflow-leaf-started.ts +35 -0
  145. package/src/api/events/workflow-progress.ts +32 -0
  146. package/src/api/events/workflow-started.ts +31 -0
  147. package/src/api/index.ts +40 -0
  148. package/src/api/responses/conversation-message.ts +28 -4
  149. package/src/api/responses/home.ts +26 -4
  150. package/src/api/responses/workflow-journal.ts +53 -0
  151. package/src/approvals/guardian-card-withdrawal.ts +145 -0
  152. package/src/approvals/guardian-decision-primitive.ts +26 -3
  153. package/src/approvals/guardian-request-resolvers.ts +183 -80
  154. package/src/calls/__tests__/channel-admission-reader.test.ts +132 -0
  155. package/src/calls/__tests__/relay-setup-router.test.ts +350 -0
  156. package/src/calls/call-pointer-messages.ts +10 -4
  157. package/src/calls/channel-admission-reader.ts +104 -0
  158. package/src/calls/guardian-dispatch.ts +17 -45
  159. package/src/calls/media-stream-server.ts +84 -2
  160. package/src/calls/relay-access-wait.ts +1 -1
  161. package/src/calls/relay-server.ts +66 -0
  162. package/src/calls/relay-setup-router.ts +82 -1
  163. package/src/calls/twilio-routes.ts +17 -8
  164. package/src/calls/voice-session-bridge.ts +2 -2
  165. package/src/cli/commands/clients.ts +3 -0
  166. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2-compare-render.test.ts +1 -1
  167. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2.test.ts +8 -7
  168. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v3.test.ts +5 -4
  169. package/src/cli/commands/memory/index.ts +30 -0
  170. package/src/cli/commands/{memory-v2-compare-render.ts → memory/memory-v2-compare-render.ts} +1 -1
  171. package/src/cli/commands/{memory-v2.ts → memory/memory-v2.ts} +6 -15
  172. package/src/cli/commands/{memory-v3.ts → memory/memory-v3.ts} +97 -11
  173. package/src/cli/commands/oauth/status.test.ts +36 -0
  174. package/src/cli/commands/oauth/status.ts +23 -3
  175. package/src/cli/commands/plugins.ts +197 -4
  176. package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
  177. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  178. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +443 -0
  179. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +111 -0
  180. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +295 -2
  181. package/src/cli/lib/diff-plugin.ts +346 -0
  182. package/src/cli/lib/inspect-plugin.ts +12 -1
  183. package/src/cli/lib/install-from-github.ts +105 -17
  184. package/src/cli/lib/merge-plugin-tree.ts +328 -0
  185. package/src/cli/lib/plugin-fingerprint.ts +14 -0
  186. package/src/cli/lib/plugin-surfaces.ts +104 -0
  187. package/src/cli/lib/upgrade-plugin.ts +298 -10
  188. package/src/cli/program.ts +2 -6
  189. package/src/config/__tests__/sync-gated-profiles.test.ts +368 -0
  190. package/src/config/assistant-feature-flags.ts +22 -7
  191. package/src/config/bundled-skills/contacts/tools/contact-search.ts +0 -1
  192. package/src/config/bundled-skills/messaging/SKILL.md +6 -4
  193. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +9 -8
  194. package/src/config/bundled-skills/subagent/SKILL.md +4 -0
  195. package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
  196. package/src/config/bundled-skills/workflows/SKILL.md +14 -8
  197. package/src/config/bundled-tool-registry.ts +2 -7
  198. package/src/config/call-site-defaults.ts +15 -2
  199. package/src/config/feature-flag-registry.json +46 -31
  200. package/src/config/inference-profile-validation.ts +26 -0
  201. package/src/config/llm-resolver.ts +3 -0
  202. package/src/config/loader.ts +4 -0
  203. package/src/config/memory-v3-gate.ts +11 -0
  204. package/src/config/profile-order.ts +28 -0
  205. package/src/config/schema.ts +8 -6
  206. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  207. package/src/config/schemas/call-site-catalog.ts +7 -0
  208. package/src/config/schemas/channels.ts +11 -0
  209. package/src/config/schemas/elevenlabs.ts +0 -1
  210. package/src/config/schemas/llm.ts +31 -0
  211. package/src/config/schemas/memory-lifecycle.ts +3 -7
  212. package/src/config/schemas/memory-v3.ts +6 -0
  213. package/src/config/schemas/platform.ts +0 -8
  214. package/src/config/schemas/services.ts +18 -0
  215. package/src/config/seed-inference-profiles.ts +109 -44
  216. package/src/config/skills.ts +21 -0
  217. package/src/config/sync-gated-profiles.ts +220 -0
  218. package/src/contacts/contact-store.ts +89 -106
  219. package/src/contacts/contacts-write.ts +5 -22
  220. package/src/contacts/types.ts +0 -1
  221. package/src/context/compactor.ts +88 -54
  222. package/src/context/strip-injections.ts +58 -10
  223. package/src/context/token-estimator.ts +1 -1
  224. package/src/credential-execution/process-manager.ts +55 -14
  225. package/src/credential-execution/prompted-credential.ts +2 -3
  226. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -2
  227. package/src/daemon/config-watcher.ts +0 -4
  228. package/src/daemon/conversation-agent-loop-handlers.ts +2 -0
  229. package/src/daemon/conversation-agent-loop.ts +114 -22
  230. package/src/daemon/conversation-history.ts +1 -1
  231. package/src/daemon/conversation-lifecycle.ts +3 -5
  232. package/src/daemon/conversation-process.ts +13 -5
  233. package/src/daemon/conversation-runtime-assembly.ts +13 -15
  234. package/src/daemon/conversation-slash.ts +2 -23
  235. package/src/daemon/conversation-surfaces.ts +26 -0
  236. package/src/daemon/conversation-tool-setup.ts +27 -14
  237. package/src/daemon/conversation.ts +66 -14
  238. package/src/daemon/disk-pressure-policy.ts +5 -3
  239. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -1
  240. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -1
  241. package/src/daemon/handlers/config-a2a.ts +0 -2
  242. package/src/daemon/handlers/config-channels.ts +15 -16
  243. package/src/daemon/handlers/config-slack-channel.ts +22 -3
  244. package/src/daemon/handlers/conversations.ts +107 -0
  245. package/src/daemon/host-browser-proxy.ts +41 -0
  246. package/src/daemon/lifecycle.ts +55 -27
  247. package/src/daemon/message-provenance.ts +2 -0
  248. package/src/daemon/message-types/contacts.ts +0 -1
  249. package/src/daemon/message-types/conversations.ts +3 -3
  250. package/src/daemon/message-types/sync.ts +0 -1
  251. package/src/daemon/message-types/web-activity.ts +7 -1
  252. package/src/daemon/message-types/workflows.ts +83 -1
  253. package/src/daemon/orphan-reaper.test.ts +0 -19
  254. package/src/daemon/orphan-reaper.ts +2 -24
  255. package/src/daemon/server.ts +0 -10
  256. package/src/daemon/tool-setup-types.ts +4 -0
  257. package/src/daemon/trust-context.ts +1 -1
  258. package/src/events/tool-audit-listener.ts +2 -2
  259. package/src/home/feed-source-enrichment.test.ts +151 -0
  260. package/src/home/feed-source-enrichment.ts +176 -0
  261. package/src/home/relationship-state.ts +2 -4
  262. package/src/instrument.ts +18 -6
  263. package/src/ipc/__tests__/binary-result-ipc.test.ts +81 -0
  264. package/src/ipc/__tests__/clients-list-ipc.test.ts +20 -0
  265. package/src/ipc/assistant-server.ts +37 -4
  266. package/src/ipc/gateway-flag-listener.ts +18 -2
  267. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +5 -16
  268. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +7 -11
  269. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +37 -7
  270. package/src/memory/__tests__/memory-retrospective-job.test.ts +229 -401
  271. package/src/memory/__tests__/onboarding-events-store.test.ts +7 -7
  272. package/src/memory/auth-fallback-events-store.ts +2 -2
  273. package/src/memory/auto-analysis-enqueue.ts +3 -5
  274. package/src/memory/bookmark-crud.ts +1 -2
  275. package/src/memory/canonical-guardian-store.ts +39 -1
  276. package/src/memory/conversation-crud.ts +9 -4
  277. package/src/memory/conversation-key-store.ts +17 -2
  278. package/src/memory/conversation-title-service.ts +64 -7
  279. package/src/memory/db-init.ts +17 -17
  280. package/src/memory/embedding-backend.ts +38 -1
  281. package/src/memory/embedding-billing-breaker.ts +96 -0
  282. package/src/memory/jobs-store.ts +25 -13
  283. package/src/memory/jobs-worker.ts +54 -1
  284. package/src/memory/lifecycle-events-store.ts +2 -2
  285. package/src/memory/memory-retrospective-constants.ts +4 -4
  286. package/src/memory/memory-retrospective-enqueue.ts +31 -6
  287. package/src/memory/memory-retrospective-job.ts +28 -227
  288. package/src/memory/migrations/129-contact-channels-access-fields.ts +18 -9
  289. package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +14 -2
  290. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +10 -0
  291. package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +72 -0
  292. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.test.ts +67 -0
  293. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.ts +25 -0
  294. package/src/memory/migrations/293-workflow-journal-leaf-tokens.ts +32 -0
  295. package/src/memory/migrations/294-drop-external-user-id.ts +31 -0
  296. package/src/memory/migrations/295-drop-approval-prompt-ts-tracker.ts +20 -0
  297. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.test.ts +110 -0
  298. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.ts +68 -0
  299. package/src/memory/migrations/__tests__/131-drop-legacy-member-guardian-tables.test.ts +154 -0
  300. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +31 -0
  301. package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +341 -0
  302. package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
  303. package/src/memory/migrations/index.ts +6 -0
  304. package/src/memory/migrations/run-migrations.ts +41 -0
  305. package/src/memory/migrations/validate-migration-state.ts +1 -1
  306. package/src/memory/onboarding-events-store.ts +3 -3
  307. package/src/memory/schema/contacts.ts +0 -5
  308. package/src/memory/skill-loaded-events-store.test.ts +7 -15
  309. package/src/memory/skill-loaded-events-store.ts +2 -2
  310. package/src/memory/tool-executed-events-store.test.ts +7 -7
  311. package/src/memory/turn-trace-store.test.ts +736 -0
  312. package/src/memory/turn-trace-store.ts +364 -0
  313. package/src/memory/v2/__tests__/consolidation-job.test.ts +8 -0
  314. package/src/memory/v2/__tests__/skill-content.test.ts +30 -0
  315. package/src/memory/v2/consolidation-job.ts +2 -2
  316. package/src/memory/v2/skill-content.ts +25 -7
  317. package/src/memory/v2/skill-store.ts +7 -1
  318. package/src/memory/v3-eval/__tests__/eval-packets.test.ts +248 -0
  319. package/src/memory/v3-eval/eval-packets.ts +546 -0
  320. package/src/messaging/providers/slack/adapter.ts +1 -1
  321. package/src/messaging/providers/slack/api.ts +31 -0
  322. package/src/messaging/providers/slack/send.test.ts +114 -2
  323. package/src/messaging/providers/slack/send.ts +30 -7
  324. package/src/messaging/providers/slack/withdraw.test.ts +200 -0
  325. package/src/messaging/providers/slack/withdraw.ts +161 -0
  326. package/src/notifications/AGENTS.md +2 -0
  327. package/src/notifications/access-request-copy.ts +72 -59
  328. package/src/notifications/adapters/shared.ts +29 -0
  329. package/src/notifications/adapters/slack.ts +58 -103
  330. package/src/notifications/adapters/telegram.ts +2 -20
  331. package/src/notifications/approval-card-data.ts +333 -0
  332. package/src/notifications/broadcaster.ts +16 -3
  333. package/src/notifications/canonical-delivery-recorder.ts +139 -0
  334. package/src/notifications/copy-composer.ts +3 -3
  335. package/src/notifications/decision-engine.ts +4 -2
  336. package/src/notifications/destination-resolver.ts +4 -6
  337. package/src/notifications/guardian-question-mode.ts +10 -0
  338. package/src/notifications/home-feed-side-effect.ts +7 -16
  339. package/src/notifications/notification-utils.ts +19 -20
  340. package/src/notifications/signal.ts +79 -43
  341. package/src/notifications/types.ts +98 -121
  342. package/src/oauth/AGENTS.md +5 -24
  343. package/src/permissions/checker.test.ts +51 -0
  344. package/src/permissions/checker.ts +185 -26
  345. package/src/permissions/ipc-risk-types.ts +24 -0
  346. package/src/permissions/question-prompter.test.ts +27 -0
  347. package/src/permissions/question-prompter.ts +4 -0
  348. package/src/platform/client.test.ts +119 -0
  349. package/src/platform/client.ts +66 -0
  350. package/src/platform/consent-cache.test.ts +267 -0
  351. package/src/platform/consent-cache.ts +174 -0
  352. package/src/plugin-api/constants.ts +1 -1
  353. package/src/plugin-api/index.ts +33 -1
  354. package/src/plugin-api/model-profiles.ts +33 -0
  355. package/src/plugin-api/types.ts +50 -2
  356. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +56 -0
  357. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +43 -0
  358. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +137 -0
  359. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +153 -0
  360. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +138 -0
  361. package/src/plugins/defaults/advisor/__tests__/transcript.test.ts +147 -0
  362. package/src/plugins/defaults/advisor/advisor-gate.ts +29 -0
  363. package/src/plugins/defaults/advisor/advisor-state-store.ts +94 -0
  364. package/src/plugins/defaults/advisor/config.ts +21 -0
  365. package/src/plugins/defaults/advisor/consult.ts +93 -0
  366. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +34 -0
  367. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +30 -0
  368. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +19 -0
  369. package/src/plugins/defaults/advisor/package.json +14 -0
  370. package/src/plugins/defaults/advisor/steering.ts +67 -0
  371. package/src/plugins/defaults/advisor/tools/advisor.ts +65 -0
  372. package/src/plugins/defaults/advisor/transcript.ts +76 -0
  373. package/src/plugins/defaults/index.ts +60 -0
  374. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +22 -9
  375. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
  376. package/src/plugins/defaults/memory-retrieval/tail-reinjection-strip.ts +64 -0
  377. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +29 -21
  378. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1 -0
  379. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  380. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +129 -9
  381. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +31 -4
  382. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +77 -2
  383. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +1 -0
  384. package/src/plugins/defaults/memory-v3-shadow/injector.ts +7 -10
  385. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +144 -11
  386. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +32 -20
  387. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +56 -3
  388. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +23 -2
  389. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
  390. package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
  391. package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
  392. package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
  393. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +3 -13
  394. package/src/plugins/defaults/title-generate/hooks/stop.ts +56 -21
  395. package/src/prompts/persona-resolver.ts +14 -4
  396. package/src/prompts/templates/system-sections.ts +7 -2
  397. package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
  398. package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
  399. package/src/providers/__tests__/retry-callsite.test.ts +176 -0
  400. package/src/providers/atlascloud/client.ts +85 -0
  401. package/src/providers/fetch-provider-catalog.ts +85 -0
  402. package/src/providers/inference/adapter-factory.ts +3 -0
  403. package/src/providers/model-catalog.ts +58 -0
  404. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +33 -0
  405. package/src/providers/openai/chat-completions-provider.ts +7 -0
  406. package/src/providers/openai/responses-provider.ts +10 -0
  407. package/src/providers/provider-send-message.ts +11 -3
  408. package/src/providers/retry.ts +53 -12
  409. package/src/providers/search-provider-catalog.ts +10 -0
  410. package/src/providers/weak-open-model.ts +22 -0
  411. package/src/runtime/AGENTS.md +0 -1
  412. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  413. package/src/runtime/__tests__/client-health.test.ts +44 -0
  414. package/src/runtime/access-request-helper.ts +21 -53
  415. package/src/runtime/actor-trust-resolver.ts +59 -63
  416. package/src/runtime/agent-wake.ts +52 -0
  417. package/src/runtime/assistant-event-hub.ts +18 -4
  418. package/src/runtime/auth/__tests__/route-policy.test.ts +12 -0
  419. package/src/runtime/auth/require-bound-guardian.ts +1 -4
  420. package/src/runtime/btw-sidechain.ts +3 -6
  421. package/src/runtime/capabilities.test.ts +120 -0
  422. package/src/runtime/capabilities.ts +197 -0
  423. package/src/runtime/channel-approval-types.ts +22 -45
  424. package/src/runtime/channel-invite-transports/telegram.ts +4 -4
  425. package/src/runtime/channel-retry-sweep.ts +1 -0
  426. package/src/runtime/channel-verification-service.ts +3 -3
  427. package/src/runtime/client-health.ts +26 -0
  428. package/src/runtime/confirmation-request-guardian-bridge.ts +38 -29
  429. package/src/runtime/effective-capabilities.test.ts +128 -0
  430. package/src/runtime/effective-capabilities.ts +84 -0
  431. package/src/runtime/guardian-reply-router.ts +106 -21
  432. package/src/runtime/invite-redemption-service.ts +9 -25
  433. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +123 -0
  434. package/src/runtime/migrations/vbundle-builder.ts +49 -20
  435. package/src/runtime/pending-interactions.ts +15 -0
  436. package/src/runtime/routes/__tests__/client-routes.test.ts +13 -0
  437. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +67 -0
  438. package/src/runtime/routes/__tests__/plugins-routes.test.ts +240 -1
  439. package/src/runtime/routes/app-routes.ts +1 -1
  440. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
  441. package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
  442. package/src/runtime/routes/browser-tabs-routes.ts +9 -0
  443. package/src/runtime/routes/btw-routes.ts +1 -27
  444. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +17 -8
  445. package/src/runtime/routes/client-routes.ts +10 -0
  446. package/src/runtime/routes/contact-routes.ts +31 -8
  447. package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
  448. package/src/runtime/routes/conversation-management-routes.ts +80 -1
  449. package/src/runtime/routes/conversation-query-routes.ts +68 -22
  450. package/src/runtime/routes/conversation-routes.ts +39 -14
  451. package/src/runtime/routes/credential-routes.ts +40 -16
  452. package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
  453. package/src/runtime/routes/events-routes.ts +1 -3
  454. package/src/runtime/routes/guardian-approval-interception.ts +14 -73
  455. package/src/runtime/routes/guardian-approval-prompt.ts +22 -4
  456. package/src/runtime/routes/home-feed-routes.ts +8 -3
  457. package/src/runtime/routes/identity-routes.ts +1 -296
  458. package/src/runtime/routes/inbound-message-handler.ts +214 -228
  459. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +89 -7
  460. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +154 -0
  461. package/src/runtime/routes/inbound-stages/admission-policy.ts +140 -0
  462. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +3 -3
  463. package/src/runtime/routes/inbound-stages/background-dispatch.ts +11 -6
  464. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +1 -2
  465. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +1 -2
  466. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +7 -7
  467. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +47 -28
  468. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +358 -0
  469. package/src/runtime/routes/index.ts +2 -0
  470. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +8 -0
  471. package/src/runtime/routes/integrations/slack/channel.ts +36 -0
  472. package/src/runtime/routes/internal-telemetry-routes.ts +1 -1
  473. package/src/runtime/routes/mcp-auth-routes.ts +233 -41
  474. package/src/runtime/routes/memory-eval-routes.ts +87 -0
  475. package/src/runtime/routes/notification-routes.ts +122 -133
  476. package/src/runtime/routes/platform-routes.ts +2 -2
  477. package/src/runtime/routes/plugins-routes.ts +202 -3
  478. package/src/runtime/routes/schedule-routes.ts +0 -22
  479. package/src/runtime/routes/secret-routes.ts +10 -0
  480. package/src/runtime/routes/surface-action-routes.ts +2 -1
  481. package/src/runtime/routes/tool-call-question-enrichment.test.ts +146 -0
  482. package/src/runtime/routes/tool-call-question-enrichment.ts +66 -0
  483. package/src/runtime/routes/workflow-routes.test.ts +229 -44
  484. package/src/runtime/routes/workflow-routes.ts +131 -29
  485. package/src/runtime/routes/workspace-greetings.ts +55 -0
  486. package/src/runtime/sync/resource-sync-events.ts +1 -11
  487. package/src/runtime/tool-grant-request-helper.ts +18 -16
  488. package/src/runtime/trust-context-resolver.ts +8 -5
  489. package/src/schedule/inference-profile.ts +2 -14
  490. package/src/schedule/schedule-store.ts +1 -1
  491. package/src/schedule/scheduler-types.ts +5 -1
  492. package/src/security/__tests__/provider-key-env-fallback.test.ts +6 -0
  493. package/src/security/secret-patterns.ts +3 -0
  494. package/src/subagent/manager.ts +17 -4
  495. package/src/subagent/types.ts +6 -0
  496. package/src/telemetry/trace-collection-policy.test.ts +28 -0
  497. package/src/telemetry/trace-collection-policy.ts +30 -0
  498. package/src/telemetry/types.ts +89 -0
  499. package/src/telemetry/usage-telemetry-reporter.test.ts +586 -36
  500. package/src/telemetry/usage-telemetry-reporter.ts +148 -41
  501. package/src/tools/AGENTS.md +3 -3
  502. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +31 -0
  503. package/src/tools/browser/browser-execution.ts +30 -19
  504. package/src/tools/document/document-tool.ts +2 -3
  505. package/src/tools/executor.ts +5 -3
  506. package/src/tools/host-terminal/host-shell.ts +5 -4
  507. package/src/tools/memory/register.ts +2 -2
  508. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +360 -0
  509. package/src/tools/network/__tests__/web-search.test.ts +143 -0
  510. package/src/tools/network/web-fetch.ts +372 -1
  511. package/src/tools/network/web-search-error.ts +1 -1
  512. package/src/tools/network/web-search.ts +213 -10
  513. package/src/tools/permission-checker.ts +4 -3
  514. package/src/tools/registry.ts +20 -0
  515. package/src/tools/schedule/create.ts +7 -12
  516. package/src/tools/schedule/update.ts +4 -11
  517. package/src/tools/shared/filesystem/path-policy.ts +39 -13
  518. package/src/tools/side-effects.ts +2 -17
  519. package/src/tools/skills/execute.ts +33 -0
  520. package/src/tools/subagent/spawn.ts +61 -12
  521. package/src/tools/terminal/shell.ts +10 -4
  522. package/src/tools/tool-approval-handler.ts +18 -13
  523. package/src/tools/tool-manifest.ts +0 -2
  524. package/src/tools/types.ts +9 -0
  525. package/src/tools/ui-surface/definitions.ts +64 -3
  526. package/src/tools/verification-control-plane-policy.ts +3 -1
  527. package/src/tools/workflows/run-workflow.test.ts +8 -18
  528. package/src/tools/workflows/run-workflow.ts +1 -0
  529. package/src/util/disk-usage.ts +78 -23
  530. package/src/util/platform.ts +10 -3
  531. package/src/watcher/telemetry.ts +2 -2
  532. package/src/workflows/capabilities.ts +2 -3
  533. package/src/workflows/engine.test.ts +175 -1
  534. package/src/workflows/engine.ts +82 -0
  535. package/src/workflows/journal-store.test.ts +70 -0
  536. package/src/workflows/journal-store.ts +18 -3
  537. package/src/workflows/run-manager.test.ts +171 -28
  538. package/src/workflows/run-manager.ts +66 -24
  539. package/src/workspace/migrations/105-enable-memory-v3-live-for-new-workspaces.ts +63 -0
  540. package/src/workspace/migrations/106-drop-collect-usage-data.ts +47 -0
  541. package/src/workspace/migrations/107-drop-send-diagnostics.ts +47 -0
  542. package/src/workspace/migrations/108-drop-balanced-economy-profile.ts +129 -0
  543. package/src/workspace/migrations/registry.ts +8 -0
  544. package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
  545. package/src/__tests__/credential-security-e2e.test.ts +0 -362
  546. package/src/__tests__/credential-vault-unit.test.ts +0 -1528
  547. package/src/__tests__/credential-vault.test.ts +0 -1706
  548. package/src/__tests__/identity-intro-cache.test.ts +0 -315
  549. package/src/__tests__/secret-onetime-send.test.ts +0 -182
  550. package/src/cli/commands/__tests__/task.test.ts +0 -914
  551. package/src/cli/commands/task.ts +0 -771
  552. package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
  553. package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
  554. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
  555. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
  556. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
  557. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
  558. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
  559. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
  560. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
  561. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
  562. package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
  563. package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
  564. package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
  565. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
  566. package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
  567. package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
  568. package/src/memory/preloaded-apps.ts +0 -116
  569. package/src/notifications/tool-approval-copy.ts +0 -142
  570. package/src/runtime/routes/approval-prompt-ts-tracker.ts +0 -78
  571. package/src/runtime/routes/identity-intro-cache.ts +0 -172
  572. package/src/tools/credentials/vault.ts +0 -712
@@ -41,9 +41,9 @@
41
41
  * block id only); it is never persisted to metadata, so the frozen card
42
42
  * prefix it follows stays byte-stable and cached regardless.
43
43
  *
44
- * Flag gating is unchanged: `memory-v3-live` attaches blocks; `memory-v3-shadow`
45
- * (live off) logs what WOULD inject (net-new slugs + bytes + spotlight refs)
46
- * and attaches nothing; both off → no orchestration.
44
+ * Gating: `memory.v3.live` (config) attaches blocks; the `memory-v3-shadow`
45
+ * flag (live off) logs what WOULD inject (net-new slugs + bytes + spotlight
46
+ * refs) and attaches nothing; both off → no orchestration.
47
47
  *
48
48
  * Both injectors apply the same personal-memory trust gate as v2
49
49
  * ({@link isPersonalMemoryAllowed}): an untrusted remote actor's turn
@@ -62,6 +62,7 @@
62
62
 
63
63
  import { isAssistantFeatureFlagEnabled } from "../../../config/assistant-feature-flags.js";
64
64
  import { getConfig } from "../../../config/loader.js";
65
+ import { isMemoryV3Live } from "../../../config/memory-v3-gate.js";
65
66
  import { isPersonalMemoryAllowed } from "../../../daemon/trust-context.js";
66
67
  import {
67
68
  wrapMemoryBlock,
@@ -85,11 +86,7 @@ import {
85
86
  renderSpotlightInner,
86
87
  type SpotlightEntry,
87
88
  } from "./render-injection.js";
88
- import {
89
- MEMORY_V3_LIVE,
90
- MEMORY_V3_SHADOW,
91
- observeTurn,
92
- } from "./shadow-plugin.js";
89
+ import { MEMORY_V3_SHADOW, observeTurn } from "./shadow-plugin.js";
93
90
  import {
94
91
  MEMORY_V3_BLOCK_ID,
95
92
  MEMORY_V3_COMMIT_META_KEY,
@@ -239,7 +236,7 @@ export const memoryV3Injector: Injector = {
239
236
  async produce(ctx: TurnContext): Promise<InjectionBlock | null> {
240
237
  const config = getConfig();
241
238
  if (config.memory.enabled === false) return null;
242
- const live = isAssistantFeatureFlagEnabled(MEMORY_V3_LIVE, config);
239
+ const live = isMemoryV3Live(config);
243
240
  const shadow = isAssistantFeatureFlagEnabled(MEMORY_V3_SHADOW, config);
244
241
  if (!live && !shadow) return null;
245
242
  if (!isPersonalMemoryAllowed(ctx.trust)) return null;
@@ -378,7 +375,7 @@ export const memoryV3SpotlightInjector: Injector = {
378
375
  // Live-only: shadow mode logs spotlight refs from the cards injector and
379
376
  // must keep the turn untouched (no ring state either, so a later
380
377
  // live-flag flip starts from a clean window).
381
- if (!isAssistantFeatureFlagEnabled(MEMORY_V3_LIVE, config)) return null;
378
+ if (!isMemoryV3Live(config)) return null;
382
379
  if (!isPersonalMemoryAllowed(ctx.trust)) return null;
383
380
 
384
381
  try {
@@ -2,7 +2,7 @@
2
2
  * Memory v3 — `memory_v3_maintain` job handler.
3
3
  *
4
4
  * A flag-gated, best-effort self-maintenance pass over the v3 section dense
5
- * store and the in-memory lanes. It runs four independent stages, in order:
5
+ * store and the in-memory lanes. It runs five independent stages, in order:
6
6
  *
7
7
  * 1. **Section re-embed** — diff the page index by `modifiedAt` against the
8
8
  * last successful pass (the high-water mark below), and for every page that
@@ -15,19 +15,26 @@
15
15
  * captured before any potential mtime bumps) so a page is not re-embedded
16
16
  * forever, yet a page whose embed failed (and whose sections were therefore
17
17
  * left deleted) stays above the mark and is retried next pass.
18
- * 2. **Deleted-page prune** — diff the dense store's stored articles
18
+ * 2. **Capability reconcile** — embed capability rows (synthetic skill/CLI
19
+ * slugs) present in the page index but missing from the section store. The
20
+ * re-embed delta above EXCLUDES capability rows (they have `modifiedAt` 0,
21
+ * no mtime to diff), so the only other embedder is the one-time
22
+ * `backfillAllSections`. Without this stage a skill enabled AFTER that
23
+ * backfill (e.g. a flag-gated skill flipped on at runtime) lands in the
24
+ * index but never reaches the dense lane. See {@link reconcileCapabilityRows}.
25
+ * 3. **Deleted-page prune** — diff the dense store's stored articles
19
26
  * (`listSectionArticles`) against the live page-index slugs and
20
27
  * `deleteSectionsForArticle` for any article that is no longer in the
21
28
  * index. A deleted page's slug never reaches the re-embed delta selector
22
29
  * (it only names live pages), so without this its section points would
23
30
  * linger in Qdrant and the dense lane could still surface the deleted page.
24
31
  * Synthetic capability rows are in the page index, so they are never pruned.
25
- * 3. **Core-set validation** — load the maintainer-curated core set
32
+ * 4. **Core-set validation** — load the maintainer-curated core set
26
33
  * (`memory/core-pages.md`) and report entries whose page no longer exists
27
34
  * in the page index (dangling slugs) via the log + outcome. The file is
28
35
  * maintainer-owned, so this stage NEVER edits it — the maintainer fixes
29
36
  * dangling entries at the next consolidation pass.
30
- * 4. **Lane invalidation** — `invalidateLanes()` so the next turn rebuilds the
37
+ * 5. **Lane invalidation** — `invalidateLanes()` so the next turn rebuilds the
31
38
  * in-memory section index, needle, and edge graph from the freshly-updated
32
39
  * pages.
33
40
  *
@@ -35,8 +42,8 @@
35
42
  * logged and recorded in the outcome but does NOT abort the others. A single
36
43
  * page whose embed fails does not abort the rest of the re-embed stage, and a
37
44
  * single prune delete that throws does not abort the rest of the prune stage.
38
- * The job is a no-op (returns a disabled outcome) unless `memory-v3-shadow` OR
39
- * `memory-v3-live` is enabled — the same flags that gate the v3 plugin.
45
+ * The job is a no-op (returns a disabled outcome) unless the `memory-v3-shadow`
46
+ * flag OR `memory.v3.live` (config) is enabled — the same gates as the v3 plugin.
40
47
  *
41
48
  * Dependency-injectable: `deps` lets tests substitute the page-index reader,
42
49
  * section builder, dense-store ops (including the prune-stage
@@ -45,11 +52,17 @@
45
52
  */
46
53
 
47
54
  import { isAssistantFeatureFlagEnabled } from "../../../config/assistant-feature-flags.js";
55
+ import { isMemoryV3Live } from "../../../config/memory-v3-gate.js";
48
56
  import type { AssistantConfig } from "../../../config/types.js";
49
57
  import {
50
58
  getMemoryCheckpoint,
51
59
  setMemoryCheckpoint,
52
60
  } from "../../../memory/checkpoints.js";
61
+ import {
62
+ EmbeddingBackendUnavailableError,
63
+ embedWithBackend,
64
+ } from "../../../memory/embedding-backend.js";
65
+ import { EmbeddingBillingBlockError } from "../../../memory/embedding-billing-breaker.js";
53
66
  import type { MemoryJob } from "../../../memory/jobs-store.js";
54
67
  import { getPageIndex } from "../../../memory/v2/page-index.js";
55
68
  import { readPage } from "../../../memory/v2/page-store.js";
@@ -68,7 +81,6 @@ import { invalidateLanes as realInvalidateLanes } from "./shadow-plugin.js";
68
81
  import type { Slug } from "./types.js";
69
82
 
70
83
  const MEMORY_V3_SHADOW = "memory-v3-shadow" as const;
71
- const MEMORY_V3_LIVE = "memory-v3-live" as const;
72
84
 
73
85
  /**
74
86
  * Durable checkpoint holding the epoch-ms high-water mark of the last successful
@@ -110,6 +122,13 @@ export interface MaintainJobDeps {
110
122
  buildSectionIndex: typeof realBuildSectionIndex;
111
123
  /** Read a page's frontmatter-stripped body for `buildSectionIndex`. */
112
124
  readPageBody: (slug: Slug) => Promise<string>;
125
+ /**
126
+ * Capability-aware body reader for the reconcile stage: synthetic skill/CLI
127
+ * slugs resolve their rendered capability content; real pages read from disk.
128
+ * The re-embed stage uses `readPageBody` (disk-only) since it only processes
129
+ * real pages — the reconcile stage needs capability bodies.
130
+ */
131
+ readCapabilityBody: (slug: Slug) => Promise<string>;
113
132
  /** Clear an article's stale section points before re-upserting. */
114
133
  deleteSectionsForArticle: typeof realDeleteSectionsForArticle;
115
134
  /** Embed + upsert an article's current sections into the dense store. */
@@ -178,6 +197,13 @@ export interface BackfillJobDeps {
178
197
  nowMs: () => number;
179
198
  /** Active assistant config (for the dense-store/embedding calls). */
180
199
  config: AssistantConfig;
200
+ /**
201
+ * Smoke-test the embedding backend before any destructive write. Throws when
202
+ * the backend is unavailable, so the backfill aborts BEFORE deleting any
203
+ * article's points (each article is processed delete-then-upsert). Injectable
204
+ * for tests.
205
+ */
206
+ embedProbe: () => Promise<void>;
181
207
  }
182
208
 
183
209
  /** Counts surfaced by {@link backfillAllSections}. */
@@ -198,6 +224,15 @@ export interface MaintainOutcome {
198
224
  reembedded: number;
199
225
  /** Pages whose re-embed threw (and was contained). */
200
226
  reembedFailures: number;
227
+ /**
228
+ * Capability rows (skills/CLI) present in the index but missing from the
229
+ * section store that were embedded this pass — how a skill enabled after the
230
+ * one-time backfill reaches the dense lane (the re-embed delta excludes
231
+ * capability rows).
232
+ */
233
+ capabilitiesReconciled: number;
234
+ /** Capability reconcile embeds that threw (and were contained). */
235
+ reconcileFailures: number;
201
236
  /**
202
237
  * Deleted-page articles whose lingering section points were pruned this pass
203
238
  * (present in the dense store but absent from the page index).
@@ -302,6 +337,8 @@ function defaultDeps(config: AssistantConfig): MaintainJobDeps {
302
337
  selectChangedPages: () => selectChangedPagesFromWorkspace(workspaceDir),
303
338
  buildSectionIndex: realBuildSectionIndex,
304
339
  readPageBody: (slug) => readPageBodyFromWorkspace(workspaceDir, slug),
340
+ readCapabilityBody: (slug) =>
341
+ backfillPageBodyFromWorkspace(workspaceDir, slug),
305
342
  deleteSectionsForArticle: realDeleteSectionsForArticle,
306
343
  upsertSections: realUpsertSections,
307
344
  commitEmbedHighWater,
@@ -325,6 +362,9 @@ function defaultBackfillDeps(config: AssistantConfig): BackfillJobDeps {
325
362
  commitEmbedHighWater,
326
363
  nowMs: () => Date.now(),
327
364
  config,
365
+ embedProbe: async () => {
366
+ await embedWithBackend(config, ["memory-v3 backfill preflight"]);
367
+ },
328
368
  };
329
369
  }
330
370
 
@@ -357,6 +397,58 @@ async function reembedChangedPages(
357
397
  return { reembedded, reembedFailures };
358
398
  }
359
399
 
400
+ /**
401
+ * Embed capability rows (synthetic skill/CLI slugs) present in the page index
402
+ * but missing from the section dense store. The incremental re-embed selector
403
+ * ({@link computeChangedPages}) excludes capability rows — they have no on-disk
404
+ * mtime to delta against — so the ONLY other embedder is the one-time
405
+ * {@link backfillAllSections}. Without this, a skill enabled AFTER that backfill
406
+ * (e.g. a flag-gated skill flipped on at runtime) lands in the page index but
407
+ * never reaches the dense lane. This stage makes the periodic maintain pass
408
+ * self-heal: diff the live capability slugs against the stored section articles
409
+ * and embed the missing ones.
410
+ *
411
+ * Each row is independent: a single build/upsert throw is logged and counted in
412
+ * `reconcileFailures` without aborting the rest. A capability row whose body
413
+ * resolves empty (its store has not seeded yet) is skipped WITHOUT deleting any
414
+ * points — never replace good points with a blank — and is retried next pass.
415
+ */
416
+ async function reconcileCapabilityRows(
417
+ deps: MaintainJobDeps,
418
+ ): Promise<{ capabilitiesReconciled: number; reconcileFailures: number }> {
419
+ const [indexedSlugs, storedArticles] = await Promise.all([
420
+ deps.listIndexedSlugs(),
421
+ deps.listSectionArticles(),
422
+ ]);
423
+ const stored = new Set(storedArticles);
424
+ const missing = indexedSlugs.filter(
425
+ (slug) => isCapabilitySlug(slug) && !stored.has(slug),
426
+ );
427
+
428
+ let capabilitiesReconciled = 0;
429
+ let reconcileFailures = 0;
430
+ for (const slug of missing) {
431
+ try {
432
+ const body = await deps.readCapabilityBody(slug);
433
+ if (body.trim().length === 0) continue; // store cold — retry next pass
434
+ const index = await deps.buildSectionIndex(
435
+ [slug],
436
+ deps.readCapabilityBody,
437
+ );
438
+ await deps.deleteSectionsForArticle(deps.config, slug);
439
+ await deps.upsertSections(deps.config, index.sections);
440
+ capabilitiesReconciled += 1;
441
+ } catch (err) {
442
+ reconcileFailures += 1;
443
+ log.warn(
444
+ { slug, err: err instanceof Error ? err.message : String(err) },
445
+ "memory-v3 maintain: capability reconcile embed failed (non-fatal)",
446
+ );
447
+ }
448
+ }
449
+ return { capabilitiesReconciled, reconcileFailures };
450
+ }
451
+
360
452
  /**
361
453
  * Prune section points for articles that no longer exist in the page index. A
362
454
  * deleted concept page leaves `getPageIndex` (so the needle and edge lanes both
@@ -444,6 +536,13 @@ export async function backfillAllSections(
444
536
  const slugs = await deps.selectAllPages();
445
537
  await deps.ensureSectionCollection(deps.config);
446
538
 
539
+ // Pre-flight: smoke-test the embedding backend BEFORE any delete. Each article
540
+ // is processed delete-then-upsert, so starting a full backfill against a down
541
+ // backend would delete every article's points and re-embed none of them. A
542
+ // throw here aborts with nothing deleted; the high-water mark stays put, so a
543
+ // later run retries once the backend is healthy.
544
+ await deps.embedProbe();
545
+
447
546
  let articles = 0;
448
547
  let sections = 0;
449
548
  let failures = 0;
@@ -466,6 +565,17 @@ export async function backfillAllSections(
466
565
  sections += index.sections.length;
467
566
  return "embedded";
468
567
  } catch (err) {
568
+ // A down embedding backend (unconfigured, or billing breaker open) fails
569
+ // EVERY article — the condition is process-wide — and each article is
570
+ // delete-then-upsert, so continuing would delete the rest of the corpus
571
+ // and re-embed none of it. Abort the run; the high-water mark is never
572
+ // advanced, so the next pass retries from here once the backend recovers.
573
+ if (
574
+ err instanceof EmbeddingBackendUnavailableError ||
575
+ err instanceof EmbeddingBillingBlockError
576
+ ) {
577
+ throw err;
578
+ }
469
579
  failures += 1;
470
580
  log.warn(
471
581
  { slug, err: err instanceof Error ? err.message : String(err) },
@@ -537,6 +647,8 @@ export async function maintainJob(
537
647
  disabled: false,
538
648
  reembedded: 0,
539
649
  reembedFailures: 0,
650
+ capabilitiesReconciled: 0,
651
+ reconcileFailures: 0,
540
652
  pruned: 0,
541
653
  pruneFailures: 0,
542
654
  danglingCoreSlugs: [],
@@ -546,7 +658,7 @@ export async function maintainJob(
546
658
 
547
659
  const enabled =
548
660
  isAssistantFeatureFlagEnabled(MEMORY_V3_SHADOW, config) ||
549
- isAssistantFeatureFlagEnabled(MEMORY_V3_LIVE, config);
661
+ isMemoryV3Live(config);
550
662
  if (!enabled) {
551
663
  outcome.disabled = true;
552
664
  return outcome;
@@ -587,7 +699,26 @@ export async function maintainJob(
587
699
  );
588
700
  }
589
701
 
590
- // Stage 2: prune section points for pages deleted from the index. A deleted
702
+ // Stage 2: embed capability rows (skills/CLI) present in the index but missing
703
+ // from the section store. They have modifiedAt 0, so computeChangedPages never
704
+ // selects them; the one-time backfill is otherwise their only embedder, so a
705
+ // skill enabled after that backfill stays invisible to the dense lane until
706
+ // this self-heals it. Contained: a failure is logged + recorded without
707
+ // aborting later stages.
708
+ try {
709
+ const { capabilitiesReconciled, reconcileFailures } =
710
+ await reconcileCapabilityRows(deps);
711
+ outcome.capabilitiesReconciled = capabilitiesReconciled;
712
+ outcome.reconcileFailures = reconcileFailures;
713
+ } catch (err) {
714
+ outcome.failures.push("capability-reconcile");
715
+ log.warn(
716
+ { err: err instanceof Error ? err.message : String(err) },
717
+ "memory-v3 maintain: capability reconcile failed (non-fatal)",
718
+ );
719
+ }
720
+
721
+ // Stage 3: prune section points for pages deleted from the index. A deleted
591
722
  // page's slug never appears in `selectChangedPages` (the delta selector only
592
723
  // names live pages), so its lingering points are cleared by diffing the dense
593
724
  // store's stored articles against the live page-index slugs. Contained: a
@@ -604,7 +735,7 @@ export async function maintainJob(
604
735
  );
605
736
  }
606
737
 
607
- // Stage 3: validate the maintainer-curated core set. Diff `core-pages.md`
738
+ // Stage 4: validate the maintainer-curated core set. Diff `core-pages.md`
608
739
  // entries against the live page-index slugs and REPORT dangling entries
609
740
  // (pages that were renamed or deleted) through the log + outcome — the file
610
741
  // is maintainer-owned, so it is never auto-edited; the maintainer fixes it at
@@ -632,7 +763,7 @@ export async function maintainJob(
632
763
  );
633
764
  }
634
765
 
635
- // Stage 4: rebuild section index + needle + edge graph on the next turn.
766
+ // Stage 5: rebuild section index + needle + edge graph on the next turn.
636
767
  try {
637
768
  deps.invalidateLanes();
638
769
  outcome.invalidated = true;
@@ -648,6 +779,8 @@ export async function maintainJob(
648
779
  {
649
780
  reembedded: outcome.reembedded,
650
781
  reembedFailures: outcome.reembedFailures,
782
+ capabilitiesReconciled: outcome.capabilitiesReconciled,
783
+ reconcileFailures: outcome.reconcileFailures,
651
784
  pruned: outcome.pruned,
652
785
  pruneFailures: outcome.pruneFailures,
653
786
  danglingCoreSlugs: outcome.danglingCoreSlugs,
@@ -18,9 +18,10 @@
18
18
  * Each lane only ever ADDS candidates, so the pool is recall-safe by
19
19
  * construction.
20
20
  * 2. Build the candidate pool in CACHE ORDER: the stable prefix —
21
- * `[...core (file order), ...hot (score order), ...fresh (recency
22
- * order)]`, all computed at lane init — followed by the finder
23
- * candidates (needle → dense → edge surfacing order). The stable prefix
21
+ * `[...core (file order), ...hot (score order), ...fresh (recency order),
22
+ * ...always-candidate (skills pinned every turn)]`, all computed at lane
23
+ * init — followed by the finder candidates (needle → dense → edge
24
+ * surfacing order). The stable prefix
24
25
  * is identical across consecutive turns while the lanes are unchanged
25
26
  * (lane invalidation at consolidation is the recompute cadence), so the
26
27
  * selector input's leading segment rides the provider KV cache (the
@@ -82,6 +83,10 @@ export interface OrchestrateDeps {
82
83
  /** The modification-recency fresh set in recency order (computed at lane
83
84
  * init with core and hot excluded). Follows hot in the stable prefix. */
84
85
  freshSlugs: Slug[];
86
+ /** Skills pinned into the candidate pool every turn regardless of retrieval
87
+ * (existence-filtered and core/hot/fresh-excluded at lane init). Follow fresh
88
+ * in the stable prefix so the selector always sees them. */
89
+ alwaysCandidateSlugs?: Slug[];
85
90
  /** Pre-rendered FULL cards for the stable-prefix slugs, keyed by slug.
86
91
  * Rendered ONCE at lane init so the selector's stable prefix is
87
92
  * byte-identical across turns (the cache contract). Every core/hot/fresh
@@ -172,14 +177,19 @@ export async function orchestrate(
172
177
  const { sections } = deps.sectionIndex;
173
178
 
174
179
  // The stable prefix: core (file order), hot (score order), fresh (recency
175
- // order). Hot is computed with core excluded at lane init, fresh with both
176
- // excluded; the filters here are a cheap defensive dedup so a misbehaving
177
- // lane can never double-list a slug.
180
+ // order), then always-candidate skills. Hot is computed with core excluded at
181
+ // lane init, fresh with both excluded, always-candidate with all three
182
+ // excluded; the filters here are a cheap defensive dedup so a misbehaving lane
183
+ // can never double-list a slug.
178
184
  const core = deps.coreSlugs;
179
185
  const hot = deps.hotSlugs.filter((slug) => !core.includes(slug));
180
186
  const coreHot = new Set<Slug>([...core, ...hot]);
181
187
  const fresh = deps.freshSlugs.filter((slug) => !coreHot.has(slug));
182
- const stablePrefix = new Set<Slug>([...coreHot, ...fresh]);
188
+ const coreHotFresh = new Set<Slug>([...coreHot, ...fresh]);
189
+ const always = (deps.alwaysCandidateSlugs ?? []).filter(
190
+ (slug) => !coreHotFresh.has(slug),
191
+ );
192
+ const stablePrefix = new Set<Slug>([...coreHotFresh, ...always]);
183
193
 
184
194
  // Step 1: needle (sync BM25) and dense (async embed + Qdrant) lanes run in
185
195
  // parallel. Both return distinct articles each tagged with their best-scoring
@@ -342,19 +352,21 @@ export async function orchestrate(
342
352
  // its own snippet line so its CURRENT relevance stays visible; `selectPool`
343
353
  // dedupes selections by slug. Finder candidates with no match text fall
344
354
  // back to the page's lead-section snippet.
345
- const stable: StableCandidate[] = [...core, ...hot, ...fresh].map((slug) => {
346
- const card = deps.prefixCards.get(slug);
347
- if (card === undefined) {
348
- // Lane init renders a card for every core/hot slug; a hole here means
349
- // the lanes and the card map are out of sync. Throw rather than render
350
- // a degraded card the caller (observeTurn) logs and skips the turn,
351
- // which is better than silently breaking the byte-stable prefix.
352
- throw new Error(
353
- `memory-v3: no pre-rendered card for stable-prefix slug "${slug}"`,
354
- );
355
- }
356
- return { slug, card };
357
- });
355
+ const stable: StableCandidate[] = [...core, ...hot, ...fresh, ...always].map(
356
+ (slug) => {
357
+ const card = deps.prefixCards.get(slug);
358
+ if (card === undefined) {
359
+ // Lane init renders a card for every core/hot slug; a hole here means
360
+ // the lanes and the card map are out of sync. Throw rather than render
361
+ // a degraded card the caller (observeTurn) logs and skips the turn,
362
+ // which is better than silently breaking the byte-stable prefix.
363
+ throw new Error(
364
+ `memory-v3: no pre-rendered card for stable-prefix slug "${slug}"`,
365
+ );
366
+ }
367
+ return { slug, card };
368
+ },
369
+ );
358
370
  const finderTail: PoolCandidate[] = finder.map((c) => ({
359
371
  slug: c.slug,
360
372
  lane: c.lane,
@@ -19,6 +19,7 @@
19
19
  import type { MemoryV3SelectionLog } from "../../../api/responses/memory-v3-selection-log.js";
20
20
  import { isAssistantFeatureFlagEnabled } from "../../../config/assistant-feature-flags.js";
21
21
  import { getConfig } from "../../../config/loader.js";
22
+ import { isMemoryV3Live } from "../../../config/memory-v3-gate.js";
22
23
  import { getDb, getSqliteFrom } from "../../../memory/db-connection.js";
23
24
  import { readPage } from "../../../memory/v2/page-store.js";
24
25
  import { getWorkspaceDir } from "../../../util/platform.js";
@@ -35,7 +36,6 @@ import {
35
36
  } from "./types.js";
36
37
 
37
38
  const MEMORY_V3_SHADOW = "memory-v3-shadow" as const;
38
- const MEMORY_V3_LIVE = "memory-v3-live" as const;
39
39
 
40
40
  interface SelectionRow {
41
41
  turn: number;
@@ -74,6 +74,52 @@ function rowsForMessageIds(messageIds: string[]): SelectionRow[] {
74
74
  .all(...messageIds) as SelectionRow[];
75
75
  }
76
76
 
77
+ const MAX_FORK_HOPS = 64;
78
+
79
+ /**
80
+ * Read the `forkSourceMessageId` back-pointer that `cloneForkMessageMetadata`
81
+ * stamps onto every message a fork copies, for the given message ids.
82
+ */
83
+ function forkSourceIdsOf(messageIds: string[]): string[] {
84
+ if (messageIds.length === 0) return [];
85
+ const placeholders = messageIds.map(() => "?").join(", ");
86
+ const rows = getSqliteFrom(getDb())
87
+ .query(
88
+ /*sql*/ `
89
+ SELECT json_extract(metadata, '$.forkSourceMessageId') AS src
90
+ FROM messages
91
+ WHERE id IN (${placeholders})
92
+ `,
93
+ )
94
+ .all(...messageIds) as Array<{ src: string | null }>;
95
+ return rows
96
+ .map((r) => r.src)
97
+ .filter((src): src is string => typeof src === "string" && src.length > 0);
98
+ }
99
+
100
+ /**
101
+ * A fork copies the parent's messages under fresh ids but does not copy their
102
+ * `memory_v3_selections` rows, so an inherited turn has no rows under its own
103
+ * message ids. Each copied message preserves a `forkSourceMessageId` pointer to
104
+ * the message it was cloned from; walk that chain (a fork of a fork chains it
105
+ * again) to the nearest ancestor generation that logged selections and return
106
+ * those rows. Returns `[]` when no ancestor has v3 rows (or the ids aren't fork
107
+ * copies).
108
+ */
109
+ function rowsViaForkSource(messageIds: string[]): SelectionRow[] {
110
+ let frontier = messageIds;
111
+ const visited = new Set(messageIds);
112
+ for (let hop = 0; hop < MAX_FORK_HOPS; hop++) {
113
+ const sources = forkSourceIdsOf(frontier).filter((id) => !visited.has(id));
114
+ if (sources.length === 0) return [];
115
+ for (const id of sources) visited.add(id);
116
+ const rows = rowsForMessageIds(sources);
117
+ if (rows.length > 0) return rows;
118
+ frontier = sources;
119
+ }
120
+ return [];
121
+ }
122
+
77
123
  /**
78
124
  * Resolve each selection's persisted matched section `(slug, ordinal)` to the
79
125
  * concrete `Section` in the CURRENT page, so the injected block renders the
@@ -133,7 +179,7 @@ async function buildSelectionLog(
133
179
 
134
180
  return {
135
181
  turn: rows[0]!.turn,
136
- live: isAssistantFeatureFlagEnabled(MEMORY_V3_LIVE, config),
182
+ live: isMemoryV3Live(config),
137
183
  shadow: isAssistantFeatureFlagEnabled(MEMORY_V3_SHADOW, config),
138
184
  selections,
139
185
  injectedText,
@@ -150,13 +196,20 @@ async function buildSelectionLog(
150
196
  * conversation with no v3 data). Message ids are globally unique, so no
151
197
  * conversation scope is needed.
152
198
  *
199
+ * For turns inherited from a fork, the copied messages carry fresh ids with no
200
+ * selection rows of their own, so the lookup falls back to the parent's rows by
201
+ * following each message's `forkSourceMessageId` back-pointer.
202
+ *
153
203
  * Selection rows are stored in selection order, so rendering them in row order
154
204
  * reproduces the block v3 would inject.
155
205
  */
156
206
  export async function getMemoryV3SelectionForInspectorByMessageIds(
157
207
  messageIds: string[],
158
208
  ): Promise<MemoryV3SelectionLog | null> {
159
- return buildSelectionLog(rowsForMessageIds(messageIds));
209
+ const rows = rowsForMessageIds(messageIds);
210
+ return buildSelectionLog(
211
+ rows.length > 0 ? rows : rowsViaForkSource(messageIds),
212
+ );
160
213
  }
161
214
 
162
215
  /**
@@ -34,6 +34,7 @@ import { existsSync, readFileSync } from "node:fs";
34
34
  import { isAssistantFeatureFlagEnabled } from "../../../config/assistant-feature-flags.js";
35
35
  import { getConfig } from "../../../config/loader.js";
36
36
  import type { AssistantConfig } from "../../../config/schema.js";
37
+ import { loadSkillCatalog } from "../../../config/skills.js";
37
38
  import { getMessages } from "../../../memory/conversation-crud.js";
38
39
  import { getDb, getSqliteFrom } from "../../../memory/db-connection.js";
39
40
  import { stringifyMessageContent } from "../../../memory/message-content.js";
@@ -68,7 +69,6 @@ import {
68
69
  } from "./types.js";
69
70
 
70
71
  export const MEMORY_V3_SHADOW = "memory-v3-shadow" as const;
71
- export const MEMORY_V3_LIVE = "memory-v3-live" as const;
72
72
 
73
73
  const log = getLogger("memory-v3-shadow");
74
74
 
@@ -107,6 +107,9 @@ export interface ShadowLanes {
107
107
  /** Modification-recency fresh set in recency order: core and hot excluded,
108
108
  * filtered to pages in the section index. */
109
109
  freshSlugs: string[];
110
+ /** Skills pinned into the stable prefix every turn (`always-candidate: true`
111
+ * in SKILL.md), existence-filtered and core/hot/fresh-excluded. */
112
+ alwaysCandidateSlugs: string[];
110
113
  /** Learned-edge graph: co-selection NPMI associations over the selection
111
114
  * log, rebuilt with the lanes (the consolidation cadence). */
112
115
  learnedGraph: EdgeGraph;
@@ -216,6 +219,18 @@ async function initLanes(config: AssistantConfig): Promise<ShadowLanes> {
216
219
  excludeSlugs: new Set([...coreSlugs, ...hotSlugs]),
217
220
  }).filter((slug) => sectionIndex.byArticle.has(slug));
218
221
 
222
+ // Always-candidate skills are pinned into the stable prefix every turn so the
223
+ // selector can choose a cross-cutting capability (e.g. workflows) even when no
224
+ // retrieval lane surfaces it — its relevance is a judgment the model makes,
225
+ // not something embedding similarity finds. Filtered to skills present in the
226
+ // section index and excluded from core/hot/fresh so the prefix never
227
+ // double-lists a slug.
228
+ const prefixSet = new Set([...coreSlugs, ...hotSlugs, ...freshSlugs]);
229
+ const alwaysCandidateSlugs = loadSkillCatalog()
230
+ .filter((summary) => summary.alwaysCandidate === true)
231
+ .map((summary) => `skills/${summary.id}`)
232
+ .filter((slug) => sectionIndex.byArticle.has(slug) && !prefixSet.has(slug));
233
+
219
234
  // Pre-render the stable-prefix cards ONCE per lane build: the selector's
220
235
  // stable prefix must be byte-identical across turns to ride the provider KV
221
236
  // cache, so the cards are frozen here (lane invalidation at consolidation is
@@ -228,7 +243,10 @@ async function initLanes(config: AssistantConfig): Promise<ShadowLanes> {
228
243
  const modifiedAtBySlug = new Map(
229
244
  pageIndex.entries.map((entry) => [entry.slug, entry.modifiedAt]),
230
245
  );
231
- const laneAnnotation = (slug: Slug, lane: "core" | "hot" | "fresh") => {
246
+ const laneAnnotation = (
247
+ slug: Slug,
248
+ lane: "core" | "hot" | "fresh" | "always",
249
+ ) => {
232
250
  if (lane !== "fresh") return `[lane: ${lane}]`;
233
251
  const modifiedAt = modifiedAtBySlug.get(slug);
234
252
  if (
@@ -249,6 +267,7 @@ async function initLanes(config: AssistantConfig): Promise<ShadowLanes> {
249
267
  ["core", coreSlugs],
250
268
  ["hot", hotSlugs],
251
269
  ["fresh", freshSlugs],
270
+ ["always", alwaysCandidateSlugs],
252
271
  ] as const) {
253
272
  for (const slug of slugs) {
254
273
  const raw = await capabilityOrDiskBody(
@@ -303,6 +322,7 @@ async function initLanes(config: AssistantConfig): Promise<ShadowLanes> {
303
322
  coreSlugs,
304
323
  hotSlugs,
305
324
  freshSlugs,
325
+ alwaysCandidateSlugs,
306
326
  prefixCards,
307
327
  };
308
328
  }
@@ -545,6 +565,7 @@ export async function observeTurn(
545
565
  coreSlugs: lanes.coreSlugs,
546
566
  hotSlugs: lanes.hotSlugs,
547
567
  freshSlugs: lanes.freshSlugs,
568
+ alwaysCandidateSlugs: lanes.alwaysCandidateSlugs,
548
569
  prefixCards: lanes.prefixCards,
549
570
  needleK: v3.needleK,
550
571
  denseK: v3.denseK,