@vellumai/assistant 0.11.3 → 0.11.4-dev.202608190019.b94dbf2

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 (596) hide show
  1. package/AGENTS.md +9 -3
  2. package/ARCHITECTURE.md +32 -10
  3. package/docs/architecture/memory.md +43 -5
  4. package/docs/architecture/security.md +96 -152
  5. package/docs/architecture/turn-actor.md +70 -0
  6. package/docs/browser-use-architecture-phase2.md +128 -56
  7. package/docs/flux-turn-detection-spike.md +248 -0
  8. package/docs/guardian-request-flow.md +35 -0
  9. package/docs/stt-provider-onboarding.md +3 -1
  10. package/knip.json +3 -0
  11. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  13. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  17. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  18. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  19. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  20. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  21. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  23. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
  24. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
  25. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  26. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  27. package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  28. package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  29. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  30. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
  31. package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  32. package/openapi.yaml +289 -113
  33. package/package.json +1 -1
  34. package/scripts/voice-ttft-spike.ts +3 -3
  35. package/scripts/write-plugin-api-shim.ts +10 -0
  36. package/src/__tests__/app-compiler.test.ts +38 -3
  37. package/src/__tests__/app-control-flow.test.ts +1 -0
  38. package/src/__tests__/approval-routes-http.test.ts +2 -2
  39. package/src/__tests__/assistant-feature-flag-guard.test.ts +25 -3
  40. package/src/__tests__/attachments-store.test.ts +21 -12
  41. package/src/__tests__/byok-default-profile-ensure.test.ts +17 -0
  42. package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
  43. package/src/__tests__/call-site-routing-provider.test.ts +1 -1
  44. package/src/__tests__/channel-availability-routes.test.ts +14 -1
  45. package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
  46. package/src/__tests__/channel-delivery-store.test.ts +14 -14
  47. package/src/__tests__/channel-inbound-disk-pressure.test.ts +4 -4
  48. package/src/__tests__/channel-setup-panel-ack.test.ts +1 -1
  49. package/src/__tests__/checker.test.ts +20 -314
  50. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
  51. package/src/__tests__/compaction-events.test.ts +8 -10
  52. package/src/__tests__/config-loader-backfill.test.ts +3 -3
  53. package/src/__tests__/config-schema.test.ts +25 -10
  54. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  55. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
  56. package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
  57. package/src/__tests__/conversation-agent-loop.test.ts +32 -21
  58. package/src/__tests__/conversation-attachments.test.ts +0 -1
  59. package/src/__tests__/conversation-attention-store.test.ts +63 -0
  60. package/src/__tests__/conversation-confirmation-signals.test.ts +112 -0
  61. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
  62. package/src/__tests__/conversation-fork-crud.test.ts +69 -0
  63. package/src/__tests__/conversation-fork-referential.test.ts +67 -0
  64. package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
  65. package/src/__tests__/conversation-load-history-repair.test.ts +209 -0
  66. package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
  67. package/src/__tests__/conversation-queue.test.ts +154 -4
  68. package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
  69. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
  70. package/src/__tests__/conversation-routes-guardian-reply.test.ts +9 -9
  71. package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
  72. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
  73. package/src/__tests__/conversation-runtime-assembly.test.ts +187 -102
  74. package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
  75. package/src/__tests__/conversation-slash-queue.test.ts +3 -0
  76. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +1 -0
  77. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +1 -0
  78. package/src/__tests__/conversation-surfaces-app-control.test.ts +1 -0
  79. package/src/__tests__/conversation-surfaces-app-open.test.ts +1 -1
  80. package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -1
  81. package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +21 -14
  82. package/src/__tests__/conversation-surfaces-queued-emit.test.ts +1 -0
  83. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1 -0
  84. package/src/__tests__/conversation-surfaces-standalone.test.ts +1 -0
  85. package/src/__tests__/conversation-surfaces-state-update.test.ts +1 -1
  86. package/src/__tests__/conversation-surfaces-table-action.test.ts +1 -1
  87. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -1
  88. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +1 -1
  89. package/src/__tests__/conversation-tool-setup-attribution.test.ts +1 -1
  90. package/src/__tests__/credential-prompt-route.test.ts +7 -10
  91. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  92. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  93. package/src/__tests__/custom-profile-ensure.test.ts +5 -1
  94. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  95. package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
  96. package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
  97. package/src/__tests__/document-append-idempotency.test.ts +233 -0
  98. package/src/__tests__/document-sync-tags.test.ts +0 -75
  99. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  100. package/src/__tests__/edit-propagation.test.ts +107 -11
  101. package/src/__tests__/file-ops-service.test.ts +163 -30
  102. package/src/__tests__/filesystem-tools.test.ts +23 -24
  103. package/src/__tests__/gateway-only-guard.test.ts +2 -5
  104. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  105. package/src/__tests__/helpers/mock-actor-context.ts +49 -0
  106. package/src/__tests__/helpers/mock-conversation.ts +13 -1
  107. package/src/__tests__/host-file-read-tool.test.ts +16 -19
  108. package/src/__tests__/http-user-message-parity.test.ts +1 -1
  109. package/src/__tests__/init-feature-flag-overrides.test.ts +49 -0
  110. package/src/__tests__/injector-chain.test.ts +63 -41
  111. package/src/__tests__/injector-disk-pressure.test.ts +11 -23
  112. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  113. package/src/__tests__/llm-context-resolution.test.ts +73 -1
  114. package/src/__tests__/llm-schema.test.ts +5 -2
  115. package/src/__tests__/managed-skill-lifecycle.test.ts +7 -0
  116. package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
  117. package/src/__tests__/media-generate-image.test.ts +131 -21
  118. package/src/__tests__/memory-retrieval-hook.test.ts +100 -7
  119. package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
  120. package/src/__tests__/mtime-cache.test.ts +3 -1
  121. package/src/__tests__/non-member-access-request.test.ts +0 -20
  122. package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
  123. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  124. package/src/__tests__/plugin-api-store-credential.test.ts +261 -0
  125. package/src/__tests__/plugin-api-webhook-url.test.ts +10 -7
  126. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  127. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  128. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  129. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
  130. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +7 -4
  131. package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
  132. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
  133. package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
  134. package/src/__tests__/proxy-approval-callback.test.ts +1 -0
  135. package/src/__tests__/qdrant-manager.test.ts +14 -1
  136. package/src/__tests__/reaction-persistence.test.ts +200 -7
  137. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  138. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  139. package/src/__tests__/run-conversation-turn-persistence.test.ts +460 -105
  140. package/src/__tests__/run-due-schedules.test.ts +21 -0
  141. package/src/__tests__/scaffold-managed-skill-tool.test.ts +187 -18
  142. package/src/__tests__/schedule-routes.test.ts +23 -0
  143. package/src/__tests__/schedule-store.test.ts +17 -0
  144. package/src/__tests__/scoped-approval-grants.test.ts +11 -6
  145. package/src/__tests__/secret-ingress-channel.test.ts +0 -1
  146. package/src/__tests__/secret-ingress-http.test.ts +1 -1
  147. package/src/__tests__/send-endpoint-busy.test.ts +3 -3
  148. package/src/__tests__/skills.test.ts +32 -0
  149. package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
  150. package/src/__tests__/starter-task-flow.test.ts +5 -4
  151. package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
  152. package/src/__tests__/subagent-fork-prompt-role.test.ts +1 -1
  153. package/src/__tests__/subagent-spawn-and-await.test.ts +18 -17
  154. package/src/__tests__/subagent-tool-gate-mode.test.ts +1 -1
  155. package/src/__tests__/subagent-tools.test.ts +81 -101
  156. package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +1 -0
  157. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  158. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  159. package/src/__tests__/tool-executor.test.ts +68 -39
  160. package/src/__tests__/turn-events-store.test.ts +43 -0
  161. package/src/__tests__/ui-choice-copy-surfaces.test.ts +1 -1
  162. package/src/__tests__/ui-shape-teaching.test.ts +33 -0
  163. package/src/__tests__/ui-visual-surface.test.ts +1 -1
  164. package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
  165. package/src/__tests__/ui-work-result-surface.test.ts +1 -1
  166. package/src/__tests__/user-plugin-loader.test.ts +3 -1
  167. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  168. package/src/__tests__/visible-app-context.test.ts +16 -9
  169. package/src/__tests__/voice-config-update.test.ts +40 -0
  170. package/src/__tests__/voice-scoped-grant-consumer.test.ts +5 -3
  171. package/src/__tests__/voice-session-bridge.test.ts +85 -29
  172. package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
  173. package/src/__tests__/worker-plugin-surface.test.ts +77 -0
  174. package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
  175. package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
  176. package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
  177. package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
  178. package/src/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -0
  179. package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
  180. package/src/acp/__tests__/auth-required.test.ts +161 -0
  181. package/src/acp/acp-claude-oauth.ts +19 -2
  182. package/src/acp/agent-process.test.ts +100 -0
  183. package/src/acp/agent-process.ts +29 -26
  184. package/src/acp/auth-required.ts +102 -0
  185. package/src/acp/session-manager.test.ts +119 -0
  186. package/src/acp/session-manager.ts +76 -3
  187. package/src/api/events/acp-auth-required.ts +55 -0
  188. package/src/api/events/host-file.ts +2 -2
  189. package/src/api/index.ts +7 -0
  190. package/src/api/surfaces.ts +12 -3
  191. package/src/apps/app-store.ts +3 -0
  192. package/src/bundler/package-resolver.ts +2 -30
  193. package/src/calls/__tests__/voice-session-bridge.test.ts +194 -11
  194. package/src/calls/__tests__/voice-triage-escalate.test.ts +102 -0
  195. package/src/calls/call-controller.ts +19 -3
  196. package/src/calls/call-setup-flow.ts +0 -1
  197. package/src/calls/media-stream-stt-session.ts +15 -0
  198. package/src/calls/voice-session-bridge.ts +115 -41
  199. package/src/calls/voice-triage-escalate.ts +105 -2
  200. package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
  201. package/src/channels/config.ts +13 -0
  202. package/src/channels/plugin-channel-declarations.ts +108 -0
  203. package/src/channels/types.ts +30 -0
  204. package/src/cli/AGENTS.md +5 -2
  205. package/src/cli/bundled-modules.ts +29 -0
  206. package/src/cli/commands/credentials.help.ts +2 -2
  207. package/src/cli/commands/db/repair.ts +4 -8
  208. package/src/cli/commands/domain.ts +6 -3
  209. package/src/cli/commands/email.ts +6 -3
  210. package/src/cli/commands/inference-providers.ts +1 -1
  211. package/src/cli/commands/keys.ts +8 -3
  212. package/src/cli/commands/mcp.help.ts +13 -4
  213. package/src/cli/commands/mcp.ts +9 -0
  214. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  215. package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
  216. package/src/cli/commands/memory/index.help.ts +84 -28
  217. package/src/cli/commands/memory/index.ts +2 -0
  218. package/src/cli/commands/memory/memory-v2.ts +58 -54
  219. package/src/cli/commands/memory/memory-v3.ts +64 -0
  220. package/src/cli/commands/memory/memory-validate.ts +18 -0
  221. package/src/cli/commands/plugins.ts +85 -36
  222. package/src/cli/commands/schedules.ts +35 -1
  223. package/src/cli/commands/stt.help.ts +27 -2
  224. package/src/cli/commands/trust.ts +3 -14
  225. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
  226. package/src/cli/lib/bundled-marketplace.json +14 -1
  227. package/src/cli/lib/upgrade-plugin.ts +42 -0
  228. package/src/config/__tests__/balanced-model-experiment.test.ts +278 -0
  229. package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
  230. package/src/config/__tests__/default-provider.test.ts +6 -1
  231. package/src/config/__tests__/profile-materialization.test.ts +75 -19
  232. package/src/config/assistant-feature-flags.ts +36 -15
  233. package/src/config/balanced-model-experiment.ts +35 -0
  234. package/src/config/bundled-skills/acp/SKILL.md +6 -7
  235. package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
  236. package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
  237. package/src/config/bundled-skills/image-studio/SKILL.md +5 -4
  238. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -1
  239. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +101 -0
  240. package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
  241. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  242. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.test.ts +65 -0
  243. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
  244. package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
  245. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
  246. package/src/config/bundled-skills/skill-management/TOOLS.json +9 -3
  247. package/src/config/bundled-skills/subagent/SKILL.md +17 -12
  248. package/src/config/bundled-skills/subagent/TOOLS.json +4 -4
  249. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
  250. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
  251. package/src/config/call-site-defaults.ts +11 -5
  252. package/src/config/default-profile-catalog.ts +179 -16
  253. package/src/config/default-profile-names.ts +4 -1
  254. package/src/config/default-provider-resolution.ts +4 -0
  255. package/src/config/feature-flag-registry.json +12 -12
  256. package/src/config/llm-context-resolution.ts +11 -3
  257. package/src/config/llm-resolver.ts +28 -1
  258. package/src/config/profile-materialization.ts +70 -22
  259. package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
  260. package/src/config/schemas/call-site-catalog.ts +4 -4
  261. package/src/config/schemas/live-voice.ts +57 -23
  262. package/src/config/schemas/llm.ts +59 -32
  263. package/src/config/schemas/mcp.ts +23 -0
  264. package/src/config/schemas/plugin-updates.ts +6 -2
  265. package/src/config/schemas/stt.ts +1 -0
  266. package/src/config/skills.ts +9 -2
  267. package/src/context/outbound-sanitize.ts +96 -1
  268. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +1 -1
  269. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
  270. package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
  271. package/src/daemon/conversation-agent-loop.ts +31 -19
  272. package/src/daemon/conversation-messaging.ts +5 -1
  273. package/src/daemon/conversation-notifiers.ts +20 -10
  274. package/src/daemon/conversation-process.ts +20 -6
  275. package/src/daemon/conversation-runtime-assembly.ts +12 -6
  276. package/src/daemon/conversation-store.ts +14 -4
  277. package/src/daemon/conversation-surfaces.ts +75 -26
  278. package/src/daemon/conversation-tool-setup.ts +3 -6
  279. package/src/daemon/conversation.ts +154 -51
  280. package/src/daemon/doordash-steps.ts +2 -2
  281. package/src/daemon/lifecycle.ts +14 -1
  282. package/src/daemon/mcp-reload-service.ts +36 -6
  283. package/src/daemon/process-message.ts +13 -26
  284. package/src/daemon/providers-setup.ts +6 -3
  285. package/src/daemon/trust-context-types.ts +29 -0
  286. package/src/daemon/trust-context.ts +12 -7
  287. package/src/daemon/wake-conversation-ops.ts +3 -2
  288. package/src/daemon/windows-compiled-entry.ts +4 -0
  289. package/src/documents/document-store.ts +143 -240
  290. package/src/hooks/hook-loader.ts +3 -3
  291. package/src/hooks/registry.ts +103 -45
  292. package/src/hooks/types.ts +5 -0
  293. package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
  294. package/src/inbound/oauth-callback-url.ts +61 -0
  295. package/src/ipc/gateway-client.test.ts +1 -1
  296. package/src/ipc/gateway-client.ts +14 -21
  297. package/src/ipc/gateway-flag-listener.ts +17 -3
  298. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
  299. package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
  300. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +1050 -0
  301. package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
  302. package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
  303. package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
  304. package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
  305. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
  306. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
  307. package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
  308. package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
  309. package/src/live-voice/live-voice-archive.ts +2 -0
  310. package/src/live-voice/live-voice-manager.ts +16 -3
  311. package/src/live-voice/live-voice-metrics.ts +57 -32
  312. package/src/live-voice/live-voice-photo.ts +1 -2
  313. package/src/live-voice/live-voice-session.ts +598 -323
  314. package/src/live-voice/progress-narration.ts +277 -0
  315. package/src/live-voice/protocol.ts +21 -1
  316. package/src/live-voice/windows-compiled-live-voice.ts +4 -0
  317. package/src/mcp/__tests__/effective-config.test.ts +238 -0
  318. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
  319. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
  320. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
  321. package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
  322. package/src/mcp/client.ts +16 -11
  323. package/src/mcp/effective-config.ts +113 -0
  324. package/src/mcp/manager.ts +11 -6
  325. package/src/mcp/mcp-auth-orchestrator.ts +13 -22
  326. package/src/mcp/mcp-oauth-provider.ts +205 -240
  327. package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
  328. package/src/monitoring/control.ts +1 -0
  329. package/src/monitoring/db-integrity-sample.ts +4 -5
  330. package/src/monitoring/plugin-auto-update.ts +128 -24
  331. package/src/notifications/AGENTS.md +2 -0
  332. package/src/notifications/approval-card-data.ts +33 -0
  333. package/src/notifications/signal.ts +1 -0
  334. package/src/permissions/AGENTS.md +16 -0
  335. package/src/permissions/checker.test.ts +75 -122
  336. package/src/permissions/checker.ts +44 -560
  337. package/src/permissions/confirmation-guardian-request.test.ts +15 -11
  338. package/src/permissions/confirmation-guardian-request.ts +2 -2
  339. package/src/permissions/prompter.ts +1 -5
  340. package/src/permissions/question-guardian-request.test.ts +14 -6
  341. package/src/permissions/question-guardian-request.ts +1 -2
  342. package/src/persistence/attachments-store.ts +8 -1
  343. package/src/persistence/bookmark-crud.ts +3 -7
  344. package/src/persistence/conversation-attention-store.ts +16 -45
  345. package/src/persistence/conversation-crud.ts +33 -4
  346. package/src/persistence/conversation-lineage.ts +9 -0
  347. package/src/persistence/conversation-queries.ts +182 -52
  348. package/src/persistence/delivery-crud.ts +150 -36
  349. package/src/persistence/embeddings/qdrant-manager.ts +84 -49
  350. package/src/persistence/external-conversation-store.ts +32 -4
  351. package/src/persistence/llm-request-log-store.ts +4 -10
  352. package/src/persistence/llm-usage-store.ts +8 -3
  353. package/src/persistence/message-reads.test.ts +197 -0
  354. package/src/persistence/message-reads.ts +211 -0
  355. package/src/persistence/migrations/360-add-document-workspace-path.ts +5 -14
  356. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
  357. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
  358. package/src/persistence/real-user-turn-filter.ts +27 -3
  359. package/src/persistence/schema/documents.ts +4 -4
  360. package/src/persistence/steps.ts +9 -0
  361. package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
  362. package/src/plugin-api/constants.ts +8 -0
  363. package/src/plugin-api/conversation-turn.ts +195 -5
  364. package/src/plugin-api/index.ts +38 -6
  365. package/src/plugin-api/resolve-credential.ts +3 -2
  366. package/src/plugin-api/store-credential.ts +146 -0
  367. package/src/plugin-api/vision-support.test.ts +1 -1
  368. package/src/plugin-api/webhook-url.ts +13 -11
  369. package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
  370. package/src/plugins/defaults/main.ts +21 -7
  371. package/src/plugins/defaults/memory/AGENTS.md +15 -3
  372. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
  373. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
  374. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
  375. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
  376. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  377. package/src/plugins/defaults/memory/buffer-format.ts +165 -0
  378. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
  379. package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +77 -0
  380. package/src/plugins/defaults/memory/graph/conversation-graph-memory.ts +24 -6
  381. package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
  382. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
  383. package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
  384. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +17 -24
  385. package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
  386. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +53 -5
  387. package/src/plugins/defaults/memory/indexer.ts +3 -1
  388. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
  389. package/src/plugins/defaults/memory/memory-retrospective-job.ts +4 -4
  390. package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
  391. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  392. package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
  393. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +147 -0
  394. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  395. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  396. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  397. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  398. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  399. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  400. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +195 -58
  401. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +93 -24
  402. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  403. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  404. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  405. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  406. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  407. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  408. package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
  409. package/src/plugins/defaults/memory/substrate/static-context.ts +160 -33
  410. package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
  411. package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
  412. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +18 -0
  413. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +66 -1
  414. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  415. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  416. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  417. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  418. package/src/plugins/defaults/memory/v3/injector.ts +8 -0
  419. package/src/plugins/defaults/memory/v3/prune.ts +2 -0
  420. package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
  421. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +23 -9
  422. package/src/plugins/defaults/memory/worker-control.ts +1 -0
  423. package/src/plugins/defaults/memory/worker.ts +6 -3
  424. package/src/plugins/defaults/worker-entrypoints.ts +3 -0
  425. package/src/plugins/external-plugin-loader.ts +47 -0
  426. package/src/plugins/mcp-servers.ts +361 -0
  427. package/src/plugins/mtime-cache.ts +37 -46
  428. package/src/plugins/pipeline.ts +14 -7
  429. package/src/plugins/plugin-execution-context.ts +35 -11
  430. package/src/plugins/worker-plugin-surface.ts +33 -0
  431. package/src/prompts/templates/system-sections.ts +0 -7
  432. package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
  433. package/src/providers/__tests__/context-overflow-error.test.ts +24 -0
  434. package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
  435. package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
  436. package/src/providers/__tests__/retry-callsite.test.ts +25 -2
  437. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  438. package/src/providers/anthropic/client.ts +332 -241
  439. package/src/providers/call-site-routing.ts +30 -3
  440. package/src/providers/connection-resolution.ts +194 -11
  441. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +4 -0
  442. package/src/providers/inference/adapter-factory.ts +3 -0
  443. package/src/providers/inference/auth.ts +6 -0
  444. package/src/providers/inference/connection-availability.ts +24 -2
  445. package/src/providers/inference/connections.ts +2 -0
  446. package/src/providers/model-catalog.ts +3 -3
  447. package/src/providers/model-intents.ts +28 -8
  448. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +163 -40
  449. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  450. package/src/providers/openai/chat-completions-provider.ts +133 -36
  451. package/src/providers/openai/codex-models.ts +2 -1
  452. package/src/providers/provider-send-message.ts +32 -3
  453. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
  454. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +625 -0
  455. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
  456. package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
  457. package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
  458. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +679 -0
  459. package/src/providers/speech-to-text/provider-catalog.ts +99 -8
  460. package/src/providers/speech-to-text/resolve.ts +25 -2
  461. package/src/routes/control.ts +1 -0
  462. package/src/routes/route-host-client.ts +1 -0
  463. package/src/routes/route-host-protocol.ts +7 -0
  464. package/src/routes/worker.ts +48 -15
  465. package/src/runtime/AGENTS.md +16 -17
  466. package/src/runtime/access-request-helper.ts +9 -12
  467. package/src/runtime/agent-wake.ts +18 -15
  468. package/src/runtime/effective-capabilities.test.ts +43 -0
  469. package/src/runtime/effective-capabilities.ts +27 -0
  470. package/src/runtime/guardian-reply-router.ts +6 -2
  471. package/src/runtime/pre-first-message-gate.ts +4 -0
  472. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
  473. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +522 -2
  474. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
  475. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
  476. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
  477. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
  478. package/src/runtime/routes/__tests__/plugins-routes.test.ts +132 -35
  479. package/src/runtime/routes/__tests__/schedule-routes-disarm-reason.test.ts +215 -0
  480. package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
  481. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  482. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  483. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +133 -1
  484. package/src/runtime/routes/channel-availability-routes.ts +32 -14
  485. package/src/runtime/routes/channel-route-shared.ts +0 -6
  486. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
  487. package/src/runtime/routes/conversation-list-routes.ts +181 -23
  488. package/src/runtime/routes/conversation-management-routes.ts +17 -12
  489. package/src/runtime/routes/conversation-query-routes.ts +40 -27
  490. package/src/runtime/routes/conversation-routes.ts +43 -29
  491. package/src/runtime/routes/credential-prompt-routes.ts +4 -7
  492. package/src/runtime/routes/credential-routes.ts +26 -82
  493. package/src/runtime/routes/default-provider-routes.ts +15 -0
  494. package/src/runtime/routes/documents-routes.ts +3 -222
  495. package/src/runtime/routes/inbound-message-handler.ts +21 -45
  496. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
  497. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
  498. package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
  499. package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
  500. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
  501. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -79
  502. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
  503. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +12 -7
  504. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -8
  505. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -79
  506. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
  507. package/src/runtime/routes/inference-profiles-routes.ts +20 -11
  508. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
  509. package/src/runtime/routes/log-export-routes.ts +3 -0
  510. package/src/runtime/routes/mcp-auth-routes.ts +148 -57
  511. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +2 -0
  512. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +3 -0
  513. package/src/runtime/routes/playground/inject-failures.ts +2 -2
  514. package/src/runtime/routes/playground/reset-circuit.ts +1 -1
  515. package/src/runtime/routes/plugins-routes.ts +38 -8
  516. package/src/runtime/routes/schedule-routes.ts +94 -7
  517. package/src/runtime/routes/settings-routes.ts +6 -6
  518. package/src/runtime/routes/stt-routes.ts +31 -25
  519. package/src/runtime/routes/surface-action-routes.ts +20 -19
  520. package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
  521. package/src/runtime/routes/user-route-dispatcher.ts +69 -15
  522. package/src/runtime/routes/user-route-import.ts +108 -0
  523. package/src/runtime/routes/user-route-resolution.ts +18 -1
  524. package/src/runtime/routes/workspace-routes.ts +0 -9
  525. package/src/runtime/routes/workspace-utils.ts +3 -13
  526. package/src/runtime/services/conversation-serializer.ts +7 -2
  527. package/src/schedule/__tests__/plugin-schedule-declarations.test.ts +68 -5
  528. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +81 -0
  529. package/src/schedule/plugin-schedule-availability.ts +58 -0
  530. package/src/schedule/plugin-schedule-declarations.ts +23 -27
  531. package/src/schedule/plugin-schedule-reconciler.ts +12 -3
  532. package/src/schedule/schedule-store.ts +5 -1
  533. package/src/schedule/scheduler.ts +9 -4
  534. package/src/schedule/worker-control.ts +1 -0
  535. package/src/schedule/worker.ts +6 -0
  536. package/src/security/oauth2.ts +6 -22
  537. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
  538. package/src/stt/__tests__/types.test.ts +94 -0
  539. package/src/stt/daemon-batch-transcriber.ts +10 -0
  540. package/src/stt/stt-stream-session.ts +8 -4
  541. package/src/stt/types.ts +103 -0
  542. package/src/subagent/__tests__/consult-prompt.test.ts +26 -15
  543. package/src/subagent/consult-context.ts +11 -11
  544. package/src/subagent/consult-prompt.ts +26 -35
  545. package/src/subagent/manager.ts +21 -40
  546. package/src/subagent/notify.ts +7 -1
  547. package/src/subagent/types.ts +15 -12
  548. package/src/tools/__tests__/tool-input-schemas.test.ts +7 -7
  549. package/src/tools/acp/spawn.test.ts +97 -0
  550. package/src/tools/acp/spawn.ts +38 -4
  551. package/src/tools/credentials/ref-parse.ts +35 -0
  552. package/src/tools/credentials/resolve.ts +4 -10
  553. package/src/tools/credentials/store.ts +168 -0
  554. package/src/tools/document/document-tool.ts +12 -3
  555. package/src/tools/executor.ts +53 -15
  556. package/src/tools/filesystem/read.ts +27 -10
  557. package/src/tools/host-filesystem/read.ts +27 -15
  558. package/src/tools/network/url-safety.ts +9 -16
  559. package/src/tools/permission-checker.ts +43 -52
  560. package/src/tools/registry.ts +2 -1
  561. package/src/tools/shared/filesystem/file-ops-service.ts +63 -35
  562. package/src/tools/shared/filesystem/legacy-read-args.ts +22 -0
  563. package/src/tools/shared/filesystem/types.ts +5 -5
  564. package/src/tools/skills/scaffold-managed.ts +25 -7
  565. package/src/tools/subagent/spawn.ts +28 -88
  566. package/src/tools/tool-types.ts +26 -13
  567. package/src/tools/types.ts +6 -6
  568. package/src/tools/ui-surface/surface-shape-docs.ts +12 -1
  569. package/src/tools/workflows/run-workflow.ts +1 -2
  570. package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
  571. package/src/tts/reasoning-tag-filter.ts +89 -0
  572. package/src/util/__tests__/worker-process-command.test.ts +37 -0
  573. package/src/util/logger.ts +16 -0
  574. package/src/util/think-tag-stream.ts +95 -0
  575. package/src/util/worker-process.ts +37 -4
  576. package/src/windows-compiled-cli.ts +32 -0
  577. package/src/windows-compiled-entry.ts +4 -0
  578. package/src/windows-compiled-logger.ts +6 -0
  579. package/src/windows-compiled-worker-entry.ts +29 -0
  580. package/src/workspace/byok-default-profile-ensure.ts +76 -24
  581. package/src/workspace/custom-profile-ensure.ts +4 -24
  582. package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
  583. package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
  584. package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
  585. package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
  586. package/src/workspace/migrations/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
  587. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
  588. package/src/workspace/migrations/registry.ts +10 -0
  589. package/src/workspace/provider-commit-message-generator.ts +7 -5
  590. package/src/__tests__/document-workspace-file.test.ts +0 -467
  591. package/src/live-voice/__tests__/front-decision.test.ts +0 -645
  592. package/src/live-voice/front-decision.ts +0 -476
  593. package/src/permissions/ipc-risk-types.ts +0 -143
  594. package/src/permissions/risk-types.ts +0 -76
  595. package/src/subagent/__tests__/consult-transcript.test.ts +0 -184
  596. package/src/subagent/consult-transcript.ts +0 -90
