@vellumai/assistant 0.8.12 → 0.9.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 (442) hide show
  1. package/AGENTS.md +0 -14
  2. package/ARCHITECTURE.md +45 -45
  3. package/README.md +1 -1
  4. package/bun.lock +200 -154
  5. package/docs/architecture/integrations.md +3 -3
  6. package/docs/architecture/memory.md +2 -2
  7. package/docs/architecture/security.md +10 -10
  8. package/docs/runbook-trusted-contacts.md +12 -12
  9. package/docs/skills.md +6 -6
  10. package/docs/workflows-testing.md +221 -0
  11. package/docs/workflows.md +510 -0
  12. package/examples/plugins/echo/README.md +5 -5
  13. package/knip.json +2 -0
  14. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +105 -0
  15. package/node_modules/@vellumai/gateway-client/src/index.ts +12 -0
  16. package/openapi.yaml +7197 -5708
  17. package/package.json +8 -4
  18. package/scripts/generate-openapi.ts +66 -114
  19. package/src/__tests__/access-request-seed-content-blocks.test.ts +213 -0
  20. package/src/__tests__/adaptive-thinking-repair.test.ts +32 -3
  21. package/src/__tests__/agent-loop-output-hooks.test.ts +183 -0
  22. package/src/__tests__/agent-loop-regrowth-guard.test.ts +506 -0
  23. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  24. package/src/__tests__/agent-wake-override-profile.test.ts +77 -0
  25. package/src/__tests__/app-compiler.test.ts +7 -1
  26. package/src/__tests__/app-dir-path-guard.test.ts +27 -3
  27. package/src/__tests__/app-executors.test.ts +43 -0
  28. package/src/__tests__/approval-cascade.test.ts +0 -5
  29. package/src/__tests__/approval-routes-http.test.ts +91 -0
  30. package/src/__tests__/assistant-stream-state.test.ts +107 -0
  31. package/src/__tests__/browser-fill-credential.test.ts +3 -3
  32. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -1
  33. package/src/__tests__/compaction-events.test.ts +63 -7
  34. package/src/__tests__/compaction-trail-store.test.ts +74 -1
  35. package/src/__tests__/compaction.benchmark.test.ts +63 -41
  36. package/src/__tests__/compactor-low-watermark-cut.test.ts +349 -0
  37. package/src/__tests__/context-window-manager-compact-retry.test.ts +64 -0
  38. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -5
  39. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -5
  40. package/src/__tests__/conversation-history-web-search.test.ts +7 -0
  41. package/src/__tests__/conversation-process-callsite.test.ts +0 -5
  42. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -5
  43. package/src/__tests__/conversation-queue.test.ts +0 -5
  44. package/src/__tests__/conversation-slash-queue.test.ts +0 -5
  45. package/src/__tests__/conversation-slash-unknown.test.ts +0 -5
  46. package/src/__tests__/conversation-speed-override.test.ts +0 -5
  47. package/src/__tests__/conversation-surfaces-data-persist.test.ts +97 -0
  48. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  49. package/src/__tests__/conversation-usage.test.ts +2 -0
  50. package/src/__tests__/conversation-workspace-injection.test.ts +0 -5
  51. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -5
  52. package/src/__tests__/credential-broker-browser-fill.test.ts +2 -2
  53. package/src/__tests__/credential-broker-server-use.test.ts +2 -2
  54. package/src/__tests__/credential-broker.test.ts +1 -1
  55. package/src/__tests__/credential-prompt-route.test.ts +417 -0
  56. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  57. package/src/__tests__/credential-vault.test.ts +37 -0
  58. package/src/__tests__/db-schedule-syntax-migration.test.ts +24 -0
  59. package/src/__tests__/dynamic-page-surface.test.ts +219 -0
  60. package/src/__tests__/empty-state-greeting-cache.test.ts +94 -0
  61. package/src/__tests__/gateway-flag-listener.test.ts +24 -7
  62. package/src/__tests__/guardian-action-sweep.test.ts +56 -219
  63. package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
  64. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  65. package/src/__tests__/list-messages-hidden-metadata.test.ts +99 -0
  66. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +87 -1
  67. package/src/__tests__/llm-resolver.test.ts +115 -0
  68. package/src/__tests__/managed-profile-guard.test.ts +6 -5
  69. package/src/__tests__/max-tokens-continue-hook.test.ts +184 -0
  70. package/src/__tests__/media-generate-image.test.ts +20 -9
  71. package/src/__tests__/mock-gateway-ipc.ts +23 -0
  72. package/src/__tests__/model-intents.test.ts +1 -1
  73. package/src/__tests__/normalize-onboarding.test.ts +26 -0
  74. package/src/__tests__/notification-decision-strategy.test.ts +4 -2
  75. package/src/__tests__/notification-telegram-adapter.test.ts +21 -3
  76. package/src/__tests__/pending-interactions-resolved-event.test.ts +62 -0
  77. package/src/__tests__/post-turn-tool-result-truncation.test.ts +72 -18
  78. package/src/__tests__/require-fresh-approval.test.ts +425 -1
  79. package/src/__tests__/resolve-app-id.test.ts +56 -0
  80. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -0
  81. package/src/__tests__/schedule-routes-workflow-validation.test.ts +408 -0
  82. package/src/__tests__/schedule-routes.test.ts +257 -4
  83. package/src/__tests__/schedule-store.test.ts +60 -0
  84. package/src/__tests__/schedule-tools.test.ts +247 -2
  85. package/src/__tests__/skill-execute-input.test.ts +85 -0
  86. package/src/__tests__/skill-secret-handling-guard.test.ts +21 -20
  87. package/src/__tests__/skills.test.ts +3 -3
  88. package/src/__tests__/slack-app-setup-skill-regression.test.ts +1 -1
  89. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  90. package/src/__tests__/subagent-tool-gate-mode.test.ts +547 -0
  91. package/src/__tests__/system-prompt.test.ts +1 -1
  92. package/src/__tests__/task-progress-nudge-hook.test.ts +372 -0
  93. package/src/__tests__/task-scheduler.test.ts +299 -0
  94. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +209 -0
  95. package/src/__tests__/tool-result-spool.test.ts +3 -1
  96. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +181 -0
  97. package/src/__tests__/workspace-migration-103-upgrade-quality-profile-to-opus-4-8.test.ts +174 -0
  98. package/src/agent/compaction-circuit.ts +11 -0
  99. package/src/agent/loop.ts +181 -12
  100. package/src/api/constants/call-sites.ts +12 -0
  101. package/src/api/events/assistant-thinking-delta.ts +10 -0
  102. package/src/api/events/usage-update.ts +7 -0
  103. package/src/api/index.ts +4 -1
  104. package/src/api/responses/memory-v3-selection-log.ts +18 -11
  105. package/src/approvals/approval-primitive.ts +2 -2
  106. package/src/background-wake/background-wake-routes.test.ts +5 -2
  107. package/src/bundler/compiler-tools.ts +1 -1
  108. package/src/bundler/package-resolver.ts +0 -1
  109. package/src/calls/call-domain.ts +1 -1
  110. package/src/calls/guardian-action-sweep.ts +16 -93
  111. package/src/cli/AGENTS.md +4 -0
  112. package/src/cli/commands/__tests__/schedules.test.ts +430 -1
  113. package/src/cli/commands/credentials.ts +28 -24
  114. package/src/cli/commands/image-generation.ts +23 -9
  115. package/src/cli/commands/notifications.ts +1 -1
  116. package/src/cli/commands/plugins.ts +89 -46
  117. package/src/cli/commands/schedules.ts +384 -11
  118. package/src/cli/lib/__tests__/inspect-plugin.test.ts +69 -5
  119. package/src/cli/lib/__tests__/install-from-github.test.ts +15 -0
  120. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +81 -4
  121. package/src/cli/lib/inspect-plugin.ts +62 -1
  122. package/src/cli/lib/install-from-github.ts +52 -5
  123. package/src/cli/lib/upgrade-plugin.ts +18 -0
  124. package/src/config/__tests__/workflows-schema.test.ts +60 -0
  125. package/src/config/bundled-skills/acp/SKILL.md +2 -2
  126. package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
  127. package/src/config/bundled-skills/app-builder/tools/app-create.ts +6 -1
  128. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +7 -1
  129. package/src/config/bundled-skills/app-builder/tools/app-refresh.ts +7 -1
  130. package/src/config/bundled-skills/app-builder/tools/app-update.ts +10 -1
  131. package/src/config/bundled-skills/image-studio/SKILL.md +66 -19
  132. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -6
  133. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +22 -3
  134. package/src/config/bundled-skills/personal-page/SKILL.md +57 -0
  135. package/src/config/bundled-skills/personal-page/TOOLS.json +27 -0
  136. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +17 -0
  137. package/src/config/bundled-skills/schedule/SKILL.md +7 -2
  138. package/src/config/bundled-skills/schedule/TOOLS.json +48 -4
  139. package/src/config/bundled-skills/workflows/SKILL.md +214 -0
  140. package/src/config/bundled-skills/workflows/TOOLS.json +84 -0
  141. package/src/config/bundled-skills/workflows/tools/manage-workflows.ts +12 -0
  142. package/src/config/bundled-skills/workflows/tools/run-workflow.ts +12 -0
  143. package/src/config/bundled-tool-registry.ts +14 -2
  144. package/src/config/call-site-defaults.ts +5 -0
  145. package/src/config/feature-flag-registry.json +12 -4
  146. package/src/config/llm-context-resolution.ts +8 -0
  147. package/src/config/llm-resolver.ts +30 -0
  148. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +22 -0
  149. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +16 -0
  150. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +77 -0
  151. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +57 -0
  152. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +28 -0
  153. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +234 -0
  154. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +48 -0
  155. package/src/config/preloaded-apps/personal-page/src/components/media.ts +16 -0
  156. package/src/config/preloaded-apps/personal-page/src/index.html +20 -0
  157. package/src/config/preloaded-apps/personal-page/src/main.tsx +7 -0
  158. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +82 -0
  159. package/src/config/preloaded-apps/personal-page/src/styles.css +759 -0
  160. package/src/config/schema.ts +2 -0
  161. package/src/config/schemas/call-site-catalog.ts +7 -0
  162. package/src/config/schemas/heartbeat.ts +4 -1
  163. package/src/config/schemas/llm.ts +33 -26
  164. package/src/config/schemas/memory-retrospective.ts +19 -0
  165. package/src/config/schemas/platform.ts +8 -0
  166. package/src/config/schemas/services.ts +5 -2
  167. package/src/config/schemas/workflows.ts +42 -0
  168. package/src/config/skills.ts +3 -3
  169. package/src/context/compactor.ts +273 -39
  170. package/src/context/post-turn-tool-result-truncation.ts +23 -6
  171. package/src/context/tool-result-spool.ts +12 -17
  172. package/src/credential-execution/executable-discovery.ts +1 -1
  173. package/src/credential-execution/process-manager.ts +37 -3
  174. package/src/credential-execution/prompted-credential.ts +205 -0
  175. package/src/daemon/conversation-agent-loop-handlers.ts +14 -0
  176. package/src/daemon/conversation-process.ts +11 -2
  177. package/src/daemon/conversation-surfaces.ts +167 -3
  178. package/src/daemon/conversation-tool-setup.ts +103 -26
  179. package/src/daemon/conversation-usage.ts +2 -0
  180. package/src/daemon/conversation.ts +115 -11
  181. package/src/daemon/handlers/shared.ts +26 -14
  182. package/src/daemon/host-cu-proxy.ts +15 -12
  183. package/src/daemon/host-file-proxy.ts +15 -12
  184. package/src/daemon/host-transfer-proxy.ts +30 -24
  185. package/src/daemon/lifecycle.ts +40 -3
  186. package/src/daemon/message-protocol.ts +3 -0
  187. package/src/daemon/message-types/messages.ts +2 -10
  188. package/src/daemon/message-types/workflows.ts +49 -0
  189. package/src/daemon/parse-actual-tokens-from-error.test.ts +62 -1
  190. package/src/daemon/parse-actual-tokens-from-error.ts +43 -4
  191. package/src/daemon/process-message.ts +6 -0
  192. package/src/daemon/tool-setup-types.ts +57 -0
  193. package/src/daemon/wake-conversation-ops.ts +18 -0
  194. package/src/heartbeat/heartbeat-run-store.ts +8 -2
  195. package/src/home/feed-types.ts +1 -1
  196. package/src/ipc/gateway-flag-listener.ts +28 -6
  197. package/src/mcp/mcp-auth-state.ts +8 -20
  198. package/src/media/__tests__/image-models.test.ts +57 -0
  199. package/src/media/image-models.ts +66 -0
  200. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +38 -0
  201. package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +12 -2
  202. package/src/memory/__tests__/memory-retrospective-job.test.ts +911 -34
  203. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +227 -5
  204. package/src/memory/__tests__/memory-retrospective-state.test.ts +195 -0
  205. package/src/memory/__tests__/preloaded-apps.test.ts +85 -0
  206. package/src/memory/auto-analysis-enqueue.ts +14 -1
  207. package/src/memory/compaction-log-store-clickhouse.ts +6 -4
  208. package/src/memory/conversation-crud.ts +9 -2
  209. package/src/memory/conversation-disk-view.ts +1 -1
  210. package/src/memory/conversation-queries.ts +22 -7
  211. package/src/memory/db-init.ts +20 -0
  212. package/src/memory/db-maintenance.ts +16 -0
  213. package/src/memory/embedding-runtime-manager.ts +1 -1
  214. package/src/memory/llm-request-log-source-clickhouse.ts +112 -14
  215. package/src/memory/llm-request-log-source-local.ts +19 -1
  216. package/src/memory/llm-request-log-source.ts +35 -6
  217. package/src/memory/llm-request-log-store.ts +90 -2
  218. package/src/memory/memory-retrospective-constants.ts +9 -0
  219. package/src/memory/memory-retrospective-enqueue.ts +3 -6
  220. package/src/memory/memory-retrospective-fork-boundary.ts +94 -0
  221. package/src/memory/memory-retrospective-job.ts +500 -208
  222. package/src/memory/memory-retrospective-startup-cleanup.ts +97 -19
  223. package/src/memory/memory-retrospective-state.ts +85 -2
  224. package/src/memory/migrations/281-memory-retrospective-remembered-log.ts +40 -0
  225. package/src/memory/migrations/282-schedule-inference-profile.test.ts +77 -0
  226. package/src/memory/migrations/282-schedule-inference-profile.ts +26 -0
  227. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.test.ts +102 -0
  228. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.ts +53 -0
  229. package/src/memory/migrations/284-workflow-runs.ts +51 -0
  230. package/src/memory/migrations/285-schedule-workflow-mode.ts +26 -0
  231. package/src/memory/migrations/286-workflow-run-trust.ts +27 -0
  232. package/src/memory/migrations/287-conversation-origin-channel-index.ts +15 -0
  233. package/src/memory/migrations/288-backfill-origin-channel-from-bindings.ts +43 -0
  234. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +115 -0
  235. package/src/memory/migrations/290-schedule-capabilities.test.ts +77 -0
  236. package/src/memory/migrations/290-schedule-capabilities.ts +25 -0
  237. package/src/memory/migrations/__tests__/281-memory-retrospective-remembered-log.test.ts +96 -0
  238. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +571 -0
  239. package/src/memory/migrations/index.ts +10 -0
  240. package/src/memory/preloaded-apps.ts +116 -0
  241. package/src/memory/schema/infrastructure.ts +4 -0
  242. package/src/memory/schema/memory-core.ts +4 -0
  243. package/src/memory/v2/__tests__/concept-page-frontmatter-schema.test.ts +45 -0
  244. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +11 -7
  245. package/src/memory/v2/__tests__/page-store.test.ts +13 -2
  246. package/src/memory/v2/__tests__/qdrant.test.ts +24 -0
  247. package/src/memory/v2/frontmatter-sweep.ts +7 -6
  248. package/src/memory/v2/page-store.ts +4 -3
  249. package/src/memory/v2/qdrant.ts +42 -3
  250. package/src/memory/v2/types.ts +16 -10
  251. package/src/messaging/draft-store.ts +1 -1
  252. package/src/notifications/access-request-copy.ts +200 -113
  253. package/src/notifications/adapters/slack.ts +250 -111
  254. package/src/notifications/adapters/telegram.ts +7 -44
  255. package/src/notifications/approval-card-builder.ts +93 -0
  256. package/src/notifications/broadcaster.ts +74 -0
  257. package/src/notifications/conversation-pairing.ts +8 -6
  258. package/src/notifications/copy-composer.ts +32 -26
  259. package/src/notifications/decision-engine.ts +59 -7
  260. package/src/notifications/guardian-question-mode.ts +145 -155
  261. package/src/notifications/home-feed-side-effect.ts +28 -11
  262. package/src/notifications/notification-utils.ts +66 -0
  263. package/src/notifications/signal.ts +6 -0
  264. package/src/notifications/tool-approval-copy.ts +142 -0
  265. package/src/notifications/types.ts +19 -0
  266. package/src/permissions/threshold.ts +11 -0
  267. package/src/plugin-api/types.ts +16 -4
  268. package/src/plugins/defaults/compaction/window-manager.ts +44 -0
  269. package/src/plugins/defaults/index.ts +46 -0
  270. package/src/plugins/defaults/max-tokens-continue/continue-state-store.ts +53 -0
  271. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +80 -0
  272. package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +20 -0
  273. package/src/plugins/defaults/max-tokens-continue/package.json +14 -0
  274. package/src/plugins/defaults/memory-retrieval/hooks/__tests__/user-prompt-submit.test.ts +37 -0
  275. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +29 -1
  276. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +8 -3
  277. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +4 -2
  278. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +28 -18
  279. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +67 -0
  280. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +122 -22
  281. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +2 -0
  282. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +63 -1
  283. package/src/plugins/defaults/memory-v3-shadow/injector.ts +61 -18
  284. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +1 -1
  285. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +39 -10
  286. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +34 -1
  287. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +112 -47
  288. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +78 -15
  289. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +206 -0
  290. package/src/plugins/defaults/task-progress-nudge/package.json +15 -0
  291. package/src/prompts/__tests__/system-prompt.test.ts +100 -1
  292. package/src/prompts/__tests__/task-progress-hint-section.test.ts +5 -7
  293. package/src/prompts/normalize-onboarding.ts +2 -0
  294. package/src/prompts/persona-resolver.ts +3 -0
  295. package/src/prompts/system-prompt.ts +51 -2
  296. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +3 -1
  297. package/src/prompts/templates/system-sections.ts +8 -3
  298. package/src/providers/call-site-routing.ts +6 -3
  299. package/src/providers/fireworks/client.ts +3 -0
  300. package/src/providers/inference/auth.ts +52 -46
  301. package/src/providers/model-intents.ts +2 -2
  302. package/src/providers/openai/__tests__/coerce-object-args.test.ts +105 -0
  303. package/src/providers/openai/chat-completions-provider.ts +47 -9
  304. package/src/providers/openai/coerce-object-args.ts +104 -0
  305. package/src/providers/retry.ts +8 -5
  306. package/src/providers/types.ts +10 -0
  307. package/src/runtime/__tests__/agent-wake.test.ts +629 -7
  308. package/src/runtime/access-request-helper.ts +16 -9
  309. package/src/runtime/agent-wake.ts +302 -51
  310. package/src/runtime/assistant-stream-state.ts +141 -8
  311. package/src/runtime/background-job-runner.ts +9 -0
  312. package/src/runtime/channel-approval-types.ts +1 -0
  313. package/src/runtime/channel-invite-transports/telegram.ts +6 -5
  314. package/src/runtime/channel-invite-transports/voice.ts +2 -2
  315. package/src/runtime/channel-invite-types.ts +4 -2
  316. package/src/runtime/channel-retry-sweep.ts +19 -41
  317. package/src/runtime/finalize-event-delivery.ts +72 -0
  318. package/src/runtime/guardian-action-message-composer.ts +0 -54
  319. package/src/runtime/http-server.ts +6 -14
  320. package/src/runtime/http-types.ts +0 -1
  321. package/src/runtime/message-composer-types.ts +0 -9
  322. package/src/runtime/middleware/__tests__/rate-limiter.test.ts +63 -0
  323. package/src/runtime/middleware/auth.ts +27 -3
  324. package/src/runtime/middleware/rate-limiter.ts +28 -1
  325. package/src/runtime/migrations/vbundle-builder.ts +6 -5
  326. package/src/runtime/pending-interactions.ts +20 -1
  327. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +232 -173
  328. package/src/runtime/routes/__tests__/plugins-routes.test.ts +18 -0
  329. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +436 -0
  330. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +11 -0
  331. package/src/runtime/routes/approval-routes.ts +35 -8
  332. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -1
  333. package/src/runtime/routes/btw-routes.ts +37 -1
  334. package/src/runtime/routes/channel-delivery-routes.ts +11 -7
  335. package/src/runtime/routes/channel-route-definitions.ts +3 -0
  336. package/src/runtime/routes/channel-route-shared.ts +3 -1
  337. package/src/runtime/routes/consolidation-routes.ts +17 -13
  338. package/src/runtime/routes/conversation-compaction-routes.ts +159 -119
  339. package/src/runtime/routes/conversation-list-routes.ts +41 -4
  340. package/src/runtime/routes/conversation-query-routes.ts +196 -11
  341. package/src/runtime/routes/conversation-routes.ts +15 -1
  342. package/src/runtime/routes/credential-prompt-routes.ts +39 -17
  343. package/src/runtime/routes/empty-state-greeting-cache.ts +65 -0
  344. package/src/runtime/routes/heartbeat-routes.ts +18 -13
  345. package/src/runtime/routes/image-generation-routes.ts +20 -2
  346. package/src/runtime/routes/inbound-message-handler.ts +13 -12
  347. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +32 -31
  348. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +7 -5
  349. package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -21
  350. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +5 -5
  351. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +6 -15
  352. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +1 -1
  353. package/src/runtime/routes/index.ts +6 -0
  354. package/src/runtime/routes/log-export/AGENTS.md +1 -1
  355. package/src/runtime/routes/log-export/workspace-allowlist.ts +1 -1
  356. package/src/runtime/routes/migration-routes.ts +5 -9
  357. package/src/runtime/routes/plugins-routes.ts +26 -0
  358. package/src/runtime/routes/ps-routes.ts +10 -8
  359. package/src/runtime/routes/retrospective-routes.ts +235 -0
  360. package/src/runtime/routes/runs-pagination.ts +75 -0
  361. package/src/runtime/routes/schedule-routes.ts +367 -59
  362. package/src/runtime/routes/sounds-config-routes.ts +239 -0
  363. package/src/runtime/routes/surface-action-routes.ts +84 -4
  364. package/src/runtime/routes/workflow-routes.test.ts +372 -0
  365. package/src/runtime/routes/workflow-routes.ts +363 -0
  366. package/src/runtime/routes/workspace-routes.test.ts +61 -1
  367. package/src/runtime/routes/workspace-routes.ts +26 -1
  368. package/src/runtime/services/__tests__/analyze-conversation.test.ts +38 -0
  369. package/src/runtime/services/analyze-conversation.ts +26 -13
  370. package/src/schedule/inference-profile.ts +28 -0
  371. package/src/schedule/schedule-store.ts +152 -4
  372. package/src/schedule/scheduler-types.ts +6 -0
  373. package/src/schedule/scheduler.ts +96 -0
  374. package/src/security/secret-allowlist.ts +1 -1
  375. package/src/skills/path-classifier.ts +1 -1
  376. package/src/tools/apps/executors.ts +23 -0
  377. package/src/tools/apps/resolve-app-id.ts +42 -0
  378. package/src/tools/browser/browser-execution.ts +9 -11
  379. package/src/tools/credentials/broker.ts +4 -4
  380. package/src/tools/credentials/vault.ts +26 -137
  381. package/src/tools/executor.ts +69 -0
  382. package/src/tools/flag-gated-tools.test.ts +76 -0
  383. package/src/tools/permission-checker.ts +8 -1
  384. package/src/tools/registry.ts +51 -0
  385. package/src/tools/schedule/create.ts +77 -1
  386. package/src/tools/schedule/list.ts +1 -0
  387. package/src/tools/schedule/update.ts +73 -1
  388. package/src/tools/skills/execute.ts +56 -0
  389. package/src/tools/terminal/shell.ts +1 -1
  390. package/src/tools/ui-surface/definitions.ts +91 -2
  391. package/src/tools/workflows/manage-workflows.ts +183 -0
  392. package/src/tools/workflows/run-workflow.test.ts +442 -0
  393. package/src/tools/workflows/run-workflow.ts +88 -0
  394. package/src/types/onboarding-context.ts +2 -0
  395. package/src/usage/attribution.ts +24 -0
  396. package/src/util/canonicalize-identity.ts +12 -3
  397. package/src/util/platform.ts +17 -17
  398. package/src/watcher/__tests__/engine.test.ts +24 -0
  399. package/src/watcher/__tests__/telemetry.test.ts +135 -0
  400. package/src/watcher/engine.ts +7 -0
  401. package/src/watcher/telemetry.ts +74 -0
  402. package/src/workflows/capabilities.test.ts +365 -0
  403. package/src/workflows/capabilities.ts +359 -0
  404. package/src/workflows/deterministic-stringify.ts +27 -0
  405. package/src/workflows/engine-integration.test.ts +656 -0
  406. package/src/workflows/engine.test.ts +1144 -0
  407. package/src/workflows/engine.ts +1078 -0
  408. package/src/workflows/fanout-load.test.ts +168 -0
  409. package/src/workflows/journal-store.test.ts +369 -0
  410. package/src/workflows/journal-store.ts +470 -0
  411. package/src/workflows/leaf-runner.test.ts +704 -0
  412. package/src/workflows/leaf-runner.ts +589 -0
  413. package/src/workflows/library.test.ts +134 -0
  414. package/src/workflows/library.ts +124 -0
  415. package/src/workflows/run-manager.test.ts +711 -0
  416. package/src/workflows/run-manager.ts +593 -0
  417. package/src/workflows/sandbox-escape.test.ts +339 -0
  418. package/src/workflows/sandbox.test.ts +251 -0
  419. package/src/workflows/sandbox.ts +447 -0
  420. package/src/workspace/adaptive-thinking-repair.ts +33 -11
  421. package/src/workspace/migrations/021-move-signals-to-workspace.ts +1 -1
  422. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +1 -1
  423. package/src/workspace/migrations/026-backfill-install-meta.ts +1 -1
  424. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +2 -1
  425. package/src/workspace/migrations/031-drop-user-md.ts +1 -4
  426. package/src/workspace/migrations/048-remove-workspace-hooks.ts +1 -1
  427. package/src/workspace/migrations/056-release-notes-inference-profile-reordering.ts +5 -2
  428. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +1 -1
  429. package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +8 -2
  430. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +41 -14
  431. package/src/workspace/migrations/102-preserve-heartbeat-enabled-for-existing-workspaces.ts +69 -0
  432. package/src/workspace/migrations/103-upgrade-quality-profile-to-opus-4-8.ts +83 -0
  433. package/src/workspace/migrations/104-recheck-adaptive-thinking-model-implied-anthropic.ts +133 -0
  434. package/src/workspace/migrations/registry.ts +6 -0
  435. package/src/workspace/migrations/runner.ts +1 -1
  436. package/tsconfig.json +1 -1
  437. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -200
  438. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -579
  439. package/src/__tests__/guardian-action-store.test.ts +0 -106
  440. package/src/daemon/guardian-action-generators.ts +0 -71
  441. package/src/memory/guardian-action-store.ts +0 -484
  442. package/src/runtime/guardian-action-grant-minter.ts +0 -150
