@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
@@ -13,10 +13,12 @@
13
13
  * by `pinned_ids`; out-of-range IDs dropped; selections deduped by slug
14
14
  * (a page can appear as both a card and a finder line; pinned flags OR).
15
15
  * - Omitted `ids` → keep ALL candidates (recall-safe, slug-deduped).
16
- * - Explicit `ids: []` → keep none (deliberate abstention).
16
+ * - Explicit `ids: []` → keep none (deliberate abstention) — a normal result.
17
+ * - Empty candidate pool → keep none (nothing to select).
17
18
  * - Finder snippets are whitespace-collapsed and truncated (~300 chars).
18
- * - No provider / missing tool_use / schema mismatch / throw → keep none
19
- * (degrade to deterministic lanes), the last three after a re-prompt retry.
19
+ * - No provider / missing tool_use / schema mismatch / provider throw → throw
20
+ * MemoryV3RetrievalUnavailableError (an INFRA failure, deliberately DISTINCT
21
+ * from a deliberate empty selection), the last three after a re-prompt retry.
20
22
  * - One forced-tool `select_pages` call on the v3 L2 call site with
21
23
  * `disableTurnStartCache` (the tail varies per turn — the provider's
22
24
  * auto-anchor would never hit).
@@ -61,7 +63,8 @@ mock.module("../../../../util/logger.js", () => ({
61
63
  }),
62
64
  }));
63
65
 
64
- const { selectPool } = await import("../pool-select.js");
66
+ const { selectPool, MemoryV3RetrievalUnavailableError } =
67
+ await import("../pool-select.js");
65
68
  type SelectorPool = Parameters<typeof selectPool>[0];
66
69
 
67
70
  // ---------------------------------------------------------------------------
@@ -226,35 +229,42 @@ describe("selectPool — id mapping", () => {
226
229
  });
227
230
 
228
231
  // ---------------------------------------------------------------------------
229
- // selectPool — recall-safe fallbacks.
232
+ // selectPool — infrastructure failures THROW (no silent degradation). A
233
+ // deliberate empty selection and an empty pool (covered above) still return
234
+ // normally; only a genuine infra failure throws so the LIVE injector can
235
+ // hard-fail the turn instead of shipping it with no memory.
230
236
  // ---------------------------------------------------------------------------
231
237
 