@@ -25,14 +25,14 @@ import { createConversation } from "../../../persistence/conversation-crud.js";
25
25
  import { getDb } from "../../../persistence/db-connection.js";
26
26
  import { initializeDb } from "../../../persistence/db-init.js";
27
27
  import { createGroup } from "../../../persistence/group-crud.js";
28
- import { rawRun } from "../../../persistence/raw-query.js";
28
+ import { rawExec, rawRun } from "../../../persistence/raw-query.js";
29
29
  import {
30
30
  conversationAssistantAttentionState,
31
31
  conversationAttentionEvents,
32
32
  conversations,
33
33
  } from "../../../persistence/schema/index.js";
34
34
  import { ROUTES as CONVERSATION_LIST_ROUTES } from "../conversation-list-routes.js";
35
- import { BadRequestError } from "../errors.js";
35
+ import { BadRequestError, NotFoundError } from "../errors.js";
36
36
  import type { RouteDefinition } from "../types.js";
37
37
 
38
38
  // ---------------------------------------------------------------------------
@@ -246,6 +246,42 @@ describe("GET /v1/conversations — conversationType", () => {
246
246
  });
247
247
  });
248
248
 
249
+ describe("GET /v1/conversations/:id", () => {
250
+ const getHandler = findHandler(CONVERSATION_LIST_ROUTES, "getConversation");
251
+
252
+ beforeEach(() => {
253
+ clearConversations();
254
+ });
255
+
256
+ test("serves a listed conversation", () => {
257
+ const conv = createConversation({ title: "listed" });
258
+
259
+ const detail = getHandler({ pathParams: { id: conv.id } }) as {
260
+ conversation: { id: string; conversationType: string };
261
+ };
262
+
263
+ expect(detail.conversation.id).toBe(conv.id);
264
+ expect(detail.conversation.conversationType).toBe("standard");
265
+ });
266
+
267
+ test("does not serve a legacy private row the listing hides", () => {
268
+ // "private" is not a creatable type (legacy rows only); every listing
269
+ // hides it by type and the wire type collapses it to "standard", so a
270
+ // client holding only a stale id must get the same answer the listing
271
+ // gives: not there.
272
+ const conv = createConversation({ title: "hidden" });
273
+ rawRun(
274
+ "test:makePrivate",
275
+ "UPDATE conversations SET conversation_type = 'private' WHERE id = ?",
276
+ conv.id,
277
+ );
278
+
279
+ expect(() => getHandler({ pathParams: { id: conv.id } })).toThrow(
280
+ NotFoundError,
281
+ );
282
+ });
283
+ });
284
+
249
285
  describe("GET /v1/conversations with groupId", () => {
250
286
  function seedInGroup(title: string, groupId: string): string {
251
287
  const conv = createConversation(title);
@@ -514,6 +550,272 @@ describe("GET /v1/conversations with groupId", () => {
514
550
  });
515
551
  });
516
552
 
553
+ describe("GET /v1/conversations with needsAttention", () => {
554
+ function seedUnseen(conversationId: string): void {
555
+ projectAssistantMessage({
556
+ conversationId,
557
+ messageId: `msg-${conversationId}`,
558
+ messageAt: Date.now(),
559
+ });
560
+ }
561
+
562
+ function markSeen(conversationId: string): void {
563
+ recordConversationSeenSignal({
564
+ conversationId,
565
+ sourceChannel: "vellum",
566
+ signalType: "macos_conversation_opened",
567
+ confidence: "explicit",
568
+ source: "test",
569
+ });
570
+ }
571
+
572
+ beforeEach(() => {
573
+ getDb().delete(conversationAttentionEvents).run();
574
+ getDb().delete(conversationAssistantAttentionState).run();
575
+ clearConversations();
576
+ });
577
+
578
+ test("returns only conversations with an unseen latest assistant message", async () => {
579
+ const unseen = createConversation("needs-attention");
580
+ seedUnseen(unseen.id);
581
+ const seen = createConversation("already-seen");
582
+ seedUnseen(seen.id);
583
+ markSeen(seen.id);
584
+ // No attention projection at all: not unseen, so not returned. The
585
+ // filter's inner join is what excludes it; a left join would leak it.
586
+ createConversation("never-projected");
587
+
588
+ const result = await invoke({ needsAttention: "true" });
589
+
590
+ expect(result.conversations.map((c) => c.title)).toEqual([
591
+ "needs-attention",
592
+ ]);
593
+ });
594
+
595
+ test("omitting the filter leaves every list unchanged, join and all", async () => {
596
+ /* The sensitivity check for the conditional join: with the filter off,
597
+ rows with no attention row must still be listed. If the join were
598
+ applied unconditionally, "never-projected" would vanish from the
599
+ plain list, which is every list the app has today. */
600
+ const unseen = createConversation("has-attention-row");
601
+ seedUnseen(unseen.id);
602
+ createConversation("never-projected");
603
+
604
+ const result = await invoke();
605
+
606
+ expect(result.conversations.map((c) => c.title).sort()).toEqual([
607
+ "has-attention-row",
608
+ "never-projected",
609
+ ]);
610
+ });
611
+
612
+ test("hasMore and the total describe the filtered set, not the whole table", async () => {
613
+ /* countConversations reads through the same where AND the same join;
614
+ a page and its total have to agree or the client's hasMore lies. */
615
+ for (let i = 0; i < 3; i++) {
616
+ const c = createConversation(`unseen-${i}`);
617
+ seedUnseen(c.id);
618
+ }
619
+ for (let i = 0; i < 5; i++) {
620
+ createConversation(`quiet-${i}`);
621
+ }
622
+
623
+ const page = await invoke({ needsAttention: "true", limit: "2" });
624
+
625
+ expect(page.conversations).toHaveLength(2);
626
+ expect(page.hasMore).toBe(true);
627
+ const rest = await invoke({
628
+ needsAttention: "true",
629
+ limit: "2",
630
+ offset: "2",
631
+ });
632
+ expect(rest.conversations).toHaveLength(1);
633
+ expect(rest.hasMore).toBe(false);
634
+ });
635
+
636
+ test("an attention-scoped first page has no pinned rows appended to it", async () => {
637
+ /* Same rule as the group-scoped page: the pinned injection exists for
638
+ a client reading Pinned out of the unfiltered list, and a caller
639
+ that asked for the unseen subset is not that client. A seen pinned
640
+ row appended here would be a row outside the filter, on a page whose
641
+ hasMore was computed from the filtered count. */
642
+ const unseen = createConversation("unseen-only");
643
+ seedUnseen(unseen.id);
644
+ const pinnedSeen = createConversation("pinned-and-seen");
645
+ rawRun(
646
+ "test:pinConversation",
647
+ "UPDATE conversations SET is_pinned = 1, group_id = 'system:pinned' WHERE id = ?",
648
+ pinnedSeen.id,
649
+ );
650
+
651
+ const result = await invoke({ needsAttention: "true" });
652
+
653
+ expect(result.conversations.map((c) => c.title)).toEqual(["unseen-only"]);
654
+ });
655
+
656
+ test("composes with the other filters", async () => {
657
+ const group = createGroup("Work");
658
+ const inGroupUnseen = createConversation("in-group-unseen");
659
+ seedUnseen(inGroupUnseen.id);
660
+ rawRun(
661
+ "test:fileInGroup",
662
+ "UPDATE conversations SET group_id = ? WHERE id = ?",
663
+ group.id,
664
+ inGroupUnseen.id,
665
+ );
666
+ const outOfGroupUnseen = createConversation("out-of-group-unseen");
667
+ seedUnseen(outOfGroupUnseen.id);
668
+
669
+ const result = await invoke({ needsAttention: "true", groupId: group.id });
670
+
671
+ expect(result.conversations.map((c) => c.title)).toEqual([
672
+ "in-group-unseen",
673
+ ]);
674
+ });
675
+
676
+ test('any value other than "true" is rejected with a 400', () => {
677
+ /* Same posture as conversationType: silently reading a typo or a newer
678
+ client's value as "no filter" would hand back the full list where a
679
+ subset was asked for, and that skew is invisible to the client. */
680
+ for (const bad of ["false", "1", "yes", "TRUE"]) {
681
+ expect(() => invoke({ needsAttention: bad })).toThrow(BadRequestError);
682
+ }
683
+ });
684
+ });
685
+
686
+ describe("GET /v1/conversations with foregroundOnly", () => {
687
+ function fileIntoGroup(conversationId: string, groupId: string): void {
688
+ rawRun(
689
+ "test:fileIntoGroup",
690
+ "UPDATE conversations SET group_id = ? WHERE id = ?",
691
+ groupId,
692
+ conversationId,
693
+ );
694
+ }
695
+
696
+ function surface(conversationId: string): void {
697
+ rawRun(
698
+ "test:surfaceConversation",
699
+ "UPDATE conversations SET surfaced_at = ? WHERE id = ?",
700
+ Date.now(),
701
+ conversationId,
702
+ );
703
+ }
704
+
705
+ beforeEach(() => {
706
+ clearConversations();
707
+ });
708
+
709
+ test("drops the automated runs the standard listing admits through a custom group", async () => {
710
+ /* The standard listing shows a background run filed in a custom group,
711
+ so a caller reading page one to find "the newest chat" can lead with
712
+ one; the filter is what lets that caller ask for chats alone. A
713
+ surfaced run is a chat for this purpose, exactly as the unread count
714
+ treats it. */
715
+ const group = createGroup("Work");
716
+ createConversation("plain-chat");
717
+ const bgInGroup = createConversation({
718
+ title: "bg-in-group",
719
+ conversationType: "background",
720
+ });
721
+ fileIntoGroup(bgInGroup.id, group.id);
722
+ const surfacedBg = createConversation({
723
+ title: "surfaced-bg",
724
+ conversationType: "background",
725
+ });
726
+ surface(surfacedBg.id);
727
+
728
+ const unfiltered = await invoke();
729
+ expect(unfiltered.conversations.map((c) => c.title).sort()).toEqual([
730
+ "bg-in-group",
731
+ "plain-chat",
732
+ "surfaced-bg",
733
+ ]);
734
+
735
+ const result = await invoke({ foregroundOnly: "true" });
736
+ expect(result.conversations.map((c) => c.title).sort()).toEqual([
737
+ "plain-chat",
738
+ "surfaced-bg",
739
+ ]);
740
+ });
741
+
742
+ test("hasMore and the total describe the filtered set, not the whole table", async () => {
743
+ /* The reason for the filter is a limit=1 read whose first row is the
744
+ answer; that read is only right if the page and its total come from
745
+ the same predicate. */
746
+ const group = createGroup("Work");
747
+ for (let i = 0; i < 3; i++) {
748
+ const bg = createConversation({
749
+ title: `bg-${i}`,
750
+ conversationType: "background",
751
+ });
752
+ fileIntoGroup(bg.id, group.id);
753
+ }
754
+ createConversation("the-only-chat");
755
+
756
+ const page = await invoke({ foregroundOnly: "true", limit: "1" });
757
+
758
+ expect(page.conversations.map((c) => c.title)).toEqual(["the-only-chat"]);
759
+ expect(page.hasMore).toBe(false);
760
+ });
761
+
762
+ test("a foreground-only first page has no pinned rows appended to it", async () => {
763
+ /* Same rule as the attention- and group-scoped pages: the injection is
764
+ for a client reading Pinned out of the unfiltered list, and a caller
765
+ that asked for the newest chat in one row is not that client. The
766
+ older pinned chat is a member of the filter, so only the injection
767
+ could put it on a limit=1 page. */
768
+ const older = createConversation("older-pinned-chat");
769
+ rawRun(
770
+ "test:pinConversation",
771
+ "UPDATE conversations SET is_pinned = 1, group_id = 'system:pinned', last_message_at = ? WHERE id = ?",
772
+ Date.now() - 60_000,
773
+ older.id,
774
+ );
775
+ const newer = createConversation("newest-chat");
776
+ rawRun(
777
+ "test:bumpRecency",
778
+ "UPDATE conversations SET last_message_at = ? WHERE id = ?",
779
+ Date.now(),
780
+ newer.id,
781
+ );
782
+
783
+ const unfiltered = await invoke({ limit: "1" });
784
+ expect(unfiltered.conversations.map((c) => c.title)).toEqual([
785
+ "newest-chat",
786
+ "older-pinned-chat",
787
+ ]);
788
+
789
+ const result = await invoke({ foregroundOnly: "true", limit: "1" });
790
+ expect(result.conversations.map((c) => c.title)).toEqual(["newest-chat"]);
791
+ });
792
+
793
+ test("composes with the other filters", async () => {
794
+ const group = createGroup("Work");
795
+ const chatInGroup = createConversation("chat-in-group");
796
+ fileIntoGroup(chatInGroup.id, group.id);
797
+ const bgInGroup = createConversation({
798
+ title: "bg-in-group",
799
+ conversationType: "background",
800
+ });
801
+ fileIntoGroup(bgInGroup.id, group.id);
802
+ createConversation("chat-outside");
803
+
804
+ const result = await invoke({
805
+ foregroundOnly: "true",
806
+ groupId: group.id,
807
+ });
808
+
809
+ expect(result.conversations.map((c) => c.title)).toEqual(["chat-in-group"]);
810
+ });
811
+
812
+ test('any value other than "true" is rejected with a 400', () => {
813
+ for (const bad of ["false", "1", "yes", "TRUE"]) {
814
+ expect(() => invoke({ foregroundOnly: bad })).toThrow(BadRequestError);
815
+ }
816
+ });
817
+ });
818
+
517
819
  describe("GET /v1/conversations/unread-count", () => {
518
820
  const unreadCountHandler = findHandler(
519
821
  CONVERSATION_LIST_ROUTES,
@@ -782,3 +1084,221 @@ describe("GET /v1/conversations/unread-count", () => {
782
1084
  expect(invokeUnreadCount()).toEqual({ count: 1 });
783
1085
  });
784
1086
  });
1087
+
1088
+ describe("GET /v1/conversations/sections", () => {
1089
+ const sectionsHandler = findHandler(
1090
+ CONVERSATION_LIST_ROUTES,
1091
+ "getConversationSections",
1092
+ );
1093
+
1094
+ interface SectionRow {
1095
+ kind: "pinned" | "group" | "channel" | "chats";
1096
+ groupId?: string;
1097
+ name?: string;
1098
+ icon?: string | null;
1099
+ sortPosition?: number;
1100
+ channelId?: string;
1101
+ total: number;
1102
+ unread: number;
1103
+ }
1104
+
1105
+ function invokeSections(): SectionRow[] {
1106
+ return (sectionsHandler({}) as { sections: SectionRow[] }).sections;
1107
+ }
1108
+
1109
+ function fileIntoGroup(conversationId: string, groupId: string): void {
1110
+ rawRun(
1111
+ "test:fileIntoGroup",
1112
+ "UPDATE conversations SET group_id = ? WHERE id = ?",
1113
+ groupId,
1114
+ conversationId,
1115
+ );
1116
+ }
1117
+
1118
+ function stampChannel(conversationId: string, channel: string): void {
1119
+ rawRun(
1120
+ "test:stampChannel",
1121
+ "UPDATE conversations SET origin_channel = ? WHERE id = ?",
1122
+ channel,
1123
+ conversationId,
1124
+ );
1125
+ }
1126
+
1127
+ function pin(conversationId: string): void {
1128
+ rawRun(
1129
+ "test:pinConversation",
1130
+ "UPDATE conversations SET is_pinned = 1, group_id = 'system:pinned' WHERE id = ?",
1131
+ conversationId,
1132
+ );
1133
+ }
1134
+
1135
+ function seedUnseen(conversationId: string): void {
1136
+ projectAssistantMessage({
1137
+ conversationId,
1138
+ messageId: `msg-${conversationId}`,
1139
+ messageAt: Date.now(),
1140
+ });
1141
+ }
1142
+
1143
+ beforeEach(() => {
1144
+ clearConversations();
1145
+ });
1146
+
1147
+ test("an empty conversation table yields Chats alone, at zero", () => {
1148
+ // Chats is the leftover bucket and renders regardless, so its counts
1149
+ // are part of the contract even when nothing exists.
1150
+ expect(invokeSections()).toEqual([{ kind: "chats", total: 0, unread: 0 }]);
1151
+ });
1152
+
1153
+ test("every section kind appears with its own totals", () => {
1154
+ const pinned = createConversation({ title: "pinned-1" });
1155
+ pin(pinned.id);
1156
+ const group = createGroup("Sections Spread Group");
1157
+ fileIntoGroup(createConversation({ title: "g-1" }).id, group.id);
1158
+ fileIntoGroup(createConversation({ title: "g-2" }).id, group.id);
1159
+ stampChannel(createConversation({ title: "slack-1" }).id, "slack");
1160
+ createConversation({ title: "native-unstamped" });
1161
+ stampChannel(createConversation({ title: "native-stamped" }).id, "vellum");
1162
+
1163
+ const sections = invokeSections();
1164
+
1165
+ expect(sections).toContainEqual({ kind: "pinned", total: 1, unread: 0 });
1166
+ expect(sections).toContainEqual({
1167
+ kind: "group",
1168
+ groupId: group.id,
1169
+ name: "Sections Spread Group",
1170
+ icon: null,
1171
+ sortPosition: group.sortPosition,
1172
+ total: 2,
1173
+ unread: 0,
1174
+ });
1175
+ expect(sections).toContainEqual({
1176
+ kind: "channel",
1177
+ channelId: "slack",
1178
+ total: 1,
1179
+ unread: 0,
1180
+ });
1181
+ // NULL origin_channel reads as native, exactly as the list filter reads
1182
+ // it, so the stamped and unstamped native rows share the Chats bucket.
1183
+ expect(sections).toContainEqual({ kind: "chats", total: 2, unread: 0 });
1184
+ });
1185
+
1186
+ test("unread counts follow the seen state per section", () => {
1187
+ const pinned = createConversation({ title: "pinned-seen" });
1188
+ pin(pinned.id);
1189
+ seedUnseen(pinned.id);
1190
+ recordConversationSeenSignal({
1191
+ conversationId: pinned.id,
1192
+ sourceChannel: "vellum",
1193
+ signalType: "macos_conversation_opened",
1194
+ confidence: "explicit",
1195
+ source: "test",
1196
+ });
1197
+
1198
+ const group = createGroup("Sections Unread Group");
1199
+ const unreadInGroup = createConversation({ title: "g-unread" });
1200
+ fileIntoGroup(unreadInGroup.id, group.id);
1201
+ seedUnseen(unreadInGroup.id);
1202
+ // A member with no attention projection at all reads as seen.
1203
+ fileIntoGroup(createConversation({ title: "g-quiet" }).id, group.id);
1204
+
1205
+ const chatsUnread = createConversation({ title: "chat-unread" });
1206
+ seedUnseen(chatsUnread.id);
1207
+
1208
+ const sections = invokeSections();
1209
+
1210
+ expect(sections).toContainEqual({ kind: "pinned", total: 1, unread: 0 });
1211
+ expect(sections).toContainEqual({
1212
+ kind: "group",
1213
+ groupId: group.id,
1214
+ name: "Sections Unread Group",
1215
+ icon: null,
1216
+ sortPosition: group.sortPosition,
1217
+ total: 2,
1218
+ unread: 1,
1219
+ });
1220
+ expect(sections).toContainEqual({ kind: "chats", total: 1, unread: 1 });
1221
+ });
1222
+
1223
+ test("a background row filed into a group counts toward total but never unread", () => {
1224
+ // Mirrors the unread-count contract: the custom-group visibility arm
1225
+ // admits the row into the section, and the not-background unread rule
1226
+ // keeps it out of the badge.
1227
+ const group = createGroup("Sections Background Group");
1228
+ const bg = createConversation({
1229
+ title: "bg-in-group",
1230
+ conversationType: "background",
1231
+ });
1232
+ fileIntoGroup(bg.id, group.id);
1233
+ seedUnseen(bg.id);
1234
+
1235
+ const sections = invokeSections();
1236
+
1237
+ expect(sections).toContainEqual({
1238
+ kind: "group",
1239
+ groupId: group.id,
1240
+ name: "Sections Background Group",
1241
+ icon: null,
1242
+ sortPosition: group.sortPosition,
1243
+ total: 1,
1244
+ unread: 0,
1245
+ });
1246
+ });
1247
+
1248
+ test("an empty custom group gets no section", () => {
1249
+ const group = createGroup("Sections Empty Group");
1250
+
1251
+ const sections = invokeSections();
1252
+
1253
+ expect(sections.some((s) => s.groupId === group.id)).toBe(false);
1254
+ });
1255
+
1256
+ test("archived rows count toward no section", () => {
1257
+ const group = createGroup("Sections Archived Group");
1258
+ fileIntoGroup(seedArchived("archived-in-group"), group.id);
1259
+
1260
+ const sections = invokeSections();
1261
+
1262
+ expect(sections.some((s) => s.groupId === group.id)).toBe(false);
1263
+ });
1264
+
1265
+ test("a dangling group id is skipped, not surfaced", () => {
1266
+ /* Live write paths cannot create this state: group_id carries a foreign
1267
+ key, and the placement write sanitizes unknown groups precisely to
1268
+ avoid violating it. A restored snapshot can, though: in-place restore
1269
+ swaps the SQLite file without running migrations in-process (the same
1270
+ window that lets legacy private rows exist transiently), so the
1271
+ fixture creates the state the way reality does, with enforcement off. */
1272
+ rawExec("PRAGMA foreign_keys = OFF");
1273
+ try {
1274
+ fileIntoGroup(
1275
+ createConversation({ title: "dangling" }).id,
1276
+ "00000000-0000-4000-8000-00000000dead",
1277
+ );
1278
+ } finally {
1279
+ rawExec("PRAGMA foreign_keys = ON");
1280
+ }
1281
+
1282
+ const sections = invokeSections();
1283
+
1284
+ expect(sections.some((s) => s.kind === "group")).toBe(false);
1285
+ // Grouped, so it does not leak into the ungrouped Chats bucket either.
1286
+ expect(sections).toContainEqual({ kind: "chats", total: 0, unread: 0 });
1287
+ });
1288
+
1289
+ test("an unsurfaced background row pinned by raw column writes stays invisible", () => {
1290
+ // Standard-listing visibility admits a pinned background row only when
1291
+ // it is surfaced (the pinned group fails the custom-group arm on its
1292
+ // system: prefix), so it appears in neither the Pinned list nor the
1293
+ // Pinned count.
1294
+ const bg = createConversation({
1295
+ title: "bg-pinned",
1296
+ conversationType: "background",
1297
+ });
1298
+ pin(bg.id);
1299
+
1300
+ const sections = invokeSections();
1301
+
1302
+ expect(sections.some((s) => s.kind === "pinned")).toBe(false);
1303
+ });
1304
+ });
@@ -1712,3 +1712,55 @@ describe("config invariant flag enrichment", () => {
1712
1712
  }
1713
1713
  });
1714
1714
  });