@@ -30,15 +30,22 @@
30
30
  * stable-prefix card and as a finder line (its current matched section);
31
31
  * selections are deduped by slug.
32
32
  *
33
- * Failure handling is recall-safe by construction:
33
+ * Failure handling distinguishes a DELIBERATE empty selection from an
34
+ * INFRASTRUCTURE failure — the two are different outcomes, not the same one:
34
35
  * - explicit `ids` → select exactly those candidates,
35
- * - explicit empty `ids: []` → select none (deliberate abstention),
36
+ * - explicit empty `ids: []` → select none (deliberate abstention) — a
37
+ * normal, non-error result; the turn proceeds,
36
38
  * - omitted `ids` → keep ALL candidates (the recall-safe "all of these are
37
39
  * relevant" signal),
38
- * - infrastructure failure (provider unavailable, a throw that survived the
39
- * provider's own retries, no usable `tool_use`, or a schema mismatch) →
40
- * select nothing after a short re-prompt retry, degrading to the
41
- * deterministic recall lanes the orchestrator unions in regardless.
40
+ * - empty candidate pool return none (nothing to select),
41
+ * - infrastructure failure (selector provider unavailable e.g. a transient
42
+ * CES credential blip drops the API key or no usable `tool_use` / schema
43
+ * mismatch surviving the short re-prompt retry) → throw
44
+ * {@link MemoryV3RetrievalUnavailableError}. There is NO deterministic-lane
45
+ * fallback: the LIVE injector propagates this to hard-fail the turn
46
+ * (retryable) rather than silently shipping it with no `<memory>` block,
47
+ * while the shadow/observation path swallows it and lets v2 retrieval serve
48
+ * the turn.
42
49
  */
43
50
 
44
51
  import { z } from "zod";
@@ -60,6 +67,24 @@ import type { MemoryRoutingTurn, SelectedPage, Slug } from "./types.js";
60
67
 
61
68
  const log = getLogger("memory-v3-pool-select");
62
69
 
70
+ /**
71
+ * Thrown when the pool selector cannot produce a selection because of an
72
+ * INFRASTRUCTURE failure — its LLM provider is unavailable (e.g. a transient
73
+ * CES credential blip drops the API key), or no usable `tool_use` survives the
74
+ * re-prompt retry. Deliberately DISTINCT from a deliberate empty selection
75
+ * (`ids: []`) and an empty candidate pool, both of which return normally.
76
+ *
77
+ * The LIVE memory-v3 injector propagates this to hard-fail the turn (a clean,
78
+ * retryable failure) rather than silently shipping with no memory; the
79
+ * shadow/observation path catches and swallows it.
80
+ */
81
+ export class MemoryV3RetrievalUnavailableError extends Error {
82
+ constructor(message: string) {
83
+ super(message);
84
+ this.name = "MemoryV3RetrievalUnavailableError";
85
+ }
86
+ }
87
+
63
88
  /** A dynamic-tail (finder) candidate: the slug plus the descriptor that
64
89
  * justifies it — a matched section for a needle/dense hit, or a curated link
65
90
  * description for an edge page. Rendered as a one-line snippet, prefixed
@@ -213,9 +238,11 @@ export async function selectPool(
213
238
  if (!provider) {
214
239
  log.warn(
215
240
  { candidateCount: ordered.length },
216
- "pool selector provider unavailable; degrading to deterministic lanes",
241
+ "pool selector provider unavailable failing the turn rather than dropping memory",
242
+ );
243
+ throw new MemoryV3RetrievalUnavailableError(
244
+ "memory-v3 pool selector provider unavailable",
217
245
  );
218
- return [];
219
246
  }
220
247
 
221
248
  // Two content blocks: the stable prefix (cards) carries the cache
@@ -265,9 +292,11 @@ export async function selectPool(
265
292
  if (parsed === null) {
266
293
  log.warn(
267
294
  { candidateCount: ordered.length },
268
- "pool selector could not obtain a selection after retries; degrading to deterministic lanes",
295
+ "pool selector could not obtain a selection after retries failing the turn rather than dropping memory",
296
+ );
297
+ throw new MemoryV3RetrievalUnavailableError(
298
+ "memory-v3 pool selector returned no usable selection after retries",
269
299
  );
270
- return [];
271
300
  }
272
301
 
273
302
  // Omitted `ids` is the recall-safe "keep all candidates" signal.
@@ -86,7 +86,40 @@ export async function ensureSectionCollection(
86
86
  const onDisk = config.memory.qdrant.onDisk;
87
87
 
88
88
  const exists = await client.collectionExists(SECTION_COLLECTION);
89
- if (!exists.exists) {
89
+ let needsCreate = !exists.exists;
90
+
91
+ // An existing collection sized to a different embedding dimension (e.g. a
92
+ // 384-dim collection now serving a 3072-dim embedder) carries the right
93
+ // vector at the wrong size — every upsert then fails with HTTP 400 and the
94
+ // collection is never repaired. Probe the schema and recreate on dimension
95
+ // drift; on a probe failure, assume compatible rather than risk a destructive
96
+ // recreate (mirrors the v2 concept-page collection posture). The next
97
+ // `backfillAllSections` regenerates the embeddings.
98
+ if (exists.exists) {
99
+ try {
100
+ const info = await client.getCollection(SECTION_COLLECTION);
101
+ const vectors = info.config?.params?.vectors;
102
+ const size =
103
+ vectors && typeof vectors === "object" && "size" in vectors
104
+ ? (vectors as { size?: unknown }).size
105
+ : undefined;
106
+ if (typeof size === "number" && size !== vectorSize) {
107
+ log.warn(
108
+ { collection: SECTION_COLLECTION, expected: vectorSize, found: size },
109
+ "Memory v3 section collection dimension drift — deleting and recreating; embeddings will be regenerated by the next backfill",
110
+ );
111
+ await client.deleteCollection(SECTION_COLLECTION);
112
+ needsCreate = true;
113
+ }
114
+ } catch (err) {
115
+ log.warn(
116
+ { err, collection: SECTION_COLLECTION },
117
+ "Failed to probe v3 section collection schema; assuming compatible",
118
+ );
119
+ }
120
+ }
121
+
122
+ if (needsCreate) {
90
123
  log.info(
91
124
  { collection: SECTION_COLLECTION, vectorSize },
92
125
  "Creating Qdrant collection for memory v3 sections",
@@ -1,25 +1,32 @@
1
1
  /**
2
- * Read-side store for the inspector's Memory V3 section. Reads the persisted
3
- * `memory_v3_selections` rows for a turn and re-renders an APPROXIMATE
4
- * `<memory>` block for what v3 selected, so the inspector can show the turn's
5
- * selection without re-running orchestration — which would be wrong anyway,
6
- * since the hot lane is frecency-stateful and can't be reproduced after the
7
- * fact.
2
+ * Read-side store for the inspector's Memory V3 panel. Reads the persisted
3
+ * `memory_v3_selections` rows for a turn (by the turn's message ids) and
4
+ * re-renders the `<memory>` block for what v3 selected, so the inspector can
5
+ * show the turn's selection without re-running orchestration — which would be
6
+ * wrong anyway, since the hot lane is frecency-stateful and can't be reproduced
7
+ * after the fact.
8
8
  *
9
- * The rendered text is inspector-only and NOT byte-identical to live
10
- * injection: the live injector freezes net-new compact CARDS into history
11
- * (`renderV3CardContent`) plus an ephemeral spotlight, while this store
12
- * re-renders the whole selection set as full/lead pages
13
- * (`renderV3SectionContent` with no persisted section identity see the
14
- * inline note at `injectedText`).
9
+ * The rendered text is inspector-only and NOT byte-identical to live injection:
10
+ * the live injector freezes net-new compact CARDS into history
11
+ * (`renderV3CardContent`) plus an ephemeral spotlight. Here we re-render each
12
+ * selection's MATCHED SECTION resolved from the persisted `(slug, ordinal)`
13
+ * against the current page when one was recorded, falling back to the
14
+ * full/lead page otherwise. Section text is re-derived from the current page,
15
+ * so it reflects bounded page-drift if the page changed since the turn (the
16
+ * same approximation the v2 inspector accepts).
15
17
  */
16
18
 
17
19
  import type { MemoryV3SelectionLog } from "../../../api/responses/memory-v3-selection-log.js";
18
20
  import { isAssistantFeatureFlagEnabled } from "../../../config/assistant-feature-flags.js";
19
21
  import { getConfig } from "../../../config/loader.js";
20
22
  import { getDb, getSqliteFrom } from "../../../memory/db-connection.js";
23
+ import { readPage } from "../../../memory/v2/page-store.js";
24
+ import { getWorkspaceDir } from "../../../util/platform.js";
25
+ import { capabilityOrDiskBody } from "./capabilities.js";
26
+ import { sectionByOrdinal } from "./orchestrate.js";
21
27
  import { renderV3SectionContent } from "./page-content.js";
22
28
  import { renderMemoryBlock } from "./render-injection.js";
29
+ import { buildSectionIndex } from "./sections.js";
23
30
  import {
24
31
  type Section,
25
32
  SELECTION_SOURCES,
@@ -31,16 +38,21 @@ const MEMORY_V3_SHADOW = "memory-v3-shadow" as const;
31
38
  const MEMORY_V3_LIVE = "memory-v3-live" as const;
32
39
 
33
40
  interface SelectionRow {
41
+ turn: number;
34
42
  slug: string;
35
43
  source: string;
36
44
  pinned: number;
45
+ section_ordinal: number | null;
46
+ section_title: string | null;
37
47
  }
38
48
 
49
+ const SELECTION_COLUMNS = `turn, slug, source, pinned, section_ordinal, section_title`;
50
+
39
51
  function rowsForTurn(conversationId: string, turn: number): SelectionRow[] {
40
52
  return getSqliteFrom(getDb())
41
53
  .query(
42
54
  /*sql*/ `
43
- SELECT slug, source, pinned FROM memory_v3_selections
55
+ SELECT ${SELECTION_COLUMNS} FROM memory_v3_selections
44
56
  WHERE conversation_id = ? AND turn = ?
45
57
  ORDER BY rowid
46
58
  `,
@@ -48,32 +60,59 @@ function rowsForTurn(conversationId: string, turn: number): SelectionRow[] {
48
60
  .all(conversationId, turn) as SelectionRow[];
49
61
  }
50
62
 
63
+ function rowsForMessageIds(messageIds: string[]): SelectionRow[] {
64
+ if (messageIds.length === 0) return [];
65
+ const placeholders = messageIds.map(() => "?").join(", ");
66
+ return getSqliteFrom(getDb())
67
+ .query(
68
+ /*sql*/ `
69
+ SELECT ${SELECTION_COLUMNS} FROM memory_v3_selections
70
+ WHERE message_id IN (${placeholders})
71
+ ORDER BY rowid
72
+ `,
73
+ )
74
+ .all(...messageIds) as SelectionRow[];
75
+ }
76
+
51
77
  /**
52
- * Build the inspector's v3 selection log for one turn of a conversation.
53
- *
54
- * `turn` is the inspected message's turn (the v2-activation log's turn). The
55
- * selection is returned ONLY for that exact turn; when there are no v3 rows for
56
- * it including when `turn` is null (the message has no v2-activation turn) —
57
- * this returns `null` rather than falling back to a different turn. A fallback
58
- * would attribute another turn's selection (e.g. a later turn's pages and
59
- * rendered block) to the inspected message, which corrupts shadow validation.
60
- *
61
- * Caveat: v3 rows are keyed by the orchestrator turn counter (`ctx.turnCount`)
62
- * while `turn` here is the v2 memory-tracker turn. They coincide for normal
63
- * turns, but if they diverge this simply yields `null` (no section) rather than
64
- * wrong data. Tying v3 rows to a message id for exact per-message attribution
65
- * regardless of counter drift is a documented follow-up.
66
- *
67
- * Selection rows are stored in selection order, so rendering them in row order
68
- * reproduces the block v3 would inject.
78
+ * Resolve each selection's persisted matched section `(slug, ordinal)` to the
79
+ * concrete `Section` in the CURRENT page, so the injected block renders the
80
+ * matched section rather than the full page. Only slugs with a recorded ordinal
81
+ * are resolved (core/hot/fresh/edge selections have none and render full-page).
82
+ * A page edited since the turn re-derives the current section at that ordinal,
83
+ * or falls back to full-page when the ordinal no longer exists.
69
84
  */
70
- export async function getMemoryV3SelectionForInspector(
71
- conversationId: string,
72
- turn: number | null | undefined,
73
- ): Promise<MemoryV3SelectionLog | null> {
74
- if (turn == null) return null;
85
+ async function reconstructMatchedSections(
86
+ rows: SelectionRow[],
87
+ ): Promise<Map<Slug, Section>> {
88
+ const sectionSlugs = rows
89
+ .filter((r) => r.section_ordinal != null)
90
+ .map((r) => r.slug);
91
+ if (sectionSlugs.length === 0) return new Map();
92
+
93
+ const workspaceDir = getWorkspaceDir();
94
+ const pageBody = (slug: Slug): Promise<string> =>
95
+ capabilityOrDiskBody(slug, async (s) => {
96
+ try {
97
+ return (await readPage(workspaceDir, s))?.body ?? "";
98
+ } catch {
99
+ return "";
100
+ }
101
+ });
102
+ const index = await buildSectionIndex(sectionSlugs, pageBody);
103
+
104
+ const sectionBySlug = new Map<Slug, Section>();
105
+ for (const row of rows) {
106
+ if (row.section_ordinal == null) continue;
107
+ const section = sectionByOrdinal(index, row.slug, row.section_ordinal);
108
+ if (section) sectionBySlug.set(row.slug, section);
109
+ }
110
+ return sectionBySlug;
111
+ }
75
112
 
76
- const rows = rowsForTurn(conversationId, turn);
113
+ async function buildSelectionLog(
114
+ rows: SelectionRow[],
115
+ ): Promise<MemoryV3SelectionLog | null> {
77
116
  if (rows.length === 0) return null;
78
117
 
79
118
  const config = getConfig();
@@ -81,25 +120,19 @@ export async function getMemoryV3SelectionForInspector(
81
120
  slug: r.slug,
82
121
  source: r.source,
83
122
  pinned: r.pinned === 1,
123
+ sectionOrdinal: r.section_ordinal,
124
+ sectionHeading: r.section_title,
84
125
  }));
85
126
  const slugs: Slug[] = selections.map((s) => s.slug);
86
- // NOTE: `injectedText` is APPROXIMATE — it renders the full/lead page for
87
- // every slug, not the exact matched section live injection used. The inspector
88
- // re-renders from persisted rows without re-running orchestration, and the
89
- // section identity is not persisted (`memory_v3_selections` stores
90
- // slug/source/pinned, not the section ordinal), so the section map is empty
91
- // and `renderV3SectionContent` falls back to the full page. Persisting the
92
- // injected section (a schema migration) is deliberately deferred: the A/B's
93
- // primary signal is slug-level recall via `summarizeSelections`, which is
94
- // exact — only this debug-oriented injected text is an approximation.
127
+ const sectionBySlug = await reconstructMatchedSections(rows);
95
128
  const injectedText = await renderMemoryBlock(
96
129
  slugs,
97
- new Map<Slug, Section>(),
130
+ sectionBySlug,
98
131
  renderV3SectionContent,
99
132
  );
100
133
 
101
134
  return {
102
- turn,
135
+ turn: rows[0]!.turn,
103
136
  live: isAssistantFeatureFlagEnabled(MEMORY_V3_LIVE, config),
104
137
  shadow: isAssistantFeatureFlagEnabled(MEMORY_V3_SHADOW, config),
105
138
  selections,
@@ -107,6 +140,38 @@ export async function getMemoryV3SelectionForInspector(
107
140
  };
108
141
  }
109
142
 
143
+ /**
144
+ * Build the inspector's v3 selection log for the inspected message's turn,
145
+ * keyed by the turn's message ids. This is the durable join: `writeSelections`
146
+ * logs rows with `message_id = NULL` and the turn-end backfill stamps them with
147
+ * the assistant message id, so a per-message lookup is robust against the drift
148
+ * between v2's tracker turn and v3's orchestrator `turnCount`. Returns `null`
149
+ * when no v3 rows match (e.g. a turn predating the message-id backfill, or a
150
+ * conversation with no v3 data). Message ids are globally unique, so no
151
+ * conversation scope is needed.
152
+ *
153
+ * Selection rows are stored in selection order, so rendering them in row order
154
+ * reproduces the block v3 would inject.
155
+ */
156
+ export async function getMemoryV3SelectionForInspectorByMessageIds(
157
+ messageIds: string[],
158
+ ): Promise<MemoryV3SelectionLog | null> {
159
+ return buildSelectionLog(rowsForMessageIds(messageIds));
160
+ }
161
+
162
+ /**
163
+ * Turn-keyed variant, retained for callers/tests that look up by an exact
164
+ * `(conversation, turn)`. Returns `null` when `turn` is null or there are no
165
+ * rows for it.
166
+ */
167
+ export async function getMemoryV3SelectionForInspector(
168
+ conversationId: string,
169
+ turn: number | null | undefined,
170
+ ): Promise<MemoryV3SelectionLog | null> {
171
+ if (turn == null) return null;
172
+ return buildSelectionLog(rowsForTurn(conversationId, turn));
173
+ }
174
+
110
175
  /**
111
176
  * Offline A/B aggregate over a conversation's logged v3 selections. Reads every
112
177
  * `memory_v3_selections` row for the conversation (all turns) and rolls them up
@@ -55,6 +55,7 @@ import { computeHotSet } from "./hot-set.js";
55
55
  import { computeLearnedEdgeGraph } from "./learned-edges.js";
56
56
  import type { OrchestrateResult } from "./orchestrate.js";
57
57
  import { orchestrate } from "./orchestrate.js";
58
+ import { MemoryV3RetrievalUnavailableError } from "./pool-select.js";
58
59
  import { ensureSectionCollection } from "./section-dense-store.js";
59
60
  import type { SectionNeedle } from "./section-needle.js";
60
61
  import { buildSectionNeedle } from "./section-needle.js";
@@ -418,6 +419,11 @@ interface SelectionRow {
418
419
  slug: Slug;
419
420
  source: SelectionSource;
420
421
  pinned: number;
422
+ /** Ordinal of the matched section a finder lane surfaced; null for
423
+ * core/hot/fresh/edge selections with no matched section. */
424
+ sectionOrdinal: number | null;
425
+ /** Heading of the matched section; null when there is no matched section. */
426
+ sectionTitle: string | null;
421
427
  }
422
428
 
423
429
  /**
@@ -438,17 +444,24 @@ export function attributeSelections(result: OrchestrateResult): SelectionRow[] {
438
444
  const finderLane = new Map(
439
445
  result.lanes.finder.map((c) => [c.slug, c.lane] as const),
440
446
  );
441
- return result.selections.map((sel) => ({
442
- slug: sel.slug,
443
- source: core.has(sel.slug)
444
- ? ("core" as const)
445
- : hot.has(sel.slug)
446
- ? ("hot" as const)
447
- : fresh.has(sel.slug)
448
- ? ("fresh" as const)
449
- : (finderLane.get(sel.slug) ?? "needle"),
450
- pinned: sel.pinned ? 1 : 0,
451
- }));
447
+ return result.selections.map((sel) => {
448
+ // The matched section is populated only for finder-lane hits (including
449
+ // hits on core/hot pages); core/hot/fresh/edge-only selections have none.
450
+ const section = result.matchedSections.get(sel.slug);
451
+ return {
452
+ slug: sel.slug,
453
+ source: core.has(sel.slug)
454
+ ? ("core" as const)
455
+ : hot.has(sel.slug)
456
+ ? ("hot" as const)
457
+ : fresh.has(sel.slug)
458
+ ? ("fresh" as const)
459
+ : (finderLane.get(sel.slug) ?? "needle"),
460
+ pinned: sel.pinned ? 1 : 0,
461
+ sectionOrdinal: section?.ordinal ?? null,
462
+ sectionTitle: section?.title ?? null,
463
+ };
464
+ });
452
465
  }
453
466
 
454
467
  /** Write the attributed selection rows to `memory_v3_selections`. */
@@ -461,17 +474,51 @@ export function writeSelections(
461
474
  const raw = getSqliteFrom(getDb());
462
475
  // PK is (conversation_id, turn, slug); OR REPLACE keeps the write
463
476
  // idempotent if the same turn is observed twice (e.g. a retried turn).
477
+ // `message_id` is written NULL here (the assistant message does not exist at
478
+ // injection time) and stamped at turn end by
479
+ // `backfillMemoryV3SelectionMessageId`.
464
480
  const stmt = raw.query(/*sql*/ `
465
481
  INSERT OR REPLACE INTO memory_v3_selections (
466
- conversation_id, turn, slug, source, pinned, created_at
467
- ) VALUES (?, ?, ?, ?, ?, ?)
482
+ conversation_id, turn, slug, source, pinned, created_at,
483
+ message_id, section_ordinal, section_title
484
+ ) VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?)
468
485
  `);
469
486
  const now = Date.now();
470
487
  for (const row of rows) {
471
- stmt.run(conversationId, turn, row.slug, row.source, row.pinned, now);
488
+ stmt.run(
489
+ conversationId,
490
+ turn,
491
+ row.slug,
492
+ row.source,
493
+ row.pinned,
494
+ now,
495
+ row.sectionOrdinal,
496
+ row.sectionTitle,
497
+ );
472
498
  }
473
499
  }
474
500
 
501
+ /**
502
+ * Stamp the turn's assistant message id onto the selection rows just written
503
+ * for it. Mirrors the v2 activation-log backfill: `writeSelections` writes
504
+ * `message_id = NULL` at injection time, and this runs at turn end once the
505
+ * assistant message exists. Relies on the single-threaded-per-conversation turn
506
+ * invariant — every NULL-`message_id` row for the conversation belongs to the
507
+ * turn that just finished. Lets the inspector look v3 selections up by the
508
+ * turn's message ids (robust against v2/v3 turn-counter drift).
509
+ */
510
+ export function backfillMemoryV3SelectionMessageId(
511
+ conversationId: string,
512
+ assistantMessageId: string,
513
+ ): void {
514
+ getSqliteFrom(getDb())
515
+ .query(
516
+ /*sql*/ `UPDATE memory_v3_selections SET message_id = ?
517
+ WHERE conversation_id = ? AND message_id IS NULL`,
518
+ )
519
+ .run(assistantMessageId, conversationId);
520
+ }
521
+
475
522
  /**
476
523
  * Run v3 orchestration for one turn and log the selection set, returning the
477
524
  * orchestrate result so a live caller can render it. Never throws — all
@@ -530,6 +577,16 @@ export async function observeTurn(
530
577
  writeSelections(conversationId, turnIndex, rows);
531
578
  return result;
532
579
  } catch (err) {
580
+ // An INFRASTRUCTURE failure (the selector lost its provider — e.g. a
581
+ // transient CES credential blip) must NOT be silently swallowed: re-throw
582
+ // so the LIVE injector hard-fails the turn (a clean, retryable failure)
583
+ // rather than shipping it with no `<memory>` block. The shadow/observation
584
+ // callers (the injector in shadow mode, runShadowObservation) catch this
585
+ // and swallow it, so observation never fails a turn. Other (non-infra)
586
+ // errors stay non-fatal and degrade to no v3 block, as before.
587
+ if (err instanceof MemoryV3RetrievalUnavailableError) {
588
+ throw err;
589
+ }
533
590
  log.warn(
534
591
  { err: err instanceof Error ? err.message : String(err), conversationId },
535
592
  "memory-v3 orchestration failed (non-fatal)",
@@ -553,5 +610,11 @@ export async function runShadowObservation(
553
610
  const config = getConfig();
554
611
  if (config.memory.enabled === false) return;
555
612
  if (!isAssistantFeatureFlagEnabled(MEMORY_V3_SHADOW, config)) return;
556
- await observeTurn(conversationId, turnIndex);
613
+ try {
614
+ await observeTurn(conversationId, turnIndex);
615
+ } catch {
616
+ // Shadow observation is fire-and-forget and must NEVER fail a turn.
617
+ // `observeTurn` now re-throws infra failures so the LIVE injector can
618
+ // hard-fail on them; here (observation only) we swallow them.
619
+ }
557
620
  }