232
- describe("selectPool — degradation on failure", () => {
233
- test("no provider → no pages, without calling the provider", async () => {
238
+ describe("selectPool — infrastructure failures throw", () => {
239
+ test("no provider → throws without calling the provider", async () => {
234
240
  providerStub = null;
235
- const result = await selectPool(makePool(), makeTurn("x"));
236
- expect(result).toEqual([]);
241
+ await expect(selectPool(makePool(), makeTurn("x"))).rejects.toThrow(
242
+ MemoryV3RetrievalUnavailableError,
243
+ );
237
244
  expect(providerCalls).toHaveLength(0);
238
245
  });
239
246
 
240
- test("missing tool_use → no pages after retrying", async () => {
247
+ test("missing tool_use → throws after retrying", async () => {
241
248
  providerStub = makeProvider(noToolResponse());
242
- const result = await selectPool(makePool(), makeTurn("x"));
243
- expect(result).toEqual([]);
249
+ await expect(selectPool(makePool(), makeTurn("x"))).rejects.toThrow(
250
+ MemoryV3RetrievalUnavailableError,
251
+ );
244
252
  expect(providerCalls).toHaveLength(3);
245
253
  });
246
254
 
247
- test("schema mismatch → no pages after retrying", async () => {
255
+ test("schema mismatch → throws after retrying", async () => {
248
256
  providerStub = makeProvider(toolUseResponse({ ids: "not-an-array" }));
249
- const result = await selectPool(makePool(), makeTurn("x"));
250
- expect(result).toEqual([]);
257
+ await expect(selectPool(makePool(), makeTurn("x"))).rejects.toThrow(
258
+ MemoryV3RetrievalUnavailableError,
259
+ );
251
260
  expect(providerCalls).toHaveLength(3);
252
261
  });
253
262
 
254
- test("provider throw → no pages after retrying", async () => {
263
+ test("provider throw → throws after retrying", async () => {
255
264
  providerStub = makeThrowingProvider();
256
- const result = await selectPool(makePool(), makeTurn("x"));
257
- expect(result).toEqual([]);
265
+ await expect(selectPool(makePool(), makeTurn("x"))).rejects.toThrow(
266
+ MemoryV3RetrievalUnavailableError,
267
+ );
258
268
  expect(providerCalls).toHaveLength(3);
259
269
  });
260
270
 
@@ -45,6 +45,12 @@ type ScrollPage = {
45
45
  next_page_offset: string | number | null;
46
46
  };
47
47
 
48
+ // A getCollection() response for an existing collection whose single dense
49
+ // vector matches the configured dimension (CONFIG.vectorSize === 4).
50
+ const MATCHING_SECTION_SCHEMA = {
51
+ config: { params: { vectors: { size: 4, distance: "Cosine" } } },
52
+ };
53
+
48
54
  const state = {
49
55
  collectionExists: false,
50
56
  collectionExistsCalls: 0,
@@ -54,6 +60,10 @@ const state = {
54
60
  upsertCalls: [] as Array<{ wait: boolean; points: MockPoint[] }>,
55
61
  deleteCalls: [] as Array<{ wait: boolean; filter: unknown }>,
56
62
  createCollectionThrows: null as Error | null,
63
+ getCollectionCalls: 0,
64
+ getCollectionInfo: MATCHING_SECTION_SCHEMA as unknown,
65
+ getCollectionThrows: null as Error | null,
66
+ deleteCollectionCalls: [] as string[],
57
67
  // Programmed `scroll` pages, consumed in order; each `scroll` call shifts one.
58
68
  scrollPages: [] as ScrollPage[],
59
69
  scrollCalls: [] as Array<{ limit: number; offset: unknown }>,
@@ -72,6 +82,16 @@ class MockQdrantClient {
72
82
  state.collectionExists = true;
73
83
  return {};
74
84
  }
85
+ async getCollection(_name: string) {
86
+ state.getCollectionCalls++;
87
+ if (state.getCollectionThrows) throw state.getCollectionThrows;
88
+ return state.getCollectionInfo;
89
+ }
90
+ async deleteCollection(name: string) {
91
+ state.deleteCollectionCalls.push(name);
92
+ state.collectionExists = false;
93
+ return {};
94
+ }
75
95
  async createPayloadIndex(
76
96
  _name: string,
77
97
  params: { field_name: string; field_schema: string },
@@ -131,6 +151,10 @@ function resetState(): void {
131
151
  state.upsertCalls.length = 0;
132
152
  state.deleteCalls.length = 0;
133
153
  state.createCollectionThrows = null;
154
+ state.getCollectionCalls = 0;
155
+ state.getCollectionInfo = MATCHING_SECTION_SCHEMA;
156
+ state.getCollectionThrows = null;
157
+ state.deleteCollectionCalls.length = 0;
134
158
  state.scrollPages.length = 0;
135
159
  state.scrollCalls.length = 0;
136
160
  embedState.calls.length = 0;
@@ -197,6 +221,49 @@ describe("memory v3 section-dense-store — collection lifecycle", () => {
197
221
  ]);
198
222
  });
199
223
 
224
+ test("recreates the collection when its dense vector dimension drifts", async () => {
225
+ state.collectionExists = true;
226
+ // Existing collection sized to a different embedding dimension than the
227
+ // configured 4 (e.g. a 384-dim collection from a prior model). Every upsert
228
+ // would fail with HTTP 400 until it is recreated; the next backfill
229
+ // re-embeds the sections.
230
+ state.getCollectionInfo = {
231
+ config: { params: { vectors: { size: 384, distance: "Cosine" } } },
232
+ };
233
+
234
+ await ensureSectionCollection(CONFIG);
235
+
236
+ expect(state.getCollectionCalls).toBe(1);
237
+ expect(state.deleteCollectionCalls).toEqual([SECTION_COLLECTION]);
238
+ expect(state.createCollectionCalls).toBe(1);
239
+ // The recreated collection is sized to the configured dimension.
240
+ const params = state.createCollectionParams as {
241
+ vectors: { size: number };
242
+ };
243
+ expect(params.vectors.size).toBe(4);
244
+ });
245
+
246
+ test("leaves a dimension-matched existing collection untouched", async () => {
247
+ state.collectionExists = true;
248
+ // Default getCollectionInfo (MATCHING_SECTION_SCHEMA) is size 4 === CONFIG.
249
+
250
+ await ensureSectionCollection(CONFIG);
251
+
252
+ expect(state.getCollectionCalls).toBe(1);
253
+ expect(state.deleteCollectionCalls).toEqual([]);
254
+ expect(state.createCollectionCalls).toBe(0);
255
+ });
256
+
257
+ test("treats a getCollection probe failure as compatible (no destructive recreate)", async () => {
258
+ state.collectionExists = true;
259
+ state.getCollectionThrows = new Error("transient REST error");
260
+
261
+ await ensureSectionCollection(CONFIG);
262
+
263
+ expect(state.deleteCollectionCalls).toEqual([]);
264
+ expect(state.createCollectionCalls).toBe(0);
265
+ });
266
+
200
267
  test("re-running ensure latches readiness (single existence probe)", async () => {
201
268
  state.collectionExists = false;
202
269
 
@@ -1,18 +1,20 @@
1
1
  /**
2
- * Tests for `assistant/src/memory/v3/selection-log-store.ts`.
2
+ * Tests for `selection-log-store.ts`.
3
3
  *
4
4
  * Asserts the inspector read path over `memory_v3_selections`:
5
- * - the selection for the exact turn;
6
- * - null for a null turn, and null for a turn with no v3 rows;
7
- * - NO fallback to another turn (would misattribute a future turn's
8
- * selection to the inspected message);
9
- * - source/pinned mapping and the rendered `<memory>` block;
5
+ * - the selection for the exact turn (turn-keyed variant);
6
+ * - the selection keyed by the turn's message ids (the route's path), which
7
+ * is robust against v2/v3 turn-counter drift and does not match rows that
8
+ * predate the message-id backfill;
9
+ * - null for a null turn, empty message ids, or no matching rows;
10
+ * - NO fallback to another turn/message;
11
+ * - source/pinned/section mapping and the rendered `<memory>` block;
10
12
  * - `live` / `shadow` reflect the flag resolver.
11
13
  *
12
14
  * `mock.module` is process-global and leaks into sibling files in a
13
- * `bun test src/memory/v3/` run, so every stub DELEGATES to the real
14
- * implementation unless this test is actively running (`storeMockActive`,
15
- * toggled in beforeEach/afterAll). Mirrors `shadow-plugin.test.ts`.
15
+ * `bun test <dir>` run, so every stub DELEGATES to the real implementation
16
+ * unless this test is actively running (`storeMockActive`, toggled in
17
+ * beforeEach/afterAll). Mirrors `shadow-plugin.test.ts`.
16
18
  */
17
19
 
18
20
  import { Database } from "bun:sqlite";
@@ -21,6 +23,7 @@ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
21
23
  import { drizzle } from "drizzle-orm/bun-sqlite";
22
24
 
23
25
  import { migrateAddMemoryV3Selections } from "../../../../memory/migrations/268-add-memory-v3-selections.js";
26
+ import { migrateMemoryV3SelectionsMessageIdAndSections } from "../../../../memory/migrations/283-memory-v3-selections-message-id-and-sections.js";
24
27
  import * as schema from "../../../../memory/schema.js";
25
28
 
26
29
  const realFlags = {
@@ -41,16 +44,27 @@ function makeDb() {
41
44
  testSqlite.exec("PRAGMA journal_mode=WAL");
42
45
  const db = drizzle(testSqlite, { schema });
43
46
  migrateAddMemoryV3Selections(db);
47
+ migrateMemoryV3SelectionsMessageIdAndSections(db);
44
48
  return db;
45
49
  }
46
50
 
47
51
  function seed(
48
52
  conversationId: string,
49
53
  turn: number,
50
- rows: Array<{ slug: string; source: string; pinned?: boolean }>,
54
+ rows: Array<{
55
+ slug: string;
56
+ source: string;
57
+ pinned?: boolean;
58
+ sectionOrdinal?: number;
59
+ sectionTitle?: string;
60
+ }>,
61
+ messageId: string | null = null,
51
62
  ): void {
52
63
  const stmt = testSqlite.query(
53
- `INSERT INTO memory_v3_selections (conversation_id, turn, slug, source, pinned, created_at) VALUES (?, ?, ?, ?, ?, ?)`,
64
+ `INSERT INTO memory_v3_selections
65
+ (conversation_id, turn, slug, source, pinned, created_at,
66
+ message_id, section_ordinal, section_title)
67
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
54
68
  );
55
69
  for (const r of rows) {
56
70
  stmt.run(
@@ -60,6 +74,9 @@ function seed(
60
74
  r.source,
61
75
  r.pinned ? 1 : 0,
62
76
  1000 + turn,
77
+ messageId,
78
+ r.sectionOrdinal ?? null,
79
+ r.sectionTitle ?? null,
63
80
  );
64
81
  }
65
82
  }
@@ -97,17 +114,23 @@ mock.module("../../../../memory/db-connection.js", () => ({
97
114
 
98
115
  mock.module("../page-content.js", () => ({
99
116
  ...realPageContent,
100
- // The inspector store renders via `renderV3SectionContent` with an empty
101
- // section map (sections are unavailable post-orchestration), so it falls back
102
- // to the full page. The mock stands in for that full-page render.
103
- renderV3SectionContent: async (slug: string) =>
117
+ // The inspector store reconstructs each selection's matched section from the
118
+ // current page; in this unit the test pages don't exist on disk, so the
119
+ // section map is empty and the renderer falls back to the full page. The mock
120
+ // stands in for that render and reflects whether a section was supplied.
121
+ renderV3SectionContent: async (slug: string, section?: { title: string }) =>
104
122
  storeMockActive
105
- ? `body for ${slug}`
123
+ ? section
124
+ ? `section[${section.title}] for ${slug}`
125
+ : `body for ${slug}`
106
126
  : realPageContent.renderV3SectionContent(slug, undefined),
107
127
  }));
108
128
 
109
- const { getMemoryV3SelectionForInspector, summarizeSelections } =
110
- await import("../selection-log-store.js");
129
+ const {
130
+ getMemoryV3SelectionForInspector,
131
+ getMemoryV3SelectionForInspectorByMessageIds,
132
+ summarizeSelections,
133
+ } = await import("../selection-log-store.js");
111
134
 
112
135
  beforeEach(() => {
113
136
  storeMockActive = true;
@@ -150,9 +173,11 @@ describe("getMemoryV3SelectionForInspector", () => {
150
173
  expect(await getMemoryV3SelectionForInspector("conv-3", 3)).toBeNull();
151
174
  });
152
175
 
153
- test("maps source/pinned and renders the <memory> block", async () => {
176
+ test("maps source/pinned/section and renders the <memory> block", async () => {
154
177
  // The second row carries a retired free-text source label (the column is
155
- // permissive); the inspector passes it through verbatim.
178
+ // permissive); the inspector passes it through verbatim. Neither row has a
179
+ // matched section, so section fields are null and the block falls back to
180
+ // full pages.
156
181
  seed("conv-4", 1, [
157
182
  { slug: "domain-a/page-1", source: "edge", pinned: true },
158
183
  { slug: "domain-b/page-2", source: "legacy-carry", pinned: false },
@@ -160,8 +185,20 @@ describe("getMemoryV3SelectionForInspector", () => {
160
185
 
161
186
  const log = await getMemoryV3SelectionForInspector("conv-4", 1);
162
187
  expect(log?.selections).toEqual([
163
- { slug: "domain-a/page-1", source: "edge", pinned: true },
164
- { slug: "domain-b/page-2", source: "legacy-carry", pinned: false },
188
+ {
189
+ slug: "domain-a/page-1",
190
+ source: "edge",
191
+ pinned: true,
192
+ sectionOrdinal: null,
193
+ sectionHeading: null,
194
+ },
195
+ {
196
+ slug: "domain-b/page-2",
197
+ source: "legacy-carry",
198
+ pinned: false,
199
+ sectionOrdinal: null,
200
+ sectionHeading: null,
201
+ },
165
202
  ]);
166
203
  expect(log?.injectedText).toContain("<memory>");
167
204
  expect(log?.injectedText).toContain("body for domain-a/page-1");
@@ -183,6 +220,69 @@ describe("getMemoryV3SelectionForInspector", () => {
183
220
  });
184
221
  });
185
222
 
223
+ describe("getMemoryV3SelectionForInspectorByMessageIds", () => {
224
+ test("returns the turn's selection (with section fields) keyed by message id", async () => {
225
+ seed(
226
+ "conv-m",
227
+ 0,
228
+ [
229
+ {
230
+ slug: "domain-a/page-1",
231
+ source: "needle",
232
+ sectionOrdinal: 2,
233
+ sectionTitle: "Heading A",
234
+ },
235
+ { slug: "domain-b/page-2", source: "core" },
236
+ ],
237
+ "msg-assistant-1",
238
+ );
239
+ // A different turn under a different message must not bleed in.
240
+ seed(
241
+ "conv-m",
242
+ 1,
243
+ [{ slug: "domain-c/page-9", source: "dense" }],
244
+ "msg-assistant-2",
245
+ );
246
+
247
+ const log = await getMemoryV3SelectionForInspectorByMessageIds([
248
+ "msg-assistant-1",
249
+ ]);
250
+ expect(log?.turn).toBe(0);
251
+ expect(log?.selections).toEqual([
252
+ {
253
+ slug: "domain-a/page-1",
254
+ source: "needle",
255
+ pinned: false,
256
+ sectionOrdinal: 2,
257
+ sectionHeading: "Heading A",
258
+ },
259
+ {
260
+ slug: "domain-b/page-2",
261
+ source: "core",
262
+ pinned: false,
263
+ sectionOrdinal: null,
264
+ sectionHeading: null,
265
+ },
266
+ ]);
267
+ expect(log?.injectedText).toContain("<memory>");
268
+ });
269
+
270
+ test("returns null for empty message ids and for an unmatched id", async () => {
271
+ seed("conv-m", 0, [{ slug: "domain-a/page-1", source: "needle" }], "msg-1");
272
+ expect(await getMemoryV3SelectionForInspectorByMessageIds([])).toBeNull();
273
+ expect(
274
+ await getMemoryV3SelectionForInspectorByMessageIds(["nope"]),
275
+ ).toBeNull();
276
+ });
277
+
278
+ test("does not match rows that predate the message-id backfill (null message_id)", async () => {
279
+ seed("conv-m", 0, [{ slug: "domain-a/page-1", source: "needle" }]); // message_id null
280
+ expect(
281
+ await getMemoryV3SelectionForInspectorByMessageIds(["any"]),
282
+ ).toBeNull();
283
+ });
284
+ });
285
+
186
286
  describe("summarizeSelections", () => {
187
287
  test("aggregates per-source counts, turn count, and distinct slugs", () => {
188
288
  // Turn 1: a needle + an edge selection.
@@ -34,6 +34,7 @@ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
34
34
  import { drizzle } from "drizzle-orm/bun-sqlite";
35
35
 
36
36
  import { migrateAddMemoryV3Selections } from "../../../../memory/migrations/268-add-memory-v3-selections.js";
37
+ import { migrateMemoryV3SelectionsMessageIdAndSections } from "../../../../memory/migrations/283-memory-v3-selections-message-id-and-sections.js";
37
38
  import * as schema from "../../../../memory/schema.js";
38
39
  import type { PageIndexEntry } from "../../../../memory/v2/page-index.js";
39
40
  import type {
@@ -105,6 +106,7 @@ function makeDb() {
105
106
  testSqlite.exec("PRAGMA journal_mode=WAL");
106
107
  const db = drizzle(testSqlite, { schema });
107
108
  migrateAddMemoryV3Selections(db);
109
+ migrateMemoryV3SelectionsMessageIdAndSections(db);
108
110
  return db;
109
111
  }
110
112
  mock.module("../../../../memory/db-connection.js", () => ({
@@ -29,6 +29,7 @@ import { drizzle } from "drizzle-orm/bun-sqlite";
29
29
 
30
30
  import { migrateAddMemoryV3Selections } from "../../../../memory/migrations/268-add-memory-v3-selections.js";
31
31
  import { migrateAddMemoryV3EverInjected } from "../../../../memory/migrations/277-add-memory-v3-ever-injected.js";
32
+ import { migrateMemoryV3SelectionsMessageIdAndSections } from "../../../../memory/migrations/283-memory-v3-selections-message-id-and-sections.js";
32
33
  import * as schema from "../../../../memory/schema.js";
33
34
  import type { HotSetEntry, HotSetOptions } from "../hot-set.js";
34
35
  import type { OrchestrateResult } from "../orchestrate.js";
@@ -154,6 +155,7 @@ function makeDb() {
154
155
  testSqlite.exec("PRAGMA journal_mode=WAL");
155
156
  const db = drizzle(testSqlite, { schema });
156
157
  migrateAddMemoryV3Selections(db);
158
+ migrateMemoryV3SelectionsMessageIdAndSections(db);
157
159
  // The live injector's net-new dedup reads/writes the everInjected store.
158
160
  migrateAddMemoryV3EverInjected(db);
159
161
  return db;
@@ -386,6 +388,7 @@ const {
386
388
  } = await import("../shadow-plugin.js");
387
389
  const { memoryV3Injector, resetMemoryV3InjectorStateForTests } =
388
390
  await import("../injector.js");
391
+ const { MemoryV3RetrievalUnavailableError } = await import("../pool-select.js");
389
392
 
390
393
  afterAll(() => {
391
394
  shadowMockActive = false;
@@ -545,7 +548,15 @@ describe("memory-v3 shadow plugin", () => {
545
548
  finder: [{ slug: "page-core", descriptor: "", lane: "needle" }],
546
549
  },
547
550
  });
548
- expect(rows).toEqual([{ slug: "page-core", source: "core", pinned: 0 }]);
551
+ expect(rows).toEqual([
552
+ {
553
+ slug: "page-core",
554
+ source: "core",
555
+ pinned: 0,
556
+ sectionOrdinal: null,
557
+ sectionTitle: null,
558
+ },
559
+ ]);
549
560
  });
550
561
 
551
562
  test("the turn passed to orchestrate carries the latest user message", async () => {
@@ -795,3 +806,54 @@ describe("memory-v3 shadow plugin", () => {
795
806
  });
796
807
  });
797
808
  });
809
+
810
+ describe("memory-v3 infrastructure-failure handling (hard-fail vs swallow)", () => {
811
+ const throwInfra = () =>
812
+ orchestrateSpy.mockImplementationOnce(async () => {
813
+ throw new MemoryV3RetrievalUnavailableError(
814
+ "selector provider unavailable",
815
+ );
816
+ });
817
+
818
+ test("LIVE injector HARD-FAILS the turn on an infra failure (no silent memory loss)", async () => {
819
+ liveEnabled = true;
820
+ shadowEnabled = false;
821
+ throwInfra();
822
+
823
+ await expect(produce("conv-infra-live", 0)).rejects.toThrow(
824
+ MemoryV3RetrievalUnavailableError,
825
+ );
826
+ });
827
+
828
+ test("SHADOW injector swallows an infra failure (v2 fallback) — no throw, no block", async () => {
829
+ liveEnabled = false;
830
+ shadowEnabled = true;
831
+ throwInfra();
832
+
833
+ // Shadow mode: v2 retrieval still ran this turn, so the v3 injector returns
834
+ // null rather than failing the turn.
835
+ expect(await produce("conv-infra-shadow", 0)).toBeNull();
836
+ });
837
+
838
+ test("runShadowObservation never throws on an infra failure (fire-and-forget)", async () => {
839
+ liveEnabled = false;
840
+ shadowEnabled = true;
841
+ throwInfra();
842
+
843
+ await expect(
844
+ runShadowObservation("conv-infra-obs", 0),
845
+ ).resolves.toBeUndefined();
846
+ });
847
+
848
+ test("LIVE injector stays NON-fatal on a non-infra error (degrades to no v3 block)", async () => {
849
+ liveEnabled = true;
850
+ shadowEnabled = false;
851
+ orchestrateSpy.mockImplementationOnce(async () => {
852
+ throw new Error("some unexpected non-infra bug");
853
+ });
854
+
855
+ // Only INFRA failures hard-fail; any other error stays non-fatal so a bug
856
+ // in one lane can't take every turn down.
857
+ expect(await produce("conv-nonfatal-live", 0)).toBeNull();
858
+ });
859
+ });
@@ -24,17 +24,22 @@
24
24
  * all-repeat turn returns an EMPTY-TEXT block: assembly attaches nothing,
25
25
  * but the block's presence still keys v2 suppression (v3 ran and owns the
26
26
  * `<memory>` layer this turn). A `null` return (failure / empty selection /
27
- * every net-new card rendered empty) leaves v2's block intact fallback to
28
- * v2 rather than a memory-less turn.
27
+ * every net-new card rendered empty) attaches no v3 block: in shadow mode
28
+ * (`memory-v3-live` off) v2 still ran this turn, so its block stays as the
29
+ * fallback; under `memory-v3-live` the user-prompt-submit hook skips v2
30
+ * retrieval entirely, so a null return leaves the turn with no NEW injected
31
+ * memory (prior turns' frozen cards still ride history).
29
32
  *
30
33
  * - {@link memoryV3SpotlightInjector} (id `memory-v3-spotlight`,
31
- * `append-user-tail`): the EPHEMERAL layer. Renders the top `spotlight.n`
34
+ * `after-memory-prefix`): the EPHEMERAL layer. Renders the top `spotlight.n`
32
35
  * selected finder hits' matched sections, plus the previous
33
36
  * `spotlight.windowTurns` turns' entries from an in-memory per-conversation
34
37
  * ring (a daemon restart simply re-warms it), as a `<memory_spotlight>`
35
- * block at the current-message tail. Assembly strip-and-replaces this block
36
- * every turn (scoped to this block id only); it is never persisted to
37
- * metadata.
38
+ * block spliced immediately after the `<memory>` cards block (so the two
39
+ * memory layers sit adjacent in the prefix, ahead of the user's message
40
+ * text). Assembly strip-and-replaces this block every turn (scoped to this
41
+ * block id only); it is never persisted to metadata, so the frozen card
42
+ * prefix it follows stays byte-stable and cached regardless.
38
43
  *
39
44
  * Flag gating is unchanged: `memory-v3-live` attaches blocks; `memory-v3-shadow`
40
45
  * (live off) logs what WOULD inject (net-new slugs + bytes + spotlight refs)
@@ -73,6 +78,7 @@ import { cardBytes } from "./card.js";
73
78
  import { getActiveSlugs, recordInjected } from "./ever-injected-store.js";
74
79
  import type { OrchestrateResult } from "./orchestrate.js";
75
80
  import { renderV3CardContent } from "./page-content.js";
81
+ import { MemoryV3RetrievalUnavailableError } from "./pool-select.js";
76
82
  import { schedulePruneValve } from "./prune.js";
77
83
  import {
78
84
  renderCardsBlockInner,
@@ -238,11 +244,32 @@ export const memoryV3Injector: Injector = {
238
244
  if (!live && !shadow) return null;
239
245
  if (!isPersonalMemoryAllowed(ctx.trust)) return null;
240
246
 
241
- const result = await observeTurnOnce(ctx.conversationId, ctx.turnIndex);
242
- // Empty selection falls back to v2 (return null): v2 suppression keys off
243
- // BOTH the flag AND a produced block, so a selector failure or a turn with
244
- // nothing selected ships v2 memory rather than no memory.
245
- if (!result || result.selections.length === 0) return null;
247
+ let observed: OrchestrateResult | null;
248
+ try {
249
+ observed = await observeTurnOnce(ctx.conversationId, ctx.turnIndex);
250
+ } catch (err) {
251
+ // A memory-v3 INFRASTRUCTURE failure (the selector lost its provider
252
+ // e.g. a transient CES credential blip). Under `memory-v3-live` the
253
+ // user-prompt-submit hook already skipped v2 retrieval, so swallowing
254
+ // here would ship the turn with NO memory at all — exactly the silent
255
+ // degradation we want to eliminate. Hard-fail the turn instead (a clean,
256
+ // retryable error). In shadow mode v2 still ran this turn, so fall back
257
+ // to it (return null). Non-infra errors are already swallowed inside
258
+ // observeTurn; anything else reaching here stays non-fatal.
259
+ if (live && err instanceof MemoryV3RetrievalUnavailableError) {
260
+ throw err;
261
+ }
262
+ return null;
263
+ }
264
+ // Empty selection → return null (attach nothing). Returning null (vs an
265
+ // empty block) preserves the shadow-mode v2 fallback: v2 suppression keys
266
+ // off BOTH the flag AND a produced block, so in shadow a turn with nothing
267
+ // selected ships v2's memory. Under `memory-v3-live` the hook skipped v2
268
+ // retrieval, so the turn simply gets no injected memory.
269
+ if (!observed || observed.selections.length === 0) return null;
270
+ // `const` so the non-null narrowing survives capture in the `commit`
271
+ // closure below (a `let` would re-widen to `OrchestrateResult | null`).
272
+ const result = observed;
246
273
 
247
274
  try {
248
275
  const active = getActiveSlugs(ctx.conversationId);
@@ -258,11 +285,13 @@ export const memoryV3Injector: Injector = {
258
285
  const card = await renderV3CardContent(slug);
259
286
  if (card.trim().length > 0) cards.push({ slug, card });
260
287
  }
261
- // Every net-new card rendered empty: return null (fall back to v2)
262
- // rather than an empty-text block suppressing v2 with nothing to show
263
- // would ship a memory-less turn. Distinct from the all-repeat case
264
- // (empty `netNew`), where the empty block correctly keeps v2 suppressed
265
- // because the cards already ride history.
288
+ // Every net-new card rendered empty: return null rather than an
289
+ // empty-text block. Returning null preserves the shadow-mode v2 fallback
290
+ // (an empty block would key v2 suppression yet show nothing); under
291
+ // `memory-v3-live` there is no v2 block, so the turn simply gets no new
292
+ // memory. Distinct from the all-repeat case (empty `netNew`), where the
293
+ // empty block correctly keeps v2 suppressed because the cards already
294
+ // ride history.
266
295
  if (netNew.length > 0 && cards.length === 0) return null;
267
296
  const entries = cards.map(({ slug, card }) => ({
268
297
  slug,
@@ -332,7 +361,7 @@ export const memoryV3Injector: Injector = {
332
361
  err: err instanceof Error ? err.message : String(err),
333
362
  conversationId: ctx.conversationId,
334
363
  },
335
- "memory-v3 live render failed (non-fatal) — falling back to v2",
364
+ "memory-v3 live render failed (non-fatal) — returning null (no v3 block this turn)",
336
365
  );
337
366
  return null;
338
367
  }
@@ -369,9 +398,23 @@ export const memoryV3SpotlightInjector: Injector = {
369
398
  return {
370
399
  id: MEMORY_V3_SPOTLIGHT_BLOCK_ID,
371
400
  text: wrapMemorySpotlightBlock(renderSpotlightInner(window)),
372
- placement: "append-user-tail",
401
+ // Splices right after the `<memory>` cards block (this injector's
402
+ // order 1001 runs after the cards' 1000, and `countMemoryPrefixBlocks`
403
+ // counts the cards `<memory>` block but not `<memory_spotlight>`, so the
404
+ // spotlight lands immediately after the cards rather than at the user
405
+ // tail). Cache-neutral: the block is strip-and-replaced from prior
406
+ // messages by block id every turn regardless of placement, so the
407
+ // frozen card prefix stays byte-stable and cached.
408
+ placement: "after-memory-prefix",
373
409
  };
374
410
  } catch (err) {
411
+ // Live-only injector: an infra failure must hard-fail the turn. The cards
412
+ // injector (ordered ahead of this one) normally throws first, so this
413
+ // path is defensive — it keeps the behavior correct if the cards injector
414
+ // is ever disabled or reordered.
415
+ if (err instanceof MemoryV3RetrievalUnavailableError) {
416
+ throw err;
417
+ }
375
418
  log.warn(
376
419
  {
377
420
  err: err instanceof Error ? err.message : String(err),
@@ -383,7 +383,7 @@ export async function orchestrate(
383
383
  * when the article or ordinal is not in the index (e.g. the dense store is
384
384
  * ahead of the in-memory rebuild).
385
385
  */
386
- function sectionByOrdinal(
386
+ export function sectionByOrdinal(
387
387
  index: SectionIndex,
388
388
  article: Slug,
389
389
  ordinal: number,