1715
+
1716
+ describe("provider membership at the write choke point", () => {
1717
+ const configPatchRoute = ROUTES.find(
1718
+ (r) => r.operationId === "config_patch",
1719
+ )!;
1720
+
1721
+ test("a pre-existing unknown provider does not block unrelated writes", async () => {
1722
+ // Read tolerance means a stored entry-name provider is legal on disk;
1723
+ // re-validating it on every write would make all later settings saves
1724
+ // fail with no in-product repair path.
1725
+ rawConfigFixture = {
1726
+ llm: {
1727
+ profiles: {
1728
+ work: {
1729
+ source: "user",
1730
+ provider: "anthropic-work",
1731
+ model: "claude-opus-4-8",
1732
+ },
1733
+ },
1734
+ },
1735
+ };
1736
+ seedRawConfig();
1737
+
1738
+ await configPatchRoute.handler({
1739
+ body: { llm: { callSites: { mainAgent: { maxTokens: 2048 } } } },
1740
+ });
1741
+
1742
+ const llm = loadRawConfig().llm as {
1743
+ callSites?: Record<string, { maxTokens?: number }>;
1744
+ profiles?: Record<string, { provider?: string }>;
1745
+ };
1746
+ expect(llm.callSites?.mainAgent?.maxTokens).toBe(2048);
1747
+ expect(llm.profiles?.work?.provider).toBe("anthropic-work");
1748
+ });
1749
+
1750
+ test("introducing an unknown provider is rejected", async () => {
1751
+ rawConfigFixture = { llm: {} };
1752
+ seedRawConfig();
1753
+
1754
+ await expect(
1755
+ configPatchRoute.handler({
1756
+ body: {
1757
+ llm: {
1758
+ profiles: {
1759
+ rogue: { source: "user", provider: "not-a-provider", model: "m" },
1760
+ },
1761
+ },
1762
+ },
1763
+ }),
1764
+ ).rejects.toThrow(/Invalid provider/);
1765
+ });
1766
+ });
@@ -432,6 +432,31 @@ describe("PUT config/llm/default-provider", () => {
432
432
  expect(availability(result)).toEqual({ status: "ok" });
433
433
  });
434
434
 
435
+ test("chatgpt persists and resolves the canonical subscription row", async () => {
436
+ seedConnection({
437
+ name: "chatgpt-subscription",
438
+ provider: "chatgpt",
439
+ auth: {
440
+ type: "oauth_subscription",
441
+ credential: "credential/chatgpt/access_token",
442
+ },
443
+ });
444
+ secureKeyResults["credential/chatgpt/access_token"] = {
445
+ value: "token",
446
+ unreachable: false,
447
+ };
448
+
449
+ const result = (await put({ provider: "chatgpt" })) as Record<
450
+ string,
451
+ unknown
452
+ >;
453
+
454
+ expect(persistedDefaultProvider()).toEqual({ provider: "chatgpt" });
455
+ expect(result.provider).toBe("chatgpt");
456
+ expect(result.resolvedConnectionName).toBe("chatgpt-subscription");
457
+ expect(availability(result)).toEqual({ status: "ok" });
458
+ });
459
+
435
460
  test("persists an explicit connectionName", async () => {
436
461
  await put({ provider: "openai", connectionName: "work-openai" });
437
462
  expect(persistedDefaultProvider()).toEqual({
@@ -449,6 +474,42 @@ describe("PUT config/llm/default-provider", () => {
449
474
  expect(availability(result).status).toBe("missing_connection");
450
475
  });
451
476
 
477
+ test("a noncanonical pin on a routing identity → 400 naming the canonical row", async () => {
478
+ for (const [provider, canonical] of [
479
+ ["chatgpt", "chatgpt-subscription"],
480
+ ["vellum", "vellum"],
481
+ ] as const) {
482
+ const err = await put({
483
+ provider,
484
+ connectionName: "some-other-row",
485
+ }).catch((e: unknown) => e);
486
+ expect(String(err)).toContain(canonical);
487
+ }
488
+ });
489
+
490
+ test("the canonical pin on a routing identity is accepted", async () => {
491
+ seedConnection({
492
+ name: "chatgpt-subscription",
493
+ provider: "chatgpt",
494
+ auth: {
495
+ type: "oauth_subscription",
496
+ credential: "credential/chatgpt/access_token",
497
+ },
498
+ });
499
+ secureKeyResults["credential/chatgpt/access_token"] = {
500
+ value: "token",
501
+ unreachable: false,
502
+ };
503
+
504
+ const result = (await put({
505
+ provider: "chatgpt",
506
+ connectionName: "chatgpt-subscription",
507
+ })) as Record<string, unknown>;
508
+
509
+ expect(result.resolvedConnectionName).toBe("chatgpt-subscription");
510
+ expect(availability(result)).toEqual({ status: "ok" });
511
+ });
512
+
452
513
  test("invalid provider → 400 naming the allowed providers", async () => {
453
514
  const err = await put({ provider: "not-a-provider" }).catch(
454
515
  (e: unknown) => e,